Coverage for lib/lottie/objects/properties.py: 33%

463 statements  

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

1import math 

2from functools import reduce 

3from .base import LottieObject, LottieProp, PseudoList, PseudoBool 

4from . import easing 

5from ..nvector import NVector 

6from .bezier import Bezier 

7from ..utils.color import Color 

8 

9 

10class KeyframeBezier: 

11 NEWTON_ITERATIONS = 4 

12 NEWTON_MIN_SLOPE = 0.001 

13 SUBDIVISION_PRECISION = 0.0000001 

14 SUBDIVISION_MAX_ITERATIONS = 10 

15 SPLINE_TABLE_SIZE = 11 

16 SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0) 

17 

18 def __init__(self, h1, h2): 

19 self.h1 = h1 

20 self.h2 = h2 

21 self._sample_values = None 

22 

23 @classmethod 

24 def from_keyframe(cls, keyframe): 

25 return cls(keyframe.out_value, keyframe.in_value) 

26 

27 def bezier(self): 

28 bez = Bezier() 

29 bez.add_point(NVector(0, 0), outp=NVector(self.h1.x, self.h1.y)) 

30 bez.add_point(NVector(1, 1), inp=NVector(self.h2.x-1, self.h2.y-1)) 

31 return bez 

32 

33 def _a(self, c1, c2): 

34 return 1 - 3 * c2 + 3 * c1 

35 

36 def _b(self, c1, c2): 

37 return 3 * c2 - 6 * c1 

38 

39 def _c(self, c1): 

40 return 3 * c1 

41 

42 def _bezier_component(self, t, c1, c2): 

43 return ((self._a(c1, c2) * t + self._b(c1, c2)) * t + self._c(c1)) * t 

44 

45 def point_at(self, t): 

46 return NVector( 

47 self._bezier_component(t, self.h1.x, self.h2.x), 

48 self._bezier_component(t, self.h1.y, self.h2.y) 

49 ) 

50 

51 def _slope_component(self, t, c1, c2): 

52 return 3 * self._a(c1, c2) * t * t + 2 * self._b(c1, c2) * t + self._c(c1) 

53 

54 def slope_at(self, t): 

55 return NVector( 

56 self._slope_component(t, self.h1.x, self.h2.x), 

57 self._slope_component(t, self.h1.y, self.h2.y) 

58 ) 

59 

60 def _binary_subdivide(self, x, interval_start, interval_end): 

61 current_x = None 

62 t = None 

63 i = 0 

64 for i in range(self.SUBDIVISION_MAX_ITERATIONS): 

65 if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISION: 

66 break 

67 t = interval_start + (interval_end - interval_start) / 2.0 

68 current_x = self._bezier_component(t, self.h1.x, self.h2.x) - x 

69 if current_x > 0.0: 

70 interval_end = t 

71 else: 

72 interval_start = t 

73 return t 

74 

75 def _newton_raphson(self, x, t_guess): 

76 for i in range(self.NEWTON_ITERATIONS): 

77 slope = self._slope_component(t_guess, self.h1.x, self.h2.x) 

78 if slope == 0: 

79 return t_guess 

80 current_x = self._bezier_component(t_guess, self.h1.x, self.h2.x) - x 

81 t_guess -= current_x / slope 

82 return t_guess 

83 

84 def _get_sample_values(self): 

85 if self._sample_values is None: 

86 self._sample_values = [ 

87 self._bezier_component(i * self.SAMPLE_STEP_SIZE, self.h1.x, self.h2.x) 

88 for i in range(self.SPLINE_TABLE_SIZE) 

89 ] 

90 return self._sample_values 

91 

92 def t_for_x(self, x): 

93 sample_values = self._get_sample_values() 

94 interval_start = 0 

95 current_sample = 1 

96 last_sample = self.SPLINE_TABLE_SIZE - 1 

97 while current_sample != last_sample and sample_values[current_sample] <= x: 

98 interval_start += self.SAMPLE_STEP_SIZE 

99 current_sample += 1 

100 current_sample -= 1 

101 

102 dist = (x - sample_values[current_sample]) / (sample_values[current_sample+1] - sample_values[current_sample]) 

