arc.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. """Convert SVG Path's elliptical arcs to Bezier curves.
  2. The code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic
  3. https://github.com/chromium/chromium/blob/93831f2/third_party/
  4. blink/renderer/core/svg/svg_path_parser.cc#L169-L278
  5. """
  6. from fontTools.misc.transform import Identity, Scale
  7. from math import atan2, ceil, cos, fabs, isfinite, pi, radians, sin, sqrt, tan
  8. TWO_PI = 2 * pi
  9. PI_OVER_TWO = 0.5 * pi
  10. def _map_point(matrix, pt):
  11. # apply Transform matrix to a point represented as a complex number
  12. r = matrix.transformPoint((pt.real, pt.imag))
  13. return r[0] + r[1] * 1j
  14. class EllipticalArc(object):
  15. def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point):
  16. self.current_point = current_point
  17. self.rx = rx
  18. self.ry = ry
  19. self.rotation = rotation
  20. self.large = large
  21. self.sweep = sweep
  22. self.target_point = target_point
  23. # SVG arc's rotation angle is expressed in degrees, whereas Transform.rotate
  24. # uses radians
  25. self.angle = radians(rotation)
  26. # these derived attributes are computed by the _parametrize method
  27. self.center_point = self.theta1 = self.theta2 = self.theta_arc = None
  28. def _parametrize(self):
  29. # convert from endopoint to center parametrization:
  30. # https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
  31. # If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a
  32. # "lineto") joining the endpoints.
  33. # http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
  34. rx = fabs(self.rx)
  35. ry = fabs(self.ry)
  36. if not (rx and ry):
  37. return False
  38. # If the current point and target point for the arc are identical, it should
  39. # be treated as a zero length path. This ensures continuity in animations.
  40. if self.target_point == self.current_point:
  41. return False
  42. mid_point_distance = (self.current_point - self.target_point) * 0.5
  43. point_transform = Identity.rotate(-self.angle)
  44. transformed_mid_point = _map_point(point_transform, mid_point_distance)
  45. square_rx = rx * rx
  46. square_ry = ry * ry
  47. square_x = transformed_mid_point.real * transformed_mid_point.real
  48. square_y = transformed_mid_point.imag * transformed_mid_point.imag
  49. # Check if the radii are big enough to draw the arc, scale radii if not.
  50. # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
  51. radii_scale = square_x / square_rx + square_y / square_ry
  52. if radii_scale > 1:
  53. rx *= sqrt(radii_scale)
  54. ry *= sqrt(radii_scale)
  55. self.rx, self.ry = rx, ry
  56. point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle)
  57. point1 = _map_point(point_transform, self.current_point)
  58. point2 = _map_point(point_transform, self.target_point)
  59. delta = point2 - point1
  60. d = delta.real * delta.real + delta.imag * delta.imag
  61. scale_factor_squared = max(1 / d - 0.25, 0.0)
  62. scale_factor = sqrt(scale_factor_squared)
  63. if self.sweep == self.large:
  64. scale_factor = -scale_factor
  65. delta *= scale_factor
  66. center_point = (point1 + point2) * 0.5
  67. center_point += complex(-delta.imag, delta.real)
  68. point1 -= center_point
  69. point2 -= center_point
  70. theta1 = atan2(point1.imag, point1.real)
  71. theta2 = atan2(point2.imag, point2.real)
  72. theta_arc = theta2 - theta1
  73. if theta_arc < 0 and self.sweep:
  74. theta_arc += TWO_PI
  75. elif theta_arc > 0 and not self.sweep:
  76. theta_arc -= TWO_PI
  77. self.theta1 = theta1
  78. self.theta2 = theta1 + theta_arc
  79. self.theta_arc = theta_arc
  80. self.center_point = center_point
  81. return True
  82. def _decompose_to_cubic_curves(self):
  83. if self.center_point is None and not self._parametrize():
  84. return
  85. point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry)
  86. # Some results of atan2 on some platform implementations are not exact
  87. # enough. So that we get more cubic curves than expected here. Adding 0.001f
  88. # reduces the count of sgements to the correct count.
  89. num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001))))
  90. for i in range(num_segments):
  91. start_theta = self.theta1 + i * self.theta_arc / num_segments
  92. end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments
  93. t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
  94. if not isfinite(t):
  95. return
  96. sin_start_theta = sin(start_theta)
  97. cos_start_theta = cos(start_theta)
  98. sin_end_theta = sin(end_theta)
  99. cos_end_theta = cos(end_theta)
  100. point1 = complex(
  101. cos_start_theta - t * sin_start_theta,
  102. sin_start_theta + t * cos_start_theta,
  103. )
  104. point1 += self.center_point
  105. target_point = complex(cos_end_theta, sin_end_theta)
  106. target_point += self.center_point
  107. point2 = target_point
  108. point2 += complex(t * sin_end_theta, -t * cos_end_theta)
  109. point1 = _map_point(point_transform, point1)
  110. point2 = _map_point(point_transform, point2)
  111. target_point = _map_point(point_transform, target_point)
  112. yield point1, point2, target_point
  113. def draw(self, pen):
  114. for point1, point2, target_point in self._decompose_to_cubic_curves():
  115. pen.curveTo(
  116. (point1.real, point1.imag),
  117. (point2.real, point2.imag),
  118. (target_point.real, target_point.imag),
  119. )