python-lottie  0.7.0+dev66cafb9
A framework to work with lottie files and telegram animated stickers (tgs)
builder.py
Go to the documentation of this file.
1 import math
2 from xml.dom import minidom
3 
4 from ... import objects
5 from ...nvector import NVector
6 from ...utils import restructure
7 from . import api, ast
8 
9 
10 blend_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 }
28 
29 
30 class 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.canvascanvas = api.Canvas()
37  self.canvascanvas.version = "1.2"
38  self.canvascanvas.gamma_r = self.canvascanvas.gamma_g = self.canvascanvas.gamma_b = gamma
39  self.autoidautoid = objects.base.Index()
40 
41  def _on_animation(self, animation: objects.Animation):
42  if animation.name:
43  self.canvascanvas.name = animation.name
44  self.canvascanvas.width = animation.width
45  self.canvascanvas.height = animation.height
46  self.canvascanvas.xres = animation.width
47  self.canvascanvas.yres = animation.height
48  self.canvascanvas.view_box = NVector(0, 0, animation.width, animation.height)
49  self.canvascanvas.fps = animation.frame_rate
50  self.canvascanvas.begin_time = api.FrameTime.frame(animation.in_point)
51  self.canvascanvas.end_time = api.FrameTime.frame(animation.out_point)
52  self.canvascanvas.antialias = True
53  return self.canvascanvas
54 
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)
60 
61  def _on_layer(self, layer_builder, dom_parent):
62  layer = self.layer_from_lottielayer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent)
63  if not layer_builder.lottie.name:
64  layer.desc = layer_builder.lottie.__class__.__name__
65 
66  bm = getattr(layer_builder.lottie, "blend_mode", None)
67  if bm is None:
68  bm = objects.BlendMode.Normal
69  layer.blend_method = blend_modes[bm]
70 
71  layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1
72 
73  in_point = getattr(layer_builder.lottie, "in_point", 0)
74  layer.time_offset.value = api.FrameTime.frame(in_point)
75 
76  #layer.canvas.end_time = api.FrameTime.frame(out_point)
77  return layer
78 
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_transformset_transform(g, transf)
87 
88  if isinstance(lottie, objects.NullLayer):
89  g.amount.value = 1
90 
91  return g
92 
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:
102  scale_y *= 1 / c
103  return NVector(scale_x, scale_y)
104  return func
105 
106  def set_transform(self, group, transform):
107  composite = group.transformation
108 
109  if transform.position:
110  composite.offset = self.process_vectorprocess_vector(transform.position)
111 
112  if transform.scale:
113  keyframes = self._merge_keyframes_merge_keyframes([transform.scale, transform.skew])
114  composite.scale = self.process_vector_extprocess_vector_ext(keyframes, self._get_scale_get_scale(transform))
115 
116  composite.skew_angle = self.process_scalarprocess_scalar(transform.skew or objects.Value(0))
117 
118  if transform.rotation:
119  composite.angle = self.process_scalarprocess_scalar(transform.rotation)
120 
121  if transform.opacity:
122  group.amount = self.process_scalarprocess_scalar(transform.opacity, 1/100)
123 
124  if transform.anchor_point:
125  group.origin = self.process_vectorprocess_vector(transform.anchor_point)
126 
127  # TODO get z_depth from position
128  composite.z_depth = 0
129 
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])
137 
138  return self.process_vector_extprocess_vector_ext(multidim.keyframes, getter)
139 
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))
146 
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
157 
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))
166 
167  return wrap
168 
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_extprocess_vector_ext(value.keyframes, getter)
179 
180  def _on_shape(self, shape, group, dom_parent):
181  layers = []
182  if not hasattr(shape, "to_bezier"):
183  return []
184 
185  if group.stroke:
186  sif_shape = self.build_pathbuild_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape)
187  self.apply_group_strokeapply_group_stroke(sif_shape, group.stroke)
188  layers.append(sif_shape)
189 
190  if group.fill:
191  sif_shape = self.build_pathbuild_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape)
192  layers.append(sif_shape)
193  self.apply_group_fillapply_group_fill(sif_shape, group.fill)
194 
195  return layers
196 
197  def _merge_keyframes(self, props):
198  keyframes = {}
199  for prop in props:
200  if prop is not None and prop.animated:
201  keyframes.update({kf.time: kf for kf in prop.keyframes})
202  return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None
203 
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()
209 
210  def apply_group_fill(self, sif_shape, fill):
211  ## @todo gradients?
212  if hasattr(fill, "colors"):
213  return
214 
215  def getter(keyframe):
216  if keyframe is None:
217  v = fill.color.value
218  else:
219  v = keyframe.start
220  return self.canvascanvas.make_color(*v)
221 
222  sif_shape.color = self.process_vector_extprocess_vector_ext(fill.color.keyframes, getter)
223 
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
231 
232  sif_shape.amount = self.process_vector_extprocess_vector_ext(fill.opacity.keyframes, get_op)
233 
234  def apply_group_stroke(self, sif_shape, stroke):
235  self.apply_group_fillapply_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_scalarprocess_scalar(stroke.width, 0.5)
241 
242  def build_path(self, type, path, dom_parent, lottie_shape):
243  layer = self.layer_from_lottielayer_from_lottie(type, lottie_shape, dom_parent)
244  self.apply_originapply_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_pointbezier_point(path, point, layer.bline, layer.origin.value)
250  return layer
251 
252  def bezier_point(self, lottie_path, point_index, sif_parent, offset):
253  composite = api.BlinePoint()
254 
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:
261  #elem.parentNode.parentNode.removeChild(elem.parentNode)
262  return
263  vert = bezier.vertices[point_index]
264  return NVector(vert[0], vert[1]) - offset
265 
266  composite.point = self.process_vector_extprocess_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
270 
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:
277  #elem.parentNode.parentNode.removeChild(elem.parentNode)
278  return
279 
280  inp = getattr(bezier, which_point)[point_index]
281  return NVector(inp.x, inp.y) * 3 * mult
282 
283  mult = -1
284  which_point = "in_tangents"
285  composite.t1 = self.process_vector_extprocess_vector_ext(lottie_path.shape.keyframes, get_tangent)
286 
287  mult = 1
288  which_point = "out_tangents"
289  composite.t2 = self.process_vector_extprocess_vector_ext(lottie_path.shape.keyframes, get_tangent)
290  sif_parent.points.append(composite)
291 
292  def _on_shapegroup(self, shape_group, dom_parent):
293  if shape_group.empty():
294  return
295 
296  layer = self.layer_from_lottielayer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent)
297 
298  self.shapegroup_process_children(shape_group, layer)
299 
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
304 
305  def _on_shape_modifier(self, modifier, shapegroup, dom_parent):
306  layer = dom_parent.add_layer(api.GroupLayer())
307  if modifier.lottie.name:
308  layer.desc = modifier.lottie.name
309 
310  inner = self._modifier_inner_group_modifier_inner_group(modifier, shapegroup, layer)
311  if isinstance(modifier.lottie, objects.Repeater):
312  self.build_repeaterbuild_repeater(modifier.lottie, inner, layer)
313 
314  def _build_repeater_defs(self, shape, name_id):
315  dup = api.Duplicate()
316  dup.id = name_id
317  self.canvascanvas.defs.append(dup)
318  self.canvascanvas.register_as(dup, name_id)
319 
320  def getter(keyframe):
321  if keyframe is None:
322  v = shape.copies.value
323  else:
324  v = keyframe.start[0]
325 
326  return v - 1
327 
328  setattr(dup, "from", self.process_vector_extprocess_vector_ext(shape.copies.keyframes, getter))
329  dup.to.value = 0
330  dup.step.value = -1
331  return dup
332 
333  def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite):
334  power = ast.SifPower()
335  setattr(scalecomposite, "xy"[comp], power)
336 
337  def getter(keyframe):
338  if keyframe is None:
339  v = shape.transform.scale.value
340  else:
341  v = keyframe.start
342  v = v[comp] / 100
343  return v
344 
345  power.base = self.process_vector_extprocess_vector_ext(shape.transform.scale.keyframes, getter)
346 
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
351 
352  def _build_repeater_transform(self, shape, inner, name_id):
353  offset_id = name_id + "_origin"
354  origin = api.ExportedValue(offset_id, self.process_vectorprocess_vector(shape.transform.anchor_point), "vector")
355  self.canvascanvas.defs.append(origin)
356  self.canvascanvas.register_as(origin, offset_id)
357  inner.origin = origin
358 
359  composite = inner.transformation
360 
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_vectorprocess_vector(shape.transform.position)
366 
367  composite.angle = ast.SifScale()
368  composite.angle.scalar.value = api.ValueReference(name_id)
369  composite.angle.link = self.process_scalarprocess_scalar(shape.transform.rotation)
370 
371  composite.scale = ast.SifVectorComposite()
372  self._build_repeater_transform_scale_component_build_repeater_transform_scale_component(shape, name_id, 0, composite.scale)
373  self._build_repeater_transform_scale_component_build_repeater_transform_scale_component(shape, name_id, 1, composite.scale)
374 
375  def _build_repeater_amount(self, shape, inner, name_id):
376  inner.amount = ast.SifSubtract()
377  inner.amount.lhs = self.process_scalarprocess_scalar(shape.transform.start_opacity, 0.01)
378 
379  inner.amount.rhs = ast.SifScale()
380  inner.amount.rhs.scalar.value = api.ValueReference(name_id)
381 
382  def getter(keyframe):
383  if keyframe is None:
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_extprocess_vector_ext(shape.transform.end_opacity.keyframes, getter)
394 
395  def build_repeater(self, shape, inner, dom_parent):
396  name_id = "duplicate_%s" % next(self.autoidautoid)
397  dup = self._build_repeater_defs_build_repeater_defs(shape, name_id)
398  self._build_repeater_transform_build_repeater_transform(shape, inner, name_id)
399  self._build_repeater_amount_build_repeater_amount(shape, inner, name_id)
400  inner.desc = "Transformation for " + (dom_parent.desc or "duplicate")
401 
402  # duplicate layer
403  duplicate = dom_parent.add_layer(api.DuplicateLayer())
404  duplicate.index = dup
405  duplicate.desc = shape.name
406 
407 
408 def to_sif(animation):
409  builder = SifBuilder()
410  builder.process(animation)
411  return builder.canvas
Simple iterator to generate increasing integers.
Definition: base.py:320
Layer with no data, useful to group layers together.
Definition: layers.py:201
An animatable property that holds a float.
Definition: properties.py:728
def _build_repeater_defs(self, shape, name_id)
Definition: builder.py:314
def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite)
Definition: builder.py:333
def apply_origin(self, sif_shape, lottie_shape)
Definition: builder.py:204
def bezier_point(self, lottie_path, point_index, sif_parent, offset)
Definition: builder.py:252
def process_vector(self, multidim)
Definition: builder.py:130
def __init__(self, gamma=1.0)
Definition: builder.py:31
def build_path(self, type, path, dom_parent, lottie_shape)
Definition: builder.py:242
def process_vector_ext(self, kframes, getter)
Definition: builder.py:140
def _build_repeater_transform(self, shape, inner, name_id)
Definition: builder.py:352
def _get_scale(self, transform)
Definition: builder.py:93
def apply_group_fill(self, sif_shape, fill)
Definition: builder.py:210
def _modifier_inner_group(self, modifier, shapegroup, dom_parent)
Definition: builder.py:300
def _build_repeater_amount(self, shape, inner, name_id)
Definition: builder.py:375
def layer_from_lottie(self, type, lottie, dom_parent)
Definition: builder.py:79
def process_scalar(self, value, mult=None)
Definition: builder.py:169
def set_transform(self, group, transform)
Definition: builder.py:106
def apply_group_stroke(self, sif_shape, stroke)
Definition: builder.py:234
def build_repeater(self, shape, inner, dom_parent)
Definition: builder.py:395
def to_sif(animation)
Definition: builder.py:408