103 t_guess = interval_start + dist * self.SAMPLE_STEP_SIZE 

104 initial_slope = self._slope_component(t_guess, self.h1.x, self.h2.x) 

105 if initial_slope >= self.NEWTON_MIN_SLOPE: 

106 return self._newton_raphson(x, t_guess) 

107 if initial_slope == 0: 

108 return t_guess 

109 return self._binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZE) 

110 

111 def y_at_x(self, x): 

112 t = self.t_for_x(x) 

113 return self._bezier_component(t, self.h1.y, self.h2.y) 

114 

115 

116## @ingroup Lottie 

117class Keyframe(LottieObject): 

118 _props = [ 

119 LottieProp("time", "t", float, False), 

120 LottieProp("in_value", "i", easing.KeyframeBezierHandle, False), 

121 LottieProp("out_value", "o", easing.KeyframeBezierHandle, False), 

122 LottieProp("hold", "h", PseudoBool), 

123 ] 

124 

125 def __init__(self, time=0, easing_function=None): 

126 """! 

127 @param time Start time of keyframe segment 

128 @param easing_function Callable that performs the easing 

129 """ 

130 ## Start time of keyframe segment. 

131 self.time = time 

132 ## Bezier curve easing in value. 

133 self.in_value = None 

134 ## Bezier curve easing out value. 

135 self.out_value = None 

136 ## Jump to the end value 

137 self.hold = None 

138 

139 if easing_function: 

140 easing_function(self) 

141 

142 @property 

143 def jump(self): 

144 return self.hold 

145 

146 @jump.setter 

147 def jump(self, v): 

148 self.hold = v 

149 

150 def bezier(self): 

151 if self.hold: 

152 bez = Bezier() 

153 bez.add_point(NVector(0, 0)) 

154 bez.add_point(NVector(1, 0)) 

155 bez.add_point(NVector(1, 1)) 

156 return bez 

157 else: 

158 return KeyframeBezier.from_keyframe(self).bezier() 

159 

160 def lerp_factor(self, ratio): 

161 return KeyframeBezier.from_keyframe(self).y_at_x(ratio) 

162 

163 def __str__(self): 

164 return "%s %s" % (self.time, self.start) 

165 

166 

167## @ingroup Lottie 

168class OffsetKeyframe(Keyframe): 

169 """! 

170 Keyframe for MultiDimensional values 

171 

172 @par Bezier easing 

173 @parblock 

174 Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1). 

175 

176 @p out_value and @p in_value are the other two handles for a quadratic bezier, 

177 expressed as absolute values in this 0-1 space. 

178 

179 See also https://cubic-bezier.com/ 

180 @endparblock 

181 """ 

182 _props = [ 

183 LottieProp("start", "s", NVector, False), 

184 LottieProp("end", "e", NVector, False), 

185 ] 

186 

187 def __init__(self, time=0, start=None, end=None, easing_function=None, in_tan=None, out_tan=None): 

188 Keyframe.__init__(self, time, easing_function) 

189 ## Start value of keyframe segment. 

190 self.start = start 

191 ## End value of keyframe segment. 

192 self.end = end 

193 ## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position) 

194 self.in_tan = in_tan 

195 ## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position) 

196 self.out_tan = out_tan 

197 

198 def interpolated_value(self, ratio, next_start=None): 

199 end = next_start if self.end is None else self.end 

200 if end is None: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true

201 return self.start 

202 if not self.in_value or not self.out_value: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true

203 return self.start 

204 if ratio == 1: 204 ↛ 206line 204 didn't jump to line 206, because the condition on line 204 was never false

205 return end 

206 if ratio == 0: 

207 return self.start 

208 if self.in_tan and self.out_tan: 

209 bezier = Bezier() 

210 bezier.add_point(self.start, NVector(0, 0), self.out_tan) 

211 bezier.add_point(end, self.in_tan, NVector(0, 0)) 

212 return bezier.point_at(ratio) 

213 

214 lerpv = self.lerp_factor(ratio) 

215 return self.start.lerp(end, lerpv) 

216 

