Coverage for lib/lottie/utils/ellipse.py: 93%
88 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
1import math
3from ..nvector import NVector, PolarVector
4from ..objects.bezier import BezierPoint, Bezier
7## @todo Just output a Bezier object
8class Ellipse:
9 def __init__(self, center, radii, xrot):
10 """
11 @param center 2D vector, center of the ellipse
12 @param radii 2D vector, x/y radius of the ellipse
13 @param xrot Angle between the main axis of the ellipse and the x axis (in radians)
14 """
15 self.center = center
16 self.radii = radii
17 self.xrot = xrot
19 def point(self, t):
20 return NVector(
21 self.center[0]
22 + self.radii[0] * math.cos(self.xrot) * math.cos(t)
23 - self.radii[1] * math.sin(self.xrot) * math.sin(t),
25 self.center[1]
26 + self.radii[0] * math.sin(self.xrot) * math.cos(t)
27 + self.radii[1] * math.cos(self.xrot) * math.sin(t)
28 )
30 def derivative(self, t):
31 return NVector(
32 - self.radii[0] * math.cos(self.xrot) * math.sin(t)
33 - self.radii[1] * math.sin(self.xrot) * math.cos(t),
35 - self.radii[0] * math.sin(self.xrot) * math.sin(t)
36 + self.radii[1] * math.cos(self.xrot) * math.cos(t)
37 )
39 def to_bezier_points(self, anglestart, angle_delta, step=math.pi / 2):
40 points = []
41 angle1 = anglestart
42 angle_left = abs(angle_delta)
43 sign = -1 if anglestart+angle_delta < angle1 else 1
44 tolerance = math.pi / 100
45 if angle_left % step > tolerance:
46 step = angle_left / max(1, round(angle_left / step))
48 # We need to fix the first handle
49 firststep = min(angle_left, step) * sign
50 alpha = self._alpha(firststep)
51 q1 = self.derivative(angle1) * alpha
52 points.append(BezierPoint(self.point(angle1), NVector(0, 0), q1))
54 # Then we iterate until the angle has been completed
55 half_step = step / 2
56 while True:
57 if angle_left < half_step:
58 break
60 lstep = min(angle_left, step)
61 step_sign = lstep * sign
62 angle2 = angle1 + step_sign
63 angle_left -= abs(lstep)
65 alpha = self._alpha(step_sign)
66 p2 = self.point(angle2)
67 q2 = self.derivative(angle2) * alpha
69 points.append(BezierPoint(p2, -q2, q2))
70 angle1 = angle2
72 return points
74 def to_bezier(self, angle_start, angle_delta, step=math.pi / 2):
75 bezier = Bezier()
76 points = self.to_bezier_points(angle_start, angle_delta, step)
78 if angle_delta == math.pi * 2: 78 ↛ 82line 78 didn't jump to line 82, because the condition on line 78 was never false
79 points.pop(0)
80 bezier.close()
82 for point in points:
83 bezier.add_point(point.vertex, point.in_tangent, point.out_tangent)
84 return bezier
86 def _alpha(self, step):
87 return math.sin(step) * (math.sqrt(4+3*math.tan(step/2)**2) - 1) / 3
89 @classmethod
90 def from_svg_arc(cls, start, rx, ry, xrot, large, sweep, dest):
91 rx = abs(rx)
92 ry = abs(ry)
94 x1 = start[0]
95 y1 = start[1]
96 x2 = dest[0]
97 y2 = dest[1]
98 phi = math.pi * xrot / 180
100 x1p, y1p = _matrix_mul(phi, (start-dest)/2, -1)
102 cr = x1p ** 2 / rx**2 + y1p**2 / ry**2
103 if cr > 1: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 s = math.sqrt(cr)
105 rx *= s
106 ry *= s
108 dq = rx**2 * y1p**2 + ry**2 * x1p**2
109 pq = (rx**2 * ry**2 - dq) / dq
110 cpm = math.sqrt(max(0, pq))
111 if large == sweep:
112 cpm = -cpm
113 cp = NVector(cpm * rx * y1p / ry, -cpm * ry * x1p / rx)
114 c = _matrix_mul(phi, cp) + NVector((x1+x2)/2, (y1+y2)/2)
115 theta1 = _angle(NVector(1, 0), NVector((x1p - cp[0]) / rx, (y1p - cp[1]) / ry))
116 deltatheta = _angle(
117 NVector((x1p - cp[0]) / rx, (y1p - cp[1]) / ry),
118 NVector((-x1p - cp[0]) / rx, (-y1p - cp[1]) / ry)
119 ) % (2*math.pi)
121 if not sweep and deltatheta > 0:
122 deltatheta -= 2*math.pi
123 elif sweep and deltatheta < 0: 123 ↛ 124line 123 didn't jump to line 124, because the condition on line 123 was never true
124 deltatheta += 2*math.pi
126 return cls(c, NVector(rx, ry), phi), theta1, deltatheta
129def _matrix_mul(phi, p, sin_mul=1):
130 c = math.cos(phi)
131 s = math.sin(phi) * sin_mul
133 xr = c * p.x - s * p.y
134 yr = s * p.x + c * p.y
135 return NVector(xr, yr)
138def _angle(u, v):
139 arg = math.acos(max(-1, min(1, u.dot(v) / (u.length * v.length))))
140 if u[0] * v[1] - u[1] * v[0] < 0:
141 return -arg
142 return arg