File indexing completed on 2025-01-19 03:59:52
0001 import math 0002 from functools import reduce 0003 from .base import LottieObject, LottieProp, PseudoList, PseudoBool 0004 from . import easing 0005 from ..nvector import NVector 0006 from .bezier import Bezier 0007 from ..utils.color import Color 0008 0009 0010 class KeyframeBezier: 0011 NEWTON_ITERATIONS = 4 0012 NEWTON_MIN_SLOPE = 0.001 0013 SUBDIVISION_PRECISION = 0.0000001 0014 SUBDIVISION_MAX_ITERATIONS = 10 0015 SPLINE_TABLE_SIZE = 11 0016 SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0) 0017 0018 def __init__(self, h1, h2): 0019 self.h1 = h1 0020 self.h2 = h2 0021 self._sample_values = None 0022 0023 @classmethod 0024 def from_keyframe(cls, keyframe): 0025 return cls(keyframe.out_value, keyframe.in_value) 0026 0027 def bezier(self): 0028 bez = Bezier() 0029 bez.add_point(NVector(0, 0), outp=NVector(self.h1.x, self.h1.y)) 0030 bez.add_point(NVector(1, 1), inp=NVector(self.h2.x-1, self.h2.y-1)) 0031 return bez 0032 0033 def _a(self, c1, c2): 0034 return 1 - 3 * c2 + 3 * c1 0035 0036 def _b(self, c1, c2): 0037 return 3 * c2 - 6 * c1 0038 0039 def _c(self, c1): 0040 return 3 * c1 0041 0042 def _bezier_component(self, t, c1, c2): 0043 return ((self._a(c1, c2) * t + self._b(c1, c2)) * t + self._c(c1)) * t 0044 0045 def point_at(self, t): 0046 return NVector( 0047 self._bezier_component(t, self.h1.x, self.h2.x), 0048 self._bezier_component(t, self.h1.y, self.h2.y) 0049 ) 0050 0051 def _slope_component(self, t, c1, c2): 0052 return 3 * self._a(c1, c2) * t * t + 2 * self._b(c1, c2) * t + self._c(c1) 0053 0054 def slope_at(self, t): 0055 return NVector( 0056 self._slope_component(t, self.h1.x, self.h2.x), 0057 self._slope_component(t, self.h1.y, self.h2.y) 0058 ) 0059 0060 def _binary_subdivide(self, x, interval_start, interval_end): 0061 current_x = None 0062 t = None 0063 i = 0 0064 for i in range(self.SUBDIVISION_MAX_ITERATIONS): 0065 if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISION: 0066 break 0067 t = interval_start + (interval_end - interval_start) / 2.0 0068 current_x = self._bezier_component(t, self.h1.x, self.h2.x) - x 0069 if current_x > 0.0: 0070 interval_end = t 0071 else: 0072 interval_start = t 0073 return t 0074 0075 def _newton_raphson(self, x, t_guess): 0076 for i in range(self.NEWTON_ITERATIONS): 0077 slope = self._slope_component(t_guess, self.h1.x, self.h2.x) 0078 if slope == 0: 0079 return t_guess 0080 current_x = self._bezier_component(t_guess, self.h1.x, self.h2.x) - x 0081 t_guess -= current_x / slope 0082 return t_guess 0083 0084 def _get_sample_values(self): 0085 if self._sample_values is None: 0086 self._sample_values = [ 0087 self._bezier_component(i * self.SAMPLE_STEP_SIZE, self.h1.x, self.h2.x) 0088 for i in range(self.SPLINE_TABLE_SIZE) 0089 ] 0090 return self._sample_values 0091 0092 def t_for_x(self, x): 0093 sample_values = self._get_sample_values() 0094 interval_start = 0 0095 current_sample = 1 0096 last_sample = self.SPLINE_TABLE_SIZE - 1 0097 while current_sample != last_sample and sample_values[current_sample] <= x: 0098 interval_start += self.SAMPLE_STEP_SIZE 0099 current_sample += 1 0100 current_sample -= 1 0101 0102 dist = (x - sample_values[current_sample]) / (sample_values[current_sample+1] - sample_values[current_sample]) 0103 t_guess = interval_start + dist * self.SAMPLE_STEP_SIZE 0104 initial_slope = self._slope_component(t_guess, self.h1.x, self.h2.x) 0105 if initial_slope >= self.NEWTON_MIN_SLOPE: 0106 return self._newton_raphson(x, t_guess) 0107 if initial_slope == 0: 0108 return t_guess 0109 return self._binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZE) 0110 0111 def y_at_x(self, x): 0112 t = self.t_for_x(x) 0113 return self._bezier_component(t, self.h1.y, self.h2.y) 0114 0115 0116 ## @ingroup Lottie 0117 class Keyframe(LottieObject): 0118 _props = [ 0119 LottieProp("time", "t", float, False), 0120 LottieProp("in_value", "i", easing.KeyframeBezierHandle, False), 0121 LottieProp("out_value", "o", easing.KeyframeBezierHandle, False), 0122 LottieProp("jump", "h", PseudoBool), 0123 ] 0124 0125 def __init__(self, time=0, easing_function=None): 0126 """! 0127 @param time Start time of keyframe segment 0128 @param easing_function Callable that performs the easing 0129 """ 0130 ## Start time of keyframe segment. 0131 self.time = time 0132 ## Bezier curve easing in value. 0133 self.in_value = None 0134 ## Bezier curve easing out value. 0135 self.out_value = None 0136 ## Jump to the end value 0137 self.jump = None 0138 0139 if easing_function: 0140 easing_function(self) 0141 0142 def bezier(self): 0143 if self.jump: 0144 bez = Bezier() 0145 bez.add_point(NVector(0, 0)) 0146 bez.add_point(NVector(1, 0)) 0147 bez.add_point(NVector(1, 1)) 0148 return bez 0149 else: 0150 return KeyframeBezier.from_keyframe(self).bezier() 0151 0152 def lerp_factor(self, ratio): 0153 return KeyframeBezier.from_keyframe(self).y_at_x(ratio) 0154 0155 def __str__(self): 0156 return "%s %s" % (self.time, self.start) 0157 0158 0159 ## @ingroup Lottie 0160 class OffsetKeyframe(Keyframe): 0161 """! 0162 Keyframe for MultiDimensional values 0163 0164 @par Bezier easing 0165 @parblock 0166 Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1). 0167 0168 @p out_value and @p in_value are the other two handles for a quadratic bezier, 0169 expressed as absoulte values in this 0-1 space. 0170 0171 See also https://cubic-bezier.com/ 0172 @endparblock 0173 """ 0174 _props = [ 0175 LottieProp("start", "s", NVector, False), 0176 LottieProp("end", "e", NVector, False), 0177 LottieProp("in_tan", "ti", NVector, False), 0178 LottieProp("out_tan", "to", NVector, False), 0179 ] 0180 0181 def __init__(self, time=0, start=None, end=None, easing_function=None, in_tan=None, out_tan=None): 0182 Keyframe.__init__(self, time, easing_function) 0183 ## Start value of keyframe segment. 0184 self.start = start 0185 ## End value of keyframe segment. 0186 self.end = end 0187 ## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position) 0188 self.in_tan = in_tan 0189 ## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position) 0190 self.out_tan = out_tan 0191 0192 def interpolated_value(self, ratio, next_start=None): 0193 end = next_start if self.end is None else self.end 0194 if end is None: 0195 return self.start 0196 if not self.in_value or not self.out_value: 0197 return self.start 0198 if ratio == 1: 0199 return end 0200 if ratio == 0: 0201 return self.start 0202 if self.in_tan and self.out_tan: 0203 bezier = Bezier() 0204 bezier.add_point(self.start, NVector(0, 0), self.out_tan) 0205 bezier.add_point(end, self.in_tan, NVector(0, 0)) 0206 return bezier.point_at(ratio) 0207 0208 lerpv = self.lerp_factor(ratio) 0209 return self.start.lerp(end, lerpv) 0210 0211 def interpolated_tangent_angle(self, ratio, next_start=None): 0212 end = next_start if self.end is None else self.end 0213 if end is None or not self.in_tan or not self.out_tan: 0214 return 0 0215 0216 bezier = Bezier() 0217 bezier.add_point(self.start, NVector(0, 0), self.out_tan) 0218 bezier.add_point(end, self.in_tan, NVector(0, 0)) 0219 return bezier.tangent_angle_at(ratio) 0220 0221 def __repr__(self): 0222 return "<%s.%s %s %s%s>" % ( 0223 type(self).__module__, 0224 type(self).__name__, 0225 self.time, 0226 self.start, 0227 (" -> %s" % self.end) if self.end is not None else "" 0228 ) 0229 0230 0231 class AnimatableMixin: 0232 keyframe_type = Keyframe 0233 0234 def __init__(self, value=None): 0235 ## Non-animated value 0236 self.value = value 0237 ## Property index 0238 self.property_index = None 0239 ## Whether it's animated 0240 self.animated = False 0241 ## Keyframe list 0242 self.keyframes = None 0243 0244 def clear_animation(self, value): 0245 """! 0246 Sets a fixed value, removing animated keyframes 0247 """ 0248 self.value = value 0249 self.animated = False 0250 self.keyframes = None 0251 0252 def add_keyframe(self, time, value, interp=easing.Linear(), *args, **kwargs): 0253 """! 0254 @param time The time this keyframe appears in 0255 @param value The value the property should have at @p time 0256 @param interp The easing callable used to update the tangents of the previous keyframe 0257 @param args Extra arguments to pass the keyframe constructor 0258 @param kwargs Extra arguments to pass the keyframe constructor 0259 @note Always call add_keyframe with increasing @p time value 0260 """ 0261 if not self.animated: 0262 self.value = None 0263 self.keyframes = [] 0264 self.animated = True 0265 else: 0266 if self.keyframes[-1].time == time: 0267 if value != self.keyframes[-1].start: 0268 self.keyframes[-1].start = value 0269 return 0270 else: 0271 self.keyframes[-1].end = value.clone() 0272 0273 self.keyframes.append(self.keyframe_type( 0274 time, 0275 value, 0276 None, 0277 interp, 0278 *args, 0279 **kwargs 0280 )) 0281 0282 def get_value(self, time=0): 0283 """! 0284 @brief Returns the value of the property at the given frame/time 0285 """ 0286 if not self.animated: 0287 return self.value 0288 0289 if not self.keyframes: 0290 return None 0291 0292 return self._get_value_helper(time)[0] 0293 0294 def _get_value_helper(self, time): 0295 val = self.keyframes[0].start 0296 for i in range(len(self.keyframes)): 0297 k = self.keyframes[i] 0298 if time - k.time <= 0: 0299 if k.start is not None: 0300 val = k.start 0301 0302 kp = self.keyframes[i-1] if i > 0 else None 0303 if kp: 0304 t = (time - kp.time) / (k.time - kp.time) 0305 end = kp.end 0306 if end is None: 0307 end = val 0308 if end is not None: 0309 val = kp.interpolated_value(t, end) 0310 return val, end, kp, t 0311 return val, None, None, None 0312 if k.end is not None: 0313 val = k.end 0314 return val, None, None, None 0315 0316 def to_dict(self): 0317 d = super().to_dict() 0318 if self.animated: 0319 last = d["k"][-1] 0320 last.pop("i", None) 0321 last.pop("o", None) 0322 return d 0323 0324 def __repr__(self): 0325 if self.keyframes and len(self.keyframes) > 1: 0326 val = "%s -> %s" % (self.keyframes[0].start, self.keyframes[-2].end) 0327 else: 0328 val = self.value 0329 return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val) 0330 0331 def __str__(self): 0332 if self.animated: 0333 return "animated" 0334 return str(self.value) 0335 0336 @classmethod 0337 def merge_keyframes(cls, items, conversion): 0338 """ 0339 @todo Remove similar functionality from SVG/sif parsers 0340 """ 0341 keyframes = [] 0342 for animatable in items: 0343 if animatable.animated: 0344 keyframes.extend(animatable.keyframes) 0345 0346 # TODO properly interpolate tangents 0347 new_kframes = [] 0348 for keyframe in sorted(keyframes, key=lambda kf: kf.time): 0349 if new_kframes and new_kframes[-1].time == keyframe.time: 0350 continue 0351 kfcopy = keyframe.clone() 0352 kfcopy.start = conversion(*(i.get_value(keyframe.time) for i in items)) 0353 new_kframes.append(kfcopy) 0354 0355 for i in range(0, len(new_kframes) - 1): 0356 new_kframes[i].end = new_kframes[i+1].start 0357 0358 return new_kframes 0359 0360 @classmethod 0361 def load(cls, lottiedict): 0362 obj = super().load(lottiedict) 0363 if "a" not in lottiedict: 0364 obj.animated = prop_animated(lottiedict) 0365 return obj 0366 0367 0368 def prop_animated(l): 0369 if "a" in l: 0370 return l["a"] 0371 if "k" not in l: 0372 return False 0373 if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict): 0374 return True 0375 return False 0376 0377 0378 def prop_not_animated(l): 0379 return not prop_animated(l) 0380 0381 0382 ## @ingroup Lottie 0383 class MultiDimensional(AnimatableMixin, LottieObject): 0384 """! 0385 An animatable property that holds a NVector 0386 """ 0387 keyframe_type = OffsetKeyframe 0388 _props = [ 0389 LottieProp("value", "k", NVector, False, prop_not_animated), 0390 LottieProp("property_index", "ix", int, False), 0391 LottieProp("animated", "a", PseudoBool, False), 0392 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), 0393 ] 0394 0395 def get_tangent_angle(self, time=0): 0396 """! 0397 @brief Returns the value tangent angle of the property at the given frame/time 0398 """ 0399 if not self.keyframes or len(self.keyframes) < 2: 0400 return 0 0401 0402 val, end, kp, t = self._get_value_helper(time) 0403 if kp: 0404 return kp.interpolated_tangent_angle(t, end) 0405 0406 if self.keyframes[0].time >= time: 0407 end = self.keyframes[0].end if self.keyframes[0].end is not None else self.keyframes[1].start 0408 return self.keyframes[0].interpolated_tangent_angle(0, end) 0409 0410 return 0 0411 0412 0413 class PositionValue(MultiDimensional): 0414 _props = [ 0415 LottieProp("value", "k", NVector, False, prop_not_animated), 0416 LottieProp("property_index", "ix", int, False), 0417 LottieProp("animated", "a", PseudoBool, False), 0418 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), 0419 ] 0420 0421 @classmethod 0422 def load(cls, lottiedict): 0423 obj = super().load(lottiedict) 0424 if lottiedict.get("s", False): 0425 cls._load_split(lottiedict, obj) 0426 0427 return obj 0428 0429 @classmethod 0430 def _load_split(cls, lottiedict, obj): 0431 components = [ 0432 Value.load(lottiedict.get("x", {})), 0433 Value.load(lottiedict.get("y", {})), 0434 ] 0435 if "z" in lottiedict: 0436 components.append(Value.load(lottiedict.get("z", {}))) 0437 0438 has_anim = any(x for x in components if x.animated) 0439 if not has_anim: 0440 obj.value = NVector(*(a.value for a in components)) 0441 obj.animated = False 0442 obj.keyframes = None 0443 return 0444 0445 obj.animated = True 0446 obj.value = None 0447 obj.keyframes = cls.merge_keyframes(components, NVector) 0448 0449 0450 class ColorValue(AnimatableMixin, LottieObject): 0451 """! 0452 An animatable property that holds a Color 0453 """ 0454 keyframe_type = OffsetKeyframe 0455 _props = [ 0456 LottieProp("value", "k", Color, False, prop_not_animated), 0457 LottieProp("property_index", "ix", int, False), 0458 LottieProp("animated", "a", PseudoBool, False), 0459 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), 0460 ] 0461 0462 0463 ## @ingroup Lottie 0464 class GradientColors(LottieObject): 0465 """! 0466 Represents colors and offsets in a gradient 0467 0468 Colors are represented as a flat list interleaving offsets and color components in weird ways 0469 There are two possible layouts: 0470 0471 Without alpha, the colors are a sequence of offset, r, g, b 0472 0473 With alpha, same as above but at the end of the list there is a sequence of offset, alpha 0474 0475 Examples: 0476 0477 For the gradient [0, red], [0.5, yellow], [1, green] 0478 The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0] 0479 0480 For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity] 0481 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] 0482 """ 0483 _props = [ 0484 LottieProp("colors", "k", MultiDimensional), 0485 LottieProp("count", "p", int), 0486 ] 0487 0488 def __init__(self, stops=[]): 0489 ## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array 0490 self.colors = MultiDimensional(NVector()) 0491 ## Number of colors 0492 self.count = 0 0493 if stops: 0494 self.set_stops(stops) 0495 0496 @staticmethod 0497 def color_to_stops(self, colors): 0498 """ 0499 Converts a list of colors (Color) to tuples (offset, color) 0500 """ 0501 return [ 0502 (i / (len(colors)-1), color) 0503 for i, color in enumerate(colors) 0504 ] 0505 0506 def set_stops(self, stops, keyframe=None): 0507 """! 0508 @param stops iterable of (offset, Color) tuples 0509 @param keyframe keyframe index (or None if not animated) 0510 """ 0511 flat = self._flatten_stops(stops) 0512 if self.colors.animated and keyframe is not None: 0513 if keyframe > 1: 0514 self.colors.keyframes[keyframe-1].end = flat 0515 self.colors.keyframes[keyframe].start = flat 0516 else: 0517 self.colors.clear_animation(flat) 0518 self.count = len(stops) 0519 0520 def _flatten_stops(self, stops): 0521 flattened_colors = NVector(*reduce( 0522 lambda a, b: a + b, 0523 ( 0524 [off] + color.components[:3] 0525 for off, color in stops 0526 ) 0527 )) 0528 0529 if any(len(c) > 3 for o, c in stops): 0530 flattened_colors.components += reduce( 0531 lambda a, b: a + b, 0532 ( 0533 [off] + [self._get_alpha(color)] 0534 for off, color in stops 0535 ) 0536 ) 0537 return flattened_colors 0538 0539 def _get_alpha(self, color): 0540 if len(color) > 3: 0541 return color[3] 0542 return 1 0543 0544 def _add_to_flattened(self, offset, color, flattened): 0545 flat = [offset] + list(color[:3]) 0546 rgb_size = 4 * self.count 0547 0548 if len(flattened) == rgb_size: 0549 # No alpha 0550 flattened.extend(flat) 0551 if self.count == 0 and len(color) > 3: 0552 flattened.append(offset) 0553 flattened.append(color[3]) 0554 else: 0555 flattened[rgb_size:rgb_size] = flat 0556 flattened.append(offset) 0557 flattened.append(self._get_alpha(color)) 0558 0559 def add_color(self, offset, color, keyframe=None): 0560 if self.colors.animated: 0561 if keyframe is None: 0562 for kf in self.colors.keyframes: 0563 if kf.start: 0564 self._add_to_flattened(offset, color, kf.start.components) 0565 if kf.end: 0566 self._add_to_flattened(offset, color, kf.end.components) 0567 else: 0568 if keyframe > 1: 0569 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe-1].end.components) 0570 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe].start.components) 0571 else: 0572 self._add_to_flattened(offset, color, self.colors.value.components) 0573 self.count += 1 0574 0575 def add_keyframe(self, time, stops, ease=easing.Linear()): 0576 """! 0577 @param time Frame time 0578 @param stops Iterable of (offset, Color) tuples 0579 @param ease Easing function 0580 """ 0581 self.colors.add_keyframe(time, self._flatten_stops(stops), ease) 0582 0583 def get_stops(self, keyframe=None): 0584 if keyframe is not None: 0585 colors = self.colors.keyframes[keyframe].start 0586 else: 0587 colors = self.colors.value 0588 return self._stops_from_flat(colors) 0589 0590 def _stops_from_flat(self, colors): 0591 if len(colors) == 4 * self.count: 0592 for i in range(self.count): 0593 off = i * 4 0594 yield colors[off], Color(*colors[off+1:off+4]) 0595 else: 0596 for i in range(self.count): 0597 off = i * 4 0598 aoff = self.count * 4 + i * 2 + 1 0599 yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff]) 0600 0601 def stops_at(self, time): 0602 return self._stops_from_flat(self.colors.get_value(time)) 0603 0604 0605 ## @ingroup Lottie 0606 class Value(AnimatableMixin, LottieObject): 0607 """! 0608 An animatable property that holds a float 0609 """ 0610 keyframe_type = OffsetKeyframe 0611 _props = [ 0612 LottieProp("value", "k", float, False, prop_not_animated), 0613 LottieProp("property_index", "ix", int, False), 0614 LottieProp("animated", "a", PseudoBool, False), 0615 LottieProp("keyframes", "k", keyframe_type, True, prop_animated), 0616 ] 0617 0618 def __init__(self, value=0): 0619 super().__init__(value) 0620 0621 def add_keyframe(self, time, value, ease=easing.Linear()): 0622 super().add_keyframe(time, NVector(value), ease) 0623 0624 def get_value(self, time=0): 0625 v = super().get_value(time) 0626 if self.animated and self.keyframes: 0627 return v[0] 0628 return v 0629 0630 0631 ## @ingroup Lottie 0632 class ShapePropKeyframe(Keyframe): 0633 """! 0634 Keyframe holding Bezier objects 0635 """ 0636 _props = [ 0637 LottieProp("start", "s", Bezier, PseudoList), 0638 LottieProp("end", "e", Bezier, PseudoList), 0639 ] 0640 0641 def __init__(self, time=0, start=None, end=None, easing_function=None): 0642 Keyframe.__init__(self, time, easing_function) 0643 ## Start value of keyframe segment. 0644 self.start = start 0645 ## End value of keyframe segment. 0646 self.end = end 0647 0648 def interpolated_value(self, ratio, next_start=None): 0649 end = next_start if self.end is None else self.end 0650 if end is None: 0651 return self.start 0652 if not self.in_value or not self.out_value: 0653 return self.start 0654 if ratio == 1: 0655 return end 0656 if ratio == 0 or len(self.start.vertices) != len(end.vertices): 0657 return self.start 0658 0659 lerpv = self.lerp_factor(ratio) 0660 bez = Bezier() 0661 bez.closed = self.start.closed 0662 for i in range(len(self.start.vertices)): 0663 bez.vertices.append(self.start.vertices[i].lerp(end.vertices[i], lerpv)) 0664 bez.in_tangents.append(self.start.in_tangents[i].lerp(end.in_tangents[i], lerpv)) 0665 bez.out_tangents.append(self.start.out_tangents[i].lerp(end.out_tangents[i], lerpv)) 0666 return bez 0667 0668 0669 ## @ingroup Lottie 0670 class ShapeProperty(AnimatableMixin, LottieObject): 0671 """! 0672 An animatable property that holds a Bezier 0673 """ 0674 keyframe_type = ShapePropKeyframe 0675 _props = [ 0676 LottieProp("value", "k", Bezier, False, prop_not_animated), 0677 #LottieProp("expression", "x", str, False), 0678 LottieProp("property_index", "ix", float, False), 0679 LottieProp("animated", "a", PseudoBool, False), 0680 LottieProp("keyframes", "k", keyframe_type, True, prop_animated), 0681 ] 0682 0683 def __init__(self, bezier=None): 0684 super().__init__(bezier or Bezier())