Coverage for lib/lottie/parsers/sif/builder.py: 90%

293 statements  

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

1import math 

2from xml.dom import minidom 

3 

4from ... import objects 

5from ...nvector import NVector 

6from ...utils import restructure 

7from . import api, ast 

8 

9 

10blend_modes = { 

11 objects.BlendMode.Normal: api.BlendMethod.Composite, 

12 objects.BlendMode.Multiply: api.BlendMethod.Multiply, 

13 objects.BlendMode.Screen: api.BlendMethod.Screen, 

14 objects.BlendMode.Overlay: api.BlendMethod.Overlay, 

15 objects.BlendMode.Darken: api.BlendMethod.Darken, 

16 objects.BlendMode.Lighten: api.BlendMethod.Lighten, 

17 objects.BlendMode.HardLight: api.BlendMethod.HardLight, 

18 objects.BlendMode.Difference: api.BlendMethod.Difference, 

19 objects.BlendMode.Hue: api.BlendMethod.Hue, 

20 objects.BlendMode.Saturation: api.BlendMethod.Saturation, 

21 objects.BlendMode.Color: api.BlendMethod.Color, 

22 objects.BlendMode.Luminosity: api.BlendMethod.Luminosity, 

23 objects.BlendMode.Exclusion: api.BlendMethod.Difference, 

24 objects.BlendMode.SoftLight: api.BlendMethod.Multiply, 

25 objects.BlendMode.ColorDodge: api.BlendMethod.Composite, 

26 objects.BlendMode.ColorBurn: api.BlendMethod.Composite, 

27} 

28 

29 

30class SifBuilder(restructure.AbstractBuilder): 

31 def __init__(self, gamma=1.0): 

32 """ 

33 @todo Add gamma option to lottie_convert.py 

34 """ 

35 super().__init__() 

36 self.canvas = api.Canvas() 

37 self.canvas.version = "1.2" 

38 self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma 

39 self.autoid = objects.base.Index() 

40 

41 def _on_animation(self, animation: objects.Animation): 

42 if animation.name: 

43 self.canvas.name = animation.name 

44 self.canvas.width = animation.width 

45 self.canvas.height = animation.height 

46 self.canvas.xres = animation.width 

47 self.canvas.yres = animation.height 

48 self.canvas.view_box = NVector(0, 0, animation.width, animation.height) 

49 self.canvas.fps = animation.frame_rate 

50 self.canvas.begin_time = api.FrameTime.frame(animation.in_point) 

51 self.canvas.end_time = api.FrameTime.frame(animation.out_point) 

52 self.canvas.antialias = True 

53 return self.canvas 

54 

55 def _on_precomp(self, id, dom_parent, layers): 

56 g = dom_parent.add_layer(api.GroupLayer()) 

57 g.desc = id 

58 for layer_builder in layers: 

59 self.process_layer(layer_builder, g) 

60 

61 def _on_layer(self, layer_builder, dom_parent): 

62 layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent) 

63 if not layer_builder.lottie.name: 63 ↛ 66line 63 didn't jump to line 66, because the condition on line 63 was never false

64 layer.desc = layer_builder.lottie.__class__.__name__ 

65 

66 bm = getattr(layer_builder.lottie, "blend_mode", None) 

67 if bm is None: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true

68 bm = objects.BlendMode.Normal 

69 layer.blend_method = blend_modes[bm] 

70 

71 layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1 

72 

73 in_point = getattr(layer_builder.lottie, "in_point", 0) 

74 layer.time_offset.value = api.FrameTime.frame(in_point) 

75 

76 #layer.canvas.end_time = api.FrameTime.frame(out_point) 

77 return layer 

78 

79 def layer_from_lottie(self, type, lottie, dom_parent): 

80 g = dom_parent.add_layer(type()) 

81 if lottie.name: 

82 g.desc = lottie.name 

83 g.active = not lottie.hidden 

84 transf = getattr(lottie, "transform", None) 

85 if transf: 

86 self.set_transform(g, transf) 

87 

88 if isinstance(lottie, objects.NullLayer): 88 ↛ 89line 88 didn't jump to line 89, because the condition on line 88 was never true

89 g.amount.value = 1 

