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