Coverage for lib/lottie/parsers/sif/converter.py: 0%

371 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-20 16:17 +0100

1import math 

2from ... import objects 

3from ...objects import easing 

4from . import api, ast 

5from ... import NVector, PolarVector 

6 

7try: 

8 from ...utils import font 

9 has_font = True 

10except ImportError: 

11 has_font = False 

12 

13 

14def convert(canvas: api.Canvas): 

15 return Converter().convert(canvas) 

16 

17 

18class Converter: 

19 def __init__(self): 

20 pass 

21 

22 def _animated(self, sifval): 

23 return isinstance(sifval, ast.SifAnimated) 

24 

25 def convert(self, canvas: api.Canvas): 

26 self.canvas = canvas 

27 self.animation = objects.Animation( 

28 self._time(canvas.end_time), 

29 canvas.fps 

30 ) 

31 self.animation.in_point = self._time(canvas.begin_time) 

32 self.animation.width = canvas.width 

33 self.animation.height = canvas.height 

34 self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1]) 

35 self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3]) 

36 self.target_size = NVector(canvas.width, canvas.height) 

37 self.shape_layer = self.animation.add_layer(objects.ShapeLayer()) 

38 self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b) 

39 self._process_layers(canvas.layers, self.shape_layer) 

40 return self.animation 

41 

42 def _time(self, t: api.FrameTime): 

43 return self.canvas.time_to_frames(t) 

44 

45 def _process_layers(self, layers, parent): 

46 old_gamma = self.gamma 

47 

48 for layer in reversed(layers): 

49 if not layer.active: 

50 continue 

51 elif isinstance(layer, api.GroupLayerBase): 

52 parent.add_shape(self._convert_group(layer)) 

53 elif isinstance(layer, api.RectangleLayer): 

54 parent.add_shape(self._convert_fill(layer, self._convert_rect)) 

55 elif isinstance(layer, api.CircleLayer): 

56 parent.add_shape(self._convert_fill(layer, self._convert_circle)) 

57 elif isinstance(layer, api.StarLayer): 

58 parent.add_shape(self._convert_fill(layer, self._convert_star)) 

59 elif isinstance(layer, api.PolygonLayer): 

60 parent.add_shape(self._convert_fill(layer, self._convert_polygon)) 

61 elif isinstance(layer, api.RegionLayer): 

62 parent.add_shape(self._convert_fill(layer, self._convert_bline)) 

63 elif isinstance(layer, api.AbstractOutline): 

64 parent.add_shape(self._convert_outline(layer, self._convert_bline)) 

65 elif isinstance(layer, api.GradientLayer): 

66 parent.add_shape(self._convert_gradient(layer, parent)) 

67 elif isinstance(layer, api.TransformDown): 

68 shape = self._convert_transform_down(layer) 

69 parent.add_shape(shape) 

70 parent = shape 

71 elif isinstance(layer, api.TextLayer): 

72 if has_font: 

73 parent.add_shape(self._convert_fill(layer, self._convert_text)) 

74 elif isinstance(layer, api.ColorCorrectLayer): 

75 self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value) 

76 

77 self.gamma = old_gamma 

78 

79 def _convert_group(self, layer: api.GroupLayer): 

80 shape = objects.Group() 

81 self._set_name(shape, layer) 

82 shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin)) 

83 self._convert_transform(layer.transformation, shape.transform) 

84 self._process_layers(layer.layers, shape) 

85 shape.transform.opacity = self._adjust_animated( 

86 self._convert_scalar(layer.amount), 

87 lambda x: x*100 

88 ) 

89 return shape 

90 

91 def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform): 

92 if isinstance(sif_transform, api.BoneLinkTransform): 

93 base_transform = sif_transform.base_value 

94 else: 

95 base_transform = sif_transform 

96 

97 position = self._adjust_coords(self._convert_vector(base_transform.offset)) 

98 rotation = self._adjust_angle(self._convert_scalar(base_transform.angle)) 

99 scale = self._adjust_animated( 

100 self._convert_vector(base_transform.scale), 

101 lambda x: x * 100 

102 ) 

