Coverage for lib/lottie/parsers/sif/converter.py: 0%
371 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 ... import objects
3from ...objects import easing
4from . import api, ast
5from ... import NVector, PolarVector
7try:
8 from ...utils import font
9 has_font = True
10except ImportError:
11 has_font = False
14def convert(canvas: api.Canvas):
15 return Converter().convert(canvas)
18class Converter:
19 def __init__(self):
20 pass
22 def _animated(self, sifval):
23 return isinstance(sifval, ast.SifAnimated)
25 def convert(self, canvas: api.Canvas):
26 self.canvas = canvas
27 self.animation = objects.Animation(
28 self._time(canvas.end_time),
29 canvas.fps
30 )
31 self.animation.in_point = self._time(canvas.begin_time)
32 self.animation.width = canvas.width
33 self.animation.height = canvas.height
34 self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1])
35 self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3])
36 self.target_size = NVector(canvas.width, canvas.height)
37 self.shape_layer = self.animation.add_layer(objects.ShapeLayer())
38 self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b)
39 self._process_layers(canvas.layers, self.shape_layer)
40 return self.animation
42 def _time(self, t: api.FrameTime):
43 return self.canvas.time_to_frames(t)
45 def _process_layers(self, layers, parent):
46 old_gamma = self.gamma
48 for layer in reversed(layers):
49 if not layer.active:
50 continue
51 elif isinstance(layer, api.GroupLayerBase):
52 parent.add_shape(self._convert_group(layer))
53 elif isinstance(layer, api.RectangleLayer):
54 parent.add_shape(self._convert_fill(layer, self._convert_rect))
55 elif isinstance(layer, api.CircleLayer):
56 parent.add_shape(self._convert_fill(layer, self._convert_circle))
57 elif isinstance(layer, api.StarLayer):
58 parent.add_shape(self._convert_fill(layer, self._convert_star))
59 elif isinstance(layer, api.PolygonLayer):
60 parent.add_shape(self._convert_fill(layer, self._convert_polygon))
61 elif isinstance(layer, api.RegionLayer):
62 parent.add_shape(self._convert_fill(layer, self._convert_bline))
63 elif isinstance(layer, api.AbstractOutline):
64 parent.add_shape(self._convert_outline(layer, self._convert_bline))
65 elif isinstance(layer, api.GradientLayer):
66 parent.add_shape(self._convert_gradient(layer, parent))
67 elif isinstance(layer, api.TransformDown):
68 shape = self._convert_transform_down(layer)
69 parent.add_shape(shape)
70 parent = shape
71 elif isinstance(layer, api.TextLayer):
72 if has_font:
73 parent.add_shape(self._convert_fill(layer, self._convert_text))
74 elif isinstance(layer, api.ColorCorrectLayer):
75 self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value)
77 self.gamma = old_gamma
79 def _convert_group(self, layer: api.GroupLayer):
80 shape = objects.Group()
81 self._set_name(shape, layer)
82 shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin))
83 self._convert_transform(layer.transformation, shape.transform)
84 self._process_layers(layer.layers, shape)
85 shape.transform.opacity = self._adjust_animated(
86 self._convert_scalar(layer.amount),
87 lambda x: x*100
88 )
89 return shape
91 def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform):
92 if isinstance(sif_transform, api.BoneLinkTransform):
93 base_transform = sif_transform.base_value
94 else:
95 base_transform = sif_transform
97 position = self._adjust_coords(self._convert_vector(base_transform.offset))
98 rotation = self._adjust_angle(self._convert_scalar(base_transform.angle))
99 scale = self._adjust_animated(
100 self._convert_vector(base_transform.scale),
101 lambda x: x * 100
102 )
104 lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle))
106 if isinstance(sif_transform, api.BoneLinkTransform):
107 lottie_transform.position = position
108 lottie_transform.rotation = rotation
109 lottie_transform.scale = scale
110 #bone = sif_transform.bone
111 #b_pos = self._adjust_coords(self._convert_vector(bone.origin))
112 #old_anchor = lottie_transform.anchor_point
114 #if sif_transform.translate:
115 #self._mix_animations_into(
116 #[position, b_pos, old_anchor],
117 #lottie_transform.position,
118 #lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2
119 #)
120 #else:
121 #lottie_transform.position = position
123 #lottie_transform.anchor_point = b_pos
124 #lottie_transform.anchor_point.value += NVector(100,0)
126 #if sif_transform.rotate:
127 #b_rot = self._convert_scalar(bone.angle)
128 #self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b)
129 #else:
130 #lottie_transform.rotation = rotation
132 #if sif_transform.scale_y:
133 #b_scale = self._convert_scalar(bone.scalelx)
134 #self._mix_animations_into(
135 #scale, b_scale, lottie_transform.scale,
136 #lambda a, b: NVector(a.x, a.y * b)
137 #)
138 #else:
139 #lottie_transform.scale = scale
140 else:
141 lottie_transform.position = position
142 lottie_transform.rotation = rotation
143 lottie_transform.scale = scale
145 def _mix_animations_into(self, animations, output, mix):
146 if not any(x.animated for x in animations):
147 output.value = mix(*(x.value for x in animations))
148 else:
149 for vals in self._mix_animations(*animations):
150 time = vals.pop(0)
151 output.add_keyframe(time, mix(*vals))
153 def _convert_fill(self, layer, converter):
154 shape = objects.Group()
155 self._set_name(shape, layer)
156 shape.add_shape(converter(layer))
157 if layer.invert.value:
158 shape.add_shape(objects.Rect(self.target_size/2, self.target_size))
160 fill = objects.Fill()
161 fill.color = self._convert_color(layer.color)
162 fill.opacity = self._adjust_animated(
163 self._convert_scalar(layer.amount),
164 lambda x: x * 100
165 )
166 shape.add_shape(fill)
167 return shape
169 def _convert_linecap(self, lc: api.LineCap):
170 if lc == api.LineCap.Rounded:
171 return objects.LineCap.Round
172 if lc == api.LineCap.Squared:
173 return objects.LineCap.Square
174 return objects.LineCap.Butt
176 def _convert_cusp(self, lc: api.CuspStyle):
177 if lc == api.CuspStyle.Miter:
178 return objects.LineJoin.Miter
179 if lc == api.CuspStyle.Bevel:
180 return objects.LineJoin.Bevel
181 return objects.LineJoin.Round
183 def _convert_outline(self, layer: api.AbstractOutline, converter):
184 shape = objects.Group()
185 self._set_name(shape, layer)
186 shape.add_shape(converter(layer))
187 stroke = objects.Stroke()
188 stroke.color = self._convert_color(layer.color)
189 stroke.line_cap = self._convert_linecap(layer.start_tip)
190 stroke.line_join = self._convert_cusp(layer.cusp_type)
191 stroke.width = self._adjust_scalar(self._convert_scalar(layer.width))
192 shape.add_shape(stroke)
193 return shape
195 def _convert_rect(self, layer: api.RectangleLayer):
196 rect = objects.Rect()
197 p1 = self._adjust_coords(self._convert_vector(layer.point1))
198 p2 = self._adjust_coords(self._convert_vector(layer.point2))
199 if p1.animated or p2.animated:
200 for time, p1v, p2v in self._mix_animations(p1, p2):
201 rect.position.add_keyframe(time, (p1v + p2v) / 2)
202 rect.size.add_keyframe(time, abs(p2v - p1v))
203 pass
204 else:
205 rect.position.value = (p1.value + p2.value) / 2
206 rect.size.value = abs(p2.value - p1.value)
207 rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel))
208 return rect
210 def _convert_circle(self, layer: api.CircleLayer):
211 shape = objects.Ellipse()
212 shape.position = self._adjust_coords(self._convert_vector(layer.origin))
213 radius = self._adjust_scalar(self._convert_scalar(layer.radius))
214 shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2)
215 return shape
217 def _convert_star(self, layer: api.StarLayer):
218 shape = objects.Star()
219 shape.position = self._adjust_coords(self._convert_vector(layer.origin))
220 shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2))
221 shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1))
222 shape.rotation = self._adjust_animated(
223 self._convert_scalar(layer.angle),
224 lambda x: 90-x
225 )
226 shape.points = self._convert_scalar(layer.points)
227 if layer.regular_polygon.value:
228 shape.star_type = objects.StarType.Polygon
229 return shape
231 def _mix_animations(self, *animatable):
232 times = set()
233 for v in animatable:
234 self._force_animated(v)
235 for kf in v.keyframes:
236 times.add(kf.time)
238 for time in sorted(times):
239 yield [time] + [v.get_value(time) for v in animatable]
241 def _force_animated(self, lottieval):
242 if not lottieval.animated:
243 v = lottieval.value
244 lottieval.add_keyframe(0, v)
245 lottieval.add_keyframe(self.animation.out_point, v)
247 def _convert_easing_part(self, interp: api.Interpolation):
248 if interp == api.Interpolation.Linear:
249 return easing.Linear()
250 return easing.Sigmoid()
252 def _convert_easing(self, start: api.Interpolation, end: api.Interpolation):
253 if api.Interpolation.Constant in (start, end):
254 return easing.Jump()
255 if start == end:
256 return self._convert_easing_part(start)
257 return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end))
259 def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin):
260 if self._animated(v):
261 if len(v.keyframes) == 1:
262 lot.value = self._convert_ast_value(v.keyframes[0].value)
263 else:
264 for i, kf in enumerate(v.keyframes):
265 if i+1 < len(v.keyframes):
266 start = kf.after
267 end = v.keyframes[i+1].before
268 ease = self._convert_easing(start, end)
269 else:
270 ease = easing.Linear()
272 lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease)
273 else:
274 lot.value = self._convert_ast_value(v)
275 return lot
277 def _convert_ast_value(self, v):
278 if isinstance(v, ast.SifRadialComposite):
279 return self._polar(v.radius.value, v.theta.value, 1)
280 elif isinstance(v, ast.SifValue):
281 return v.value
282 elif isinstance(v, ast.SifVectorComposite):
283 return NVector(v.x.value, v.y.value)
284 else:
285 return v
287 def _converted_vector_values(self, v):
288 if isinstance(v, ast.SifRadialComposite):
289 return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)]
290 return self._convert_vector(v)
292 def _convert_color(self, v: ast.SifAstNode):
293 return self._adjust_animated(
294 self._convert_animatable(v, objects.ColorValue()),
295 self._color_gamma
296 )
298 def _convert_vector(self, v: ast.SifAstNode):
299 return self._convert_animatable(v, objects.MultiDimensional())
301 def _convert_scalar(self, v: ast.SifAstNode):
302 return self._convert_animatable(v, objects.Value())
304 def _color_gamma(self, color):
305 color = color.clone()
306 for i in range(3):
307 color[i] = color[i] ** (1/self.gamma[i])
308 return color
310 def _adjust_animated(self, lottieval, transform):
311 if lottieval.animated:
312 for kf in lottieval.keyframes:
313 if kf.start is not None:
314 kf.start = transform(kf.start)
315 if kf.end is not None:
316 kf.end = transform(kf.end)
317 else:
318 lottieval.value = transform(lottieval.value)
319 return lottieval
321 def _adjust_scalar(self, lottieval: objects.Value):
322 return self._adjust_animated(lottieval, self._scalar_mult)
324 def _adjust_angle(self, lottieval: objects.Value):
325 return self._adjust_animated(lottieval, lambda x: -x)
327 def _adjust_add_dimension(self, lottieval, transform):
328 to_val = objects.MultiDimensional()
329 to_val.animated = lottieval.animated
330 if lottieval.animated:
331 to_val.keyframes = []
332 for kf in lottieval.keyframes:
333 if kf.start is not None:
334 kf.start = transform(kf.start[0])
335 if kf.end is not None:
336 kf.end = transform(kf.end[0])
337 to_val.keyframes.append(kf)
338 else:
339 to_val.value = transform(lottieval.value)
340 return to_val
342 def _scalar_mult(self, x):
343 return x * 60
345 def _adjust_coords(self, lottieval: objects.MultiDimensional):
346 return self._adjust_animated(lottieval, self._coord)
348 def _coord(self, val: NVector):
349 return NVector(
350 self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5),
351 self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5),
352 )
354 def _convert_polygon(self, layer: api.PolygonLayer):
355 lot = objects.Path()
356 animatables = [self._convert_vector(layer.origin)] + [
357 self._convert_vector(p)
358 for p in layer.points
359 ]
360 animated = any(x.animated for x in animatables)
361 if not animated:
362 lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value)
363 else:
364 for values in self._mix_animations(*animatables):
365 time = values[0]
366 origin = values[1]
367 points = values[2:]
368 lot.shape.add_keyframe(time, self._polygon(points, origin))
369 return lot
371 def _polygon(self, points, origin):
372 bezier = objects.Bezier()
373 bezier.closed = True
374 for point in points:
375 bezier.add_point(self._coord(point+origin))
376 return bezier
378 def _convert_bline(self, layer: api.AbstractOutline):
379 lot = objects.Path()
380 closed = layer.bline.loop
381 animatables = [
382 self._convert_vector(layer.origin)
383 ]
384 for p in layer.bline.points:
385 animatables += [
386 self._convert_vector(p.point),
387 self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0),
388 self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0),
389 self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0),
390 self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0)
391 ]
392 animated = any(x.animated for x in animatables)
393 if not animated:
394 lot.shape.value = self._bezier(
395 closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points
396 )
397 else:
398 for values in self._mix_animations(*animatables):
399 time = values[0]
400 origin = values[1]
401 values = values[2:]
402 lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points))
403 return lot
405 def _bezier(self, closed, values, origin, points):
406 chunk_size = 5
407 bezier = objects.Bezier()
408 bezier.closed = closed
409 for i in range(0, len(values), chunk_size):
410 point, r1, a1, r2, a2 = values[i:i+chunk_size]
411 sifvert = point+origin
412 vert = self._coord(sifvert)
413 if not points[i//chunk_size].split_radius.value:
414 r2 = r1
415 if not points[i//chunk_size].split_angle.value:
416 a2 = a1
417 t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert
418 t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert
419 bezier.add_point(vert, t1, t2)
420 return bezier
422 def _polar(self, radius, angle, dir):
423 offset_angle = 0
424 if dir == 1:
425 offset_angle += 180
426 return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180)
428 def _convert_transform_down(self, tl: api.TransformDown):
429 group = objects.Group()
430 self._set_name(group, tl)
432 if isinstance(tl, api.TranslateLayer):
433 group.transform.anchor_point.value = self.target_size / 2
434 group.transform.position = self._adjust_coords(self._convert_vector(tl.origin))
435 elif isinstance(tl, api.RotateLayer):
436 group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin))
437 group.transform.position = group.transform.anchor_point.clone()
438 group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount))
439 elif isinstance(tl, api.ScaleLayer):
440 group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center))
441 group.transform.position = group.transform.anchor_point.clone()
442 group.transform.scale = self._adjust_add_dimension(
443 self._convert_scalar(tl.amount),
444 self._zoom_to_scale
445 )
447 return group
449 def _zoom_to_scale(self, value):
450 zoom = math.e ** value * 100
451 return NVector(zoom, zoom)
453 def _set_name(self, lottie, sif):
454 lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__
456 def _convert_gradient(self, layer: api.GradientLayer, parent):
457 group = objects.Group()
459 parent_shapes = parent.shapes
460 parent.shapes = []
461 if isinstance(parent, objects.Group):
462 parent.shapes.append(parent_shapes[-1])
464 self._gradient_gather_shapes(parent_shapes, group)
466 gradient = objects.GradientFill()
467 self._set_name(gradient, layer)
468 group.add_shape(gradient)
469 gradient.colors = self._convert_gradient_stops(layer.gradient)
470 gradient.opacity = self._adjust_animated(
471 self._convert_scalar(layer.amount),
472 lambda x: x * 100
473 )
475 if isinstance(layer, api.LinearGradient):
476 gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1))
477 gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2))
478 gradient.gradient_type = objects.GradientType.Linear
479 elif isinstance(layer, api.RadialGradient):
480 gradient.gradient_type = objects.GradientType.Radial
481 gradient.start_point = self._adjust_coords(self._convert_vector(layer.center))
482 radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45)
483 if not radius.animated and not gradient.start_point.animated:
484 gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value)
485 else:
486 for time, c, r in self._mix_animations(gradient.start_point.clone(), radius):
487 gradient.end_point.add_keyframe(time, c + NVector(r + r))
489 return group
491 def _gradient_gather_shapes(self, shapes, output: objects.Group):
492 for shape in shapes:
493 if isinstance(shape, objects.Shape):
494 output.add_shape(shape)
495 elif isinstance(shape, objects.Group):
496 self._gradient_gather_shapes(shape.shapes, output)
498 def _convert_gradient_stops(self, sif_gradient):
499 stops = objects.GradientColors()
500 if not self._animated(sif_gradient):
501 stops.set_stops(self._flatten_gradient_colors(sif_gradient.value))
502 stops.count = len(sif_gradient.value)
503 else:
504 # TODO easing
505 for kf in sif_gradient.keyframes:
506 stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value))
507 stops.count = len(kf.value)
509 return stops
511 def _flatten_gradient_colors(self, stops):
512 return [
513 (stop.pos, self._color_gamma(stop.color))
514 for stop in stops
515 ]
517 def _convert_text(self, layer: api.TextLayer):
518 shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center))
519 shape.refresh()
520 trans = shape.wrapped.transform
521 trans.anchor_point.value = shape.wrapped.bounding_box().center()
522 trans.anchor_point.value.x /= 2
523 trans.position = self._adjust_coords(self._convert_vector(layer.origin))
524 trans.scale = self._adjust_animated(
525 self._convert_vector(layer.size),
526 lambda v: v * 100
527 )
528 return shape