Files
OmniLottie/lottie/parsers/sif/converter.py
T

529 lines
21 KiB
Python
Raw Normal View History

2026-03-01 21:36:54 +08:00
import math
from ... import objects
from ...objects import easing
from . import api, ast
from ... import NVector, PolarVector
try:
from ...utils import font
has_font = True
except ImportError:
has_font = False
def convert(canvas: api.Canvas):
return Converter().convert(canvas)
class Converter:
def __init__(self):
pass
def _animated(self, sifval):
return isinstance(sifval, ast.SifAnimated)
def convert(self, canvas: api.Canvas):
self.canvas = canvas
self.animation = objects.Animation(
self._time(canvas.end_time),
canvas.fps
)
self.animation.in_point = self._time(canvas.begin_time)
self.animation.width = canvas.width
self.animation.height = canvas.height
self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1])
self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3])
self.target_size = NVector(canvas.width, canvas.height)
self.shape_layer = self.animation.add_layer(objects.ShapeLayer())
self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b)
self._process_layers(canvas.layers, self.shape_layer)
return self.animation
def _time(self, t: api.FrameTime):
return self.canvas.time_to_frames(t)
def _process_layers(self, layers, parent):
old_gamma = self.gamma
for layer in reversed(layers):
if not layer.active:
continue
elif isinstance(layer, api.GroupLayerBase):
parent.add_shape(self._convert_group(layer))
elif isinstance(layer, api.RectangleLayer):
parent.add_shape(self._convert_fill(layer, self._convert_rect))
elif isinstance(layer, api.CircleLayer):
parent.add_shape(self._convert_fill(layer, self._convert_circle))
elif isinstance(layer, api.StarLayer):
parent.add_shape(self._convert_fill(layer, self._convert_star))
elif isinstance(layer, api.PolygonLayer):
parent.add_shape(self._convert_fill(layer, self._convert_polygon))
elif isinstance(layer, api.RegionLayer):
parent.add_shape(self._convert_fill(layer, self._convert_bline))
elif isinstance(layer, api.AbstractOutline):
parent.add_shape(self._convert_outline(layer, self._convert_bline))
elif isinstance(layer, api.GradientLayer):
parent.add_shape(self._convert_gradient(layer, parent))
elif isinstance(layer, api.TransformDown):
shape = self._convert_transform_down(layer)
parent.add_shape(shape)
parent = shape
elif isinstance(layer, api.TextLayer):
if has_font:
parent.add_shape(self._convert_fill(layer, self._convert_text))
elif isinstance(layer, api.ColorCorrectLayer):
self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value)
self.gamma = old_gamma
def _convert_group(self, layer: api.GroupLayer):
shape = objects.Group()
self._set_name(shape, layer)
shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin))
self._convert_transform(layer.transformation, shape.transform)
self._process_layers(layer.layers, shape)
shape.transform.opacity = self._adjust_animated(
self._convert_scalar(layer.amount),
lambda x: x*100
)
return shape
def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform):
if isinstance(sif_transform, api.BoneLinkTransform):
base_transform = sif_transform.base_value
else:
base_transform = sif_transform
position = self._adjust_coords(self._convert_vector(base_transform.offset))
rotation = self._adjust_angle(self._convert_scalar(base_transform.angle))
scale = self._adjust_animated(
self._convert_vector(base_transform.scale),
lambda x: x * 100
)
lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle))
if isinstance(sif_transform, api.BoneLinkTransform):
lottie_transform.position = position
lottie_transform.rotation = rotation
lottie_transform.scale = scale
#bone = sif_transform.bone
#b_pos = self._adjust_coords(self._convert_vector(bone.origin))
#old_anchor = lottie_transform.anchor_point
#if sif_transform.translate:
#self._mix_animations_into(
#[position, b_pos, old_anchor],
#lottie_transform.position,
#lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2
#)
#else:
#lottie_transform.position = position
#lottie_transform.anchor_point = b_pos
#lottie_transform.anchor_point.value += NVector(100,0)
#if sif_transform.rotate:
#b_rot = self._convert_scalar(bone.angle)
#self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b)
#else:
#lottie_transform.rotation = rotation
#if sif_transform.scale_y:
#b_scale = self._convert_scalar(bone.scalelx)
#self._mix_animations_into(
#scale, b_scale, lottie_transform.scale,
#lambda a, b: NVector(a.x, a.y * b)
#)
#else:
#lottie_transform.scale = scale
else:
lottie_transform.position = position
lottie_transform.rotation = rotation
lottie_transform.scale = scale
def _mix_animations_into(self, animations, output, mix):
if not any(x.animated for x in animations):
output.value = mix(*(x.value for x in animations))
else:
for vals in self._mix_animations(*animations):
time = vals.pop(0)
output.add_keyframe(time, mix(*vals))
def _convert_fill(self, layer, converter):
shape = objects.Group()
self._set_name(shape, layer)
shape.add_shape(converter(layer))
if layer.invert.value:
shape.add_shape(objects.Rect(self.target_size/2, self.target_size))
fill = objects.Fill()
fill.color = self._convert_color(layer.color)
fill.opacity = self._adjust_animated(
self._convert_scalar(layer.amount),
lambda x: x * 100
)
shape.add_shape(fill)
return shape
def _convert_linecap(self, lc: api.LineCap):
if lc == api.LineCap.Rounded:
return objects.LineCap.Round
if lc == api.LineCap.Squared:
return objects.LineCap.Square
return objects.LineCap.Butt
def _convert_cusp(self, lc: api.CuspStyle):
if lc == api.CuspStyle.Miter:
return objects.LineJoin.Miter
if lc == api.CuspStyle.Bevel:
return objects.LineJoin.Bevel
return objects.LineJoin.Round
def _convert_outline(self, layer: api.AbstractOutline, converter):
shape = objects.Group()
self._set_name(shape, layer)
shape.add_shape(converter(layer))
stroke = objects.Stroke()
stroke.color = self._convert_color(layer.color)
stroke.line_cap = self._convert_linecap(layer.start_tip)
stroke.line_join = self._convert_cusp(layer.cusp_type)
stroke.width = self._adjust_scalar(self._convert_scalar(layer.width))
shape.add_shape(stroke)
return shape
def _convert_rect(self, layer: api.RectangleLayer):
rect = objects.Rect()
p1 = self._adjust_coords(self._convert_vector(layer.point1))
p2 = self._adjust_coords(self._convert_vector(layer.point2))
if p1.animated or p2.animated:
for time, p1v, p2v in self._mix_animations(p1, p2):
rect.position.add_keyframe(time, (p1v + p2v) / 2)
rect.size.add_keyframe(time, abs(p2v - p1v))
pass
else:
rect.position.value = (p1.value + p2.value) / 2
rect.size.value = abs(p2.value - p1.value)
rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel))
return rect
def _convert_circle(self, layer: api.CircleLayer):
shape = objects.Ellipse()
shape.position = self._adjust_coords(self._convert_vector(layer.origin))
radius = self._adjust_scalar(self._convert_scalar(layer.radius))
shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2)
return shape
def _convert_star(self, layer: api.StarLayer):
shape = objects.Star()
shape.position = self._adjust_coords(self._convert_vector(layer.origin))
shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2))
shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1))
shape.rotation = self._adjust_animated(
self._convert_scalar(layer.angle),
lambda x: 90-x
)
shape.points = self._convert_scalar(layer.points)
if layer.regular_polygon.value:
shape.star_type = objects.StarType.Polygon
return shape
def _mix_animations(self, *animatable):
times = set()
for v in animatable:
self._force_animated(v)
for kf in v.keyframes:
times.add(kf.time)
for time in sorted(times):
yield [time] + [v.get_value(time) for v in animatable]
def _force_animated(self, lottieval):
if not lottieval.animated:
v = lottieval.value
lottieval.add_keyframe(0, v)
lottieval.add_keyframe(self.animation.out_point, v)
def _convert_easing_part(self, interp: api.Interpolation):
if interp == api.Interpolation.Linear:
return easing.Linear()
return easing.Sigmoid()
def _convert_easing(self, start: api.Interpolation, end: api.Interpolation):
if api.Interpolation.Constant in (start, end):
return easing.Jump()
if start == end:
return self._convert_easing_part(start)
return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end))
def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin):
if self._animated(v):
if len(v.keyframes) == 1:
lot.value = self._convert_ast_value(v.keyframes[0].value)
else:
for i, kf in enumerate(v.keyframes):
if i+1 < len(v.keyframes):
start = kf.after
end = v.keyframes[i+1].before
ease = self._convert_easing(start, end)
else:
ease = easing.Linear()
lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease)
else:
lot.value = self._convert_ast_value(v)
return lot
def _convert_ast_value(self, v):
if isinstance(v, ast.SifRadialComposite):
return self._polar(v.radius.value, v.theta.value, 1)
elif isinstance(v, ast.SifValue):
return v.value
elif isinstance(v, ast.SifVectorComposite):
return NVector(v.x.value, v.y.value)
else:
return v
def _converted_vector_values(self, v):
if isinstance(v, ast.SifRadialComposite):
return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)]
return self._convert_vector(v)
def _convert_color(self, v: ast.SifAstNode):
return self._adjust_animated(
self._convert_animatable(v, objects.ColorValue()),
self._color_gamma
)
def _convert_vector(self, v: ast.SifAstNode):
return self._convert_animatable(v, objects.MultiDimensional())
def _convert_scalar(self, v: ast.SifAstNode):
return self._convert_animatable(v, objects.Value())
def _color_gamma(self, color):
color = color.clone()
for i in range(3):
color[i] = color[i] ** (1/self.gamma[i])
return color
def _adjust_animated(self, lottieval, transform):
if lottieval.animated:
for kf in lottieval.keyframes:
if kf.start is not None:
kf.start = transform(kf.start)
if kf.end is not None:
kf.end = transform(kf.end)
else:
lottieval.value = transform(lottieval.value)
return lottieval
def _adjust_scalar(self, lottieval: objects.Value):
return self._adjust_animated(lottieval, self._scalar_mult)
def _adjust_angle(self, lottieval: objects.Value):
return self._adjust_animated(lottieval, lambda x: -x)
def _adjust_add_dimension(self, lottieval, transform):
to_val = objects.MultiDimensional()
to_val.animated = lottieval.animated
if lottieval.animated:
to_val.keyframes = []
for kf in lottieval.keyframes:
if kf.start is not None:
kf.start = transform(kf.start[0])
if kf.end is not None:
kf.end = transform(kf.end[0])
to_val.keyframes.append(kf)
else:
to_val.value = transform(lottieval.value)
return to_val
def _scalar_mult(self, x):
return x * 60
def _adjust_coords(self, lottieval: objects.MultiDimensional):
return self._adjust_animated(lottieval, self._coord)
def _coord(self, val: NVector):
return NVector(
self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5),
self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5),
)
def _convert_polygon(self, layer: api.PolygonLayer):
lot = objects.Path()
animatables = [self._convert_vector(layer.origin)] + [
self._convert_vector(p)
for p in layer.points
]
animated = any(x.animated for x in animatables)
if not animated:
lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value)
else:
for values in self._mix_animations(*animatables):
time = values[0]
origin = values[1]
points = values[2:]
lot.shape.add_keyframe(time, self._polygon(points, origin))
return lot
def _polygon(self, points, origin):
bezier = objects.Bezier()
bezier.closed = True
for point in points:
bezier.add_point(self._coord(point+origin))
return bezier
def _convert_bline(self, layer: api.AbstractOutline):
lot = objects.Path()
closed = layer.bline.loop
animatables = [
self._convert_vector(layer.origin)
]
for p in layer.bline.points:
animatables += [
self._convert_vector(p.point),
self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0),
self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0),
self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0),
self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0)
]
animated = any(x.animated for x in animatables)
if not animated:
lot.shape.value = self._bezier(
closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points
)
else:
for values in self._mix_animations(*animatables):
time = values[0]
origin = values[1]
values = values[2:]
lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points))
return lot
def _bezier(self, closed, values, origin, points):
chunk_size = 5
bezier = objects.Bezier()
bezier.closed = closed
for i in range(0, len(values), chunk_size):
point, r1, a1, r2, a2 = values[i:i+chunk_size]
sifvert = point+origin
vert = self._coord(sifvert)
if not points[i//chunk_size].split_radius.value:
r2 = r1
if not points[i//chunk_size].split_angle.value:
a2 = a1
t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert
t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert
bezier.add_point(vert, t1, t2)
return bezier
def _polar(self, radius, angle, dir):
offset_angle = 0
if dir == 1:
offset_angle += 180
return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180)
def _convert_transform_down(self, tl: api.TransformDown):
group = objects.Group()
self._set_name(group, tl)
if isinstance(tl, api.TranslateLayer):
group.transform.anchor_point.value = self.target_size / 2
group.transform.position = self._adjust_coords(self._convert_vector(tl.origin))
elif isinstance(tl, api.RotateLayer):
group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin))
group.transform.position = group.transform.anchor_point.clone()
group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount))
elif isinstance(tl, api.ScaleLayer):
group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center))
group.transform.position = group.transform.anchor_point.clone()
group.transform.scale = self._adjust_add_dimension(
self._convert_scalar(tl.amount),
self._zoom_to_scale
)
return group
def _zoom_to_scale(self, value):
zoom = math.e ** value * 100
return NVector(zoom, zoom)
def _set_name(self, lottie, sif):
lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__
def _convert_gradient(self, layer: api.GradientLayer, parent):
group = objects.Group()
parent_shapes = parent.shapes
parent.shapes = []
if isinstance(parent, objects.Group):
parent.shapes.append(parent_shapes[-1])
self._gradient_gather_shapes(parent_shapes, group)
gradient = objects.GradientFill()
self._set_name(gradient, layer)
group.add_shape(gradient)
gradient.colors = self._convert_gradient_stops(layer.gradient)
gradient.opacity = self._adjust_animated(
self._convert_scalar(layer.amount),
lambda x: x * 100
)
if isinstance(layer, api.LinearGradient):
gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1))
gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2))
gradient.gradient_type = objects.GradientType.Linear
elif isinstance(layer, api.RadialGradient):
gradient.gradient_type = objects.GradientType.Radial
gradient.start_point = self._adjust_coords(self._convert_vector(layer.center))
radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45)
if not radius.animated and not gradient.start_point.animated:
gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value)
else:
for time, c, r in self._mix_animations(gradient.start_point.clone(), radius):
gradient.end_point.add_keyframe(time, c + NVector(r + r))
return group
def _gradient_gather_shapes(self, shapes, output: objects.Group):
for shape in shapes:
if isinstance(shape, objects.Shape):
output.add_shape(shape)
elif isinstance(shape, objects.Group):
self._gradient_gather_shapes(shape.shapes, output)
def _convert_gradient_stops(self, sif_gradient):
stops = objects.GradientColors()
if not self._animated(sif_gradient):
stops.set_stops(self._flatten_gradient_colors(sif_gradient.value))
stops.count = len(sif_gradient.value)
else:
# TODO easing
for kf in sif_gradient.keyframes:
stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value))
stops.count = len(kf.value)
return stops
def _flatten_gradient_colors(self, stops):
return [
(stop.pos, self._color_gamma(stop.color))
for stop in stops
]
def _convert_text(self, layer: api.TextLayer):
shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center))
shape.refresh()
trans = shape.wrapped.transform
trans.anchor_point.value = shape.wrapped.bounding_box().center()
trans.anchor_point.value.x /= 2
trans.position = self._adjust_coords(self._convert_vector(layer.origin))
trans.scale = self._adjust_animated(
self._convert_vector(layer.size),
lambda v: v * 100
)
return shape