File indexing completed on 2025-04-27 04:01:14

0001 import math
0002 from ... import objects
0003 from ...objects import easing
0004 from . import api, ast
0005 from ... import NVector, PolarVector
0006 
0007 try:
0008     from ...utils import font
0009     has_font = True
0010 except ImportError:
0011     has_font = False
0012 
0013 
0014 def convert(canvas: api.Canvas):
0015     return Converter().convert(canvas)
0016 
0017 
0018 class Converter:
0019     def __init__(self):
0020         pass
0021 
0022     def _animated(self, sifval):
0023         return isinstance(sifval, ast.SifAnimated)
0024 
0025     def convert(self, canvas: api.Canvas):
0026         self.canvas = canvas
0027         self.animation = objects.Animation(
0028             self._time(canvas.end_time),
0029             canvas.fps
0030         )
0031         self.animation.in_point = self._time(canvas.begin_time)
0032         self.animation.width = canvas.width
0033         self.animation.height = canvas.height
0034         self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1])
0035         self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3])
0036         self.target_size = NVector(canvas.width, canvas.height)
0037         self.shape_layer = self.animation.add_layer(objects.ShapeLayer())
0038         self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b)
0039         self._process_layers(canvas.layers, self.shape_layer)
0040         return self.animation
0041 
0042     def _time(self, t: api.FrameTime):
0043         return self.canvas.time_to_frames(t)
0044 
0045     def _process_layers(self, layers, parent):
0046         old_gamma = self.gamma
0047 
0048         for layer in reversed(layers):
0049             if not layer.active:
0050                 continue
0051             elif isinstance(layer, api.GroupLayerBase):
0052                 parent.add_shape(self._convert_group(layer))
0053             elif isinstance(layer, api.RectangleLayer):
0054                 parent.add_shape(self._convert_fill(layer, self._convert_rect))
0055             elif isinstance(layer, api.CircleLayer):
0056                 parent.add_shape(self._convert_fill(layer, self._convert_circle))
0057             elif isinstance(layer, api.StarLayer):
0058                 parent.add_shape(self._convert_fill(layer, self._convert_star))
0059             elif isinstance(layer, api.PolygonLayer):
0060                 parent.add_shape(self._convert_fill(layer, self._convert_polygon))
0061             elif isinstance(layer, api.RegionLayer):
0062                 parent.add_shape(self._convert_fill(layer, self._convert_bline))
0063             elif isinstance(layer, api.AbstractOutline):
0064                 parent.add_shape(self._convert_outline(layer, self._convert_bline))
0065             elif isinstance(layer, api.GradientLayer):
0066                 parent.add_shape(self._convert_gradient(layer, parent))
0067             elif isinstance(layer, api.TransformDown):
0068                 shape = self._convert_transform_down(layer)
0069                 parent.add_shape(shape)
0070                 parent = shape
0071             elif isinstance(layer, api.TextLayer):
0072                 if has_font:
0073                     parent.add_shape(self._convert_fill(layer, self._convert_text))
0074             elif isinstance(layer, api.ColorCorrectLayer):
0075                 self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value)
0076 
0077         self.gamma = old_gamma
0078 
0079     def _convert_group(self, layer: api.GroupLayer):
0080         shape = objects.Group()
0081         self._set_name(shape, layer)
0082         shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin))
0083         self._convert_transform(layer.transformation, shape.transform)
0084         self._process_layers(layer.layers, shape)
0085         shape.transform.opacity = self._adjust_animated(
0086             self._convert_scalar(layer.amount),
0087             lambda x: x*100
0088         )
0089         return shape
0090 
0091     def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform):
0092         if isinstance(sif_transform, api.BoneLinkTransform):
0093             base_transform = sif_transform.base_value
0094         else:
0095             base_transform = sif_transform
0096 
0097         position = self._adjust_coords(self._convert_vector(base_transform.offset))
0098         rotation = self._adjust_angle(self._convert_scalar(base_transform.angle))
0099         scale = self._adjust_animated(
0100             self._convert_vector(base_transform.scale),
0101             lambda x: x * 100
0102         )
0103 
0104         lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle))
0105 
0106         if isinstance(sif_transform, api.BoneLinkTransform):
0107             lottie_transform.position = position
0108             lottie_transform.rotation = rotation
0109             lottie_transform.scale = scale
0110             #bone = sif_transform.bone
0111             #b_pos = self._adjust_coords(self._convert_vector(bone.origin))
0112             #old_anchor = lottie_transform.anchor_point
0113 
0114             #if sif_transform.translate:
0115                 #self._mix_animations_into(
0116                     #[position, b_pos, old_anchor],
0117                     #lottie_transform.position,
0118                     #lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2
0119                 #)
0120             #else:
0121                 #lottie_transform.position = position
0122 
0123             #lottie_transform.anchor_point = b_pos
0124             #lottie_transform.anchor_point.value += NVector(100,0)
0125 
0126             #if sif_transform.rotate:
0127                 #b_rot = self._convert_scalar(bone.angle)
0128                 #self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b)
0129             #else:
0130                 #lottie_transform.rotation = rotation
0131 
0132             #if sif_transform.scale_y:
0133                 #b_scale = self._convert_scalar(bone.scalelx)
0134                 #self._mix_animations_into(
0135                     #scale, b_scale, lottie_transform.scale,
0136                     #lambda a, b: NVector(a.x, a.y * b)
0137                 #)
0138             #else:
0139                 #lottie_transform.scale = scale
0140         else:
0141             lottie_transform.position = position
0142             lottie_transform.rotation = rotation
0143             lottie_transform.scale = scale
0144 
0145     def _mix_animations_into(self, animations, output, mix):
0146         if not any(x.animated for x in animations):
0147             output.value = mix(*(x.value for x in animations))
0148         else:
0149             for vals in self._mix_animations(*animations):
0150                 time = vals.pop(0)
0151                 output.add_keyframe(time, mix(*vals))
0152 
0153     def _convert_fill(self, layer, converter):
0154         shape = objects.Group()
0155         self._set_name(shape, layer)
0156         shape.add_shape(converter(layer))
0157         if layer.invert.value:
0158             shape.add_shape(objects.Rect(self.target_size/2, self.target_size))
0159 
0160         fill = objects.Fill()
0161         fill.color = self._convert_color(layer.color)
0162         fill.opacity = self._adjust_animated(
0163             self._convert_scalar(layer.amount),
0164             lambda x: x * 100
0165         )
0166         shape.add_shape(fill)
0167         return shape
0168 
0169     def _convert_linecap(self, lc: api.LineCap):
0170         if lc == api.LineCap.Rounded:
0171             return objects.LineCap.Round
0172         if lc == api.LineCap.Squared:
0173             return objects.LineCap.Square
0174         return objects.LineCap.Butt
0175 
0176     def _convert_cusp(self, lc: api.CuspStyle):
0177         if lc == api.CuspStyle.Miter:
0178             return objects.LineJoin.Miter
0179         if lc == api.CuspStyle.Bevel:
0180             return objects.LineJoin.Bevel
0181         return objects.LineJoin.Round
0182 
0183     def _convert_outline(self, layer: api.AbstractOutline, converter):
0184         shape = objects.Group()
0185         self._set_name(shape, layer)
0186         shape.add_shape(converter(layer))
0187         stroke = objects.Stroke()
0188         stroke.color = self._convert_color(layer.color)
0189         stroke.line_cap = self._convert_linecap(layer.start_tip)
0190         stroke.line_join = self._convert_cusp(layer.cusp_type)
0191         stroke.width = self._adjust_scalar(self._convert_scalar(layer.width))
0192         shape.add_shape(stroke)
0193         return shape
0194 
0195     def _convert_rect(self, layer: api.RectangleLayer):
0196         rect = objects.Rect()
0197         p1 = self._adjust_coords(self._convert_vector(layer.point1))
0198         p2 = self._adjust_coords(self._convert_vector(layer.point2))
0199         if p1.animated or p2.animated:
0200             for time, p1v, p2v in self._mix_animations(p1, p2):
0201                 rect.position.add_keyframe(time, (p1v + p2v) / 2)
0202                 rect.size.add_keyframe(time, abs(p2v - p1v))
0203             pass
0204         else:
0205             rect.position.value = (p1.value + p2.value) / 2
0206             rect.size.value = abs(p2.value - p1.value)
0207         rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel))
0208         return rect
0209 
0210     def _convert_circle(self, layer: api.CircleLayer):
0211         shape = objects.Ellipse()
0212         shape.position = self._adjust_coords(self._convert_vector(layer.origin))
0213         radius = self._adjust_scalar(self._convert_scalar(layer.radius))
0214         shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2)
0215         return shape
0216 
0217     def _convert_star(self, layer: api.StarLayer):
0218         shape = objects.Star()
0219         shape.position = self._adjust_coords(self._convert_vector(layer.origin))
0220         shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2))
0221         shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1))
0222         shape.rotation = self._adjust_animated(
0223             self._convert_scalar(layer.angle),
0224             lambda x: 90-x
0225         )
0226         shape.points = self._convert_scalar(layer.points)
0227         if layer.regular_polygon.value:
0228             shape.star_type = objects.StarType.Polygon
0229         return shape
0230 
0231     def _mix_animations(self, *animatable):
0232         times = set()
0233         for v in animatable:
0234             self._force_animated(v)
0235             for kf in v.keyframes:
0236                 times.add(kf.time)
0237 
0238         for time in sorted(times):
0239             yield [time] + [v.get_value(time) for v in animatable]
0240 
0241     def _force_animated(self, lottieval):
0242         if not lottieval.animated:
0243             v = lottieval.value
0244             lottieval.add_keyframe(0, v)
0245             lottieval.add_keyframe(self.animation.out_point, v)
0246 
0247     def _convert_easing_part(self, interp: api.Interpolation):
0248         if interp == api.Interpolation.Linear:
0249             return easing.Linear()
0250         return easing.Sigmoid()
0251 
0252     def _convert_easing(self, start: api.Interpolation, end: api.Interpolation):
0253         if api.Interpolation.Constant in (start, end):
0254             return easing.Jump()
0255         if start == end:
0256             return self._convert_easing_part(start)
0257         return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end))
0258 
0259     def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin):
0260         if self._animated(v):
0261             if len(v.keyframes) == 1:
0262                 lot.value = self._convert_ast_value(v.keyframes[0].value)
0263             else:
0264                 for i, kf in enumerate(v.keyframes):
0265                     if i+1 < len(v.keyframes):
0266                         start = kf.after
0267                         end = v.keyframes[i+1].before
0268                         ease = self._convert_easing(start, end)
0269                     else:
0270                         ease = easing.Linear()
0271 
0272                     lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease)
0273         else:
0274             lot.value = self._convert_ast_value(v)
0275         return lot
0276 
0277     def _convert_ast_value(self, v):
0278         if isinstance(v, ast.SifRadialComposite):
0279             return self._polar(v.radius.value, v.theta.value, 1)
0280         elif isinstance(v, ast.SifValue):
0281             return v.value
0282         elif isinstance(v, ast.SifVectorComposite):
0283             return NVector(v.x.value, v.y.value)
0284         else:
0285             return v
0286 
0287     def _converted_vector_values(self, v):
0288         if isinstance(v, ast.SifRadialComposite):
0289             return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)]
0290         return self._convert_vector(v)
0291 
0292     def _convert_color(self, v: ast.SifAstNode):
0293         return self._adjust_animated(
0294             self._convert_animatable(v, objects.ColorValue()),
0295             self._color_gamma
0296         )
0297 
0298     def _convert_vector(self, v: ast.SifAstNode):
0299         return self._convert_animatable(v, objects.MultiDimensional())
0300 
0301     def _convert_scalar(self, v: ast.SifAstNode):
0302         return self._convert_animatable(v, objects.Value())
0303 
0304     def _color_gamma(self, color):
0305         color = color.clone()
0306         for i in range(3):
0307             color[i] = color[i] ** (1/self.gamma[i])
0308         return color
0309 
0310     def _adjust_animated(self, lottieval, transform):
0311         if lottieval.animated:
0312             for kf in lottieval.keyframes:
0313                 if kf.start is not None:
0314                     kf.start = transform(kf.start)
0315                 if kf.end is not None:
0316                     kf.end = transform(kf.end)
0317         else:
0318             lottieval.value = transform(lottieval.value)
0319         return lottieval
0320 
0321     def _adjust_scalar(self, lottieval: objects.Value):
0322         return self._adjust_animated(lottieval, self._scalar_mult)
0323 
0324     def _adjust_angle(self, lottieval: objects.Value):
0325         return self._adjust_animated(lottieval, lambda x: -x)
0326 
0327     def _adjust_add_dimension(self, lottieval, transform):
0328         to_val = objects.MultiDimensional()
0329         to_val.animated = lottieval.animated
0330         if lottieval.animated:
0331             to_val.keyframes = []
0332             for kf in lottieval.keyframes:
0333                 if kf.start is not None:
0334                     kf.start = transform(kf.start[0])
0335                 if kf.end is not None:
0336                     kf.end = transform(kf.end[0])
0337                 to_val.keyframes.append(kf)
0338         else:
0339             to_val.value = transform(lottieval.value)
0340         return to_val
0341 
0342     def _scalar_mult(self, x):
0343         return x * 60
0344 
0345     def _adjust_coords(self, lottieval: objects.MultiDimensional):
0346         return self._adjust_animated(lottieval, self._coord)
0347 
0348     def _coord(self, val: NVector):
0349         return NVector(
0350             self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5),
0351             self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5),
0352         )
0353 
0354     def _convert_polygon(self, layer: api.PolygonLayer):
0355         lot = objects.Path()
0356         animatables = [self._convert_vector(layer.origin)] + [
0357             self._convert_vector(p)
0358             for p in layer.points
0359         ]
0360         animated = any(x.animated for x in animatables)
0361         if not animated:
0362             lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value)
0363         else:
0364             for values in self._mix_animations(*animatables):
0365                 time = values[0]
0366                 origin = values[1]
0367                 points = values[2:]
0368                 lot.shape.add_keyframe(time, self._polygon(points, origin))
0369         return lot
0370 
0371     def _polygon(self, points, origin):
0372         bezier = objects.Bezier()
0373         bezier.closed = True
0374         for point in points:
0375             bezier.add_point(self._coord(point+origin))
0376         return bezier
0377 
0378     def _convert_bline(self, layer: api.AbstractOutline):
0379         lot = objects.Path()
0380         closed = layer.bline.loop
0381         animatables = [
0382             self._convert_vector(layer.origin)
0383         ]
0384         for p in layer.bline.points:
0385             animatables += [
0386                 self._convert_vector(p.point),
0387                 self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0),
0388                 self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0),
0389                 self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0),
0390                 self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0)
0391             ]
0392         animated = any(x.animated for x in animatables)
0393         if not animated:
0394             lot.shape.value = self._bezier(
0395                 closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points
0396             )
0397         else:
0398             for values in self._mix_animations(*animatables):
0399                 time = values[0]
0400                 origin = values[1]
0401                 values = values[2:]
0402                 lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points))
0403         return lot
0404 
0405     def _bezier(self, closed, values, origin, points):
0406         chunk_size = 5
0407         bezier = objects.Bezier()
0408         bezier.closed = closed
0409         for i in range(0, len(values), chunk_size):
0410             point, r1, a1, r2, a2 = values[i:i+chunk_size]
0411             sifvert = point+origin
0412             vert = self._coord(sifvert)
0413             if not points[i//chunk_size].split_radius.value:
0414                 r2 = r1
0415             if not points[i//chunk_size].split_angle.value:
0416                 a2 = a1
0417             t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert
0418             t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert
0419             bezier.add_point(vert, t1, t2)
0420         return bezier
0421 
0422     def _polar(self, radius, angle, dir):
0423         offset_angle = 0
0424         if dir == 1:
0425             offset_angle += 180
0426         return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180)
0427 
0428     def _convert_transform_down(self, tl: api.TransformDown):
0429         group = objects.Group()
0430         self._set_name(group, tl)
0431 
0432         if isinstance(tl, api.TranslateLayer):
0433             group.transform.anchor_point.value = self.target_size / 2
0434             group.transform.position = self._adjust_coords(self._convert_vector(tl.origin))
0435         elif isinstance(tl, api.RotateLayer):
0436             group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin))
0437             group.transform.position = group.transform.anchor_point.clone()
0438             group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount))
0439         elif isinstance(tl, api.ScaleLayer):
0440             group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center))
0441             group.transform.position = group.transform.anchor_point.clone()
0442             group.transform.scale = self._adjust_add_dimension(
0443                 self._convert_scalar(tl.amount),
0444                 self._zoom_to_scale
0445             )
0446 
0447         return group
0448 
0449     def _zoom_to_scale(self, value):
0450         zoom = math.e ** value * 100
0451         return NVector(zoom, zoom)
0452 
0453     def _set_name(self, lottie, sif):
0454         lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__
0455 
0456     def _convert_gradient(self, layer: api.GradientLayer, parent):
0457         group = objects.Group()
0458 
0459         parent_shapes = parent.shapes
0460         parent.shapes = []
0461         if isinstance(parent, objects.Group):
0462             parent.shapes.append(parent_shapes[-1])
0463 
0464         self._gradient_gather_shapes(parent_shapes, group)
0465 
0466         gradient = objects.GradientFill()
0467         self._set_name(gradient, layer)
0468         group.add_shape(gradient)
0469         gradient.colors = self._convert_gradient_stops(layer.gradient)
0470         gradient.opacity = self._adjust_animated(
0471             self._convert_scalar(layer.amount),
0472             lambda x: x * 100
0473         )
0474 
0475         if isinstance(layer, api.LinearGradient):
0476             gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1))
0477             gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2))
0478             gradient.gradient_type = objects.GradientType.Linear
0479         elif isinstance(layer, api.RadialGradient):
0480             gradient.gradient_type = objects.GradientType.Radial
0481             gradient.start_point = self._adjust_coords(self._convert_vector(layer.center))
0482             radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45)
0483             if not radius.animated and not gradient.start_point.animated:
0484                 gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value)
0485             else:
0486                 for time, c, r in self._mix_animations(gradient.start_point.clone(), radius):
0487                     gradient.end_point.add_keyframe(time, c + NVector(r + r))
0488 
0489         return group
0490 
0491     def _gradient_gather_shapes(self, shapes, output: objects.Group):
0492         for shape in shapes:
0493             if isinstance(shape, objects.Shape):
0494                 output.add_shape(shape)
0495             elif isinstance(shape, objects.Group):
0496                 self._gradient_gather_shapes(shape.shapes, output)
0497 
0498     def _convert_gradient_stops(self, sif_gradient):
0499         stops = objects.GradientColors()
0500         if not self._animated(sif_gradient):
0501             stops.set_stops(self._flatten_gradient_colors(sif_gradient.value))
0502             stops.count = len(sif_gradient.value)
0503         else:
0504             # TODO easing
0505             for kf in sif_gradient.keyframes:
0506                 stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value))
0507                 stops.count = len(kf.value)
0508 
0509         return stops
0510 
0511     def _flatten_gradient_colors(self, stops):
0512         return [
0513             (stop.pos, self._color_gamma(stop.color))
0514             for stop in stops
0515         ]
0516 
0517     def _convert_text(self, layer: api.TextLayer):
0518         shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center))
0519         shape.refresh()
0520         trans = shape.wrapped.transform
0521         trans.anchor_point.value = shape.wrapped.bounding_box().center()
0522         trans.anchor_point.value.x /= 2
0523         trans.position = self._adjust_coords(self._convert_vector(layer.origin))
0524         trans.scale = self._adjust_animated(
0525             self._convert_vector(layer.size),
0526             lambda v: v * 100
0527         )
0528         return shape