Coverage for lib/lottie/objects/bezier.py: 23%
298 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
2from .base import LottieObject, LottieProp
3from ..nvector import NVector
6class BezierPoint:
7 def __init__(self, vertex, in_tangent=None, out_tangent=None):
8 self.vertex = vertex
9 self.in_tangent = in_tangent or NVector(0, 0)
10 self.out_tangent = out_tangent or NVector(0, 0)
12 def relative(self):
13 return self
15 @classmethod
16 def smooth(cls, point, in_tangent):
17 return cls(point, in_tangent, -in_tangent)
19 @classmethod
20 def from_absolute(cls, point, in_tangent=None, out_tangent=None):
21 if not in_tangent:
22 in_tangent = point.clone()
23 if not out_tangent:
24 out_tangent = point.clone()
25 return BezierPoint(point, in_tangent, out_tangent)
28class BezierPointView:
29 """
30 View for bezier point
31 """
32 def __init__(self, bezier, index):
33 self.bezier = bezier
34 self.index = index
36 @property
37 def vertex(self):
38 return self.bezier.vertices[self.index]
40 @vertex.setter
41 def vertex(self, point):
42 self.bezier.vertices[self.index] = point
44 @property
45 def in_tangent(self):
46 return self.bezier.in_tangents[self.index]
48 @in_tangent.setter
49 def in_tangent(self, point):
50 self.bezier.in_tangents[self.index] = point
52 @property
53 def out_tangent(self):
54 return self.bezier.out_tangents[self.index]
56 @out_tangent.setter
57 def out_tangent(self, point):
58 self.bezier.out_tangents[self.index] = point
60 def relative(self):
61 return self
64class AbsoluteBezierPointView(BezierPointView):
65 @property
66 def in_tangent(self):
67 return self.bezier.in_tangents[self.index] + self.vertex
69 @in_tangent.setter
70 def in_tangent(self, point):
71 self.bezier.in_tangents[self.index] = point - self.vertex
73 @property
74 def out_tangent(self):
75 return self.bezier.out_tangents[self.index] + self.vertex
77 @out_tangent.setter
78 def out_tangent(self, point):
79 self.bezier.out_tangents[self.index] = point - self.vertex
81 def relative(self):
82 return BezierPointView(self.bezier, self.index)
85class BezierView:
86 def __init__(self, bezier, absolute=False):
87 self.bezier = bezier
88 self.is_absolute = absolute
90 def point(self, index):
91 if self.is_absolute:
92 return AbsoluteBezierPointView(self.bezier, index)
93 return BezierPointView(self.bezier, index)
95 def __len__(self):
96 return len(self.bezier.vertices)
98 def __getitem__(self, key):
99 if isinstance(key, slice):
100 return [
101 self.point(i)
102 for i in key
103 ]
104 return self.point(key)
106 def __iter__(self):
107 for i in range(len(self)):
108 yield self.point(i)
110 def append(self, point):
111 if isinstance(point, NVector):
112 self.bezier.add_point(point.clone())
113 else:
114 bpt = point.relative()
115 self.bezier.add_point(bpt.vertex.clone(), bpt.in_tangent.clone(), bpt.out_tangent.clone())
117 @property
118 def absolute(self):
119 return BezierView(self.bezier, True)
122## @ingroup Lottie
123class Bezier(LottieObject):
124 """!
125 Single bezier curve
126 """
127 _props = [
128 LottieProp("closed", "c", bool, False),
129 LottieProp("in_tangents", "i", NVector, True),
130 LottieProp("out_tangents", "o", NVector, True),
131 LottieProp("vertices", "v", NVector, True),
132 ]
134 def __init__(self):
135 ## Closed property of shape
136 self.closed = False
137 ## Cubic bezier handles for the segments before each vertex
138 self.in_tangents = []
139 ## Cubic bezier handles for the segments after each vertex
140 self.out_tangents = []
141 ## Bezier curve vertices.
142 self.vertices = []
143 #self.rel_tangents = rel_tangents
144 ## More convent way to access points
145 self.points = BezierView(self)
147 def clone(self):
148 clone = Bezier()
149 clone.closed = self.closed
150 clone.in_tangents = [p.clone() for p in self.in_tangents]
151 clone.out_tangents = [p.clone() for p in self.out_tangents]
152 clone.vertices = [p.clone() for p in self.vertices]
153 #clone.rel_tangents = self.rel_tangents
154 return clone
156 def insert_point(self, index, pos, inp=NVector(0, 0), outp=NVector(0, 0)):
157 """!
158 Inserts a point at the given index
159 @param index Index to insert the point at
160 @param pos Point to add
161 @param inp Tangent entering the point, as a vector relative to @p pos
162 @param outp Tangent exiting the point, as a vector relative to @p pos
163 @returns @c self, for easy chaining
164 """
165 self.vertices.insert(index, pos)
166 self.in_tangents.insert(index, inp.clone())
167 self.out_tangents.insert(index, outp.clone())
168 #if not self.rel_tangents:
169 #self.in_tangents[-1] += pos
170 #self.out_tangents[-1] += pos
171 return self
173 def add_point(self, pos, inp=NVector(0, 0), outp=NVector(0, 0)):
174 """!
175 Appends a point to the curve
176 @see insert_point
177 """
178 self.insert_point(len(self.vertices), pos, inp, outp)
179 return self
181 def add_smooth_point(self, pos, inp):
182 """!
183 Appends a point with symmetrical tangents
184 @see insert_point
185 """
186 self.add_point(pos, inp, -inp)
187 return self
189 def close(self, closed=True):
190 """!
191 Updates self.closed
192 @returns @c self, for easy chaining
193 """
194 self.closed = closed
195 return self
197 def point_at(self, t):
198 """!
199 @param t A value between 0 and 1, percentage along the length of the curve
200 @returns The point at @p t in the curve
201 """
202 i, t = self._index_t(t)
203 points = self._bezier_points(i, True)
204 return self._solve_bezier(t, points)
206 def tangent_angle_at(self, t):
207 i, t = self._index_t(t)
208 points = self._bezier_points(i, True)
210 n = len(points) - 1
211 if n > 0:
212 delta = sum((
213 (points[i+1] - points[i]) * n * self._solve_bezier_coeff(i, n - 1, t)
214 for i in range(n)
215 ), NVector(0, 0))
216 return math.atan2(delta.y, delta.x)
218 return 0
220 def _split(self, t):
221 i, t = self._index_t(t)
222 cub = self._bezier_points(i, True)
223 split1, split2 = self._split_segment(t, cub)
224 return i, split1, split2
226 def _split_segment(self, t, cub):
227 if len(cub) == 2:
228 k = self._solve_bezier_step(t, cub)[0]
229 split1 = [cub[0], NVector(0, 0), NVector(0, 0), k]
230 split2 = [k, NVector(0, 0), NVector(0, 0), cub[-1]]
231 return split1, split2
233 if len(cub) == 3:
234 quad = cub
235 else:
236 quad = self._solve_bezier_step(t, cub)
237 lin = self._solve_bezier_step(t, quad)
238 k = self._solve_bezier_step(t, lin)[0]
239 split1 = [cub[0], quad[0]-cub[0], lin[0]-k, k]
240 split2 = [k, lin[-1]-k, quad[-1]-cub[-1], cub[-1]]
241 return split1, split2
243 def split_at(self, t):
244 """!
245 Get two pieces out of a Bezier curve
246 @param t A value between 0 and 1, percentage along the length of the curve
247 @returns Two Bezier objects that correspond to self, but split at @p t
248 """
249 if len(self.vertices) < 2:
250 return Bezier(), self.clone()
252 i, split1, split2 = self._split(t)
254 seg1 = Bezier()
255 seg2 = Bezier()
256 for j in range(i):
257 seg1.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone())
258 for j in range(i+2, len(self.vertices)):
259 seg2.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone())
261 seg1.add_point(split1[0], self.in_tangents[i].clone(), split1[1])
262 seg1.add_point(split1[3], split1[2], split2[1])
264 seg2.insert_point(0, split2[0], split1[2], split2[1])
265 seg2.insert_point(1, split2[3], split2[2], self.out_tangents[i+1].clone())
267 return seg1, seg2
269 def segment(self, t1, t2):
270 """!
271 Splits a Bezier in two points and returns the segment between the
272 @param t1 A value between 0 and 1, percentage along the length of the curve
273 @param t2 A value between 0 and 1, percentage along the length of the curve
274 @returns Bezier object that correspond to the segment between @p t1 and @p t2
275 """
276 if self.closed and self.vertices and self.vertices[-1] != self.vertices[0]:
277 copy = self.clone()
278 copy.add_point(self.vertices[0])
279 copy.closed = False
280 return copy.segment(t1, t2)
282 if t1 > 1:
283 t1 = 1
284 if t2 > 1:
285 t2 = 1
287 if t1 > t2:
288 t1, t2 = t2, t1
289 elif t1 == t2:
290 seg = Bezier()
291 p = self.point_at(t1)
292 seg.add_point(p)
293 seg.add_point(p)
294 return seg
296 seg1, seg2 = self.split_at(t1)
297 t2p = (t2-t1) / (1-t1)
298 seg3, seg4 = seg2.split_at(t2p)
299 return seg3
301 def split_self_multi(self, positions):
302 """!
303 Adds more points to the Bezier
304 @param positions list of percentages along the curve
305 """
306 if not len(positions):
307 return
308 t1 = positions[0]
309 seg1, seg2 = self.split_at(t1)
310 self.vertices = []
311 self.in_tangents = []
312 self.out_tangents = []
314 self.vertices = seg1.vertices[:-1]
315 self.in_tangents = seg1.in_tangents[:-1]
316 self.out_tangents = seg1.out_tangents[:-1]
318 for t2 in positions[1:]:
319 t = (t2-t1) / (1-t1)
320 seg1, seg2 = seg2.split_at(t)
321 t1 = t
322 self.vertices += seg1.vertices[:-1]
323 self.in_tangents += seg1.in_tangents[:-1]
324 self.out_tangents += seg1.out_tangents[:-1]
326 self.vertices += seg2.vertices
327 self.in_tangents += seg2.in_tangents
328 self.out_tangents += seg2.out_tangents
330 def split_each_segment(self):
331 """!
332 Adds a point in the middle of the segment between every pair of points in the Bezier
333 """
334 vertices = self.vertices
335 in_tangents = self.in_tangents
336 out_tangents = self.out_tangents
338 self.vertices = []
339 self.in_tangents = []
340 self.out_tangents = []
342 for i in range(len(vertices)-1):
343 tocut = [vertices[i], out_tangents[i]+vertices[i], in_tangents[i+1]+vertices[i+1], vertices[i+1]]
344 split1, split2 = self._split_segment(0.5, tocut)
345 if i:
346 self.out_tangents[-1] = split1[1]
347 else:
348 self.add_point(vertices[0], in_tangents[0], split1[1])
349 self.add_point(split1[3], split1[2], split2[1])
350 self.add_point(vertices[i+1], split2[2], NVector(0, 0))
352 def split_self_chunks(self, n_chunks):
353 """!
354 Adds points the Bezier, splitting it into @p n_chunks additional chunks.
355 """
356 splits = [i/n_chunks for i in range(1, n_chunks)]
357 return self.split_self_multi(splits)
359 def _bezier_points(self, i, optimize):
360 v1 = self.vertices[i].clone()
361 v2 = self.vertices[i+1].clone()
362 points = [v1]
363 t1 = self.out_tangents[i].clone()
364 if not optimize or t1.length != 0:
365 points.append(t1+v1)
366 t2 = self.in_tangents[i+1].clone()
367 if not optimize or t1.length != 0:
368 points.append(t2+v2)
369 points.append(v2)
370 return points
372 def _solve_bezier_step(self, t, points):
373 next = []
374 p1 = points[0]
375 for p2 in points[1:]:
376 next.append(p1 * (1-t) + p2 * t)
377 p1 = p2
378 return next
380 def _solve_bezier_coeff(self, i, n, t):
381 return (
382 math.factorial(n) / (math.factorial(i) * math.factorial(n - i)) # (n choose i)
383 * (t ** i) * ((1 - t) ** (n-i))
384 )
386 def _solve_bezier(self, t, points):
387 n = len(points) - 1
388 if n > 0:
389 return sum((
390 points[i] * self._solve_bezier_coeff(i, n, t)
391 for i in range(n+1)
392 ), NVector(0, 0))
394 #while len(points) > 1:
395 #points = self._solve_bezier_step(t, points)
396 return points[0]
398 def _index_t(self, t):
399 if t <= 0:
400 return 0, 0
402 if t >= 1:
403 return len(self.vertices)-2, 1
405 n = len(self.vertices)-1
406 for i in range(n):
407 if (i+1) / n > t:
408 break
410 return i, (t - (i/n)) * n
412 def reverse(self):
413 """!
414 Reverses the Bezier curve
415 """
416 self.vertices = list(reversed(self.vertices))
417 out_tangents = list(reversed(self.in_tangents))
418 in_tangents = list(reversed(self.out_tangents))
419 self.in_tangents = in_tangents
420 self.out_tangents = out_tangents
422 """def to_absolute(self):
423 if self.rel_tangents:
424 self.rel_tangents = False
425 for i in range(len(self.vertices)):
426 p = self.vertices[i]
427 self.in_tangents[i] += p
428 self.out_tangents[i] += p
429 return self"""
431 def rounded(self, round_distance):
432 cloned = Bezier()
433 cloned.closed = self.closed
434 # value from https://spencermortensen.com/articles/bezier-circle/
435 round_corner = 0.5519
437 def _get_vt(closest_index):
438 closer_v = self.vertices[closest_index]
439 distance = (current - closer_v).length
440 new_pos_perc = min(distance/2, round_distance) / distance if distance else 0
441 vert = current + (closer_v - current) * new_pos_perc
442 tan = - (vert - current) * round_corner
443 return vert, tan
445 for i, current in enumerate(self.vertices):
446 if not self.closed and (i == 0 or i == len(self.points) - 1):
447 cloned.points.append(self.points[i])
448 else:
449 vert1, out_t = _get_vt(i - 1)
450 cloned.add_point(vert1, NVector(0, 0), out_t)
451 vert2, in_t = _get_vt((i+1) % len(self.points))
452 cloned.add_point(vert2, in_t, NVector(0, 0))
454 return cloned
456 def scale(self, amount):
457 for vl in (self.vertices, self.in_tangents, self.out_tangents):
458 for v in vl:
459 v *= amount
461 def lerp(self, other, t):
462 if len(other.vertices) != len(self.vertices):
463 if t < 1:
464 return self.clone()
465 return other.clone()
467 bez = Bezier()
468 bez.closed = self.closed
470 for vlist_name in ["vertices", "in_tangents", "out_tangents"]:
471 vlist = getattr(self, vlist_name)
472 olist = getattr(other, vlist_name)
473 out = getattr(bez, vlist_name)
474 for v, o in zip(vlist, olist):
475 out.append(v.lerp(o, t))
477 return bez
479 def rough_length(self):
480 if len(self.vertices) < 2:
481 return 0
482 last = self.vertices[0]
483 length = 0
484 for v in self.vertices[1:]:
485 length += (v-last).length
486 last = v
487 if self.closed:
488 length += (last-self.vertices[0]).length
489 return length