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