103 

104 lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle)) 

105 

106 if isinstance(sif_transform, api.BoneLinkTransform): 

107 lottie_transform.position = position 

108 lottie_transform.rotation = rotation 

109 lottie_transform.scale = scale 

110 #bone = sif_transform.bone 

111 #b_pos = self._adjust_coords(self._convert_vector(bone.origin)) 

112 #old_anchor = lottie_transform.anchor_point 

113 

114 #if sif_transform.translate: 

115 #self._mix_animations_into( 

116 #[position, b_pos, old_anchor], 

117 #lottie_transform.position, 

118 #lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2 

119 #) 

120 #else: 

121 #lottie_transform.position = position 

122 

123 #lottie_transform.anchor_point = b_pos 

124 #lottie_transform.anchor_point.value += NVector(100,0) 

125 

126 #if sif_transform.rotate: 

127 #b_rot = self._convert_scalar(bone.angle) 

128 #self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b) 

129 #else: 

130 #lottie_transform.rotation = rotation 

131 

132 #if sif_transform.scale_y: 

133 #b_scale = self._convert_scalar(bone.scalelx) 

134 #self._mix_animations_into( 

135 #scale, b_scale, lottie_transform.scale, 

136 #lambda a, b: NVector(a.x, a.y * b) 

137 #) 

138 #else: 

139 #lottie_transform.scale = scale 

140 else: 

141 lottie_transform.position = position 

142 lottie_transform.rotation = rotation 

143 lottie_transform.scale = scale 

144 

145 def _mix_animations_into(self, animations, output, mix): 

146 if not any(x.animated for x in animations): 

147 output.value = mix(*(x.value for x in animations)) 

148 else: 

149 for vals in self._mix_animations(*animations): 

150 time = vals.pop(0) 

151 output.add_keyframe(time, mix(*vals)) 

152 

153 def _convert_fill(self, layer, converter): 

154 shape = objects.Group() 

155 self._set_name(shape, layer) 

156 shape.add_shape(converter(layer)) 

157 if layer.invert.value: 

158 shape.add_shape(objects.Rect(self.target_size/2, self.target_size)) 

159 

160 fill = objects.Fill() 

161 fill.color = self._convert_color(layer.color) 

162 fill.opacity = self._adjust_animated( 

163 self._convert_scalar(layer.amount), 

164 lambda x: x * 100 

165 ) 

166 shape.add_shape(fill) 

167 return shape 

168 

169 def _convert_linecap(self, lc: api.LineCap): 

170 if lc == api.LineCap.Rounded: 

171 return objects.LineCap.Round 

172 if lc == api.LineCap.Squared: 

173 return objects.LineCap.Square 

174 return objects.LineCap.Butt 

175 

176 def _convert_cusp(self, lc: api.CuspStyle): 

177 if lc == api.CuspStyle.Miter: 

178 return objects.LineJoin.Miter 

179 if lc == api.CuspStyle.Bevel: 

180 return objects.LineJoin.Bevel 

181 return objects.LineJoin.Round 

182 

183 def _convert_outline(self, layer: api.AbstractOutline, converter): 

184 shape = objects.Group() 

185 self._set_name(shape, layer) 

186 shape.add_shape(converter(layer)) 

187 stroke = objects.Stroke() 

188 stroke.color = self._convert_color(layer.color) 

189 stroke.line_cap = self._convert_linecap(layer.start_tip) 

190 stroke.line_join = self._convert_cusp(layer.cusp_type) 

191 stroke.width = self._adjust_scalar(self._convert_scalar(layer.width)) 

192 shape.add_shape(stroke) 

193 return shape 

194 

195 def _convert_rect(self, layer: api.RectangleLayer): 

196 rect = objects.Rect() 

197 p1 = self._adjust_coords(self._convert_vector(layer.point1)) 

198 p2 = self._adjust_coords(self._convert_vector(layer.point2)) 

199 if p1.animated or p2.animated: 

200 for time, p1v, p2v in self._mix_animations(p1, p2): 

