Coverage for lib/lottie/objects/properties.py: 33%
463 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 functools import reduce
3from .base import LottieObject, LottieProp, PseudoList, PseudoBool
4from . import easing
5from ..nvector import NVector
6from .bezier import Bezier
7from ..utils.color import Color
10class KeyframeBezier:
11 NEWTON_ITERATIONS = 4
12 NEWTON_MIN_SLOPE = 0.001
13 SUBDIVISION_PRECISION = 0.0000001
14 SUBDIVISION_MAX_ITERATIONS = 10
15 SPLINE_TABLE_SIZE = 11
16 SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0)
18 def __init__(self, h1, h2):
19 self.h1 = h1
20 self.h2 = h2
21 self._sample_values = None
23 @classmethod
24 def from_keyframe(cls, keyframe):
25 return cls(keyframe.out_value, keyframe.in_value)
27 def bezier(self):
28 bez = Bezier()
29 bez.add_point(NVector(0, 0), outp=NVector(self.h1.x, self.h1.y))
30 bez.add_point(NVector(1, 1), inp=NVector(self.h2.x-1, self.h2.y-1))
31 return bez
33 def _a(self, c1, c2):
34 return 1 - 3 * c2 + 3 * c1
36 def _b(self, c1, c2):
37 return 3 * c2 - 6 * c1
39 def _c(self, c1):
40 return 3 * c1
42 def _bezier_component(self, t, c1, c2):
43 return ((self._a(c1, c2) * t + self._b(c1, c2)) * t + self._c(c1)) * t
45 def point_at(self, t):
46 return NVector(
47 self._bezier_component(t, self.h1.x, self.h2.x),
48 self._bezier_component(t, self.h1.y, self.h2.y)
49 )
51 def _slope_component(self, t, c1, c2):
52 return 3 * self._a(c1, c2) * t * t + 2 * self._b(c1, c2) * t + self._c(c1)
54 def slope_at(self, t):
55 return NVector(
56 self._slope_component(t, self.h1.x, self.h2.x),
57 self._slope_component(t, self.h1.y, self.h2.y)
58 )
60 def _binary_subdivide(self, x, interval_start, interval_end):
61 current_x = None
62 t = None
63 i = 0
64 for i in range(self.SUBDIVISION_MAX_ITERATIONS):
65 if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISION:
66 break
67 t = interval_start + (interval_end - interval_start) / 2.0
68 current_x = self._bezier_component(t, self.h1.x, self.h2.x) - x
69 if current_x > 0.0:
70 interval_end = t
71 else:
72 interval_start = t
73 return t
75 def _newton_raphson(self, x, t_guess):
76 for i in range(self.NEWTON_ITERATIONS):
77 slope = self._slope_component(t_guess, self.h1.x, self.h2.x)
78 if slope == 0:
79 return t_guess
80 current_x = self._bezier_component(t_guess, self.h1.x, self.h2.x) - x
81 t_guess -= current_x / slope
82 return t_guess
84 def _get_sample_values(self):
85 if self._sample_values is None:
86 self._sample_values = [
87 self._bezier_component(i * self.SAMPLE_STEP_SIZE, self.h1.x, self.h2.x)
88 for i in range(self.SPLINE_TABLE_SIZE)
89 ]
90 return self._sample_values
92 def t_for_x(self, x):
93 sample_values = self._get_sample_values()
94 interval_start = 0
95 current_sample = 1
96 last_sample = self.SPLINE_TABLE_SIZE - 1
97 while current_sample != last_sample and sample_values[current_sample] <= x:
98 interval_start += self.SAMPLE_STEP_SIZE
99 current_sample += 1
100 current_sample -= 1
102 dist = (x - sample_values[current_sample]) / (sample_values[current_sample+1] - sample_values[current_sample])
103 t_guess = interval_start + dist * self.SAMPLE_STEP_SIZE
104 initial_slope = self._slope_component(t_guess, self.h1.x, self.h2.x)
105 if initial_slope >= self.NEWTON_MIN_SLOPE:
106 return self._newton_raphson(x, t_guess)
107 if initial_slope == 0:
108 return t_guess
109 return self._binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZE)
111 def y_at_x(self, x):
112 t = self.t_for_x(x)
113 return self._bezier_component(t, self.h1.y, self.h2.y)
116## @ingroup Lottie
117class Keyframe(LottieObject):
118 _props = [
119 LottieProp("time", "t", float, False),
120 LottieProp("in_value", "i", easing.KeyframeBezierHandle, False),
121 LottieProp("out_value", "o", easing.KeyframeBezierHandle, False),
122 LottieProp("hold", "h", PseudoBool),
123 ]
125 def __init__(self, time=0, easing_function=None):
126 """!
127 @param time Start time of keyframe segment
128 @param easing_function Callable that performs the easing
129 """
130 ## Start time of keyframe segment.
131 self.time = time
132 ## Bezier curve easing in value.
133 self.in_value = None
134 ## Bezier curve easing out value.
135 self.out_value = None
136 ## Jump to the end value
137 self.hold = None
139 if easing_function:
140 easing_function(self)
142 @property
143 def jump(self):
144 return self.hold
146 @jump.setter
147 def jump(self, v):
148 self.hold = v
150 def bezier(self):
151 if self.hold:
152 bez = Bezier()
153 bez.add_point(NVector(0, 0))
154 bez.add_point(NVector(1, 0))
155 bez.add_point(NVector(1, 1))
156 return bez
157 else:
158 return KeyframeBezier.from_keyframe(self).bezier()
160 def lerp_factor(self, ratio):
161 return KeyframeBezier.from_keyframe(self).y_at_x(ratio)
163 def __str__(self):
164 return "%s %s" % (self.time, self.start)
167## @ingroup Lottie
168class OffsetKeyframe(Keyframe):
169 """!
170 Keyframe for MultiDimensional values
172 @par Bezier easing
173 @parblock
174 Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1).
176 @p out_value and @p in_value are the other two handles for a quadratic bezier,
177 expressed as absolute values in this 0-1 space.
179 See also https://cubic-bezier.com/
180 @endparblock
181 """
182 _props = [
183 LottieProp("start", "s", NVector, False),
184 LottieProp("end", "e", NVector, False),
185 ]
187 def __init__(self, time=0, start=None, end=None, easing_function=None, in_tan=None, out_tan=None):
188 Keyframe.__init__(self, time, easing_function)
189 ## Start value of keyframe segment.
190 self.start = start
191 ## End value of keyframe segment.
192 self.end = end
193 ## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
194 self.in_tan = in_tan
195 ## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
196 self.out_tan = out_tan
198 def interpolated_value(self, ratio, next_start=None):
199 end = next_start if self.end is None else self.end
200 if end is None: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true
201 return self.start
202 if not self.in_value or not self.out_value: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true
203 return self.start
204 if ratio == 1: 204 ↛ 206line 204 didn't jump to line 206, because the condition on line 204 was never false
205 return end
206 if ratio == 0:
207 return self.start
208 if self.in_tan and self.out_tan:
209 bezier = Bezier()
210 bezier.add_point(self.start, NVector(0, 0), self.out_tan)
211 bezier.add_point(end, self.in_tan, NVector(0, 0))
212 return bezier.point_at(ratio)
214 lerpv = self.lerp_factor(ratio)
215 return self.start.lerp(end, lerpv)
217 def interpolated_tangent_angle(self, ratio, next_start=None):
218 end = next_start if self.end is None else self.end
219 if end is None or not self.in_tan or not self.out_tan:
220 return 0
222 bezier = Bezier()
223 bezier.add_point(self.start, NVector(0, 0), self.out_tan)
224 bezier.add_point(end, self.in_tan, NVector(0, 0))
225 return bezier.tangent_angle_at(ratio)
227 def __repr__(self):
228 return "<%s.%s %s %s%s>" % (
229 type(self).__module__,
230 type(self).__name__,
231 self.time,
232 self.start,
233 (" -> %s" % self.end) if self.end is not None else ""
234 )
237class AnimatableMixin:
238 keyframe_type = Keyframe
240 def __init__(self, value=None):
241 ## Non-animated value
242 self.value = value
243 ## Property index
244 self.property_index = None
245 ## Whether it's animated
246 self.animated = False
247 ## Keyframe list
248 self.keyframes = None
249 ## Expression
250 self.expression = None
252 def clear_animation(self, value):
253 """!
254 Sets a fixed value, removing animated keyframes
255 """
256 self.value = value
257 self.animated = False
258 self.keyframes = None
260 def add_keyframe(self, time, value, interp=easing.Linear(), *args, **kwargs):
261 """!
262 @param time The time this keyframe appears in
263 @param value The value the property should have at @p time
264 @param interp The easing callable used to update the tangents of the previous keyframe
265 @param args Extra arguments to pass the keyframe constructor
266 @param kwargs Extra arguments to pass the keyframe constructor
267 @note Always call add_keyframe with increasing @p time value
268 """
269 if not self.animated:
270 self.value = None
271 self.keyframes = []
272 self.animated = True
273 else:
274 if self.keyframes[-1].time == time: 274 ↛ 275line 274 didn't jump to line 275, because the condition on line 274 was never true
275 if value != self.keyframes[-1].start:
276 self.keyframes[-1].start = value
277 return
278 else:
279 self.keyframes[-1].end = value.clone()
281 self.keyframes.append(self.keyframe_type(
282 time,
283 value,
284 None,
285 interp,
286 *args,
287 **kwargs
288 ))
290 def get_value(self, time=0):
291 """!
292 @brief Returns the value of the property at the given frame/time
293 """
294 if not self.animated:
295 return self.value
297 if not self.keyframes:
298 return None
300 return self._get_value_helper(time)[0]
302 def _get_value_helper(self, time):
303 val = self.keyframes[0].start
304 for i in range(len(self.keyframes)):
305 k = self.keyframes[i]
306 if time - k.time <= 0:
307 if k.start is not None:
308 val = k.start
310 kp = self.keyframes[i-1] if i > 0 else None
311 if kp:
312 t = (time - kp.time) / (k.time - kp.time)
313 end = kp.end
314 if end is None: 314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true
315 end = val
316 if end is not None: 316 ↛ 318line 316 didn't jump to line 318, because the condition on line 316 was never false
317 val = kp.interpolated_value(t, end)
318 return val, end, kp, t
319 return val, None, None, None
320 if k.end is not None:
321 val = k.end
322 return val, None, None, None
324 def to_dict(self):
325 d = super().to_dict()
326 if self.animated:
327 if "k" not in d: 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true
328 return d
329 last = d["k"][-1]
330 last.pop("i", None)
331 last.pop("o", None)
332 return d
334 def __repr__(self):
335 if self.keyframes and len(self.keyframes) > 1:
336 val = "%s -> %s" % (self.keyframes[0].start, self.keyframes[-2].end)
337 else:
338 val = self.value
339 return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val)
341 def __str__(self):
342 if self.animated:
343 return "animated"
344 return str(self.value)
346 @classmethod
347 def merge_keyframes(cls, items, conversion):
348 """
349 @todo Remove similar functionality from SVG/sif parsers
350 """
351 keyframes = []
352 for animatable in items:
353 if animatable.animated:
354 keyframes.extend(animatable.keyframes)
356 # TODO properly interpolate tangents
357 new_kframes = []
358 for keyframe in sorted(keyframes, key=lambda kf: kf.time):
359 if new_kframes and new_kframes[-1].time == keyframe.time:
360 continue
361 kfcopy = keyframe.clone()
362 kfcopy.start = conversion(*(i.get_value(keyframe.time) for i in items))
363 new_kframes.append(kfcopy)
365 for i in range(0, len(new_kframes) - 1):
366 new_kframes[i].end = new_kframes[i+1].start
368 return new_kframes
370 @classmethod
371 def load(cls, lottiedict):
372 obj = super().load(lottiedict)
373 if "a" not in lottiedict: 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true
374 obj.animated = prop_animated(lottiedict)
375 return obj
377 def average(self, start, end, value_map=lambda v: v): 377 ↛ exitline 377 didn't run the lambda on line 377
378 if not self.animated or len(self.keyframes) == 0:
379 return value_map(self.value)
380 elif len(self.keyframes) == 1:
381 return value_map(self.keyframes[0].start)
383 total_weight = 0
384 value = value_map(self.keyframes[0].start) * 0
385 kf_index = 0
387 if self.keyframes[0].time > start:
388 weight = self.keyframes[0].time - start
389 value += value_map(self.keyframes[0].start) * weight
390 total_weight += weight
391 elif self.keyframes[0].time < start:
392 avg = (self.keyframes[1].time + self.keyframes[0].time) / 2
393 kf_index = 1
394 if self.keyframes[0].hold:
395 weight = self.keyframes[1].time - start
396 value += value_map(self.keyframes[0].start) * weight
397 total_weight += weight
398 elif start < avg:
399 weight = avg - start
400 value += value_map(self.keyframes[0].start) * weight
401 total_weight += weight
403 half = (self.keyframes[1].time - self.keyframes[0].time) / 2
404 value += value_map(self.keyframes[1].start) * half
405 total_weight += half
406 else:
407 weight = self.keyframes[1].time - start
408 value += value_map(self.keyframes[1].start) * weight
409 total_weight += weight
411 for i in range(kf_index, len(self.keyframes) - 1):
412 kf = self.keyframes[i]
413 kfn = self.keyframes[i + 1]
414 if kfn.time > end:
415 break
417 delta = kfn.time - kf.time
418 total_weight += delta
419 if kf.hold:
420 value += value_map(kf.start) * delta
421 else:
422 value += value_map(kf.start) * (delta / 2)
423 value += value_map(kfn.start) * (delta / 2)
424 else:
425 kf = self.keyframes[-1]
426 kfn = self.keyframes[-1]
428 if kfn.time < end:
429 weight = end - kfn.time
430 value += value_map(kfn.start) * weight
431 total_weight += weight
432 elif kf.hold:
433 weight = end - kf.time
434 value += value_map(kf.start) * weight
435 total_weight += weight
436 elif kfn.time > end:
437 avg = (kf.time + kfn.time) / 2
438 if end < avg:
439 weight = end - kf.time
440 value += value_map(kf.start) * weight
441 total_weight += weight
442 else:
443 half = (kfn.time - kf.time) / 2
444 value += value_map(kf.start) * half
445 total_weight += half
447 weight = end - avg
448 value += value_map(kfn.start) * weight
449 total_weight += weight
451 if total_weight == 0:
452 return value
453 return value / total_weight
456def prop_animated(l):
457 if "a" in l: 457 ↛ 459line 457 didn't jump to line 459, because the condition on line 457 was never false
458 return l["a"]
459 if "k" not in l:
460 return False
461 if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict):
462 return True
463 return False
466def prop_not_animated(l):
467 return not prop_animated(l)
470## @ingroup Lottie
471class MultiDimensional(AnimatableMixin, LottieObject):
472 """!
473 An animatable property that holds a NVector
474 """
475 keyframe_type = OffsetKeyframe
476 _props = [
477 LottieProp("value", "k", NVector, False, prop_not_animated),
478 LottieProp("property_index", "ix", int, False),
479 LottieProp("animated", "a", PseudoBool, False),
480 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
481 LottieProp("expression", "x", str, False),
482 ]
484 def get_tangent_angle(self, time=0):
485 """!
486 @brief Returns the value tangent angle of the property at the given frame/time
487 """
488 if not self.keyframes or len(self.keyframes) < 2:
489 return 0
491 val, end, kp, t = self._get_value_helper(time)
492 if kp:
493 return kp.interpolated_tangent_angle(t, end)
495 if self.keyframes[0].time >= time:
496 end = self.keyframes[0].end if self.keyframes[0].end is not None else self.keyframes[1].start
497 return self.keyframes[0].interpolated_tangent_angle(0, end)
499 return 0
502## @ingroup Lottie
503class PositionKeyframe(OffsetKeyframe):
504 """!
505 Keyframe for Positional values
506 """
507 _props = [
508 LottieProp("in_tan", "ti", NVector, False),
509 LottieProp("out_tan", "to", NVector, False),
510 ]
513class PositionValue(MultiDimensional):
514 keyframe_type = PositionKeyframe
515 _props = [
516 LottieProp("value", "k", NVector, False, prop_not_animated),
517 LottieProp("property_index", "ix", int, False),
518 LottieProp("animated", "a", PseudoBool, False),
519 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
520 ]
522 @classmethod
523 def load(cls, lottiedict):
524 obj = super().load(lottiedict)
525 if lottiedict.get("s", False): 525 ↛ 526line 525 didn't jump to line 526, because the condition on line 525 was never true
526 cls._load_split(lottiedict, obj)
528 return obj
530 @classmethod
531 def _load_split(cls, lottiedict, obj):
532 components = [
533 Value.load(lottiedict.get("x", {})),
534 Value.load(lottiedict.get("y", {})),
535 ]
536 if "z" in lottiedict:
537 components.append(Value.load(lottiedict.get("z", {})))
539 has_anim = any(x for x in components if x.animated)
540 if not has_anim:
541 obj.value = NVector(*(a.value for a in components))
542 obj.animated = False
543 obj.keyframes = None
544 return
546 obj.animated = True
547 obj.value = None
548 obj.keyframes = cls.merge_keyframes(components, NVector)
551class ColorValue(AnimatableMixin, LottieObject):
552 """!
553 An animatable property that holds a Color
554 """
555 keyframe_type = OffsetKeyframe
556 _props = [
557 LottieProp("value", "k", Color, False, prop_not_animated),
558 LottieProp("property_index", "ix", int, False),
559 LottieProp("animated", "a", PseudoBool, False),
560 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
561 LottieProp("expression", "x", str, False),
562 ]
565## @ingroup Lottie
566class GradientColors(LottieObject):
567 """!
568 Represents colors and offsets in a gradient
570 Colors are represented as a flat list interleaving offsets and color components in weird ways
571 There are two possible layouts:
573 Without alpha, the colors are a sequence of offset, r, g, b
575 With alpha, same as above but at the end of the list there is a sequence of offset, alpha
577 Examples:
579 For the gradient [0, red], [0.5, yellow], [1, green]
580 The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0]
582 For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity]
583 The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0, 0, 0.8, 0.5, 0.7, 1, 0.6]
584 """
585 _props = [
586 LottieProp("colors", "k", MultiDimensional),
587 LottieProp("count", "p", int),
588 ]
590 def __init__(self, stops=[]):
591 ## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array
592 self.colors = MultiDimensional(NVector())
593 ## Number of colors
594 self.count = 0
595 if stops:
596 self.set_stops(stops)
598 @staticmethod
599 def color_to_stops(self, colors):
600 """
601 Converts a list of colors (Color) to tuples (offset, color)
602 """
603 return [
604 (i / (len(colors)-1), color)
605 for i, color in enumerate(colors)
606 ]
608 def set_stops(self, stops, keyframe=None):
609 """!
610 @param stops iterable of (offset, Color) tuples
611 @param keyframe keyframe index (or None if not animated)
612 """
613 flat = self._flatten_stops(stops)
614 if self.colors.animated and keyframe is not None:
615 if keyframe > 1:
616 self.colors.keyframes[keyframe-1].end = flat
617 self.colors.keyframes[keyframe].start = flat
618 else:
619 self.colors.clear_animation(flat)
620 self.count = len(stops)
622 def _flatten_stops(self, stops):
623 flattened_colors = NVector(*reduce(
624 lambda a, b: a + b,
625 (
626 [off] + color.components[:3]
627 for off, color in stops
628 )
629 ))
631 if any(len(c) > 3 for o, c in stops):
632 flattened_colors.components += reduce(
633 lambda a, b: a + b,
634 (
635 [off] + [self._get_alpha(color)]
636 for off, color in stops
637 )
638 )
639 return flattened_colors
641 def _get_alpha(self, color):
642 if len(color) > 3:
643 return color[3]
644 return 1
646 def _add_to_flattened(self, offset, color, flattened):
647 flat = [offset] + list(color[:3])
648 rgb_size = 4 * self.count
650 if len(flattened) == rgb_size:
651 # No alpha
652 flattened.extend(flat)
653 if self.count == 0 and len(color) > 3:
654 flattened.append(offset)
655 flattened.append(color[3])
656 else:
657 flattened[rgb_size:rgb_size] = flat
658 flattened.append(offset)
659 flattened.append(self._get_alpha(color))
661 def add_color(self, offset, color, keyframe=None):
662 if self.colors.animated:
663 if keyframe is None:
664 for kf in self.colors.keyframes:
665 if kf.start:
666 self._add_to_flattened(offset, color, kf.start.components)
667 if kf.end:
668 self._add_to_flattened(offset, color, kf.end.components)
669 else:
670 if keyframe > 1:
671 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe-1].end.components)
672 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe].start.components)
673 else:
674 self._add_to_flattened(offset, color, self.colors.value.components)
675 self.count += 1
677 def add_keyframe(self, time, stops, ease=easing.Linear()):
678 """!
679 @param time Frame time
680 @param stops Iterable of (offset, Color) tuples
681 @param ease Easing function
682 """
683 self.colors.add_keyframe(time, self._flatten_stops(stops), ease)
685 def get_stops(self, keyframe=0):
686 if keyframe is not None:
687 colors = self.colors.keyframes[keyframe].start
688 else:
689 colors = self.colors.value
690 return self._stops_from_flat(colors)
692 def _stops_from_flat(self, colors):
693 if len(colors) == 4 * self.count:
694 for i in range(self.count):
695 off = i * 4
696 yield colors[off], Color(*colors[off+1:off+4])
697 else:
698 for i in range(self.count):
699 off = i * 4
700 aoff = self.count * 4 + i * 2 + 1
701 yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff])
703 def stops_at(self, time):
704 return self._stops_from_flat(self.colors.get_value(time))
707## @ingroup Lottie
708class Value(AnimatableMixin, LottieObject):
709 """!
710 An animatable property that holds a float
711 """
712 keyframe_type = OffsetKeyframe
713 _props = [
714 LottieProp("value", "k", float, False, prop_not_animated),
715 LottieProp("property_index", "ix", int, False),
716 LottieProp("animated", "a", PseudoBool, False),
717 LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
718 LottieProp("expression", "x", str, False),
719 ]
721 def __init__(self, value=0):
722 super().__init__(value)
724 def add_keyframe(self, time, value, ease=easing.Linear()):
725 super().add_keyframe(time, NVector(value), ease)
727 def get_value(self, time=0):
728 v = super().get_value(time)
729 if self.animated and self.keyframes: 729 ↛ 730line 729 didn't jump to line 730, because the condition on line 729 was never true
730 return v[0]
731 return v
734## @ingroup Lottie
735class ShapePropKeyframe(Keyframe):
736 """!
737 Keyframe holding Bezier objects
738 """
739 _props = [
740 LottieProp("start", "s", Bezier, PseudoList),
741 LottieProp("end", "e", Bezier, PseudoList),
742 ]
744 def __init__(self, time=0, start=None, end=None, easing_function=None):
745 Keyframe.__init__(self, time, easing_function)
746 ## Start value of keyframe segment.
747 self.start = start
748 ## End value of keyframe segment.
749 self.end = end
751 def interpolated_value(self, ratio, next_start=None):
752 end = next_start if self.end is None else self.end
753 if end is None:
754 return self.start
755 if not self.in_value or not self.out_value:
756 return self.start
757 if ratio == 1:
758 return end
759 if ratio == 0 or len(self.start.vertices) != len(end.vertices):
760 return self.start
762 lerpv = self.lerp_factor(ratio)
763 bez = Bezier()
764 bez.closed = self.start.closed
765 for i in range(len(self.start.vertices)):
766 bez.vertices.append(self.start.vertices[i].lerp(end.vertices[i], lerpv))
767 bez.in_tangents.append(self.start.in_tangents[i].lerp(end.in_tangents[i], lerpv))
768 bez.out_tangents.append(self.start.out_tangents[i].lerp(end.out_tangents[i], lerpv))
769 return bez
772## @ingroup Lottie
773class ShapeProperty(AnimatableMixin, LottieObject):
774 """!
775 An animatable property that holds a Bezier
776 """
777 keyframe_type = ShapePropKeyframe
778 _props = [
779 LottieProp("value", "k", Bezier, False, prop_not_animated),
780 LottieProp("expression", "x", str, False),
781 LottieProp("property_index", "ix", float, False),
782 LottieProp("animated", "a", PseudoBool, False),
783 LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
784 ]
786 def __init__(self, bezier=None):
787 super().__init__(bezier or Bezier())
790#ingroup Lottie
791class SplitVector(LottieObject):
792 """!
793 An animatable property that is split into individually anaimated components
794 """
795 _props = [
796 LottieProp("split", "s", bool, False),
797 LottieProp("x", "x", Value, False),
798 LottieProp("y", "y", Value, False),
799 LottieProp("z", "z", Value, False),
800 ]
802 @property
803 def split(self):
804 return True
806 def __init__(self, x=0, y=0):
807 super().__init__()
809 self.x = Value(x)
810 self.y = Value(y)
811 self.z = None