Coverage for lib/lottie/utils/animation.py: 0%

301 statements  

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

1import random 

2import math 

3from ..nvector import NVector 

4from ..objects.shapes import Path 

5from .. import objects 

6from ..objects import easing 

7from ..objects import properties 

8 

9 

10def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()): 

11 if not isinstance(position_prop, list): 

12 position_prop = [position_prop] 

13 

14 n_frames = int(round(n_frames)) 

15 frame_time = (end_time - start_time) / n_frames 

16 startpoints = list(map( 

17 lambda pp: pp.get_value(start_time), 

18 position_prop 

19 )) 

20 

21 for i in range(n_frames): 

22 x = (random.random() * 2 - 1) * x_radius 

23 y = (random.random() * 2 - 1) * y_radius 

24 for pp, start in zip(position_prop, startpoints): 

25 px = start[0] + x 

26 py = start[1] + y 

27 pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp) 

28 

29 for pp, start in zip(position_prop, startpoints): 

30 pp.add_keyframe(end_time, start, interp) 

31 

32 

33def rot_shake(rotation_prop, angles, start_time, end_time, n_frames): 

34 frame_time = (end_time - start_time) / n_frames 

35 start = rotation_prop.get_value(start_time) 

36 

37 for i in range(0, n_frames): 

38 a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi) 

39 rotation_prop.add_keyframe(start_time + i * frame_time, start + a) 

40 rotation_prop.add_keyframe(end_time, start) 

41 

42 

43def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7): 

44 start = position_prop.get_value(start_time) 

45 d = start-point 

46 

47 delta = (end_time - start_time) / oscillations 

48 

49 for i in range(oscillations): 

50 time_x = i / oscillations 

51 factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff)) 

52 p = point + d * factor 

53 position_prop.add_keyframe(start_time + delta * i, p) 

54 

55 position_prop.add_keyframe(end_time, point) 

56 

57 

58def follow_path(position_prop, bezier, start_time, end_time, n_keyframes, 

59 reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0): 

60 delta = (end_time - start_time) / (n_keyframes-1) 

61 fact = start_t 

62 factd = 1 / (n_keyframes-1) 

63 

64 if rotation_prop: 

65 start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset 

66 

67 for i in range(n_keyframes): 

68 time = start_time + i * delta 

69 

70 if fact > 1 + factd/2: 

71 fact -= 1 

72 if time != start_time: 

73 easing.Jump()(position_prop.keyframes[-1]) 

74 if rotation_prop: 

75 easing.Jump()(rotation_prop.keyframes[-1]) 

76 

77 f = 1 - fact if reverse else fact 

78 position_prop.add_keyframe(time, bezier.point_at(f)+offset) 

79 

80 if rotation_prop: 

81 rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot) 

82 

83 fact += factd 

84 

85 

86def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False): 

87 obj = Path() 

88 beziers = [] 

89 maxp = 0 

90 

91 time_delta = (appear_end - appear_start) / n_keyframes 

92 for i in range(n_keyframes+1): 

93 time = appear_start + i * time_delta 

94 t2 = (time - appear_start) / (appear_end - appear_start) 

95 

96 if reverse: 

97 t2 = 1 - t2 

98 segment = bezier.segment(t2, 1) 

99 segment.reverse() 

100 else: 

101 segment = bezier.segment(0, t2) 

102 

103 beziers.append(segment) 

104 if len(segment.vertices) > maxp: 

105 maxp = len(segment.vertices) 

106 

107 obj.shape.add_keyframe(time, segment) 

108 

109 for segment in beziers: 

110 deltap = maxp - len(segment.vertices) 

111 if deltap > 0: 

112 segment.vertices += [segment.vertices[-1]] * deltap 

113 segment.in_tangents += [NVector(0, 0)] * deltap 

114 segment.out_tangents += [NVector(0, 0)] * deltap 

115 

116 return obj 

117 

118 

119def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False): 

120 obj = Path() 

121 beziers = [] 

122 maxp = 0 

123 

124 time_delta = (disappear_end - disappear_start) / n_keyframes 

125 for i in range(n_keyframes+1): 

126 time = disappear_start + i * time_delta 

127 t1 = (time - disappear_start) / (disappear_end - disappear_start) 

128 if reverse: 

129 t1 = 1 - t1 

130 segment = bezier.segment(0, t1) 

131 else: 

132 segment = bezier.segment(1, t1) 

133 segment.reverse() 

134 

135 beziers.append(segment) 

136 if len(segment.vertices) > maxp: 

137 maxp = len(segment.vertices) 

138 

139 obj.shape.add_keyframe(time, segment) 