201 rect.position.add_keyframe(time, (p1v + p2v) / 2) 

202 rect.size.add_keyframe(time, abs(p2v - p1v)) 

203 pass 

204 else: 

205 rect.position.value = (p1.value + p2.value) / 2 

206 rect.size.value = abs(p2.value - p1.value) 

207 rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel)) 

208 return rect 

209 

210 def _convert_circle(self, layer: api.CircleLayer): 

211 shape = objects.Ellipse() 

212 shape.position = self._adjust_coords(self._convert_vector(layer.origin)) 

213 radius = self._adjust_scalar(self._convert_scalar(layer.radius)) 

214 shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2) 

215 return shape 

216 

217 def _convert_star(self, layer: api.StarLayer): 

218 shape = objects.Star() 

219 shape.position = self._adjust_coords(self._convert_vector(layer.origin)) 

220 shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2)) 

221 shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1)) 

222 shape.rotation = self._adjust_animated( 

223 self._convert_scalar(layer.angle), 

224 lambda x: 90-x 

225 ) 

226 shape.points = self._convert_scalar(layer.points) 

227 if layer.regular_polygon.value: 

228 shape.star_type = objects.StarType.Polygon 

229 return shape 

230 

231 def _mix_animations(self, *animatable): 

232 times = set() 

233 for v in animatable: 

234 self._force_animated(v) 

235 for kf in v.keyframes: 

236 times.add(kf.time) 

237 

238 for time in sorted(times): 

239 yield [time] + [v.get_value(time) for v in animatable] 

240 

241 def _force_animated(self, lottieval): 

242 if not lottieval.animated: 

243 v = lottieval.value 

244 lottieval.add_keyframe(0, v) 

245 lottieval.add_keyframe(self.animation.out_point, v) 

246 

247 def _convert_easing_part(self, interp: api.Interpolation): 

248 if interp == api.Interpolation.Linear: 

249 return easing.Linear() 

250 return easing.Sigmoid() 

251 

252 def _convert_easing(self, start: api.Interpolation, end: api.Interpolation): 

253 if api.Interpolation.Constant in (start, end): 

254 return easing.Jump() 

255 if start == end: 

256 return self._convert_easing_part(start) 

257 return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end)) 

258 

259 def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin): 

260 if self._animated(v): 

261 if len(v.keyframes) == 1: 

262 lot.value = self._convert_ast_value(v.keyframes[0].value) 

263 else: 

264 for i, kf in enumerate(v.keyframes): 

265 if i+1 < len(v.keyframes): 

266 start = kf.after 

267 end = v.keyframes[i+1].before 

268 ease = self._convert_easing(start, end) 

269 else: 

270 ease = easing.Linear() 

271 

272 lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease) 

273 else: 

274 lot.value = self._convert_ast_value(v) 

275 return lot 

276 

277 def _convert_ast_value(self, v): 

278 if isinstance(v, ast.SifRadialComposite): 

279 return self._polar(v.radius.value, v.theta.value, 1) 

280 elif isinstance(v, ast.SifValue): 

281 return v.value 

282 elif isinstance(v, ast.SifVectorComposite): 

283 return NVector(v.x.value, v.y.value) 

284 else: 

285 return v 

286 

287 def _converted_vector_values(self, v): 

288 if isinstance(v, ast.SifRadialComposite): 

289 return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)] 

290 return self._convert_vector(v) 

291 

292 def _convert_color(self, v: ast.SifAstNode): 

293 return self._adjust_animated( 

294 self._convert_animatable(v, objects.ColorValue()), 

295 self._color_gamma 

296 ) 

297 

298 def _convert_vector(self, v: ast.SifAstNode): 

299 return self._convert_animatable(v, objects.MultiDimensional()) 

300 

301 def _convert_scalar(self, v: ast.SifAstNode): 

302 return self._convert_animatable(v, objects.Value()) 

303 

304 def _color_gamma(self, color): 

305 color = color.clone() 

306 for i in range(3): 