217 def interpolated_tangent_angle(self, ratio, next_start=None): 

218 end = next_start if self.end is None else self.end 

219 if end is None or not self.in_tan or not self.out_tan: 

220 return 0 

221 

222 bezier = Bezier() 

223 bezier.add_point(self.start, NVector(0, 0), self.out_tan) 

224 bezier.add_point(end, self.in_tan, NVector(0, 0)) 

225 return bezier.tangent_angle_at(ratio) 

226 

227 def __repr__(self): 

228 return "<%s.%s %s %s%s>" % ( 

229 type(self).__module__, 

230 type(self).__name__, 

231 self.time, 

232 self.start, 

233 (" -> %s" % self.end) if self.end is not None else "" 

234 ) 

235 

236 

237class AnimatableMixin: 

238 keyframe_type = Keyframe 

239 

240 def __init__(self, value=None): 

241 ## Non-animated value 

242 self.value = value 

243 ## Property index 

244 self.property_index = None 

245 ## Whether it's animated 

246 self.animated = False 

247 ## Keyframe list 

248 self.keyframes = None 

249 ## Expression 

250 self.expression = None 

251 

252 def clear_animation(self, value): 

253 """! 

254 Sets a fixed value, removing animated keyframes 

255 """ 

256 self.value = value 

257 self.animated = False 

258 self.keyframes = None 

259 

260 def add_keyframe(self, time, value, interp=easing.Linear(), *args, **kwargs): 

261 """! 

262 @param time The time this keyframe appears in 

263 @param value The value the property should have at @p time 

264 @param interp The easing callable used to update the tangents of the previous keyframe 

265 @param args Extra arguments to pass the keyframe constructor 

266 @param kwargs Extra arguments to pass the keyframe constructor 

267 @note Always call add_keyframe with increasing @p time value 

268 """ 

269 if not self.animated: 

270 self.value = None 

271 self.keyframes = [] 

272 self.animated = True 

273 else: 

274 if self.keyframes[-1].time == time: 274 ↛ 275line 274 didn't jump to line 275, because the condition on line 274 was never true

275 if value != self.keyframes[-1].start: 

276 self.keyframes[-1].start = value 

277 return 

278 else: 

279 self.keyframes[-1].end = value.clone() 

280 

281 self.keyframes.append(self.keyframe_type( 

282 time, 

283 value, 

284 None, 

285 interp, 

286 *args, 

287 **kwargs 

288 )) 

289 

290 def get_value(self, time=0): 

291 """! 

292 @brief Returns the value of the property at the given frame/time 

293 """ 

294 if not self.animated: 

295 return self.value 

296 

297 if not self.keyframes: 

298 return None 

299 

300 return self._get_value_helper(time)[0] 

301 

302 def _get_value_helper(self, time): 

303 val = self.keyframes[0].start 

304 for i in range(len(self.keyframes)): 

305 k = self.keyframes[i] 

306 if time - k.time <= 0: 

307 if k.start is not None: 

308 val = k.start 

309 

310 kp = self.keyframes[i-1] if i > 0 else None 

311 if kp: 

312 t = (time - kp.time) / (k.time - kp.time) 

313 end = kp.end 

314 if end is None: 314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true

315 end = val 

316 if end is not None: 316 ↛ 318line 316 didn't jump to line 318, because the condition on line 316 was never false

317 val = kp.interpolated_value(t, end) 

318 return val, end, kp, t 

319 return val, None, None, None 

320 if k.end is not None: 

321 val = k.end 

322 return val, None, None, None 

323 

324 def to_dict(self): 

325 d = super().to_dict() 

326 if self.animated: 

327 if "k" not in d: 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true

328 return d 

329 last = d["k"][-1] 

330 last.pop("i", None) 

331 last.pop("o", None) 

332 return d 

333 

334 def __repr__(self): 

335 if self.keyframes and len(self.keyframes) > 1: 

336 val = "%s -> %s" % (self.keyframes[0].start, self.keyframes[-2].end) 

337 else: 

338 val = self.value 

339 return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val) 

340 

341 def __str__(self): 

342 if self.animated: 

343 return "animated" 