140 

141 for segment in beziers: 

142 deltap = maxp - len(segment.vertices) 

143 if deltap > 0: 

144 segment.vertices += [segment.vertices[-1]] * deltap 

145 segment.in_tangents += [NVector(0, 0)] * deltap 

146 segment.out_tangents += [NVector(0, 0)] * deltap 

147 

148 return obj 

149 

150 

151def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False): 

152 obj = Path() 

153 beziers = [] 

154 maxp = 0 

155 

156 # HACK: For some reason reversed works better 

157 if not reverse: 

158 bezier.reverse() 

159 

160 time_delta = (appear_end - appear_start) / n_keyframes 

161 for i in range(n_keyframes+1): 

162 time = appear_start + i * time_delta 

163 t1 = (time - disappear_start) / (disappear_end - disappear_start) 

164 t2 = (time - appear_start) / (appear_end - appear_start) 

165 

166 t1 = max(0, min(1, t1)) 

167 t2 = max(0, min(1, t2)) 

168 

169 #if reverse: 

170 if True: 

171 t1 = 1 - t1 

172 t2 = 1 - t2 

173 segment = bezier.segment(t2, t1) 

174 segment.reverse() 

175 #else: 

176 #segment = bezier.segment(t1, t2) 

177 #segment.reverse() 

178 

179 beziers.append(segment) 

180 if len(segment.vertices) > maxp: 

181 maxp = len(segment.vertices) 

182 

183 obj.shape.add_keyframe(time, segment) 

184 

185 for segment in beziers: 

186 deltap = maxp - len(segment.vertices) 

187 if deltap > 0: 

188 segment.split_self_chunks(deltap+1) 

189 

190 # HACK: Restore 

191 if not reverse: 

192 bezier.reverse() 

193 return obj 

194 

195 

196class PointDisplacer: 

197 def __init__(self, time_start, time_end, n_frames): 

198 """! 

199 @param time_start When the animation shall start 

200 @param time_end When the animation shall end 

201 @param n_frames Number of frames in the animation 

202 """ 

203 ## When the animation shall start 

204 self.time_start = time_start 

205 ## When the animation shall end 

206 self.time_end = time_end 

207 ## Number of frames in the animation 

208 self.n_frames = n_frames 

209 ## Length of a frame 

210 self.time_delta = (time_end - time_start) / n_frames 

211 

212 def animate_point(self, prop): 

213 startpos = prop.get_value(self.time_start) 

214 for f in range(self.n_frames+1): 

215 p = self._on_displace(startpos, f) 

216 prop.add_keyframe(self.frame_time(f), startpos+p) 

217 

218 def _on_displace(self, startpos, f): 

219 raise NotImplementedError() 

220 

221 def animate_bezier(self, prop): 

222 initial = prop.get_value(self.time_start) 

223 

224 for f in range(self.n_frames+1): 

225 bezier = objects.Bezier() 

226 bezier.closed = initial.closed 

227 

228 for pi in range(len(initial.vertices)): 

229 startpos = initial.vertices[pi] 

230 dp = self._on_displace(startpos, f) 

231 t1sp = initial.in_tangents[pi] + startpos 

232 t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp 

233 t2sp = initial.out_tangents[pi] + startpos 

234 t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp 

235 

236 bezier.add_point(dp + startpos, t1fin, t2fin) 

237 

238 prop.add_keyframe(self.frame_time(f), bezier) 

239 

240 def frame_time(self, f): 

241 return f * self.time_delta + self.time_start 

242 

243 def _init_lerp(self, val_from, val_to, easing): 

244 self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing) 

245 

246 def _lerp_get(self, offset): 

247 return self._kf.interpolated_value(offset / self.n_frames)[0] 

248 

249 

250class SineDisplacer(PointDisplacer): 

251 def __init__( 

252 self, 

253 wavelength, 

254 amplitude, 

255 time_start, 

256 time_end, 

257 n_frames, 

258 speed=1, 

259 axis=90, 

260 ): 

261 """! 

262 Displaces points as if they were following a sine wave 

263 

264 @param wavelength Distance between consecutive peaks 

265 @param amplitude Distance from a peak to the original position 

266 @param time_start When the animation shall start 

267 @param time_end When the animation shall end 

268 @param n_frames Number of keyframes to add 

269 @param speed Number of peaks a point will go through in the given time 

270 If negative, it will go the other way 

271 @param axis Wave peak direction 

272 """ 

273 super().__init__(time_start, time_end, n_frames) 

274 

275 self.wavelength = wavelength 

276 self.amplitude = amplitude 