307 color[i] = color[i] ** (1/self.gamma[i]) 

308 return color 

309 

310 def _adjust_animated(self, lottieval, transform): 

311 if lottieval.animated: 

312 for kf in lottieval.keyframes: 

313 if kf.start is not None: 

314 kf.start = transform(kf.start) 

315 if kf.end is not None: 

316 kf.end = transform(kf.end) 

317 else: 

318 lottieval.value = transform(lottieval.value) 

319 return lottieval 

320 

321 def _adjust_scalar(self, lottieval: objects.Value): 

322 return self._adjust_animated(lottieval, self._scalar_mult) 

323 

324 def _adjust_angle(self, lottieval: objects.Value): 

325 return self._adjust_animated(lottieval, lambda x: -x) 

326 

327 def _adjust_add_dimension(self, lottieval, transform): 

328 to_val = objects.MultiDimensional() 

329 to_val.animated = lottieval.animated 

330 if lottieval.animated: 

331 to_val.keyframes = [] 

332 for kf in lottieval.keyframes: 

333 if kf.start is not None: 

334 kf.start = transform(kf.start[0]) 

335 if kf.end is not None: 

336 kf.end = transform(kf.end[0]) 

337 to_val.keyframes.append(kf) 

338 else: 

339 to_val.value = transform(lottieval.value) 

340 return to_val 

341 

342 def _scalar_mult(self, x): 

343 return x * 60 

344 

345 def _adjust_coords(self, lottieval: objects.MultiDimensional): 

346 return self._adjust_animated(lottieval, self._coord) 

347 

348 def _coord(self, val: NVector): 

349 return NVector( 

350 self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5), 

351 self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5), 

352 ) 

353 

354 def _convert_polygon(self, layer: api.PolygonLayer): 

355 lot = objects.Path() 

356 animatables = [self._convert_vector(layer.origin)] + [ 

357 self._convert_vector(p) 

358 for p in layer.points 

359 ] 

360 animated = any(x.animated for x in animatables) 

361 if not animated: 

362 lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value) 

363 else: 

364 for values in self._mix_animations(*animatables): 

365 time = values[0] 

366 origin = values[1] 

367 points = values[2:] 

368 lot.shape.add_keyframe(time, self._polygon(points, origin)) 

369 return lot 

370 

371 def _polygon(self, points, origin): 

372 bezier = objects.Bezier() 

373 bezier.closed = True 

374 for point in points: 

375 bezier.add_point(self._coord(point+origin)) 

376 return bezier 

377 

378 def _convert_bline(self, layer: api.AbstractOutline): 

379 lot = objects.Path() 

380 closed = layer.bline.loop 

381 animatables = [ 

382 self._convert_vector(layer.origin) 

383 ] 

384 for p in layer.bline.points: 

385 animatables += [ 

386 self._convert_vector(p.point), 

387 self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0), 

388 self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0), 

389 self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0), 

390 self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0) 

391 ] 

392 animated = any(x.animated for x in animatables) 

393 if not animated: 

394 lot.shape.value = self._bezier( 

395 closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points 

396 ) 

397 else: 

398 for values in self._mix_animations(*animatables): 

399 time = values[0] 

400 origin = values[1] 

401 values = values[2:] 

402 lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points)) 

403 return lot 

404 

405 def _bezier(self, closed, values, origin, points): 

406 chunk_size = 5 

407 bezier = objects.Bezier() 

408 bezier.closed = closed 

409 for i in range(0, len(values), chunk_size): 

410 point, r1, a1, r2, a2 = values[i:i+chunk_size] 

411 sifvert = point+origin 

412 vert = self._coord(sifvert) 

