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
|
"""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),
)
|