277 self.speed_f = math.pi * 2 * speed 

278 self.axis = axis / 180 * math.pi 

279 

280 def _on_displace(self, startpos, f): 

281 off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude 

282 return NVector(off * math.cos(self.axis), off * math.sin(self.axis)) 

283 

284 

285class MultiSineDisplacer(PointDisplacer): 

286 def __init__( 

287 self, 

288 waves, 

289 time_start, 

290 time_end, 

291 n_frames, 

292 speed=1, 

293 axis=90, 

294 amplitude_scale=1, 

295 ): 

296 """! 

297 Displaces points as if they were following a sine wave 

298 

299 @param waves List of tuples (wavelength, amplitude) 

300 @param time_start When the animation shall start 

301 @param time_end When the animation shall end 

302 @param n_frames Number of keyframes to add 

303 @param speed Number of peaks a point will go through in the given time 

304 If negative, it will go the other way 

305 @param axis Wave peak direction 

306 @param amplitude_scale Multiplies the resulting amplitude by this factor 

307 """ 

308 super().__init__(time_start, time_end, n_frames) 

309 

310 self.waves = waves 

311 self.speed_f = math.pi * 2 * speed 

312 self.axis = axis / 180 * math.pi 

313 self.amplitude_scale = amplitude_scale 

314 

315 def _on_displace(self, startpos, f): 

316 off = 0 

317 for wavelength, amplitude in self.waves: 

318 off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude 

319 

320 off *= self.amplitude_scale 

321 return NVector(off * math.cos(self.axis), off * math.sin(self.axis)) 

322 

323 

324class DepthRotationAxis: 

325 def __init__(self, x, y, keep): 

326 self.x = x / x.length 

327 self.y = y / y.length 

328 self.keep = keep / keep.length # should be the cross product 

329 

330 def rot_center(self, center, point): 

331 return ( 

332 self.x * self.x.dot(center) + 

333 self.y * self.y.dot(center) + 

334 self.keep * self.keep.dot(point) 

335 ) 

336 

337 def extract_component(self, vector, axis): 

338 return sum(vector.element_scaled(axis).components) 

339 

340 @classmethod 

341 def from_points(cls, keep_point, center=NVector(0, 0, 0)): 

342 keep = keep_point - center 

343 keep /= keep.length 

344 # Hughes-Moller to find x and y 

345 if abs(keep.x) > abs(keep.z): 

346 y = NVector(-keep.y, keep.x, 0) 

347 else: 

348 y = NVector(0, -keep.z, keep.y) 

349 y /= y.length 

350 x = y.cross(keep) 

351 return cls(x, y, keep) 

352 

353 

354class DepthRotation: 

355 axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0)) 

356 axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0)) 

357 axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1)) 

358 

359 def __init__(self, center): 

360 self.center = center 

361 

362 def rotate3d_y(self, point, angle): 

363 return self.rotate3d(point, angle, self.axis_y) 

364 # Hard-coded version: 

365 #c = NVector(self.center.x, point.y, self.center.z) 

366 #rad = angle * math.pi / 180 

367 #delta = point - c 

368 #pol_l = delta.length 

369 #pol_a = math.atan2(delta.z, delta.x) 

370 #dest_a = pol_a + rad 

371 #return NVector( 

372 # c.x + pol_l * math.cos(dest_a), 

373 # point.y, 

374 # c.z + pol_l * math.sin(dest_a) 

375 #) 

376 

377 def rotate3d_x(self, point, angle): 

378 return self.rotate3d(point, angle, self.axis_x) 

379 # Hard-coded version: 

380 #c = NVector(point.x, self.center.y, self.center.z) 

381 #rad = angle * math.pi / 180 

382 #delta = point - c 

383 #pol_l = delta.length 

384 #pol_a = math.atan2(delta.y, delta.z) 

385 #dest_a = pol_a + rad 

386 #return NVector( 

387 # point.x, 

388 # c.y + pol_l * math.sin(dest_a), 

389 # c.z + pol_l * math.cos(dest_a), 

390 #) 

391 

392 def rotate3d_z(self, point, angle): 

393 return self.rotate3d(point, angle, self.axis_z) 

394 

395 def rotate3d(self, point, angle, axis): 

396 c = axis.rot_center(self.center, point) 

397 rad = angle * math.pi / 180 

398 delta = point - c 

399 pol_l = delta.length 

400 pol_a = math.atan2( 

401 axis.extract_component(delta, axis.y), 

402 axis.extract_component(delta, axis.x) 

403 ) 

404 dest_a = pol_a + rad 

405 return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a) 

406 

407 

