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

1import math 

2 

3from ..nvector import NVector, PolarVector 

4from ..objects.bezier import BezierPoint, Bezier 

5 

6 

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 

18 

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

24 

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 ) 

29 

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

34 

35 - self.radii[0] * math.sin(self.xrot) * math.sin(t) 

36 + self.radii[1] * math.cos(self.xrot) * math.cos(t) 

37 ) 

38 

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

47 

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

53 

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 

59 

60 lstep = min(angle_left, step) 

61 step_sign = lstep * sign 

62 angle2 = angle1 + step_sign 

63 angle_left -= abs(lstep) 

64 

65 alpha = self._alpha(step_sign) 

66 p2 = self.point(angle2) 

67 q2 = self.derivative(angle2) * alpha 

68 

69 points.append(BezierPoint(p2, -q2, q2)) 

70 angle1 = angle2 

71 

72 return points 

73 

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) 

77 

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() 

81 

82 for point in points: 

83 bezier.add_point(point.vertex, point.in_tangent, point.out_tangent) 

84 return bezier 

85 

86 def _alpha(self, step): 

87 return math.sin(step) * (math.sqrt(4+3*math.tan(step/2)**2) - 1) / 3 

88 

89 @classmethod 

90 def from_svg_arc(cls, start, rx, ry, xrot, large, sweep, dest): 

91 rx = abs(rx) 

92 ry = abs(ry) 

93 

94 x1 = start[0] 

95 y1 = start[1] 

96 x2 = dest[0] 

97 y2 = dest[1] 

98 phi = math.pi * xrot / 180 

99 

100 x1p, y1p = _matrix_mul(phi, (start-dest)/2, -1) 

101 

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 

107 

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) 

120 

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 

125 

126 return cls(c, NVector(rx, ry), phi), theta1, deltatheta 

127 

128 

129def _matrix_mul(phi, p, sin_mul=1): 

130 c = math.cos(phi) 

131 s = math.sin(phi) * sin_mul 

132 

133 xr = c * p.x - s * p.y 

134 yr = s * p.x + c * p.y 

135 return NVector(xr, yr) 

136 

137 

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