File indexing completed on 2025-01-19 03:59:53

0001 import math
0002 from xml.dom import minidom
0003 
0004 from ... import objects
0005 from ...nvector import NVector
0006 from ...utils import restructure
0007 from . import api, ast
0008 
0009 
0010 blend_modes = {
0011     objects.BlendMode.Normal: api.BlendMethod.Composite,
0012     objects.BlendMode.Multiply: api.BlendMethod.Multiply,
0013     objects.BlendMode.Screen: api.BlendMethod.Screen,
0014     objects.BlendMode.Overlay: api.BlendMethod.Overlay,
0015     objects.BlendMode.Darken: api.BlendMethod.Darken,
0016     objects.BlendMode.Lighten: api.BlendMethod.Lighten,
0017     objects.BlendMode.HardLight: api.BlendMethod.HardLight,
0018     objects.BlendMode.Difference: api.BlendMethod.Difference,
0019     objects.BlendMode.Hue: api.BlendMethod.Hue,
0020     objects.BlendMode.Saturation: api.BlendMethod.Saturation,
0021     objects.BlendMode.Color: api.BlendMethod.Color,
0022     objects.BlendMode.Luminosity: api.BlendMethod.Luminosity,
0023     objects.BlendMode.Exclusion: api.BlendMethod.Difference,
0024     objects.BlendMode.SoftLight: api.BlendMethod.Multiply,
0025     objects.BlendMode.ColorDodge: api.BlendMethod.Composite,
0026     objects.BlendMode.ColorBurn: api.BlendMethod.Composite,
0027 }
0028 
0029 
0030 class SifBuilder(restructure.AbstractBuilder):
0031     def __init__(self, gamma=1.0):
0032         """
0033         @todo Add gamma option to lottie_convert.py
0034         """
0035         super().__init__()
0036         self.canvas = api.Canvas()
0037         self.canvas.version = "1.2"
0038         self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma
0039         self.autoid = objects.base.Index()
0040 
0041     def _on_animation(self, animation: objects.Animation):
0042         if animation.name:
0043             self.canvas.name = animation.name
0044         self.canvas.width = animation.width
0045         self.canvas.height = animation.height
0046         self.canvas.xres = animation.width
0047         self.canvas.yres = animation.height
0048         self.canvas.view_box = NVector(0, 0, animation.width, animation.height)
0049         self.canvas.fps = animation.frame_rate
0050         self.canvas.begin_time = api.FrameTime.frame(animation.in_point)
0051         self.canvas.end_time = api.FrameTime.frame(animation.out_point)
0052         self.canvas.antialias = True
0053         return self.canvas
0054 
0055     def _on_precomp(self, id, dom_parent, layers):
0056         g = dom_parent.add_layer(api.GroupLayer())
0057         g.desc = id
0058         for layer_builder in layers:
0059             self.process_layer(layer_builder, g)
0060 
0061     def _on_layer(self, layer_builder, dom_parent):
0062         layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent)
0063         if not layer_builder.lottie.name:
0064             layer.desc = layer_builder.lottie.__class__.__name__
0065 
0066         bm = getattr(layer_builder.lottie, "blend_mode", None)
0067         if bm is None:
0068             bm = objects.BlendMode.Normal
0069         layer.blend_method = blend_modes[bm]
0070 
0071         layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1
0072 
0073         in_point = getattr(layer_builder.lottie, "in_point", 0)
0074         layer.time_offset.value = api.FrameTime.frame(in_point)
0075 
0076         #layer.canvas.end_time = api.FrameTime.frame(out_point)
0077         return layer
0078 
0079     def layer_from_lottie(self, type, lottie, dom_parent):
0080         g = dom_parent.add_layer(type())
0081         if lottie.name:
0082             g.desc = lottie.name
0083         g.active = not lottie.hidden
0084         transf = getattr(lottie, "transform", None)
0085         if transf:
0086             self.set_transform(g, transf)
0087 
0088         if isinstance(lottie, objects.NullLayer):
0089             g.amount.value = 1
0090 
0091         return g
0092 
0093     def _get_scale(self, transform):
0094         def func(keyframe):
0095             t = keyframe.time if keyframe else 0
0096             scale_x, scale_y = transform.scale.get_value(t)[:2]
0097             scale_x /= 100
0098             scale_y /= 100
0099             skew = transform.skew.get_value(t) if transform.skew else 0
0100             c = math.cos(skew * math.pi / 180)
0101             if c != 0:
0102                 scale_y *= 1 / c
0103             return NVector(scale_x, scale_y)
0104         return func
0105 
0106     def set_transform(self, group, transform):
0107         composite = group.transformation
0108 
0109         if transform.position:
0110             composite.offset = self.process_vector(transform.position)
0111 
0112         if transform.scale:
0113             keyframes = self._merge_keyframes([transform.scale, transform.skew])
0114             composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform))
0115 
0116         composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0))
0117 
0118         if transform.rotation:
0119             composite.angle = self.process_scalar(transform.rotation)
0120 
0121         if transform.opacity:
0122             group.amount = self.process_scalar(transform.opacity, 1/100)
0123 
0124         if transform.anchor_point:
0125             group.origin = self.process_vector(transform.anchor_point)
0126 
0127         # TODO get z_depth from position
0128         composite.z_depth = 0
0129 
0130     def process_vector(self, multidim):
0131         def getter(keyframe):
0132             if keyframe is None:
0133                 v = multidim.value
0134             else:
0135                 v = keyframe.start
0136             return NVector(v[0], v[1])
0137 
0138         return self.process_vector_ext(multidim.keyframes, getter)
0139 
0140     def process_vector_ext(self, kframes, getter):
0141         if kframes is not None:
0142             wrap = ast.SifAnimated()
0143             for i in range(len(kframes)):
0144                 keyframe = kframes[i]
0145                 waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time))
0146 
0147                 if i > 0:
0148                     prev = kframes[i-1]
0149                     if prev.jump:
0150                         waypoint.before = api.Interpolation.Constant
0151                     elif prev.in_value and prev.in_value.x < 1:
0152                         waypoint.before = api.Interpolation.Ease
0153                     else:
0154                         waypoint.before = api.Interpolation.Linear
0155                 else:
0156                     waypoint.before = api.Interpolation.Linear
0157 
0158                 if keyframe.jump:
0159                     waypoint.after = api.Interpolation.Constant
0160                 elif keyframe.out_value and keyframe.out_value.x > 0:
0161                     waypoint.after = api.Interpolation.Ease
0162                 else:
0163                     waypoint.after = api.Interpolation.Linear
0164         else:
0165             wrap = api.SifValue(getter(None))
0166 
0167         return wrap
0168 
0169     def process_scalar(self, value, mult=None):
0170         def getter(keyframe):
0171             if keyframe is None:
0172                 v = value.value
0173             else:
0174                 v = keyframe.start[0]
0175             if mult is not None:
0176                 v *= mult
0177             return v
0178         return self.process_vector_ext(value.keyframes, getter)
0179 
0180     def _on_shape(self, shape, group, dom_parent):
0181         layers = []
0182         if not hasattr(shape, "to_bezier"):
0183             return []
0184 
0185         if group.stroke:
0186             sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape)
0187             self.apply_group_stroke(sif_shape, group.stroke)
0188             layers.append(sif_shape)
0189 
0190         if group.fill:
0191             sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape)
0192             layers.append(sif_shape)
0193             self.apply_group_fill(sif_shape, group.fill)
0194 
0195         return layers
0196 
0197     def _merge_keyframes(self, props):
0198         keyframes = {}
0199         for prop in props:
0200             if prop is not None and prop.animated:
0201                 keyframes.update({kf.time: kf for kf in prop.keyframes})
0202         return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None
0203 
0204     def apply_origin(self, sif_shape, lottie_shape):
0205         if hasattr(lottie_shape, "position"):
0206             sif_shape.origin.value = lottie_shape.position.get_value()
0207         else:
0208             sif_shape.origin.value = lottie_shape.bounding_box().center()
0209 
0210     def apply_group_fill(self, sif_shape, fill):
0211         ## @todo gradients?
0212         if hasattr(fill, "colors"):
0213             return
0214 
0215         def getter(keyframe):
0216             if keyframe is None:
0217                 v = fill.color.value
0218             else:
0219                 v = keyframe.start
0220             return self.canvas.make_color(*v)
0221 
0222         sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter)
0223 
0224         def get_op(keyframe):
0225             if keyframe is None:
0226                 v = fill.opacity.value
0227             else:
0228                 v = keyframe.start[0]
0229             v /= 100
0230             return v
0231 
0232         sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op)
0233 
0234     def apply_group_stroke(self, sif_shape, stroke):
0235         self.apply_group_fill(sif_shape, stroke)
0236         sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter
0237         round_cap = stroke.line_cap == objects.LineCap.Round
0238         sif_shape.round_tip_0.value = round_cap
0239         sif_shape.round_tip_1.value = round_cap
0240         sif_shape.width = self.process_scalar(stroke.width, 0.5)
0241 
0242     def build_path(self, type, path, dom_parent, lottie_shape):
0243         layer = self.layer_from_lottie(type, lottie_shape, dom_parent)
0244         self.apply_origin(layer, lottie_shape)
0245         startbez = path.shape.get_value()
0246         layer.bline.loop = startbez.closed
0247         nverts = len(startbez.vertices)
0248         for point in range(nverts):
0249             self.bezier_point(path, point, layer.bline, layer.origin.value)
0250         return layer
0251 
0252     def bezier_point(self, lottie_path, point_index, sif_parent, offset):
0253         composite = api.BlinePoint()
0254 
0255         def get_point(keyframe):
0256             if keyframe is None:
0257                 bezier = lottie_path.shape.value
0258             else:
0259                 bezier = keyframe.start
0260             if not bezier:
0261                 #elem.parentNode.parentNode.removeChild(elem.parentNode)
0262                 return
0263             vert = bezier.vertices[point_index]
0264             return NVector(vert[0], vert[1]) - offset
0265 
0266         composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point)
0267         composite.split.value = True
0268         composite.split_radius.value = True
0269         composite.split_angle.value = True
0270 
0271         def get_tangent(keyframe):
0272             if keyframe is None:
0273                 bezier = lottie_path.shape.value
0274             else:
0275                 bezier = keyframe.start
0276             if not bezier:
0277                 #elem.parentNode.parentNode.removeChild(elem.parentNode)
0278                 return
0279 
0280             inp = getattr(bezier, which_point)[point_index]
0281             return NVector(inp.x, inp.y) * 3 * mult
0282 
0283         mult = -1
0284         which_point = "in_tangents"
0285         composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
0286 
0287         mult = 1
0288         which_point = "out_tangents"
0289         composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
0290         sif_parent.points.append(composite)
0291 
0292     def _on_shapegroup(self, shape_group, dom_parent):
0293         if shape_group.empty():
0294             return
0295 
0296         layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent)
0297 
0298         self.shapegroup_process_children(shape_group, layer)
0299 
0300     def _modifier_inner_group(self, modifier, shapegroup, dom_parent):
0301         layer = dom_parent.add_layer(api.GroupLayer())
0302         self.shapegroup_process_child(modifier.child, shapegroup, layer)
0303         return layer
0304 
0305     def _on_shape_modifier(self, modifier, shapegroup, dom_parent):
0306         layer = dom_parent.add_layer(api.GroupLayer())
0307         if modifier.lottie.name:
0308             layer.desc = modifier.lottie.name
0309 
0310         inner = self._modifier_inner_group(modifier, shapegroup, layer)
0311         if isinstance(modifier.lottie, objects.Repeater):
0312             self.build_repeater(modifier.lottie, inner, layer)
0313 
0314     def _build_repeater_defs(self, shape, name_id):
0315         dup = api.Duplicate()
0316         dup.id = name_id
0317         self.canvas.defs.append(dup)
0318         self.canvas.register_as(dup, name_id)
0319 
0320         def getter(keyframe):
0321             if keyframe is None:
0322                 v = shape.copies.value
0323             else:
0324                 v = keyframe.start[0]
0325 
0326             return v - 1
0327 
0328         setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter))
0329         dup.to.value = 0
0330         dup.step.value = -1
0331         return dup
0332 
0333     def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite):
0334         power = ast.SifPower()
0335         setattr(scalecomposite, "xy"[comp], power)
0336 
0337         def getter(keyframe):
0338             if keyframe is None:
0339                 v = shape.transform.scale.value
0340             else:
0341                 v = keyframe.start
0342             v = v[comp] / 100
0343             return v
0344 
0345         power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter)
0346 
0347         # HACK work around an issue in Synfig
0348         power.power = ast.SifAdd()
0349         power.power.lhs.value = api.ValueReference(name_id)
0350         power.power.rhs.value = 0.000001
0351 
0352     def _build_repeater_transform(self, shape, inner, name_id):
0353         offset_id = name_id + "_origin"
0354         origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector")
0355         self.canvas.defs.append(origin)
0356         self.canvas.register_as(origin, offset_id)
0357         inner.origin = origin
0358 
0359         composite = inner.transformation
0360 
0361         composite.offset = ast.SifAdd()
0362         composite.offset.rhs.value = api.ValueReference(offset_id)
0363         composite.offset.lhs = ast.SifScale()
0364         composite.offset.lhs.scalar.value = api.ValueReference(name_id)
0365         composite.offset.lhs.link = self.process_vector(shape.transform.position)
0366 
0367         composite.angle = ast.SifScale()
0368         composite.angle.scalar.value = api.ValueReference(name_id)
0369         composite.angle.link = self.process_scalar(shape.transform.rotation)
0370 
0371         composite.scale = ast.SifVectorComposite()
0372         self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale)
0373         self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale)
0374 
0375     def _build_repeater_amount(self, shape, inner, name_id):
0376         inner.amount = ast.SifSubtract()
0377         inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01)
0378 
0379         inner.amount.rhs = ast.SifScale()
0380         inner.amount.rhs.scalar.value = api.ValueReference(name_id)
0381 
0382         def getter(keyframe):
0383             if keyframe is None:
0384                 t = 0
0385                 end = shape.transform.end_opacity.value
0386             else:
0387                 t = keyframe.time
0388                 end = keyframe.start[0]
0389             start = shape.transform.start_opacity.get_value(t)
0390             n = shape.copies.get_value(t)
0391             v = (start - end) / (n - 1) / 100 if n > 0 else 0
0392             return v
0393         inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter)
0394 
0395     def build_repeater(self, shape, inner, dom_parent):
0396         name_id = "duplicate_%s" % next(self.autoid)
0397         dup = self._build_repeater_defs(shape, name_id)
0398         self._build_repeater_transform(shape, inner, name_id)
0399         self._build_repeater_amount(shape, inner, name_id)
0400         inner.desc = "Transformation for " + (dom_parent.desc or "duplicate")
0401 
0402         # duplicate layer
0403         duplicate = dom_parent.add_layer(api.DuplicateLayer())
0404         duplicate.index = dup
0405         duplicate.desc = shape.name
0406 
0407 
0408 def to_sif(animation):
0409     builder = SifBuilder()
0410     builder.process(animation)
0411     return builder.canvas