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