Coverage for lib/lottie/parsers/sif/builder.py: 90%
293 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 xml.dom import minidom
4from ... import objects
5from ...nvector import NVector
6from ...utils import restructure
7from . import api, ast
10blend_modes = {
11 objects.BlendMode.Normal: api.BlendMethod.Composite,
12 objects.BlendMode.Multiply: api.BlendMethod.Multiply,
13 objects.BlendMode.Screen: api.BlendMethod.Screen,
14 objects.BlendMode.Overlay: api.BlendMethod.Overlay,
15 objects.BlendMode.Darken: api.BlendMethod.Darken,
16 objects.BlendMode.Lighten: api.BlendMethod.Lighten,
17 objects.BlendMode.HardLight: api.BlendMethod.HardLight,
18 objects.BlendMode.Difference: api.BlendMethod.Difference,
19 objects.BlendMode.Hue: api.BlendMethod.Hue,
20 objects.BlendMode.Saturation: api.BlendMethod.Saturation,
21 objects.BlendMode.Color: api.BlendMethod.Color,
22 objects.BlendMode.Luminosity: api.BlendMethod.Luminosity,
23 objects.BlendMode.Exclusion: api.BlendMethod.Difference,
24 objects.BlendMode.SoftLight: api.BlendMethod.Multiply,
25 objects.BlendMode.ColorDodge: api.BlendMethod.Composite,
26 objects.BlendMode.ColorBurn: api.BlendMethod.Composite,
27}
30class SifBuilder(restructure.AbstractBuilder):
31 def __init__(self, gamma=1.0):
32 """
33 @todo Add gamma option to lottie_convert.py
34 """
35 super().__init__()
36 self.canvas = api.Canvas()
37 self.canvas.version = "1.2"
38 self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma
39 self.autoid = objects.base.Index()
41 def _on_animation(self, animation: objects.Animation):
42 if animation.name:
43 self.canvas.name = animation.name
44 self.canvas.width = animation.width
45 self.canvas.height = animation.height
46 self.canvas.xres = animation.width
47 self.canvas.yres = animation.height
48 self.canvas.view_box = NVector(0, 0, animation.width, animation.height)
49 self.canvas.fps = animation.frame_rate
50 self.canvas.begin_time = api.FrameTime.frame(animation.in_point)
51 self.canvas.end_time = api.FrameTime.frame(animation.out_point)
52 self.canvas.antialias = True
53 return self.canvas
55 def _on_precomp(self, id, dom_parent, layers):
56 g = dom_parent.add_layer(api.GroupLayer())
57 g.desc = id
58 for layer_builder in layers:
59 self.process_layer(layer_builder, g)
61 def _on_layer(self, layer_builder, dom_parent):
62 layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent)
63 if not layer_builder.lottie.name: 63 ↛ 66line 63 didn't jump to line 66, because the condition on line 63 was never false
64 layer.desc = layer_builder.lottie.__class__.__name__
66 bm = getattr(layer_builder.lottie, "blend_mode", None)
67 if bm is None: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true
68 bm = objects.BlendMode.Normal
69 layer.blend_method = blend_modes[bm]
71 layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1
73 in_point = getattr(layer_builder.lottie, "in_point", 0)
74 layer.time_offset.value = api.FrameTime.frame(in_point)
76 #layer.canvas.end_time = api.FrameTime.frame(out_point)
77 return layer
79 def layer_from_lottie(self, type, lottie, dom_parent):
80 g = dom_parent.add_layer(type())
81 if lottie.name:
82 g.desc = lottie.name
83 g.active = not lottie.hidden
84 transf = getattr(lottie, "transform", None)
85 if transf:
86 self.set_transform(g, transf)
88 if isinstance(lottie, objects.NullLayer): 88 ↛ 89line 88 didn't jump to line 89, because the condition on line 88 was never true
89 g.amount.value = 1
91 return g
93 def _get_scale(self, transform):
94 def func(keyframe):
95 t = keyframe.time if keyframe else 0
96 scale_x, scale_y = transform.scale.get_value(t)[:2]
97 scale_x /= 100
98 scale_y /= 100
99 skew = transform.skew.get_value(t) if transform.skew else 0
100 c = math.cos(skew * math.pi / 180)
101 if c != 0: 101 ↛ 103line 101 didn't jump to line 103, because the condition on line 101 was never false
102 scale_y *= 1 / c
103 return NVector(scale_x, scale_y)
104 return func
106 def set_transform(self, group, transform):
107 composite = group.transformation
109 if transform.position: 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false
110 composite.offset = self.process_vector(transform.position)
112 if transform.scale: 112 ↛ 116line 112 didn't jump to line 116, because the condition on line 112 was never false
113 keyframes = self._merge_keyframes([transform.scale, transform.skew])
114 composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform))
116 composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0))
118 if transform.rotation: 118 ↛ 121line 118 didn't jump to line 121, because the condition on line 118 was never false
119 composite.angle = self.process_scalar(transform.rotation)
121 if transform.opacity: 121 ↛ 124line 121 didn't jump to line 124, because the condition on line 121 was never false
122 group.amount = self.process_scalar(transform.opacity, 1/100)
124 if transform.anchor_point: 124 ↛ 128line 124 didn't jump to line 128, because the condition on line 124 was never false
125 group.origin = self.process_vector(transform.anchor_point)
127 # TODO get z_depth from position
128 composite.z_depth = 0
130 def process_vector(self, multidim):
131 def getter(keyframe):
132 if keyframe is None:
133 v = multidim.value
134 else:
135 v = keyframe.start
136 return NVector(v[0], v[1])
138 return self.process_vector_ext(multidim.keyframes, getter)
140 def process_vector_ext(self, kframes, getter):
141 if kframes is not None:
142 wrap = ast.SifAnimated()
143 for i in range(len(kframes)):
144 keyframe = kframes[i]
145 waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time))
147 if i > 0:
148 prev = kframes[i-1]
149 if prev.hold:
150 waypoint.before = api.Interpolation.Constant
151 elif prev.in_value and prev.in_value.x < 1:
152 waypoint.before = api.Interpolation.Ease
153 else:
154 waypoint.before = api.Interpolation.Linear
155 else:
156 waypoint.before = api.Interpolation.Linear
158 if keyframe.hold:
159 waypoint.after = api.Interpolation.Constant
160 elif keyframe.out_value and keyframe.out_value.x > 0:
161 waypoint.after = api.Interpolation.Ease
162 else:
163 waypoint.after = api.Interpolation.Linear
164 else:
165 wrap = api.SifValue(getter(None))
167 return wrap
169 def process_scalar(self, value, mult=None):
170 def getter(keyframe):
171 if keyframe is None:
172 v = value.value
173 else:
174 v = keyframe.start[0]
175 if mult is not None:
176 v *= mult
177 return v
178 return self.process_vector_ext(value.keyframes, getter)
180 def _on_shape(self, shape, group, dom_parent):
181 layers = []
182 if not hasattr(shape, "to_bezier"):
183 return []
185 if group.stroke:
186 sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape)
187 self.apply_group_stroke(sif_shape, group.stroke)
188 layers.append(sif_shape)
190 if group.fill:
191 sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape)
192 layers.append(sif_shape)
193 self.apply_group_fill(sif_shape, group.fill)
195 return layers
197 def _merge_keyframes(self, props):
198 keyframes = {}
199 for prop in props:
200 if prop is not None and prop.animated: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true
201 keyframes.update({kf.time: kf for kf in prop.keyframes})
202 return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None 202 ↛ exitline 202 didn't run the lambda on line 202
204 def apply_origin(self, sif_shape, lottie_shape):
205 if hasattr(lottie_shape, "position"):
206 sif_shape.origin.value = lottie_shape.position.get_value()
207 else:
208 sif_shape.origin.value = lottie_shape.bounding_box().center()
210 def apply_group_fill(self, sif_shape, fill):
211 ## @todo gradients?
212 if hasattr(fill, "colors"): 212 ↛ 213line 212 didn't jump to line 213, because the condition on line 212 was never true
213 return
215 def getter(keyframe):
216 if keyframe is None:
217 v = fill.color.value
218 else:
219 v = keyframe.start
220 return self.canvas.make_color(*v)
222 sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter)
224 def get_op(keyframe):
225 if keyframe is None:
226 v = fill.opacity.value
227 else:
228 v = keyframe.start[0]
229 v /= 100
230 return v
232 sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op)
234 def apply_group_stroke(self, sif_shape, stroke):
235 self.apply_group_fill(sif_shape, stroke)
236 sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter
237 round_cap = stroke.line_cap == objects.LineCap.Round
238 sif_shape.round_tip_0.value = round_cap
239 sif_shape.round_tip_1.value = round_cap
240 sif_shape.width = self.process_scalar(stroke.width, 0.5)
242 def build_path(self, type, path, dom_parent, lottie_shape):
243 layer = self.layer_from_lottie(type, lottie_shape, dom_parent)
244 self.apply_origin(layer, lottie_shape)
245 startbez = path.shape.get_value()
246 layer.bline.loop = startbez.closed
247 nverts = len(startbez.vertices)
248 for point in range(nverts):
249 self.bezier_point(path, point, layer.bline, layer.origin.value)
250 return layer
252 def bezier_point(self, lottie_path, point_index, sif_parent, offset):
253 composite = api.BlinePoint()
255 def get_point(keyframe):
256 if keyframe is None:
257 bezier = lottie_path.shape.value
258 else:
259 bezier = keyframe.start
260 if not bezier: 260 ↛ 262line 260 didn't jump to line 262, because the condition on line 260 was never true
261 #elem.parentNode.parentNode.removeChild(elem.parentNode)
262 return
263 vert = bezier.vertices[point_index]
264 return NVector(vert[0], vert[1]) - offset
266 composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point)
267 composite.split.value = True
268 composite.split_radius.value = True
269 composite.split_angle.value = True
271 def get_tangent(keyframe):
272 if keyframe is None:
273 bezier = lottie_path.shape.value
274 else:
275 bezier = keyframe.start
276 if not bezier: 276 ↛ 278line 276 didn't jump to line 278, because the condition on line 276 was never true
277 #elem.parentNode.parentNode.removeChild(elem.parentNode)
278 return
280 inp = getattr(bezier, which_point)[point_index]
281 return NVector(inp.x, inp.y) * 3 * mult
283 mult = -1
284 which_point = "in_tangents"
285 composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
287 mult = 1
288 which_point = "out_tangents"
289 composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
290 sif_parent.points.append(composite)
292 def _on_shapegroup(self, shape_group, dom_parent):
293 if shape_group.empty(): 293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true
294 return
296 layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent)
298 self.shapegroup_process_children(shape_group, layer)
300 def _modifier_inner_group(self, modifier, shapegroup, dom_parent):
301 layer = dom_parent.add_layer(api.GroupLayer())
302 self.shapegroup_process_child(modifier.child, shapegroup, layer)
303 return layer
305 def _on_shape_modifier(self, modifier, shapegroup, dom_parent):
306 layer = dom_parent.add_layer(api.GroupLayer())
307 if modifier.lottie.name: 307 ↛ 310line 307 didn't jump to line 310, because the condition on line 307 was never false
308 layer.desc = modifier.lottie.name
310 inner = self._modifier_inner_group(modifier, shapegroup, layer)
311 if isinstance(modifier.lottie, objects.Repeater): 311 ↛ exitline 311 didn't return from function '_on_shape_modifier', because the condition on line 311 was never false
312 self.build_repeater(modifier.lottie, inner, layer)
314 def _build_repeater_defs(self, shape, name_id):
315 dup = api.Duplicate()
316 dup.id = name_id
317 self.canvas.defs.append(dup)
318 self.canvas.register_as(dup, name_id)
320 def getter(keyframe):
321 if keyframe is None: 321 ↛ 324line 321 didn't jump to line 324, because the condition on line 321 was never false
322 v = shape.copies.value
323 else:
324 v = keyframe.start[0]
326 return v - 1
328 setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter))
329 dup.to.value = 0
330 dup.step.value = -1
331 return dup
333 def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite):
334 power = ast.SifPower()
335 setattr(scalecomposite, "xy"[comp], power)
337 def getter(keyframe):
338 if keyframe is None: 338 ↛ 341line 338 didn't jump to line 341, because the condition on line 338 was never false
339 v = shape.transform.scale.value
340 else:
341 v = keyframe.start
342 v = v[comp] / 100
343 return v
345 power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter)
347 # HACK work around an issue in Synfig
348 power.power = ast.SifAdd()
349 power.power.lhs.value = api.ValueReference(name_id)
350 power.power.rhs.value = 0.000001
352 def _build_repeater_transform(self, shape, inner, name_id):
353 offset_id = name_id + "_origin"
354 origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector")
355 self.canvas.defs.append(origin)
356 self.canvas.register_as(origin, offset_id)
357 inner.origin = origin
359 composite = inner.transformation
361 composite.offset = ast.SifAdd()
362 composite.offset.rhs.value = api.ValueReference(offset_id)
363 composite.offset.lhs = ast.SifScale()
364 composite.offset.lhs.scalar.value = api.ValueReference(name_id)
365 composite.offset.lhs.link = self.process_vector(shape.transform.position)
367 composite.angle = ast.SifScale()
368 composite.angle.scalar.value = api.ValueReference(name_id)
369 composite.angle.link = self.process_scalar(shape.transform.rotation)
371 composite.scale = ast.SifVectorComposite()
372 self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale)
373 self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale)
375 def _build_repeater_amount(self, shape, inner, name_id):
376 inner.amount = ast.SifSubtract()
377 inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01)
379 inner.amount.rhs = ast.SifScale()
380 inner.amount.rhs.scalar.value = api.ValueReference(name_id)
382 def getter(keyframe):
383 if keyframe is None: 383 ↛ 387line 383 didn't jump to line 387, because the condition on line 383 was never false
384 t = 0
385 end = shape.transform.end_opacity.value
386 else:
387 t = keyframe.time
388 end = keyframe.start[0]
389 start = shape.transform.start_opacity.get_value(t)
390 n = shape.copies.get_value(t)
391 v = (start - end) / (n - 1) / 100 if n > 0 else 0
392 return v
393 inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter)
395 def build_repeater(self, shape, inner, dom_parent):
396 name_id = "duplicate_%s" % next(self.autoid)
397 dup = self._build_repeater_defs(shape, name_id)
398 self._build_repeater_transform(shape, inner, name_id)
399 self._build_repeater_amount(shape, inner, name_id)
400 inner.desc = "Transformation for " + (dom_parent.desc or "duplicate")
402 # duplicate layer
403 duplicate = dom_parent.add_layer(api.DuplicateLayer())
404 duplicate.index = dup
405 duplicate.desc = shape.name
408def to_sif(animation):
409 builder = SifBuilder()
410 builder.process(animation)
411 return builder.canvas