Coverage for lib/lottie/utils/animation.py: 0%
301 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 random
2import math
3from ..nvector import NVector
4from ..objects.shapes import Path
5from .. import objects
6from ..objects import easing
7from ..objects import properties
10def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()):
11 if not isinstance(position_prop, list):
12 position_prop = [position_prop]
14 n_frames = int(round(n_frames))
15 frame_time = (end_time - start_time) / n_frames
16 startpoints = list(map(
17 lambda pp: pp.get_value(start_time),
18 position_prop
19 ))
21 for i in range(n_frames):
22 x = (random.random() * 2 - 1) * x_radius
23 y = (random.random() * 2 - 1) * y_radius
24 for pp, start in zip(position_prop, startpoints):
25 px = start[0] + x
26 py = start[1] + y
27 pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp)
29 for pp, start in zip(position_prop, startpoints):
30 pp.add_keyframe(end_time, start, interp)
33def rot_shake(rotation_prop, angles, start_time, end_time, n_frames):
34 frame_time = (end_time - start_time) / n_frames
35 start = rotation_prop.get_value(start_time)
37 for i in range(0, n_frames):
38 a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi)
39 rotation_prop.add_keyframe(start_time + i * frame_time, start + a)
40 rotation_prop.add_keyframe(end_time, start)
43def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7):
44 start = position_prop.get_value(start_time)
45 d = start-point
47 delta = (end_time - start_time) / oscillations
49 for i in range(oscillations):
50 time_x = i / oscillations
51 factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff))
52 p = point + d * factor
53 position_prop.add_keyframe(start_time + delta * i, p)
55 position_prop.add_keyframe(end_time, point)
58def follow_path(position_prop, bezier, start_time, end_time, n_keyframes,
59 reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0):
60 delta = (end_time - start_time) / (n_keyframes-1)
61 fact = start_t
62 factd = 1 / (n_keyframes-1)
64 if rotation_prop:
65 start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset
67 for i in range(n_keyframes):
68 time = start_time + i * delta
70 if fact > 1 + factd/2:
71 fact -= 1
72 if time != start_time:
73 easing.Jump()(position_prop.keyframes[-1])
74 if rotation_prop:
75 easing.Jump()(rotation_prop.keyframes[-1])
77 f = 1 - fact if reverse else fact
78 position_prop.add_keyframe(time, bezier.point_at(f)+offset)
80 if rotation_prop:
81 rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot)
83 fact += factd
86def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False):
87 obj = Path()
88 beziers = []
89 maxp = 0
91 time_delta = (appear_end - appear_start) / n_keyframes
92 for i in range(n_keyframes+1):
93 time = appear_start + i * time_delta
94 t2 = (time - appear_start) / (appear_end - appear_start)
96 if reverse:
97 t2 = 1 - t2
98 segment = bezier.segment(t2, 1)
99 segment.reverse()
100 else:
101 segment = bezier.segment(0, t2)
103 beziers.append(segment)
104 if len(segment.vertices) > maxp:
105 maxp = len(segment.vertices)
107 obj.shape.add_keyframe(time, segment)
109 for segment in beziers:
110 deltap = maxp - len(segment.vertices)
111 if deltap > 0:
112 segment.vertices += [segment.vertices[-1]] * deltap
113 segment.in_tangents += [NVector(0, 0)] * deltap
114 segment.out_tangents += [NVector(0, 0)] * deltap
116 return obj
119def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False):
120 obj = Path()
121 beziers = []
122 maxp = 0
124 time_delta = (disappear_end - disappear_start) / n_keyframes
125 for i in range(n_keyframes+1):
126 time = disappear_start + i * time_delta
127 t1 = (time - disappear_start) / (disappear_end - disappear_start)
128 if reverse:
129 t1 = 1 - t1
130 segment = bezier.segment(0, t1)
131 else:
132 segment = bezier.segment(1, t1)
133 segment.reverse()
135 beziers.append(segment)
136 if len(segment.vertices) > maxp:
137 maxp = len(segment.vertices)
139 obj.shape.add_keyframe(time, segment)
141 for segment in beziers:
142 deltap = maxp - len(segment.vertices)
143 if deltap > 0:
144 segment.vertices += [segment.vertices[-1]] * deltap
145 segment.in_tangents += [NVector(0, 0)] * deltap
146 segment.out_tangents += [NVector(0, 0)] * deltap
148 return obj
151def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False):
152 obj = Path()
153 beziers = []
154 maxp = 0
156 # HACK: For some reason reversed works better
157 if not reverse:
158 bezier.reverse()
160 time_delta = (appear_end - appear_start) / n_keyframes
161 for i in range(n_keyframes+1):
162 time = appear_start + i * time_delta
163 t1 = (time - disappear_start) / (disappear_end - disappear_start)
164 t2 = (time - appear_start) / (appear_end - appear_start)
166 t1 = max(0, min(1, t1))
167 t2 = max(0, min(1, t2))
169 #if reverse:
170 if True:
171 t1 = 1 - t1
172 t2 = 1 - t2
173 segment = bezier.segment(t2, t1)
174 segment.reverse()
175 #else:
176 #segment = bezier.segment(t1, t2)
177 #segment.reverse()
179 beziers.append(segment)
180 if len(segment.vertices) > maxp:
181 maxp = len(segment.vertices)
183 obj.shape.add_keyframe(time, segment)
185 for segment in beziers:
186 deltap = maxp - len(segment.vertices)
187 if deltap > 0:
188 segment.split_self_chunks(deltap+1)
190 # HACK: Restore
191 if not reverse:
192 bezier.reverse()
193 return obj
196class PointDisplacer:
197 def __init__(self, time_start, time_end, n_frames):
198 """!
199 @param time_start When the animation shall start
200 @param time_end When the animation shall end
201 @param n_frames Number of frames in the animation
202 """
203 ## When the animation shall start
204 self.time_start = time_start
205 ## When the animation shall end
206 self.time_end = time_end
207 ## Number of frames in the animation
208 self.n_frames = n_frames
209 ## Length of a frame
210 self.time_delta = (time_end - time_start) / n_frames
212 def animate_point(self, prop):
213 startpos = prop.get_value(self.time_start)
214 for f in range(self.n_frames+1):
215 p = self._on_displace(startpos, f)
216 prop.add_keyframe(self.frame_time(f), startpos+p)
218 def _on_displace(self, startpos, f):
219 raise NotImplementedError()
221 def animate_bezier(self, prop):
222 initial = prop.get_value(self.time_start)
224 for f in range(self.n_frames+1):
225 bezier = objects.Bezier()
226 bezier.closed = initial.closed
228 for pi in range(len(initial.vertices)):
229 startpos = initial.vertices[pi]
230 dp = self._on_displace(startpos, f)
231 t1sp = initial.in_tangents[pi] + startpos
232 t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp
233 t2sp = initial.out_tangents[pi] + startpos
234 t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp
236 bezier.add_point(dp + startpos, t1fin, t2fin)
238 prop.add_keyframe(self.frame_time(f), bezier)
240 def frame_time(self, f):
241 return f * self.time_delta + self.time_start
243 def _init_lerp(self, val_from, val_to, easing):
244 self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing)
246 def _lerp_get(self, offset):
247 return self._kf.interpolated_value(offset / self.n_frames)[0]
250class SineDisplacer(PointDisplacer):
251 def __init__(
252 self,
253 wavelength,
254 amplitude,
255 time_start,
256 time_end,
257 n_frames,
258 speed=1,
259 axis=90,
260 ):
261 """!
262 Displaces points as if they were following a sine wave
264 @param wavelength Distance between consecutive peaks
265 @param amplitude Distance from a peak to the original position
266 @param time_start When the animation shall start
267 @param time_end When the animation shall end
268 @param n_frames Number of keyframes to add
269 @param speed Number of peaks a point will go through in the given time
270 If negative, it will go the other way
271 @param axis Wave peak direction
272 """
273 super().__init__(time_start, time_end, n_frames)
275 self.wavelength = wavelength
276 self.amplitude = amplitude
277 self.speed_f = math.pi * 2 * speed
278 self.axis = axis / 180 * math.pi
280 def _on_displace(self, startpos, f):
281 off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude
282 return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
285class MultiSineDisplacer(PointDisplacer):
286 def __init__(
287 self,
288 waves,
289 time_start,
290 time_end,
291 n_frames,
292 speed=1,
293 axis=90,
294 amplitude_scale=1,
295 ):
296 """!
297 Displaces points as if they were following a sine wave
299 @param waves List of tuples (wavelength, amplitude)
300 @param time_start When the animation shall start
301 @param time_end When the animation shall end
302 @param n_frames Number of keyframes to add
303 @param speed Number of peaks a point will go through in the given time
304 If negative, it will go the other way
305 @param axis Wave peak direction
306 @param amplitude_scale Multiplies the resulting amplitude by this factor
307 """
308 super().__init__(time_start, time_end, n_frames)
310 self.waves = waves
311 self.speed_f = math.pi * 2 * speed
312 self.axis = axis / 180 * math.pi
313 self.amplitude_scale = amplitude_scale
315 def _on_displace(self, startpos, f):
316 off = 0
317 for wavelength, amplitude in self.waves:
318 off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude
320 off *= self.amplitude_scale
321 return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
324class DepthRotationAxis:
325 def __init__(self, x, y, keep):
326 self.x = x / x.length
327 self.y = y / y.length
328 self.keep = keep / keep.length # should be the cross product
330 def rot_center(self, center, point):
331 return (
332 self.x * self.x.dot(center) +
333 self.y * self.y.dot(center) +
334 self.keep * self.keep.dot(point)
335 )
337 def extract_component(self, vector, axis):
338 return sum(vector.element_scaled(axis).components)
340 @classmethod
341 def from_points(cls, keep_point, center=NVector(0, 0, 0)):
342 keep = keep_point - center
343 keep /= keep.length
344 # Hughes-Moller to find x and y
345 if abs(keep.x) > abs(keep.z):
346 y = NVector(-keep.y, keep.x, 0)
347 else:
348 y = NVector(0, -keep.z, keep.y)
349 y /= y.length
350 x = y.cross(keep)
351 return cls(x, y, keep)
354class DepthRotation:
355 axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0))
356 axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0))
357 axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1))
359 def __init__(self, center):
360 self.center = center
362 def rotate3d_y(self, point, angle):
363 return self.rotate3d(point, angle, self.axis_y)
364 # Hard-coded version:
365 #c = NVector(self.center.x, point.y, self.center.z)
366 #rad = angle * math.pi / 180
367 #delta = point - c
368 #pol_l = delta.length
369 #pol_a = math.atan2(delta.z, delta.x)
370 #dest_a = pol_a + rad
371 #return NVector(
372 # c.x + pol_l * math.cos(dest_a),
373 # point.y,
374 # c.z + pol_l * math.sin(dest_a)
375 #)
377 def rotate3d_x(self, point, angle):
378 return self.rotate3d(point, angle, self.axis_x)
379 # Hard-coded version:
380 #c = NVector(point.x, self.center.y, self.center.z)
381 #rad = angle * math.pi / 180
382 #delta = point - c
383 #pol_l = delta.length
384 #pol_a = math.atan2(delta.y, delta.z)
385 #dest_a = pol_a + rad
386 #return NVector(
387 # point.x,
388 # c.y + pol_l * math.sin(dest_a),
389 # c.z + pol_l * math.cos(dest_a),
390 #)
392 def rotate3d_z(self, point, angle):
393 return self.rotate3d(point, angle, self.axis_z)
395 def rotate3d(self, point, angle, axis):
396 c = axis.rot_center(self.center, point)
397 rad = angle * math.pi / 180
398 delta = point - c
399 pol_l = delta.length
400 pol_a = math.atan2(
401 axis.extract_component(delta, axis.y),
402 axis.extract_component(delta, axis.x)
403 )
404 dest_a = pol_a + rad
405 return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a)
408class DepthRotationDisplacer(PointDisplacer):
409 axis_x = DepthRotation.axis_x
410 axis_y = DepthRotation.axis_y
411 axis_z = DepthRotation.axis_z
413 def __init__(self, center, time_start, time_end, n_frames, axis,
414 depth=0, angle=360, anglestart=0, ease=easing.Linear()):
415 super().__init__(time_start, time_end, n_frames)
416 self.rotation = DepthRotation(center)
417 if isinstance(axis, NVector):
418 axis = DepthRotationAxis.from_points(axis)
419 self.axis = axis
420 self.depth = depth
421 self._angle = angle
422 self.anglestart = anglestart
423 self.ease = ease
424 self._init_lerp(0, angle, ease)
426 @property
427 def angle(self):
428 return self._angle
430 @angle.setter
431 def angle(self, value):
432 self._angle = value
433 self._init_lerp(0, value, self.ease)
435 def _on_displace(self, startpos, f):
436 angle = self.anglestart + self._lerp_get(f)
437 if len(startpos) < 3:
438 startpos = NVector(*(startpos.components + [self.depth]))
439 return self.rotation.rotate3d(startpos, angle, self.axis) - startpos
442class EnvelopeDeformation(PointDisplacer):
443 def __init__(self, topleft, bottomright):
444 self.topleft = topleft
445 self.size = bottomright - topleft
446 self.keyframes = []
448 @property
449 def time_start(self):
450 return self.keyframes[0][0]
452 def add_reset_keyframe(self, time):
453 self.add_keyframe(
454 time,
455 self.topleft.clone(),
456 NVector(self.topleft.x + self.size.x, self.topleft.y),
457 NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y),
458 NVector(self.topleft.x, self.topleft.y + self.size.y),
459 )
461 def add_keyframe(self, time, tl, tr, br, bl):
462 self.keyframes.append([
463 time,
464 tl.clone(),
465 tr.clone(),
466 br.clone(),
467 bl.clone()
468 ])
470 def _on_displace(self, startpos, f):
471 _, tl, tr, br, bl = self.keyframes[f]
472 relp = startpos - self.topleft
473 relp.x /= self.size.x
474 relp.y /= self.size.y
476 x1 = tl.lerp(tr, relp.x)
477 x2 = bl.lerp(br, relp.x)
479 #return x1.lerp(x2, relp.y)
480 return x1.lerp(x2, relp.y) - startpos
482 @property
483 def n_frames(self):
484 return len(self.keyframes)-1
486 def frame_time(self, f):
487 return self.keyframes[f][0]
490class DisplacerDampener(PointDisplacer):
491 """!
492 Given a displacer and a function that returns a factor for a point,
493 multiplies the effect of the displacer by the factor
494 """
495 def __init__(self, displacer, dampener):
496 self.displacer = displacer
497 self.dampener = dampener
499 @property
500 def time_start(self):
501 return self.displacer.time_start
503 def _on_displace(self, startpos, f):
504 disp = self.displacer._on_displace(startpos, f)
505 damp = self.dampener(startpos)
506 return disp * damp
508 @property
509 def n_frames(self):
510 return self.displacer.n_frames
512 def frame_time(self, f):
513 return self.displacer.frame_time(f)
516class FollowDisplacer(PointDisplacer):
517 def __init__(
518 self,
519 origin,
520 range,
521 offset_func,
522 time_start, time_end, n_frames,
523 falloff_exp=1,
524 ):
525 """!
526 @brief Uses a custom offset function, and applies a falloff to the displacement
528 @param origin Origin point for the falloff
529 @param range Radius after which the points will not move
530 @param offset_func Function returning an offset given a ratio of the time
531 @param time_start When the animation shall start
532 @param time_end When the animation shall end
533 @param n_frames Number of frames in the animation
534 @param falloff_exp Exponent for the falloff
535 """
536 super().__init__(time_start, time_end, n_frames)
537 self.origin = origin
538 self.range = range
539 self.offset_func = offset_func
540 self.falloff_exp = falloff_exp
542 def _on_displace(self, startpos, f):
543 influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp
544 return self.offset_func(f / self.n_frames) * influence