344 return str(self.value) 

345 

346 @classmethod 

347 def merge_keyframes(cls, items, conversion): 

348 """ 

349 @todo Remove similar functionality from SVG/sif parsers 

350 """ 

351 keyframes = [] 

352 for animatable in items: 

353 if animatable.animated: 

354 keyframes.extend(animatable.keyframes) 

355 

356 # TODO properly interpolate tangents 

357 new_kframes = [] 

358 for keyframe in sorted(keyframes, key=lambda kf: kf.time): 

359 if new_kframes and new_kframes[-1].time == keyframe.time: 

360 continue 

361 kfcopy = keyframe.clone() 

362 kfcopy.start = conversion(*(i.get_value(keyframe.time) for i in items)) 

363 new_kframes.append(kfcopy) 

364 

365 for i in range(0, len(new_kframes) - 1): 

366 new_kframes[i].end = new_kframes[i+1].start 

367 

368 return new_kframes 

369 

370 @classmethod 

371 def load(cls, lottiedict): 

372 obj = super().load(lottiedict) 

373 if "a" not in lottiedict: 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true

374 obj.animated = prop_animated(lottiedict) 

375 return obj 

376 

377 def average(self, start, end, value_map=lambda v: v): 377 ↛ exitline 377 didn't run the lambda on line 377

378 if not self.animated or len(self.keyframes) == 0: 

379 return value_map(self.value) 

380 elif len(self.keyframes) == 1: 

381 return value_map(self.keyframes[0].start) 

382 

383 total_weight = 0 

384 value = value_map(self.keyframes[0].start) * 0 

385 kf_index = 0 

386 

387 if self.keyframes[0].time > start: 

388 weight = self.keyframes[0].time - start 

389 value += value_map(self.keyframes[0].start) * weight 

390 total_weight += weight 

391 elif self.keyframes[0].time < start: 

392 avg = (self.keyframes[1].time + self.keyframes[0].time) / 2 

393 kf_index = 1 

394 if self.keyframes[0].hold: 

395 weight = self.keyframes[1].time - start 

396 value += value_map(self.keyframes[0].start) * weight 

397 total_weight += weight 

398 elif start < avg: 

399 weight = avg - start 

400 value += value_map(self.keyframes[0].start) * weight 

401 total_weight += weight 

402 

403 half = (self.keyframes[1].time - self.keyframes[0].time) / 2 

404 value += value_map(self.keyframes[1].start) * half 

405 total_weight += half 

406 else: 

407 weight = self.keyframes[1].time - start 

408 value += value_map(self.keyframes[1].start) * weight 

409 total_weight += weight 

410 

411 for i in range(kf_index, len(self.keyframes) - 1): 

412 kf = self.keyframes[i] 

413 kfn = self.keyframes[i + 1] 

414 if kfn.time > end: 

415 break 

416 

417 delta = kfn.time - kf.time 

418 total_weight += delta 

419 if kf.hold: 

420 value += value_map(kf.start) * delta 

421 else: 

422 value += value_map(kf.start) * (delta / 2) 

423 value += value_map(kfn.start) * (delta / 2) 

424 else: 

425 kf = self.keyframes[-1] 

426 kfn = self.keyframes[-1] 

427 

428 if kfn.time < end: 

429 weight = end - kfn.time 

430 value += value_map(kfn.start) * weight 

431 total_weight += weight 

432 elif kf.hold: 

433 weight = end - kf.time 

434 value += value_map(kf.start) * weight 

435 total_weight += weight 

436 elif kfn.time > end: 

437 avg = (kf.time + kfn.time) / 2 

438 if end < avg: 

439 weight = end - kf.time 

440 value += value_map(kf.start) * weight 

441 total_weight += weight 

442 else: 

443 half = (kfn.time - kf.time) / 2 

444 value += value_map(kf.start) * half 

445 total_weight += half 

446 

447 weight = end - avg 

448 value += value_map(kfn.start) * weight 

449 total_weight += weight 

450 

451 if total_weight == 0: 

452 return value 

453 return value / total_weight 

454 

455 

456def prop_animated(l): 