90 

91 return g 

92 

93 def _get_scale(self, transform): 

94 def func(keyframe): 

95 t = keyframe.time if keyframe else 0 

96 scale_x, scale_y = transform.scale.get_value(t)[:2] 

97 scale_x /= 100 

98 scale_y /= 100 

99 skew = transform.skew.get_value(t) if transform.skew else 0 

100 c = math.cos(skew * math.pi / 180) 

101 if c != 0: 101 ↛ 103line 101 didn't jump to line 103, because the condition on line 101 was never false

102 scale_y *= 1 / c 

103 return NVector(scale_x, scale_y) 

104 return func 

105 

106 def set_transform(self, group, transform): 

107 composite = group.transformation 

108 

109 if transform.position: 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false

110 composite.offset = self.process_vector(transform.position) 

111 

112 if transform.scale: 112 ↛ 116line 112 didn't jump to line 116, because the condition on line 112 was never false

113 keyframes = self._merge_keyframes([transform.scale, transform.skew]) 

114 composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform)) 

115 

116 composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0)) 

117 

118 if transform.rotation: 118 ↛ 121line 118 didn't jump to line 121, because the condition on line 118 was never false

119 composite.angle = self.process_scalar(transform.rotation) 

120 

121 if transform.opacity: 121 ↛ 124line 121 didn't jump to line 124, because the condition on line 121 was never false

122 group.amount = self.process_scalar(transform.opacity, 1/100) 

123 

124 if transform.anchor_point: 124 ↛ 128line 124 didn't jump to line 128, because the condition on line 124 was never false

125 group.origin = self.process_vector(transform.anchor_point) 

126 

127 # TODO get z_depth from position 

128 composite.z_depth = 0 

129 

130 def process_vector(self, multidim): 

131 def getter(keyframe): 

132 if keyframe is None: 

133 v = multidim.value 

134 else: 

135 v = keyframe.start 

136 return NVector(v[0], v[1]) 

137 

138 return self.process_vector_ext(multidim.keyframes, getter) 

139 

140 def process_vector_ext(self, kframes, getter): 

141 if kframes is not None: 

142 wrap = ast.SifAnimated() 

143 for i in range(len(kframes)): 

144 keyframe = kframes[i] 

145 waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time)) 

146 

147 if i > 0: 

148 prev = kframes[i-1] 

149 if prev.hold: 

150 waypoint.before = api.Interpolation.Constant 

151 elif prev.in_value and prev.in_value.x < 1: 

152 waypoint.before = api.Interpolation.Ease 

153 else: 

154 waypoint.before = api.Interpolation.Linear 

155 else: 

156 waypoint.before = api.Interpolation.Linear 

157 

158 if keyframe.hold: 

159 waypoint.after = api.Interpolation.Constant 

160 elif keyframe.out_value and keyframe.out_value.x > 0: 

161 waypoint.after = api.Interpolation.Ease 

162 else: 

163 waypoint.after = api.Interpolation.Linear 

164 else: 

165 wrap = api.SifValue(getter(None)) 

166 

167 return wrap 

168 

169 def process_scalar(self, value, mult=None): 

170 def getter(keyframe): 

171 if keyframe is None: 

172 v = value.value 

173 else: 

174 v = keyframe.start[0] 

175 if mult is not None: 

176 v *= mult 

177 return v 

178 return self.process_vector_ext(value.keyframes, getter) 

179 

180 def _on_shape(self, shape, group, dom_parent): 

181 layers = [] 

182 if not hasattr(shape, "to_bezier"): 

183 return [] 

184 

185 if group.stroke: 

186 sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape) 

187 self.apply_group_stroke(sif_shape, group.stroke) 

188 layers.append(sif_shape) 

189 

190 if group.fill: 

191 sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape) 

192 layers.append(sif_shape) 

193 self.apply_group_fill(sif_shape, group.fill) 

194 

195 return layers 

196 

197 def _merge_keyframes(self, props): 

198 keyframes = {} 

199 for prop in props: 

200 if prop is not None and prop.animated: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true

201 keyframes.update({kf.time: kf for kf in prop.keyframes}) 

202 return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None 202 ↛ exitline 202 didn't run the lambda on line 202

