Round Corners
-
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)