408class DepthRotationDisplacer(PointDisplacer): 

409 axis_x = DepthRotation.axis_x 

410 axis_y = DepthRotation.axis_y 

411 axis_z = DepthRotation.axis_z 

412 

413 def __init__(self, center, time_start, time_end, n_frames, axis, 

414 depth=0, angle=360, anglestart=0, ease=easing.Linear()): 

415 super().__init__(time_start, time_end, n_frames) 

416 self.rotation = DepthRotation(center) 

417 if isinstance(axis, NVector): 

418 axis = DepthRotationAxis.from_points(axis) 

419 self.axis = axis 

420 self.depth = depth 

421 self._angle = angle 

422 self.anglestart = anglestart 

423 self.ease = ease 

424 self._init_lerp(0, angle, ease) 

425 

426 @property 

427 def angle(self): 

428 return self._angle 

429 

430 @angle.setter 

431 def angle(self, value): 

432 self._angle = value 

433 self._init_lerp(0, value, self.ease) 

434 

435 def _on_displace(self, startpos, f): 

436 angle = self.anglestart + self._lerp_get(f) 

437 if len(startpos) < 3: 

438 startpos = NVector(*(startpos.components + [self.depth])) 

439 return self.rotation.rotate3d(startpos, angle, self.axis) - startpos 

440 

441 

442class EnvelopeDeformation(PointDisplacer): 

443 def __init__(self, topleft, bottomright): 

444 self.topleft = topleft 

445 self.size = bottomright - topleft 

446 self.keyframes = [] 

447 

448 @property 

449 def time_start(self): 

450 return self.keyframes[0][0] 

451 

452 def add_reset_keyframe(self, time): 

453 self.add_keyframe( 

454 time, 

455 self.topleft.clone(), 

456 NVector(self.topleft.x + self.size.x, self.topleft.y), 

457 NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y), 

458 NVector(self.topleft.x, self.topleft.y + self.size.y), 

459 ) 

460 

461 def add_keyframe(self, time, tl, tr, br, bl): 

462 self.keyframes.append([ 

463 time, 

464 tl.clone(), 

465 tr.clone(), 

466 br.clone(), 

467 bl.clone() 

468 ]) 

469 

470 def _on_displace(self, startpos, f): 

471 _, tl, tr, br, bl = self.keyframes[f] 

472 relp = startpos - self.topleft 

473 relp.x /= self.size.x 

474 relp.y /= self.size.y 

475 

476 x1 = tl.lerp(tr, relp.x) 

477 x2 = bl.lerp(br, relp.x) 

478 

479 #return x1.lerp(x2, relp.y) 

480 return x1.lerp(x2, relp.y) - startpos 

481 

482 @property 

483 def n_frames(self): 

484 return len(self.keyframes)-1 

485 

486 def frame_time(self, f): 

487 return self.keyframes[f][0] 

488 

489 

490class DisplacerDampener(PointDisplacer): 

491 """! 

492 Given a displacer and a function that returns a factor for a point, 

493 multiplies the effect of the displacer by the factor 

494 """ 

495 def __init__(self, displacer, dampener): 

496 self.displacer = displacer 

497 self.dampener = dampener 

498 

499 @property 

500 def time_start(self): 

501 return self.displacer.time_start 

502 

503 def _on_displace(self, startpos, f): 

504 disp = self.displacer._on_displace(startpos, f) 

505 damp = self.dampener(startpos) 

506 return disp * damp 

507 

508 @property 

509 def n_frames(self): 

510 return self.displacer.n_frames 

511 

512 def frame_time(self, f): 

513 return self.displacer.frame_time(f) 

514 

515 

516class FollowDisplacer(PointDisplacer): 

517 def __init__( 

518 self, 

519 origin, 

520 range, 

521 offset_func, 

522 time_start, time_end, n_frames, 

523 falloff_exp=1, 

524 ): 

525 """! 

526 @brief Uses a custom offset function, and applies a falloff to the displacement 

527 

528 @param origin Origin point for the falloff 

529 @param range Radius after which the points will not move 

530 @param offset_func Function returning an offset given a ratio of the time 

531 @param time_start When the animation shall start 

532 @param time_end When the animation shall end 

533 @param n_frames Number of frames in the animation 

534 @param falloff_exp Exponent for the falloff 

535 """ 

536 super().__init__(time_start, time_end, n_frames) 

537 self.origin = origin 

538 self.range = range 

539 self.offset_func = offset_func 

540 self.falloff_exp = falloff_exp 

541 

542 def _on_displace(self, startpos, f): 

543 influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp 

544 return self.offset_func(f / self.n_frames) * influence