aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/svgLib/path/arc.py
blob: 318107121faf52da3c49ac1fef8e74048ccf39ee (plain)
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),
            )