203 

204 def apply_origin(self, sif_shape, lottie_shape): 

205 if hasattr(lottie_shape, "position"): 

206 sif_shape.origin.value = lottie_shape.position.get_value() 

207 else: 

208 sif_shape.origin.value = lottie_shape.bounding_box().center() 

209 

210 def apply_group_fill(self, sif_shape, fill): 

211 ## @todo gradients? 

212 if hasattr(fill, "colors"): 212 ↛ 213line 212 didn't jump to line 213, because the condition on line 212 was never true

213 return 

214 

215 def getter(keyframe): 

216 if keyframe is None: 

217 v = fill.color.value 

218 else: 

219 v = keyframe.start 

220 return self.canvas.make_color(*v) 

221 

222 sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter) 

223 

224 def get_op(keyframe): 

225 if keyframe is None: 

226 v = fill.opacity.value 

227 else: 

228 v = keyframe.start[0] 

229 v /= 100 

230 return v 

231 

232 sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op) 

233 

234 def apply_group_stroke(self, sif_shape, stroke): 

235 self.apply_group_fill(sif_shape, stroke) 

236 sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter 

237 round_cap = stroke.line_cap == objects.LineCap.Round 

238 sif_shape.round_tip_0.value = round_cap 

239 sif_shape.round_tip_1.value = round_cap 

240 sif_shape.width = self.process_scalar(stroke.width, 0.5) 

241 

242 def build_path(self, type, path, dom_parent, lottie_shape): 

243 layer = self.layer_from_lottie(type, lottie_shape, dom_parent) 

244 self.apply_origin(layer, lottie_shape) 

245 startbez = path.shape.get_value() 

246 layer.bline.loop = startbez.closed 

247 nverts = len(startbez.vertices) 

248 for point in range(nverts): 

249 self.bezier_point(path, point, layer.bline, layer.origin.value) 

250 return layer 

251 

252 def bezier_point(self, lottie_path, point_index, sif_parent, offset): 

253 composite = api.BlinePoint() 

254 

255 def get_point(keyframe): 

256 if keyframe is None: 

257 bezier = lottie_path.shape.value 

258 else: 

259 bezier = keyframe.start 

260 if not bezier: 260 ↛ 262line 260 didn't jump to line 262, because the condition on line 260 was never true

261 #elem.parentNode.parentNode.removeChild(elem.parentNode) 

262 return 

263 vert = bezier.vertices[point_index] 

264 return NVector(vert[0], vert[1]) - offset 

265 

266 composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point) 

267 composite.split.value = True 

268 composite.split_radius.value = True 

269 composite.split_angle.value = True 

270 

271 def get_tangent(keyframe): 

272 if keyframe is None: 

273 bezier = lottie_path.shape.value 

274 else: 

275 bezier = keyframe.start 

276 if not bezier: 276 ↛ 278line 276 didn't jump to line 278, because the condition on line 276 was never true

277 #elem.parentNode.parentNode.removeChild(elem.parentNode) 

278 return 

279 

280 inp = getattr(bezier, which_point)[point_index] 

281 return NVector(inp.x, inp.y) * 3 * mult 

282 

283 mult = -1 

284 which_point = "in_tangents" 

285 composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent) 

286 

287 mult = 1 

288 which_point = "out_tangents" 

289 composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent) 

290 sif_parent.points.append(composite) 

291 

292 def _on_shapegroup(self, shape_group, dom_parent): 

293 if shape_group.empty(): 293 ↛ 294line 293 didn't jump to line 294, because the condition on line 293 was never true

294 return 

295 

296 layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent) 

297 

298 self.shapegroup_process_children(shape_group, layer) 

299 

300 def _modifier_inner_group(self, modifier, shapegroup, dom_parent): 

301 layer = dom_parent.add_layer(api.GroupLayer()) 

302 self.shapegroup_process_child(modifier.child, shapegroup, layer) 

303 return layer 

304 

305 def _on_shape_modifier(self, modifier, shapegroup, dom_parent): 

306 layer = dom_parent.add_layer(api.GroupLayer()) 

307 if modifier.lottie.name: 307 ↛ 310line 307 didn't jump to line 310, because the condition on line 307 was never false