457 if "a" in l: 457 ↛ 459line 457 didn't jump to line 459, because the condition on line 457 was never false

458 return l["a"] 

459 if "k" not in l: 

460 return False 

461 if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict): 

462 return True 

463 return False 

464 

465 

466def prop_not_animated(l): 

467 return not prop_animated(l) 

468 

469 

470## @ingroup Lottie 

471class MultiDimensional(AnimatableMixin, LottieObject): 

472 """! 

473 An animatable property that holds a NVector 

474 """ 

475 keyframe_type = OffsetKeyframe 

476 _props = [ 

477 LottieProp("value", "k", NVector, False, prop_not_animated), 

478 LottieProp("property_index", "ix", int, False), 

479 LottieProp("animated", "a", PseudoBool, False), 

480 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), 

481 LottieProp("expression", "x", str, False), 

482 ] 

483 

484 def get_tangent_angle(self, time=0): 

485 """! 

486 @brief Returns the value tangent angle of the property at the given frame/time 

487 """ 

488 if not self.keyframes or len(self.keyframes) < 2: 

489 return 0 

490 

491 val, end, kp, t = self._get_value_helper(time) 

492 if kp: 

493 return kp.interpolated_tangent_angle(t, end) 

494 

495 if self.keyframes[0].time >= time: 

496 end = self.keyframes[0].end if self.keyframes[0].end is not None else self.keyframes[1].start 

497 return self.keyframes[0].interpolated_tangent_angle(0, end) 

498 

499 return 0 

500 

501 

502## @ingroup Lottie 

503class PositionKeyframe(OffsetKeyframe): 

504 """! 

505 Keyframe for Positional values 

506 """ 

507 _props = [ 

508 LottieProp("in_tan", "ti", NVector, False), 

509 LottieProp("out_tan", "to", NVector, False), 

510 ] 

511 

512 

513class PositionValue(MultiDimensional): 

514 keyframe_type = PositionKeyframe 

515 _props = [ 

516 LottieProp("value", "k", NVector, False, prop_not_animated), 

517 LottieProp("property_index", "ix", int, False), 

518 LottieProp("animated", "a", PseudoBool, False), 

519 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), 

520 ] 

521 

522 @classmethod 

523 def load(cls, lottiedict): 

524 obj = super().load(lottiedict) 

525 if lottiedict.get("s", False): 525 ↛ 526line 525 didn't jump to line 526, because the condition on line 525 was never true

526 cls._load_split(lottiedict, obj) 

527 

528 return obj 

529 

530 @classmethod 

531 def _load_split(cls, lottiedict, obj): 

532 components = [ 

533 Value.load(lottiedict.get("x", {})), 

534 Value.load(lottiedict.get("y", {})), 

535 ] 

536 if "z" in lottiedict: 

537 components.append(Value.load(lottiedict.get("z", {}))) 

538 

539 has_anim = any(x for x in components if x.animated) 

540 if not has_anim: 

541 obj.value = NVector(*(a.value for a in components)) 

542 obj.animated = False 

543 obj.keyframes = None 

544 return 

545 

546 obj.animated = True 

547 obj.value = None 

548 obj.keyframes = cls.merge_keyframes(components, NVector) 

549 

550 

551class ColorValue(AnimatableMixin, LottieObject): 

552 """! 

553 An animatable property that holds a Color 

554 """ 

555 keyframe_type = OffsetKeyframe 

556 _props = [ 

557 LottieProp("value", "k", Color, False, prop_not_animated), 

558 LottieProp("property_index", "ix", int, False), 

559 LottieProp("animated", "a", PseudoBool, False), 

560 LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated), 

561 LottieProp("expression", "x", str, False), 

562 ] 

563 

564 

565## @ingroup Lottie 

566class GradientColors(LottieObject): 

567 """! 

568 Represents colors and offsets in a gradient 

569 

570 Colors are represented as a flat list interleaving offsets and color components in weird ways 

571 There are two possible layouts: 

572 

573 Without alpha, the colors are a sequence of offset, r, g, b 

574 

575 With alpha, same as above but at the end of the list there is a sequence of offset, alpha 

576 

577 Examples: 

578 

579 For the gradient [0, red], [0.5, yellow], [1, green] 

580 The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0] 

581 

582 For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity] 

583 The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0, 0, 0.8, 0.5, 0.7, 1, 0.6] 

584 """ 

