python-lottie  0.7.0+dev66cafb9
A framework to work with lottie files and telegram animated stickers (tgs)
properties.py
Go to the documentation of this file.
1 import math
2 from functools import reduce
3 from .base import LottieObject, LottieProp, PseudoList, PseudoBool
4 from . import easing
5 from ..nvector import NVector
6 from .bezier import Bezier
7 from ..utils.color import Color
8 
9 
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)
17 
18  def __init__(self, h1, h2):
19  self.h1h1 = h1
20  self.h2h2 = h2
21  self._sample_values_sample_values = None
22 
23  @classmethod
24  def from_keyframe(cls, keyframe):
25  return cls(keyframe.out_value, keyframe.in_value)
26 
27  def bezier(self):
28  bez = Bezier()
29  bez.add_point(NVector(0, 0), outp=NVector(self.h1h1.x, self.h1h1.y))
30  bez.add_point(NVector(1, 1), inp=NVector(self.h2h2.x-1, self.h2h2.y-1))
31  return bez
32 
33  def _a(self, c1, c2):
34  return 1 - 3 * c2 + 3 * c1
35 
36  def _b(self, c1, c2):
37  return 3 * c2 - 6 * c1
38 
39  def _c(self, c1):
40  return 3 * c1
41 
42  def _bezier_component(self, t, c1, c2):
43  return ((self._a_a(c1, c2) * t + self._b_b(c1, c2)) * t + self._c_c(c1)) * t
44 
45  def point_at(self, t):
46  return NVector(
47  self._bezier_component_bezier_component(t, self.h1h1.x, self.h2h2.x),
48  self._bezier_component_bezier_component(t, self.h1h1.y, self.h2h2.y)
49  )
50 
51  def _slope_component(self, t, c1, c2):
52  return 3 * self._a_a(c1, c2) * t * t + 2 * self._b_b(c1, c2) * t + self._c_c(c1)
53 
54  def slope_at(self, t):
55  return NVector(
56  self._slope_component_slope_component(t, self.h1h1.x, self.h2h2.x),
57  self._slope_component_slope_component(t, self.h1h1.y, self.h2h2.y)
58  )
59 
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_ITERATIONSSUBDIVISION_MAX_ITERATIONS):
65  if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISIONSUBDIVISION_PRECISION:
66  break
67  t = interval_start + (interval_end - interval_start) / 2.0
68  current_x = self._bezier_component_bezier_component(t, self.h1h1.x, self.h2h2.x) - x
69  if current_x > 0.0:
70  interval_end = t
71  else:
72  interval_start = t
73  return t
74 
75  def _newton_raphson(self, x, t_guess):
76  for i in range(self.NEWTON_ITERATIONSNEWTON_ITERATIONS):
77  slope = self._slope_component_slope_component(t_guess, self.h1h1.x, self.h2h2.x)
78  if slope == 0:
79  return t_guess
80  current_x = self._bezier_component_bezier_component(t_guess, self.h1h1.x, self.h2h2.x) - x
81  t_guess -= current_x / slope
82  return t_guess
83 
84  def _get_sample_values(self):
85  if self._sample_values_sample_values is None:
86  self._sample_values_sample_values = [
87  self._bezier_component_bezier_component(i * self.SAMPLE_STEP_SIZESAMPLE_STEP_SIZE, self.h1h1.x, self.h2h2.x)
88  for i in range(self.SPLINE_TABLE_SIZESPLINE_TABLE_SIZE)
89  ]
90  return self._sample_values_sample_values
91 
92  def t_for_x(self, x):
93  sample_values = self._get_sample_values_get_sample_values()
94  interval_start = 0
95  current_sample = 1
96  last_sample = self.SPLINE_TABLE_SIZESPLINE_TABLE_SIZE - 1
97  while current_sample != last_sample and sample_values[current_sample] <= x:
98  interval_start += self.SAMPLE_STEP_SIZESAMPLE_STEP_SIZE
99  current_sample += 1
100  current_sample -= 1
101 
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_SIZESAMPLE_STEP_SIZE
104  initial_slope = self._slope_component_slope_component(t_guess, self.h1h1.x, self.h2h2.x)
105  if initial_slope >= self.NEWTON_MIN_SLOPENEWTON_MIN_SLOPE:
106  return self._newton_raphson_newton_raphson(x, t_guess)
107  if initial_slope == 0:
108  return t_guess
109  return self._binary_subdivide_binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZESAMPLE_STEP_SIZE)
110 
111  def y_at_x(self, x):
112  t = self.t_for_xt_for_x(x)
113  return self._bezier_component_bezier_component(t, self.h1h1.y, self.h2h2.y)
114 
115 
116 ## @ingroup Lottie
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  ]
124 
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.timetime = time
132  ## Bezier curve easing in value.
133  self.in_valuein_value = None
134  ## Bezier curve easing out value.
135  self.out_valueout_value = None
136  ## Jump to the end value
137  self.holdhold = None
138 
139  if easing_function:
140  easing_function(self)
141 
142  @property
143  def jump(self):
144  return self.holdhold
145 
146  @jump.setter
147  def jump(self, v):
148  self.holdhold = v
149 
150  def bezier(self):
151  if self.holdhold:
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()
159 
160  def lerp_factor(self, ratio):
161  return KeyframeBezier.from_keyframe(self).y_at_x(ratio)
162 
163  def __str__(self):
164  return "%s %s" % (self.timetime, self.value)
165 
166 
167 ## @ingroup Lottie
169  """!
170  Keyframe for MultiDimensional values
171 
172  @par Bezier easing
173  @parblock
174  Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1).
175 
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.
178 
179  See also https://cubic-bezier.com/
180  @endparblock
181  """
182  _props = [
183  LottieProp("value", "s", NVector, False),
184  LottieProp("end", "e", NVector, False),
185  ]
186 
187  def __init__(self, time=0, value=None, easing_function=None, end=None):
188  Keyframe.__init__(self, time, easing_function)
189  ## Start value of keyframe segment.
190  self.valuevalue = value
191  ## End value of keyframe segment.
192  self.endend = end
193 
194  @property
195  def start(self):
196  return self.valuevalue
197 
198  @start.setter
199  def start(self, v):
200  self.valuevalue = v
201  return v
202 
203  def interpolated_value(self, ratio, next_value=None):
204  end = next_value if self.endend is None else self.endend
205  if end is None:
206  return self.valuevalue
207  if not self.in_valuein_value or not self.out_valueout_value:
208  return self.valuevalue
209  if ratio == 1:
210  return end
211  if ratio == 0:
212  return self.valuevalue
213  if isinstance(self, PositionKeyframe) and self.in_tan and self.out_tan:
214  bezier = Bezier()
215  bezier.add_point(self.valuevalue, NVector(0, 0), self.out_tan)
216  bezier.add_point(end, self.in_tan, NVector(0, 0))
217  return bezier.point_at(ratio)
218 
219  lerpv = self.lerp_factorlerp_factor(ratio)
220  return self.valuevalue.lerp(end, lerpv)
221 
222  def interpolated_tangent_angle(self, ratio, next_value=None):
223  end = next_value if self.endend is None else self.endend
224  if end is None or not self.in_tan or not self.out_tan:
225  return 0
226 
227  bezier = Bezier()
228  bezier.add_point(self.valuevalue, NVector(0, 0), self.out_tan)
229  bezier.add_point(end, self.in_tan, NVector(0, 0))
230  return bezier.tangent_angle_at(ratio)
231 
232  def __repr__(self):
233  return "<%s.%s %s %s%s>" % (
234  type(self).__module__,
235  type(self).__name__,
236  self.timetime,
237  self.valuevalue,
238  (" -> %s" % self.endend) if self.endend is not None else ""
239  )
240 
241 
243  keyframe_type = Keyframe
244 
245  def __init__(self, value=None):
246  ## Non-animated value
247  self.valuevalue = value
248  ## Property index
249  self.property_indexproperty_index = None
250  ## Whether it's animated
251  self.animatedanimated = False
252  ## Keyframe list
253  self.keyframeskeyframes = None
254  ## Expression
255  self.expressionexpression = None
256  ## Slot name
257  self.slot_idslot_id = None
258 
259  def clear_animation(self, value):
260  """!
261  Sets a fixed value, removing animated keyframes
262  """
263  self.valuevalue = value
264  self.animatedanimated = False
265  self.keyframeskeyframes = None
266 
267  def add_keyframe(self, time, value, interp=easing.Linear(), *args, end=None, **kwargs):
268  """!
269  @param time The time this keyframe appears in
270  @param value The value the property should have at @p time
271  @param interp The easing callable used to update the tangents of the previous keyframe
272  @param args Extra arguments to pass the keyframe constructor
273  @param kwargs Extra arguments to pass the keyframe constructor
274  @note Always call add_keyframe with increasing @p time value
275  """
276  if not self.animatedanimated:
277  self.valuevalue = None
278  self.keyframeskeyframes = []
279  self.animatedanimated = True
280  else:
281  if self.keyframeskeyframes[-1].time == time:
282  if value != self.keyframeskeyframes[-1].value:
283  self.keyframeskeyframes[-1].value = value
284  return
285  elif end:
286  self.keyframeskeyframes[-1].end = value.clone()
287 
288  kf = self.keyframe_typekeyframe_type(
289  time=time,
290  value=value,
291  easing_function=interp,
292  end=end,
293  *args,
294  **kwargs
295  )
296  self.keyframeskeyframes.append(kf)
297  return kf
298 
299  def get_value(self, time=0):
300  """!
301  @brief Returns the value of the property at the given frame/time
302  """
303  if not self.animatedanimated:
304  return self.valuevalue
305 
306  if not self.keyframeskeyframes:
307  return None
308 
309  return self._get_value_helper_get_value_helper(time)[0]
310 
311  def _get_value_helper(self, time):
312  val = self.keyframeskeyframes[0].value
313  for i in range(len(self.keyframeskeyframes)):
314  k = self.keyframeskeyframes[i]
315  if time - k.time <= 0:
316  if k.value is not None:
317  val = k.value
318 
319  kp = self.keyframeskeyframes[i-1] if i > 0 else None
320  if kp:
321  t = (time - kp.time) / (k.time - kp.time)
322  end = kp.end
323  if end is None:
324  end = val
325  if end is not None:
326  val = kp.interpolated_value(t, end)
327  return val, end, kp, t
328  return val, None, None, None
329  if k.end is not None:
330  val = k.end
331  elif k.value is not None:
332  val = k.value
333  return val, None, None, None
334 
335  def to_dict(self):
336  d = super().to_dict()
337  if self.animatedanimated:
338  if "k" not in d:
339  return d
340  last = d["k"][-1]
341  last.pop("i", None)
342  last.pop("o", None)
343  return d
344 
345  def __repr__(self):
346  if self.keyframeskeyframes and len(self.keyframeskeyframes) > 1:
347  val = "%s -> %s" % (self.keyframeskeyframes[0].value, self.keyframeskeyframes[-2].end)
348  else:
349  val = self.valuevalue
350  return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val)
351 
352  def __str__(self):
353  if self.animatedanimated:
354  return "animated"
355  return str(self.valuevalue)
356 
357  @classmethod
358  def merge_keyframes(cls, items, conversion):
359  """
360  @todo Remove similar functionality from SVG/sif parsers
361  """
362  keyframes = []
363  for animatable in items:
364  if animatable.animated:
365  keyframes.extend(animatable.keyframes)
366 
367  # TODO properly interpolate tangents
368  new_kframes = []
369  for keyframe in sorted(keyframes, key=lambda kf: kf.time):
370  if new_kframes and new_kframes[-1].time == keyframe.time:
371  continue
372  kfcopy = keyframe.clone()
373  kfcopy.value = conversion(*(i.get_value(keyframe.time) for i in items))
374  new_kframes.append(kfcopy)
375 
376  for i in range(0, len(new_kframes) - 1):
377  new_kframes[i].end = new_kframes[i+1].value
378 
379  return new_kframes
380 
381  @classmethod
382  def load(cls, lottiedict):
383  obj = super().load(lottiedict)
384  if "a" not in lottiedict:
385  obj.animated = prop_animated(lottiedict)
386  return obj
387 
388  def average(self, value, end, value_map=lambda v: v):
389  if not self.animatedanimated or len(self.keyframeskeyframes) == 0:
390  return value_map(self.valuevalue)
391  elif len(self.keyframeskeyframes) == 1:
392  return value_map(self.keyframeskeyframes[0].value)
393 
394  total_weight = 0
395  value = value_map(self.keyframeskeyframes[0].value) * 0
396  kf_index = 0
397 
398  if self.keyframeskeyframes[0].time > value:
399  weight = self.keyframeskeyframes[0].time - value
400  value += value_map(self.keyframeskeyframes[0].value) * weight
401  total_weight += weight
402  elif self.keyframeskeyframes[0].time < value:
403  avg = (self.keyframeskeyframes[1].time + self.keyframeskeyframes[0].time) / 2
404  kf_index = 1
405  if self.keyframeskeyframes[0].hold:
406  weight = self.keyframeskeyframes[1].time - value
407  value += value_map(self.keyframeskeyframes[0].value) * weight
408  total_weight += weight
409  elif value < avg:
410  weight = avg - value
411  value += value_map(self.keyframeskeyframes[0].value) * weight
412  total_weight += weight
413 
414  half = (self.keyframeskeyframes[1].time - self.keyframeskeyframes[0].time) / 2
415  value += value_map(self.keyframeskeyframes[1].value) * half
416  total_weight += half
417  else:
418  weight = self.keyframeskeyframes[1].time - value
419  value += value_map(self.keyframeskeyframes[1].value) * weight
420  total_weight += weight
421 
422  for i in range(kf_index, len(self.keyframeskeyframes) - 1):
423  kf = self.keyframeskeyframes[i]
424  kfn = self.keyframeskeyframes[i + 1]
425  if kfn.time > end:
426  break
427 
428  delta = kfn.time - kf.time
429  total_weight += delta
430  if kf.hold:
431  value += value_map(kf.value) * delta
432  else:
433  value += value_map(kf.value) * (delta / 2)
434  value += value_map(kfn.value) * (delta / 2)
435  else:
436  kf = self.keyframeskeyframes[-1]
437  kfn = self.keyframeskeyframes[-1]
438 
439  if kfn.time < end:
440  weight = end - kfn.time
441  value += value_map(kfn.value) * weight
442  total_weight += weight
443  elif kf.hold:
444  weight = end - kf.time
445  value += value_map(kf.value) * weight
446  total_weight += weight
447  elif kfn.time > end:
448  avg = (kf.time + kfn.time) / 2
449  if end < avg:
450  weight = end - kf.time
451  value += value_map(kf.value) * weight
452  total_weight += weight
453  else:
454  half = (kfn.time - kf.time) / 2
455  value += value_map(kf.value) * half
456  total_weight += half
457 
458  weight = end - avg
459  value += value_map(kfn.value) * weight
460  total_weight += weight
461 
462  if total_weight == 0:
463  return value
464  return value / total_weight
465 
466 
468  if "a" in l:
469  return l["a"]
470  if "k" not in l:
471  return False
472  if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict):
473  return True
474  return False
475 
476 
478  return not prop_animated(l)
479 
480 
481 ## @ingroup Lottie
483  """!
484  An animatable property that holds a NVector
485  """
486  keyframe_type = OffsetKeyframe
487  _props = [
488  LottieProp("value", "k", NVector, False, prop_not_animated),
489  LottieProp("property_index", "ix", int, False),
490  LottieProp("animated", "a", PseudoBool, False),
491  LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
492  LottieProp("expression", "x", str, False),
493  LottieProp("slot_id", "sid", str, False),
494  ]
495 
496  def get_tangent_angle(self, time=0):
497  """!
498  @brief Returns the value tangent angle of the property at the given frame/time
499  """
500  if not self.keyframeskeyframes or len(self.keyframeskeyframes) < 2:
501  return 0
502 
503  val, end, kp, t = self._get_value_helper_get_value_helper(time)
504  if kp:
505  return kp.interpolated_tangent_angle(t, end)
506 
507  if self.keyframeskeyframes[0].time >= time:
508  end = self.keyframeskeyframes[0].end if self.keyframeskeyframes[0].end is not None else self.keyframeskeyframes[1].value
509  return self.keyframeskeyframes[0].interpolated_tangent_angle(0, end)
510 
511  return 0
512 
513 
514 ## @ingroup Lottie
516  """!
517  Keyframe for Positional values
518  """
519  _props = [
520  LottieProp("in_tan", "ti", NVector, False),
521  LottieProp("out_tan", "to", NVector, False),
522  ]
523 
524  def __init__(self, time=0, value=None, easing_function=None, in_tan=None, out_tan=None, end=None):
525  super().__init__(time, value, easing_function, end)
526  ## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
527  self.in_tanin_tan = in_tan
528  ## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
529  self.out_tanout_tan = out_tan
530 
531 
533  keyframe_type = PositionKeyframe
534  _props = [
535  LottieProp("value", "k", NVector, False, prop_not_animated),
536  LottieProp("property_index", "ix", int, False),
537  LottieProp("animated", "a", PseudoBool, False),
538  LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
539  ]
540 
541  @classmethod
542  def load(cls, lottiedict):
543  obj = super().load(lottiedict)
544  if lottiedict.get("s", False):
545  cls._load_split_load_split(lottiedict, obj)
546 
547  return obj
548 
549  @classmethod
550  def _load_split(cls, lottiedict, obj):
551  components = [
552  Value.load(lottiedict.get("x", {})),
553  Value.load(lottiedict.get("y", {})),
554  ]
555  if "z" in lottiedict:
556  components.append(Value.load(lottiedict.get("z", {})))
557 
558  has_anim = any(x for x in components if x.animated)
559  if not has_anim:
560  obj.value = NVector(*(a.value for a in components))
561  obj.animated = False
562  obj.keyframes = None
563  return
564 
565  obj.animated = True
566  obj.value = None
567  obj.keyframes = cls.merge_keyframesmerge_keyframes(components, NVector)
568 
569 
571  """!
572  An animatable property that holds a Color
573  """
574  keyframe_type = OffsetKeyframe
575  _props = [
576  LottieProp("value", "k", Color, False, prop_not_animated),
577  LottieProp("property_index", "ix", int, False),
578  LottieProp("animated", "a", PseudoBool, False),
579  LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
580  LottieProp("expression", "x", str, False),
581  LottieProp("slot_id", "sid", str, False),
582  ]
583 
584 
585 ## @ingroup Lottie
587  """!
588  Represents colors and offsets in a gradient
589 
590  Colors are represented as a flat list interleaving offsets and color components in weird ways
591  There are two possible layouts:
592 
593  Without alpha, the colors are a sequence of offset, r, g, b
594 
595  With alpha, same as above but at the end of the list there is a sequence of offset, alpha
596 
597  Examples:
598 
599  For the gradient [0, red], [0.5, yellow], [1, green]
600  The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0]
601 
602  For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity]
603  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]
604  """
605  _props = [
606  LottieProp("colors", "k", MultiDimensional),
607  LottieProp("count", "p", int),
608  ]
609 
610  def __init__(self, stops=[]):
611  ## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array
613  ## Number of colors
614  self.countcount = 0
615  if stops:
616  self.set_stopsset_stops(stops)
617 
618  @staticmethod
619  def color_to_stops(self, colors):
620  """
621  Converts a list of colors (Color) to tuples (offset, color)
622  """
623  return [
624  (i / (len(colors)-1), color)
625  for i, color in enumerate(colors)
626  ]
627 
628  def set_stops(self, stops, keyframe=None):
629  """!
630  @param stops iterable of (offset, Color) tuples
631  @param keyframe keyframe index (or None if not animated)
632  """
633  flat = self._flatten_stops_flatten_stops(stops)
634  if self.colorscolors.animated and keyframe is not None:
635  if keyframe > 1:
636  self.colorscolors.keyframes[keyframe-1].end = flat
637  self.colorscolors.keyframes[keyframe].value = flat
638  else:
639  self.colorscolors.clear_animation(flat)
640  self.countcount = len(stops)
641 
642  def _flatten_stops(self, stops):
643  flattened_colors = NVector(*reduce(
644  lambda a, b: a + b,
645  (
646  [off] + color.components[:3]
647  for off, color in stops
648  )
649  ))
650 
651  if any(len(c) > 3 for o, c in stops):
652  flattened_colors.components += reduce(
653  lambda a, b: a + b,
654  (
655  [off] + [self._get_alpha_get_alpha(color)]
656  for off, color in stops
657  )
658  )
659  return flattened_colors
660 
661  def _get_alpha(self, color):
662  if len(color) > 3:
663  return color[3]
664  return 1
665 
666  def _add_to_flattened(self, offset, color, flattened):
667  flat = [offset] + list(color[:3])
668  rgb_size = 4 * self.countcount
669 
670  if len(flattened) == rgb_size:
671  # No alpha
672  flattened.extend(flat)
673  if self.countcount == 0 and len(color) > 3:
674  flattened.append(offset)
675  flattened.append(color[3])
676  else:
677  flattened[rgb_size:rgb_size] = flat
678  flattened.append(offset)
679  flattened.append(self._get_alpha_get_alpha(color))
680 
681  def add_color(self, offset, color, keyframe=None):
682  if self.colorscolors.animated:
683  if keyframe is None:
684  for kf in self.colorscolors.keyframes:
685  if kf.value:
686  self._add_to_flattened_add_to_flattened(offset, color, kf.value.components)
687  if kf.end:
688  self._add_to_flattened_add_to_flattened(offset, color, kf.end.components)
689  else:
690  if keyframe > 1:
691  self._add_to_flattened_add_to_flattened(offset, color, self.colorscolors.keyframes[keyframe-1].end.components)
692  self._add_to_flattened_add_to_flattened(offset, color, self.colorscolors.keyframes[keyframe].value.components)
693  else:
694  self._add_to_flattened_add_to_flattened(offset, color, self.colorscolors.value.components)
695  self.countcount += 1
696 
697  def add_keyframe(self, time, stops, ease=easing.Linear()):
698  """!
699  @param time Frame time
700  @param stops Iterable of (offset, Color) tuples
701  @param ease Easing function
702  """
703  self.colorscolors.add_keyframe(time, self._flatten_stops_flatten_stops(stops), ease)
704 
705  def get_stops(self, keyframe=0):
706  if keyframe is not None:
707  colors = self.colorscolors.keyframes[keyframe].value
708  else:
709  colors = self.colorscolors.value
710  return self._stops_from_flat_stops_from_flat(colors)
711 
712  def _stops_from_flat(self, colors):
713  if len(colors) == 4 * self.countcount:
714  for i in range(self.countcount):
715  off = i * 4
716  yield colors[off], Color(*colors[off+1:off+4])
717  else:
718  for i in range(self.countcount):
719  off = i * 4
720  aoff = self.countcount * 4 + i * 2 + 1
721  yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff])
722 
723  def stops_at(self, time):
724  return self._stops_from_flat_stops_from_flat(self.colorscolors.get_value(time))
725 
726 
727 ## @ingroup Lottie
729  """!
730  An animatable property that holds a float
731  """
732  keyframe_type = OffsetKeyframe
733  _props = [
734  LottieProp("value", "k", float, False, prop_not_animated),
735  LottieProp("property_index", "ix", int, False),
736  LottieProp("animated", "a", PseudoBool, False),
737  LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
738  LottieProp("expression", "x", str, False),
739  LottieProp("slot_id", "sid", str, False),
740  ]
741 
742  def __init__(self, value=0):
743  super().__init__(value)
744 
745  def add_keyframe(self, time, value, ease=easing.Linear()):
746  super().add_keyframe(time, NVector(value), ease)
747 
748  def get_value(self, time=0):
749  v = super().get_value(time)
750  if self.animatedanimated and self.keyframeskeyframes:
751  return v[0]
752  return v
753 
754 
755 ## @ingroup Lottie
757  """!
758  Keyframe holding Bezier objects
759  """
760  _props = [
761  LottieProp("value", "s", Bezier, PseudoList),
762  LottieProp("end", "e", Bezier, PseudoList),
763  ]
764 
765  def __init__(self, time=0, value=None, easing_function=None, end=None):
766  Keyframe.__init__(self, time, easing_function)
767  ## Start value of keyframe segment.
768  self.valuevalue = value
769  ## End value of keyframe segment.
770  self.endend = end
771 
772  @property
773  def start(self):
774  return self.valuevalue
775 
776  @start.setter
777  def start(self, v):
778  self.valuevalue = v
779  return v
780 
781  def interpolated_value(self, ratio, next_value=None):
782  end = next_value if self.endend is None else self.endend
783  if end is None:
784  return self.valuevalue
785  if not self.in_valuein_value or not self.out_valueout_value:
786  return self.valuevalue
787  if ratio == 1:
788  return end
789  if ratio == 0 or len(self.valuevalue.vertices) != len(end.vertices):
790  return self.valuevalue
791 
792  lerpv = self.lerp_factorlerp_factor(ratio)
793  bez = Bezier()
794  bez.closed = self.valuevalue.closed
795  for i in range(len(self.valuevalue.vertices)):
796  bez.vertices.append(self.valuevalue.vertices[i].lerp(end.vertices[i], lerpv))
797  bez.in_tangents.append(self.valuevalue.in_tangents[i].lerp(end.in_tangents[i], lerpv))
798  bez.out_tangents.append(self.valuevalue.out_tangents[i].lerp(end.out_tangents[i], lerpv))
799  return bez
800 
801 
802 ## @ingroup Lottie
804  """!
805  An animatable property that holds a Bezier
806  """
807  keyframe_type = ShapePropKeyframe
808  _props = [
809  LottieProp("value", "k", Bezier, False, prop_not_animated),
810  LottieProp("expression", "x", str, False),
811  LottieProp("property_index", "ix", float, False),
812  LottieProp("animated", "a", PseudoBool, False),
813  LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
814  LottieProp("slot_id", "sid", str, False),
815  ]
816 
817  def __init__(self, bezier=None):
818  super().__init__(bezier or Bezier())
819 
820 
821 #ingroup Lottie
823  """!
824  An animatable property that is split into individually anaimated components
825  """
826  _props = [
827  LottieProp("split", "s", bool, False),
828  LottieProp("x", "x", Value, False),
829  LottieProp("y", "y", Value, False),
830  LottieProp("z", "z", Value, False),
831  ]
832 
833  @property
834  def split(self):
835  return True
836 
837  def __init__(self, x=0, y=0):
838  super().__init__()
839 
840  self.xx = Value(x)
841  self.yy = Value(y)
842  self.zz = None
Base class for mapping Python classes into Lottie JSON objects.
Definition: base.py:230
Lottie <-> Python property mapper.
Definition: base.py:88
Single bezier curve.
Definition: bezier.py:123
Bezier handle for keyframe interpolation.
Definition: easing.py:7
def add_keyframe(self, time, value, interp=easing.Linear(), *args, end=None, **kwargs)
Definition: properties.py:267
def clear_animation(self, value)
Sets a fixed value, removing animated keyframes.
Definition: properties.py:259
def get_value(self, time=0)
Returns the value of the property at the given frame/time.
Definition: properties.py:299
def merge_keyframes(cls, items, conversion)
Definition: properties.py:358
def average(self, value, end, value_map=lambda v:v)
Definition: properties.py:388
An animatable property that holds a Color.
Definition: properties.py:570
Represents colors and offsets in a gradient.
Definition: properties.py:586
def add_keyframe(self, time, stops, ease=easing.Linear())
Definition: properties.py:697
def add_color(self, offset, color, keyframe=None)
Definition: properties.py:681
colors
Animatable colors, as a vector containing [offset, r, g, b] values as a flat array.
Definition: properties.py:612
def _add_to_flattened(self, offset, color, flattened)
Definition: properties.py:666
def set_stops(self, stops, keyframe=None)
Definition: properties.py:628
def _slope_component(self, t, c1, c2)
Definition: properties.py:51
def _binary_subdivide(self, x, interval_start, interval_end)
Definition: properties.py:60
def _newton_raphson(self, x, t_guess)
Definition: properties.py:75
def _bezier_component(self, t, c1, c2)
Definition: properties.py:42
time
Start time of keyframe segment.
Definition: properties.py:131
out_value
Bezier curve easing out value.
Definition: properties.py:135
hold
Jump to the end value.
Definition: properties.py:137
def __init__(self, time=0, easing_function=None)
Definition: properties.py:125
in_value
Bezier curve easing in value.
Definition: properties.py:133
An animatable property that holds a NVector.
Definition: properties.py:482
def get_tangent_angle(self, time=0)
Returns the value tangent angle of the property at the given frame/time.
Definition: properties.py:496
Keyframe for MultiDimensional values.
Definition: properties.py:168
value
Start value of keyframe segment.
Definition: properties.py:190
end
End value of keyframe segment.
Definition: properties.py:192
def interpolated_value(self, ratio, next_value=None)
Definition: properties.py:203
def interpolated_tangent_angle(self, ratio, next_value=None)
Definition: properties.py:222
def __init__(self, time=0, value=None, easing_function=None, end=None)
Definition: properties.py:187
Keyframe for Positional values.
Definition: properties.py:515
def __init__(self, time=0, value=None, easing_function=None, in_tan=None, out_tan=None, end=None)
Definition: properties.py:524
def load(cls, lottiedict)
Loads from a JSON object.
Definition: properties.py:542
def _load_split(cls, lottiedict, obj)
Definition: properties.py:550
Keyframe holding Bezier objects.
Definition: properties.py:756
def __init__(self, time=0, value=None, easing_function=None, end=None)
Definition: properties.py:765
end
End value of keyframe segment.
Definition: properties.py:770
def interpolated_value(self, ratio, next_value=None)
Definition: properties.py:781
value
Start value of keyframe segment.
Definition: properties.py:768
An animatable property that holds a Bezier.
Definition: properties.py:803
An animatable property that is split into individually anaimated components.
Definition: properties.py:822
An animatable property that holds a float.
Definition: properties.py:728
def __init__(self, value=0)
Definition: properties.py:742
def get_value(self, time=0)
Returns the value of the property at the given frame/time.
Definition: properties.py:748
def add_keyframe(self, time, value, ease=easing.Linear())
Definition: properties.py:745