mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 15:46:26 +00:00
412 lines
15 KiB
Python
412 lines
15 KiB
Python
import math
|
|
from xml.dom import minidom
|
|
|
|
from ... import objects
|
|
from ...nvector import NVector
|
|
from ...utils import restructure
|
|
from . import api, ast
|
|
|
|
|
|
blend_modes = {
|
|
objects.BlendMode.Normal: api.BlendMethod.Composite,
|
|
objects.BlendMode.Multiply: api.BlendMethod.Multiply,
|
|
objects.BlendMode.Screen: api.BlendMethod.Screen,
|
|
objects.BlendMode.Overlay: api.BlendMethod.Overlay,
|
|
objects.BlendMode.Darken: api.BlendMethod.Darken,
|
|
objects.BlendMode.Lighten: api.BlendMethod.Lighten,
|
|
objects.BlendMode.HardLight: api.BlendMethod.HardLight,
|
|
objects.BlendMode.Difference: api.BlendMethod.Difference,
|
|
objects.BlendMode.Hue: api.BlendMethod.Hue,
|
|
objects.BlendMode.Saturation: api.BlendMethod.Saturation,
|
|
objects.BlendMode.Color: api.BlendMethod.Color,
|
|
objects.BlendMode.Luminosity: api.BlendMethod.Luminosity,
|
|
objects.BlendMode.Exclusion: api.BlendMethod.Difference,
|
|
objects.BlendMode.SoftLight: api.BlendMethod.Multiply,
|
|
objects.BlendMode.ColorDodge: api.BlendMethod.Composite,
|
|
objects.BlendMode.ColorBurn: api.BlendMethod.Composite,
|
|
}
|
|
|
|
|
|
class SifBuilder(restructure.AbstractBuilder):
|
|
def __init__(self, gamma=1.0):
|
|
"""
|
|
@todo Add gamma option to lottie_convert.py
|
|
"""
|
|
super().__init__()
|
|
self.canvas = api.Canvas()
|
|
self.canvas.version = "1.2"
|
|
self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma
|
|
self.autoid = objects.base.Index()
|
|
|
|
def _on_animation(self, animation: objects.Animation):
|
|
if animation.name:
|
|
self.canvas.name = animation.name
|
|
self.canvas.width = animation.width
|
|
self.canvas.height = animation.height
|
|
self.canvas.xres = animation.width
|
|
self.canvas.yres = animation.height
|
|
self.canvas.view_box = NVector(0, 0, animation.width, animation.height)
|
|
self.canvas.fps = animation.frame_rate
|
|
self.canvas.begin_time = api.FrameTime.frame(animation.in_point)
|
|
self.canvas.end_time = api.FrameTime.frame(animation.out_point)
|
|
self.canvas.antialias = True
|
|
return self.canvas
|
|
|
|
def _on_precomp(self, id, dom_parent, layers):
|
|
g = dom_parent.add_layer(api.GroupLayer())
|
|
g.desc = id
|
|
for layer_builder in layers:
|
|
self.process_layer(layer_builder, g)
|
|
|
|
def _on_layer(self, layer_builder, dom_parent):
|
|
layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent)
|
|
if not layer_builder.lottie.name:
|
|
layer.desc = layer_builder.lottie.__class__.__name__
|
|
|
|
bm = getattr(layer_builder.lottie, "blend_mode", None)
|
|
if bm is None:
|
|
bm = objects.BlendMode.Normal
|
|
layer.blend_method = blend_modes[bm]
|
|
|
|
layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1
|
|
|
|
in_point = getattr(layer_builder.lottie, "in_point", 0)
|
|
layer.time_offset.value = api.FrameTime.frame(in_point)
|
|
|
|
#layer.canvas.end_time = api.FrameTime.frame(out_point)
|
|
return layer
|
|
|
|
def layer_from_lottie(self, type, lottie, dom_parent):
|
|
g = dom_parent.add_layer(type())
|
|
if lottie.name:
|
|
g.desc = lottie.name
|
|
g.active = not lottie.hidden
|
|
transf = getattr(lottie, "transform", None)
|
|
if transf:
|
|
self.set_transform(g, transf)
|
|
|
|
if isinstance(lottie, objects.NullLayer):
|
|
g.amount.value = 1
|
|
|
|
return g
|
|
|
|
def _get_scale(self, transform):
|
|
def func(keyframe):
|
|
t = keyframe.time if keyframe else 0
|
|
scale_x, scale_y = transform.scale.get_value(t)[:2]
|
|
scale_x /= 100
|
|
scale_y /= 100
|
|
skew = transform.skew.get_value(t) if transform.skew else 0
|
|
c = math.cos(skew * math.pi / 180)
|
|
if c != 0:
|
|
scale_y *= 1 / c
|
|
return NVector(scale_x, scale_y)
|
|
return func
|
|
|
|
def set_transform(self, group, transform):
|
|
composite = group.transformation
|
|
|
|
if transform.position:
|
|
composite.offset = self.process_vector(transform.position)
|
|
|
|
if transform.scale:
|
|
keyframes = self._merge_keyframes([transform.scale, transform.skew])
|
|
composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform))
|
|
|
|
composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0))
|
|
|
|
if transform.rotation:
|
|
composite.angle = self.process_scalar(transform.rotation)
|
|
|
|
if transform.opacity:
|
|
group.amount = self.process_scalar(transform.opacity, 1/100)
|
|
|
|
if transform.anchor_point:
|
|
group.origin = self.process_vector(transform.anchor_point)
|
|
|
|
# TODO get z_depth from position
|
|
composite.z_depth = 0
|
|
|
|
def process_vector(self, multidim):
|
|
def getter(keyframe):
|
|
if keyframe is None:
|
|
v = multidim.value
|
|
else:
|
|
v = keyframe.start
|
|
return NVector(v[0], v[1])
|
|
|
|
return self.process_vector_ext(multidim.keyframes, getter)
|
|
|
|
def process_vector_ext(self, kframes, getter):
|
|
if kframes is not None:
|
|
wrap = ast.SifAnimated()
|
|
for i in range(len(kframes)):
|
|
keyframe = kframes[i]
|
|
waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time))
|
|
|
|
if i > 0:
|
|
prev = kframes[i-1]
|
|
if prev.jump:
|
|
waypoint.before = api.Interpolation.Constant
|
|
elif prev.in_value and prev.in_value.x < 1:
|
|
waypoint.before = api.Interpolation.Ease
|
|
else:
|
|
waypoint.before = api.Interpolation.Linear
|
|
else:
|
|
waypoint.before = api.Interpolation.Linear
|
|
|
|
if keyframe.jump:
|
|
waypoint.after = api.Interpolation.Constant
|
|
elif keyframe.out_value and keyframe.out_value.x > 0:
|
|
waypoint.after = api.Interpolation.Ease
|
|
else:
|
|
waypoint.after = api.Interpolation.Linear
|
|
else:
|
|
wrap = api.SifValue(getter(None))
|
|
|
|
return wrap
|
|
|
|
def process_scalar(self, value, mult=None):
|
|
def getter(keyframe):
|
|
if keyframe is None:
|
|
v = value.value
|
|
else:
|
|
v = keyframe.start[0]
|
|
if mult is not None:
|
|
v *= mult
|
|
return v
|
|
return self.process_vector_ext(value.keyframes, getter)
|
|
|
|
def _on_shape(self, shape, group, dom_parent):
|
|
layers = []
|
|
if not hasattr(shape, "to_bezier"):
|
|
return []
|
|
|
|
if group.stroke:
|
|
sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape)
|
|
self.apply_group_stroke(sif_shape, group.stroke)
|
|
layers.append(sif_shape)
|
|
|
|
if group.fill:
|
|
sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape)
|
|
layers.append(sif_shape)
|
|
self.apply_group_fill(sif_shape, group.fill)
|
|
|
|
return layers
|
|
|
|
def _merge_keyframes(self, props):
|
|
keyframes = {}
|
|
for prop in props:
|
|
if prop is not None and prop.animated:
|
|
keyframes.update({kf.time: kf for kf in prop.keyframes})
|
|
return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None
|
|
|
|
def apply_origin(self, sif_shape, lottie_shape):
|
|
if hasattr(lottie_shape, "position"):
|
|
sif_shape.origin.value = lottie_shape.position.get_value()
|
|
else:
|
|
sif_shape.origin.value = lottie_shape.bounding_box().center()
|
|
|
|
def apply_group_fill(self, sif_shape, fill):
|
|
## @todo gradients?
|
|
if hasattr(fill, "colors"):
|
|
return
|
|
|
|
def getter(keyframe):
|
|
if keyframe is None:
|
|
v = fill.color.value
|
|
else:
|
|
v = keyframe.start
|
|
return self.canvas.make_color(*v)
|
|
|
|
sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter)
|
|
|
|
def get_op(keyframe):
|
|
if keyframe is None:
|
|
v = fill.opacity.value
|
|
else:
|
|
v = keyframe.start[0]
|
|
v /= 100
|
|
return v
|
|
|
|
sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op)
|
|
|
|
def apply_group_stroke(self, sif_shape, stroke):
|
|
self.apply_group_fill(sif_shape, stroke)
|
|
sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter
|
|
round_cap = stroke.line_cap == objects.LineCap.Round
|
|
sif_shape.round_tip_0.value = round_cap
|
|
sif_shape.round_tip_1.value = round_cap
|
|
sif_shape.width = self.process_scalar(stroke.width, 0.5)
|
|
|
|
def build_path(self, type, path, dom_parent, lottie_shape):
|
|
layer = self.layer_from_lottie(type, lottie_shape, dom_parent)
|
|
self.apply_origin(layer, lottie_shape)
|
|
startbez = path.shape.get_value()
|
|
layer.bline.loop = startbez.closed
|
|
nverts = len(startbez.vertices)
|
|
for point in range(nverts):
|
|
self.bezier_point(path, point, layer.bline, layer.origin.value)
|
|
return layer
|
|
|
|
def bezier_point(self, lottie_path, point_index, sif_parent, offset):
|
|
composite = api.BlinePoint()
|
|
|
|
def get_point(keyframe):
|
|
if keyframe is None:
|
|
bezier = lottie_path.shape.value
|
|
else:
|
|
bezier = keyframe.start
|
|
if not bezier:
|
|
#elem.parentNode.parentNode.removeChild(elem.parentNode)
|
|
return
|
|
vert = bezier.vertices[point_index]
|
|
return NVector(vert[0], vert[1]) - offset
|
|
|
|
composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point)
|
|
composite.split.value = True
|
|
composite.split_radius.value = True
|
|
composite.split_angle.value = True
|
|
|
|
def get_tangent(keyframe):
|
|
if keyframe is None:
|
|
bezier = lottie_path.shape.value
|
|
else:
|
|
bezier = keyframe.start
|
|
if not bezier:
|
|
#elem.parentNode.parentNode.removeChild(elem.parentNode)
|
|
return
|
|
|
|
inp = getattr(bezier, which_point)[point_index]
|
|
return NVector(inp.x, inp.y) * 3 * mult
|
|
|
|
mult = -1
|
|
which_point = "in_tangents"
|
|
composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
|
|
|
|
mult = 1
|
|
which_point = "out_tangents"
|
|
composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
|
|
sif_parent.points.append(composite)
|
|
|
|
def _on_shapegroup(self, shape_group, dom_parent):
|
|
if shape_group.empty():
|
|
return
|
|
|
|
layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent)
|
|
|
|
self.shapegroup_process_children(shape_group, layer)
|
|
|
|
def _modifier_inner_group(self, modifier, shapegroup, dom_parent):
|
|
layer = dom_parent.add_layer(api.GroupLayer())
|
|
self.shapegroup_process_child(modifier.child, shapegroup, layer)
|
|
return layer
|
|
|
|
def _on_shape_modifier(self, modifier, shapegroup, dom_parent):
|
|
layer = dom_parent.add_layer(api.GroupLayer())
|
|
if modifier.lottie.name:
|
|
layer.desc = modifier.lottie.name
|
|
|
|
inner = self._modifier_inner_group(modifier, shapegroup, layer)
|
|
if isinstance(modifier.lottie, objects.Repeater):
|
|
self.build_repeater(modifier.lottie, inner, layer)
|
|
|
|
def _build_repeater_defs(self, shape, name_id):
|
|
dup = api.Duplicate()
|
|
dup.id = name_id
|
|
self.canvas.defs.append(dup)
|
|
self.canvas.register_as(dup, name_id)
|
|
|
|
def getter(keyframe):
|
|
if keyframe is None:
|
|
v = shape.copies.value
|
|
else:
|
|
v = keyframe.start[0]
|
|
|
|
return v - 1
|
|
|
|
setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter))
|
|
dup.to.value = 0
|
|
dup.step.value = -1
|
|
return dup
|
|
|
|
def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite):
|
|
power = ast.SifPower()
|
|
setattr(scalecomposite, "xy"[comp], power)
|
|
|
|
def getter(keyframe):
|
|
if keyframe is None:
|
|
v = shape.transform.scale.value
|
|
else:
|
|
v = keyframe.start
|
|
v = v[comp] / 100
|
|
return v
|
|
|
|
power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter)
|
|
|
|
# HACK work around an issue in Synfig
|
|
power.power = ast.SifAdd()
|
|
power.power.lhs.value = api.ValueReference(name_id)
|
|
power.power.rhs.value = 0.000001
|
|
|
|
def _build_repeater_transform(self, shape, inner, name_id):
|
|
offset_id = name_id + "_origin"
|
|
origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector")
|
|
self.canvas.defs.append(origin)
|
|
self.canvas.register_as(origin, offset_id)
|
|
inner.origin = origin
|
|
|
|
composite = inner.transformation
|
|
|
|
composite.offset = ast.SifAdd()
|
|
composite.offset.rhs.value = api.ValueReference(offset_id)
|
|
composite.offset.lhs = ast.SifScale()
|
|
composite.offset.lhs.scalar.value = api.ValueReference(name_id)
|
|
composite.offset.lhs.link = self.process_vector(shape.transform.position)
|
|
|
|
composite.angle = ast.SifScale()
|
|
composite.angle.scalar.value = api.ValueReference(name_id)
|
|
composite.angle.link = self.process_scalar(shape.transform.rotation)
|
|
|
|
composite.scale = ast.SifVectorComposite()
|
|
self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale)
|
|
self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale)
|
|
|
|
def _build_repeater_amount(self, shape, inner, name_id):
|
|
inner.amount = ast.SifSubtract()
|
|
inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01)
|
|
|
|
inner.amount.rhs = ast.SifScale()
|
|
inner.amount.rhs.scalar.value = api.ValueReference(name_id)
|
|
|
|
def getter(keyframe):
|
|
if keyframe is None:
|
|
t = 0
|
|
end = shape.transform.end_opacity.value
|
|
else:
|
|
t = keyframe.time
|
|
end = keyframe.start[0]
|
|
start = shape.transform.start_opacity.get_value(t)
|
|
n = shape.copies.get_value(t)
|
|
v = (start - end) / (n - 1) / 100 if n > 0 else 0
|
|
return v
|
|
inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter)
|
|
|
|
def build_repeater(self, shape, inner, dom_parent):
|
|
name_id = "duplicate_%s" % next(self.autoid)
|
|
dup = self._build_repeater_defs(shape, name_id)
|
|
self._build_repeater_transform(shape, inner, name_id)
|
|
self._build_repeater_amount(shape, inner, name_id)
|
|
inner.desc = "Transformation for " + (dom_parent.desc or "duplicate")
|
|
|
|
# duplicate layer
|
|
duplicate = dom_parent.add_layer(api.DuplicateLayer())
|
|
duplicate.index = dup
|
|
duplicate.desc = shape.name
|
|
|
|
|
|
def to_sif(animation):
|
|
builder = SifBuilder()
|
|
builder.process(animation)
|
|
return builder.canvas
|