413 if not points[i//chunk_size].split_radius.value: 

414 r2 = r1 

415 if not points[i//chunk_size].split_angle.value: 

416 a2 = a1 

417 t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert 

418 t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert 

419 bezier.add_point(vert, t1, t2) 

420 return bezier 

421 

422 def _polar(self, radius, angle, dir): 

423 offset_angle = 0 

424 if dir == 1: 

425 offset_angle += 180 

426 return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180) 

427 

428 def _convert_transform_down(self, tl: api.TransformDown): 

429 group = objects.Group() 

430 self._set_name(group, tl) 

431 

432 if isinstance(tl, api.TranslateLayer): 

433 group.transform.anchor_point.value = self.target_size / 2 

434 group.transform.position = self._adjust_coords(self._convert_vector(tl.origin)) 

435 elif isinstance(tl, api.RotateLayer): 

436 group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin)) 

437 group.transform.position = group.transform.anchor_point.clone() 

438 group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount)) 

439 elif isinstance(tl, api.ScaleLayer): 

440 group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center)) 

441 group.transform.position = group.transform.anchor_point.clone() 

442 group.transform.scale = self._adjust_add_dimension( 

443 self._convert_scalar(tl.amount), 

444 self._zoom_to_scale 

445 ) 

446 

447 return group 

448 

449 def _zoom_to_scale(self, value): 

450 zoom = math.e ** value * 100 

451 return NVector(zoom, zoom) 

452 

453 def _set_name(self, lottie, sif): 

454 lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__ 

455 

456 def _convert_gradient(self, layer: api.GradientLayer, parent): 

457 group = objects.Group() 

458 

459 parent_shapes = parent.shapes 

460 parent.shapes = [] 

461 if isinstance(parent, objects.Group): 

462 parent.shapes.append(parent_shapes[-1]) 

463 

464 self._gradient_gather_shapes(parent_shapes, group) 

465 

466 gradient = objects.GradientFill() 

467 self._set_name(gradient, layer) 

468 group.add_shape(gradient) 

469 gradient.colors = self._convert_gradient_stops(layer.gradient) 

470 gradient.opacity = self._adjust_animated( 

471 self._convert_scalar(layer.amount), 

472 lambda x: x * 100 

473 ) 

474 

475 if isinstance(layer, api.LinearGradient): 

476 gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1)) 

477 gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2)) 

478 gradient.gradient_type = objects.GradientType.Linear 

479 elif isinstance(layer, api.RadialGradient): 

480 gradient.gradient_type = objects.GradientType.Radial 

481 gradient.start_point = self._adjust_coords(self._convert_vector(layer.center)) 

482 radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45) 

483 if not radius.animated and not gradient.start_point.animated: 

484 gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value) 

485 else: 

486 for time, c, r in self._mix_animations(gradient.start_point.clone(), radius): 

487 gradient.end_point.add_keyframe(time, c + NVector(r + r)) 

488 

489 return group 

490 

491 def _gradient_gather_shapes(self, shapes, output: objects.Group): 

492 for shape in shapes: 

493 if isinstance(shape, objects.Shape): 

494 output.add_shape(shape) 

495 elif isinstance(shape, objects.Group): 

496 self._gradient_gather_shapes(shape.shapes, output) 

497 

498 def _convert_gradient_stops(self, sif_gradient): 

499 stops = objects.GradientColors() 

500 if not self._animated(sif_gradient): 

501 stops.set_stops(self._flatten_gradient_colors(sif_gradient.value)) 

502 stops.count = len(sif_gradient.value) 

503 else: 

504 # TODO easing 

505 for kf in sif_gradient.keyframes: 

506 stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value)) 

507 stops.count = len(kf.value) 

508 

509 return stops 

510 

511 def _flatten_gradient_colors(self, stops): 

512 return [ 

513 (stop.pos, self._color_gamma(stop.color)) 

514 for stop in stops 

515 ] 

516 

517 def _convert_text(self, layer: api.TextLayer): 

518 shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center)) 

519 shape.refresh() 

520 trans = shape.wrapped.transform 

521 trans.anchor_point.value = shape.wrapped.bounding_box().center() 

522 trans.anchor_point.value.x /= 2 

523 trans.position = self._adjust_coords(self._convert_vector(layer.origin)) 

524 trans.scale = self._adjust_animated( 

525 self._convert_vector(layer.size), 

526 lambda v: v * 100 

527 ) 

528 return shape