308 layer.desc = modifier.lottie.name 

309 

310 inner = self._modifier_inner_group(modifier, shapegroup, layer) 

311 if isinstance(modifier.lottie, objects.Repeater): 311 ↛ exitline 311 didn't return from function '_on_shape_modifier', because the condition on line 311 was never false

312 self.build_repeater(modifier.lottie, inner, layer) 

313 

314 def _build_repeater_defs(self, shape, name_id): 

315 dup = api.Duplicate() 

316 dup.id = name_id 

317 self.canvas.defs.append(dup) 

318 self.canvas.register_as(dup, name_id) 

319 

320 def getter(keyframe): 

321 if keyframe is None: 321 ↛ 324line 321 didn't jump to line 324, because the condition on line 321 was never false

322 v = shape.copies.value 

323 else: 

324 v = keyframe.start[0] 

325 

326 return v - 1 

327 

328 setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter)) 

329 dup.to.value = 0 

330 dup.step.value = -1 

331 return dup 

332 

333 def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite): 

334 power = ast.SifPower() 

335 setattr(scalecomposite, "xy"[comp], power) 

336 

337 def getter(keyframe): 

338 if keyframe is None: 338 ↛ 341line 338 didn't jump to line 341, because the condition on line 338 was never false

339 v = shape.transform.scale.value 

340 else: 

341 v = keyframe.start 

342 v = v[comp] / 100 

343 return v 

344 

345 power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter) 

346 

347 # HACK work around an issue in Synfig 

348 power.power = ast.SifAdd() 

349 power.power.lhs.value = api.ValueReference(name_id) 

350 power.power.rhs.value = 0.000001 

351 

352 def _build_repeater_transform(self, shape, inner, name_id): 

353 offset_id = name_id + "_origin" 

354 origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector") 

355 self.canvas.defs.append(origin) 

356 self.canvas.register_as(origin, offset_id) 

357 inner.origin = origin 

358 

359 composite = inner.transformation 

360 

361 composite.offset = ast.SifAdd() 

362 composite.offset.rhs.value = api.ValueReference(offset_id) 

363 composite.offset.lhs = ast.SifScale() 

364 composite.offset.lhs.scalar.value = api.ValueReference(name_id) 

365 composite.offset.lhs.link = self.process_vector(shape.transform.position) 

366 

367 composite.angle = ast.SifScale() 

368 composite.angle.scalar.value = api.ValueReference(name_id) 

369 composite.angle.link = self.process_scalar(shape.transform.rotation) 

370 

371 composite.scale = ast.SifVectorComposite() 

372 self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale) 

373 self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale) 

374 

375 def _build_repeater_amount(self, shape, inner, name_id): 

376 inner.amount = ast.SifSubtract() 

377 inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01) 

378 

379 inner.amount.rhs = ast.SifScale() 

380 inner.amount.rhs.scalar.value = api.ValueReference(name_id) 

381 

382 def getter(keyframe): 

383 if keyframe is None: 383 ↛ 387line 383 didn't jump to line 387, because the condition on line 383 was never false

384 t = 0 

385 end = shape.transform.end_opacity.value 

386 else: 

387 t = keyframe.time 

388 end = keyframe.start[0] 

389 start = shape.transform.start_opacity.get_value(t) 

390 n = shape.copies.get_value(t) 

391 v = (start - end) / (n - 1) / 100 if n > 0 else 0 

392 return v 

393 inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter) 

394 

395 def build_repeater(self, shape, inner, dom_parent): 

396 name_id = "duplicate_%s" % next(self.autoid) 

397 dup = self._build_repeater_defs(shape, name_id) 

398 self._build_repeater_transform(shape, inner, name_id) 

399 self._build_repeater_amount(shape, inner, name_id) 

400 inner.desc = "Transformation for " + (dom_parent.desc or "duplicate") 

401 

402 # duplicate layer 

403 duplicate = dom_parent.add_layer(api.DuplicateLayer()) 

404 duplicate.index = dup 

405 duplicate.desc = shape.name 

406 

407 

408def to_sif(animation): 

409 builder = SifBuilder() 

410 builder.process(animation) 

411 return builder.canvas