585 _props = [ 

586 LottieProp("colors", "k", MultiDimensional), 

587 LottieProp("count", "p", int), 

588 ] 

589 

590 def __init__(self, stops=[]): 

591 ## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array 

592 self.colors = MultiDimensional(NVector()) 

593 ## Number of colors 

594 self.count = 0 

595 if stops: 

596 self.set_stops(stops) 

597 

598 @staticmethod 

599 def color_to_stops(self, colors): 

600 """ 

601 Converts a list of colors (Color) to tuples (offset, color) 

602 """ 

603 return [ 

604 (i / (len(colors)-1), color) 

605 for i, color in enumerate(colors) 

606 ] 

607 

608 def set_stops(self, stops, keyframe=None): 

609 """! 

610 @param stops iterable of (offset, Color) tuples 

611 @param keyframe keyframe index (or None if not animated) 

612 """ 

613 flat = self._flatten_stops(stops) 

614 if self.colors.animated and keyframe is not None: 

615 if keyframe > 1: 

616 self.colors.keyframes[keyframe-1].end = flat 

617 self.colors.keyframes[keyframe].start = flat 

618 else: 

619 self.colors.clear_animation(flat) 

620 self.count = len(stops) 

621 

622 def _flatten_stops(self, stops): 

623 flattened_colors = NVector(*reduce( 

624 lambda a, b: a + b, 

625 ( 

626 [off] + color.components[:3] 

627 for off, color in stops 

628 ) 

629 )) 

630 

631 if any(len(c) > 3 for o, c in stops): 

632 flattened_colors.components += reduce( 

633 lambda a, b: a + b, 

634 ( 

635 [off] + [self._get_alpha(color)] 

636 for off, color in stops 

637 ) 

638 ) 

639 return flattened_colors 

640 

641 def _get_alpha(self, color): 

642 if len(color) > 3: 

643 return color[3] 

644 return 1 

645 

646 def _add_to_flattened(self, offset, color, flattened): 

647 flat = [offset] + list(color[:3]) 

648 rgb_size = 4 * self.count 

649 

650 if len(flattened) == rgb_size: 

651 # No alpha 

652 flattened.extend(flat) 

653 if self.count == 0 and len(color) > 3: 

654 flattened.append(offset) 

655 flattened.append(color[3]) 

656 else: 

657 flattened[rgb_size:rgb_size] = flat 

658 flattened.append(offset) 

659 flattened.append(self._get_alpha(color)) 

660 

661 def add_color(self, offset, color, keyframe=None): 

662 if self.colors.animated: 

663 if keyframe is None: 

664 for kf in self.colors.keyframes: 

665 if kf.start: 

666 self._add_to_flattened(offset, color, kf.start.components) 

667 if kf.end: 

668 self._add_to_flattened(offset, color, kf.end.components) 

669 else: 

670 if keyframe > 1: 

671 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe-1].end.components) 

672 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe].start.components) 

673 else: 

674 self._add_to_flattened(offset, color, self.colors.value.components) 

675 self.count += 1 

676 

677 def add_keyframe(self, time, stops, ease=easing.Linear()): 

678 """! 

679 @param time Frame time 

680 @param stops Iterable of (offset, Color) tuples 

681 @param ease Easing function 

682 """ 

683 self.colors.add_keyframe(time, self._flatten_stops(stops), ease) 

684 

685 def get_stops(self, keyframe=0): 

686 if keyframe is not None: 

687 colors = self.colors.keyframes[keyframe].start 

688 else: 

689 colors = self.colors.value 

690 return self._stops_from_flat(colors) 

691 

692 def _stops_from_flat(self, colors): 

693 if len(colors) == 4 * self.count: 

694 for i in range(self.count): 

695 off = i * 4 

696 yield colors[off], Color(*colors[off+1:off+4]) 

697 else: 

698 for i in range(self.count): 

699 off = i * 4 

