The code responsible for the rounding of the paths in a small animation I made.
DEBUG = False
# A path has points.
# A point has only an anchor [(x,y)], or has an anchor and in and out point [(x,y), (x,y), (x,y)].
# To close a path, repeat the first point at the last position.
jagged_line =[[(500,210)], [(400,250)], [(600,350)], [(450,600)], [(500,790)]]
rectangle =[[(220,220)], [(780,220)], [(780,780)], [(220,780)], [(220,220)]]
circle = [[(500.0, 800.0), (335.0, 800.0), (665.0, 800.0)], [(800.0, 500.0), (800.0, 665.0), (800.0, 335.0)], [(500.0, 200.0), (665.0, 200.0), (335.0, 200.0)], [(200.0, 500.0), (200.0, 335.0), (200.0, 665.0)], [(500.0, 800.0), (335.0, 800.0), (665.0, 800.0)]]
def draw(path):
bez = BezierPath()
bez.moveTo(path[0][0])
for i in range(1, len(path)):
p0 = path[i-1]
p1 = path[i]
if len(p0) == 1 and len(p1) == 1:
# straight line between points
bez.lineTo(p1[0])
elif len(p0) == 3 and len(p1) == 1:
# from curve point to straight point
bez.curveTo(p0[2], p1[0], p1[0])
elif len(p0) == 1 and len(p1) == 3:
# from straight point to curve point
bez.curveTo(p0[0], p1[1], p1[0])
elif len(p0) == 3 and len(p1) == 3:
# curve point on both sides
bez.curveTo(p0[2], p1[1], p1[0])
if path[-1] == path[0]:
bez.closePath()
drawPath(bez)
def round_corners(path, roundness):
if len(path) > 2:
new_path = []
new_path.append(path[0])
new_path.append(path[1])
for i in range(1, len(path) - 1):
p1 = new_path[i - 1]
p2 = path[i]
p3 = path[i + 1]
p1, p2, p3 = round_segment(p1, p2, p3, roundness)
new_path[i - 1] = p1
new_path[i] = p2
new_path.append(p3)
# If the path is closed, we need (the handle of) the first point
if path[-1] == path[0]:
p1 = new_path[-2]
p2 = path[0]
p3 = new_path[1]
p1, p2, p3 = round_segment(p1, p2, p3, roundness)
new_path[-2] = p1
new_path[-1] = p2
new_path[0] = p2
return new_path
else:
return path
def round_segment(p1, p2, p3, roundness):
if roundable(p1, p2, p3):
p2_in, p2_out = create_handles(p1[0], p2[0], p3[0], roundness)
p2 = [p2[0], p2_in, p2_out]
return p1, p2, p3
def roundable(p1, p2, p3):
# If two of the three points are in the same spot,
# we can’t calculate a curve between two points.
p1_anchor = p1[0]
p2_anchor = p2[0]
p3_anchor = p3[0]
d12 = distance_between(p1_anchor, p2_anchor)
d23 = distance_between(p2_anchor, p3_anchor)
if d12 == 0 or d23 == 0:
return False
else:
return True
def create_handles(A, B, C, smoothness):
# A is the point before point B
# B is the point to create the handles for
# C is the point after point B
d_AB = distance_between(A, B)
d_BC = distance_between(B, C)
# Create an isosceles triangle A, B, p4 based on triangle A, B, C.
# Side B, p4 is the same length as side A, B.
# Side B, p4 has the same direction as side B, C
p4_x = ((C[0] - B[0]) * (d_AB / d_BC)) + B[0]
p4_y = ((C[1] - B[1]) * (d_AB / d_BC)) + B[1]
p4 = (p4_x, p4_y)
if DEBUG:
draw_handle(B, p4)
# Calculate a point p5 on the base of the isosceles triangle,
# exactly in between A and B.
p5_x = A[0] + ((p4[0] - A[0]) / 2)
p5_y = A[1] + ((p4[1] - A[1]) / 2)
p5 = (p5_x, p5_y)
if DEBUG:
draw_point(p5, 10)
draw_line(A, p4)
# The line from the top of the isosceles triangle B to
# the point p5 on the base of that triangle
# divides the corner p1, p2, p3 in two equal parts
if DEBUG:
draw_line(B, p5)
# Direction of the handles is perpendicular
vx = p5[0] - B[0]
vy = p5[1] - B[1]
handle_vx = 0
handle_vy = 0
if vx == 0 and vy == 0:
# The three points are on one line, so there will never be a curve.
pass
elif vx == 0:
# prevent a possible division by 0
handle_vx = 1
handle_vy = 0
elif vy == 0:
# prevent a possible division by 0
handle_vx = 0
handle_vy = 1
elif abs(vx) < abs(vy):
handle_vx = 1
handle_vy = vx / vy
else:
handle_vx = vy / vx
handle_vy = 1
# Define handles
handle_a = (B[0] + handle_vx, B[1] - handle_vy)
handle_b = (B[0] - handle_vx, B[1] + handle_vy)
# The handle closest to point A will be the incoming handle of point B
d_ha_A = distance_between(A, handle_a)
d_hb_A = distance_between(A, handle_b)
# I have to make this better. Also, where’s that 0.8 coming from? What was I thinking?
incoming_handle_lenght = d_AB * smoothness
outgoing_handle_length = d_BC * smoothness
total_handle_length = incoming_handle_lenght + outgoing_handle_length
max_handle_length = 0.8 * total_handle_length
if incoming_handle_lenght > max_handle_length:
outgoing_handle_length += incoming_handle_lenght - max_handle_length
incoming_handle_lenght = max_handle_length
if outgoing_handle_length > max_handle_length:
incoming_handle_lenght += outgoing_handle_length - max_handle_length
outgoing_handle_length = max_handle_length
# finally, the in and out points
if d_ha_A < d_hb_A:
B_incoming = (B[0] + handle_vx * incoming_handle_lenght, B[1] - handle_vy * incoming_handle_lenght)
B_outgoing = (B[0] - handle_vx * outgoing_handle_length, B[1] + handle_vy * outgoing_handle_length)
else:
B_incoming = (B[0] - handle_vx * incoming_handle_lenght, B[1] + handle_vy * incoming_handle_lenght)
B_outgoing = (B[0] + handle_vx * outgoing_handle_length, B[1] - handle_vy * outgoing_handle_length)
if DEBUG:
draw_point(B_incoming, 6)
draw_point(B_outgoing, 6)
draw_line(B, B_incoming)
draw_line(B, B_outgoing)
draw_line(A, B)
return B_incoming, B_outgoing
def distance_between(p1, p2):
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
return pow((dx * dx + dy * dy), 0.5)
def draw_point(point, size):
with savedState():
fill(0, 0.7, 1)
stroke(None)
oval(point[0] - 0.5 * size, point[1] - 0.5 * size, size, size)
def draw_line(a, b):
with savedState():
fill(None)
strokeWidth(1)
stroke(0, 0.7, 1)
line((100, 100), (900, 900))
line(a, b)
def draw_handle(p, h):
draw_point(p, 9)
draw_line(p, h)
fill(None)
# Drawing the original shape and the rounded shape, slightly thicker.
stroke(1, 0, 0)
strokeWidth(2)
draw(jagged_line)
rounded_line = round_corners(jagged_line, 0.4)
strokeWidth(4)
draw(rounded_line)
# A rounding of 0.28 seems to get me as close to a circle as I can get.
stroke(0, 1, 0)
strokeWidth(2)
draw(rectangle)
rounded_rectangle = round_corners(rectangle, 0.28)
strokeWidth(4)
draw(rounded_rectangle)
# Rounding an oval by zero results in a rhombus.
stroke(0, 0, 1)
strokeWidth(2)
draw(circle)
rounded_circle = round_corners(circle, 0)
strokeWidth(4)
draw(rounded_circle)