700 aoff = self.count * 4 + i * 2 + 1 

701 yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff]) 

702 

703 def stops_at(self, time): 

704 return self._stops_from_flat(self.colors.get_value(time)) 

705 

706 

707## @ingroup Lottie 

708class Value(AnimatableMixin, LottieObject): 

709 """! 

710 An animatable property that holds a float 

711 """ 

712 keyframe_type = OffsetKeyframe 

713 _props = [ 

714 LottieProp("value", "k", float, False, prop_not_animated), 

715 LottieProp("property_index", "ix", int, False), 

716 LottieProp("animated", "a", PseudoBool, False), 

717 LottieProp("keyframes", "k", keyframe_type, True, prop_animated), 

718 LottieProp("expression", "x", str, False), 

719 ] 

720 

721 def __init__(self, value=0): 

722 super().__init__(value) 

723 

724 def add_keyframe(self, time, value, ease=easing.Linear()): 

725 super().add_keyframe(time, NVector(value), ease) 

726 

727 def get_value(self, time=0): 

728 v = super().get_value(time) 

729 if self.animated and self.keyframes: 729 ↛ 730line 729 didn't jump to line 730, because the condition on line 729 was never true

730 return v[0] 

731 return v 

732 

733 

734## @ingroup Lottie 

735class ShapePropKeyframe(Keyframe): 

736 """! 

737 Keyframe holding Bezier objects 

738 """ 

739 _props = [ 

740 LottieProp("start", "s", Bezier, PseudoList), 

741 LottieProp("end", "e", Bezier, PseudoList), 

742 ] 

743 

744 def __init__(self, time=0, start=None, end=None, easing_function=None): 

745 Keyframe.__init__(self, time, easing_function) 

746 ## Start value of keyframe segment. 

747 self.start = start 

748 ## End value of keyframe segment. 

749 self.end = end 

750 

751 def interpolated_value(self, ratio, next_start=None): 

752 end = next_start if self.end is None else self.end 

753 if end is None: 

754 return self.start 

755 if not self.in_value or not self.out_value: 

756 return self.start 

757 if ratio == 1: 

758 return end 

759 if ratio == 0 or len(self.start.vertices) != len(end.vertices): 

760 return self.start 

761 

762 lerpv = self.lerp_factor(ratio) 

763 bez = Bezier() 

764 bez.closed = self.start.closed 

765 for i in range(len(self.start.vertices)): 

766 bez.vertices.append(self.start.vertices[i].lerp(end.vertices[i], lerpv)) 

767 bez.in_tangents.append(self.start.in_tangents[i].lerp(end.in_tangents[i], lerpv)) 

768 bez.out_tangents.append(self.start.out_tangents[i].lerp(end.out_tangents[i], lerpv)) 

769 return bez 

770 

771 

772## @ingroup Lottie 

773class ShapeProperty(AnimatableMixin, LottieObject): 

774 """! 

775 An animatable property that holds a Bezier 

776 """ 

777 keyframe_type = ShapePropKeyframe 

778 _props = [ 

779 LottieProp("value", "k", Bezier, False, prop_not_animated), 

780 LottieProp("expression", "x", str, False), 

781 LottieProp("property_index", "ix", float, False), 

782 LottieProp("animated", "a", PseudoBool, False), 

783 LottieProp("keyframes", "k", keyframe_type, True, prop_animated), 

784 ] 

785 

786 def __init__(self, bezier=None): 

787 super().__init__(bezier or Bezier()) 

788 

789 

790#ingroup Lottie 

791class SplitVector(LottieObject): 

792 """! 

793 An animatable property that is split into individually anaimated components 

794 """ 

795 _props = [ 

796 LottieProp("split", "s", bool, False), 

797 LottieProp("x", "x", Value, False), 

798 LottieProp("y", "y", Value, False), 

799 LottieProp("z", "z", Value, False), 

800 ] 

801 

802 @property 

803 def split(self): 

804 return True 

805 

806 def __init__(self, x=0, y=0): 

807 super().__init__() 

808 

809 self.x = Value(x) 

810 self.y = Value(y) 

811 self.z = None