Coverage for lib/lottie/objects/bezier.py: 23%

298 statements  

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

1import math 

2from .base import LottieObject, LottieProp 

3from ..nvector import NVector 

4 

5 

6class BezierPoint: 

7 def __init__(self, vertex, in_tangent=None, out_tangent=None): 

8 self.vertex = vertex 

9 self.in_tangent = in_tangent or NVector(0, 0) 

10 self.out_tangent = out_tangent or NVector(0, 0) 

11 

12 def relative(self): 

13 return self 

14 

15 @classmethod 

16 def smooth(cls, point, in_tangent): 

17 return cls(point, in_tangent, -in_tangent) 

18 

19 @classmethod 

20 def from_absolute(cls, point, in_tangent=None, out_tangent=None): 

21 if not in_tangent: 

22 in_tangent = point.clone() 

23 if not out_tangent: 

24 out_tangent = point.clone() 

25 return BezierPoint(point, in_tangent, out_tangent) 

26 

27 

28class BezierPointView: 

29 """ 

30 View for bezier point 

31 """ 

32 def __init__(self, bezier, index): 

33 self.bezier = bezier 

34 self.index = index 

35 

36 @property 

37 def vertex(self): 

38 return self.bezier.vertices[self.index] 

39 

40 @vertex.setter 

41 def vertex(self, point): 

42 self.bezier.vertices[self.index] = point 

43 

44 @property 

45 def in_tangent(self): 

46 return self.bezier.in_tangents[self.index] 

47 

48 @in_tangent.setter 

49 def in_tangent(self, point): 

50 self.bezier.in_tangents[self.index] = point 

51 

52 @property 

53 def out_tangent(self): 

54 return self.bezier.out_tangents[self.index] 

55 

56 @out_tangent.setter 

57 def out_tangent(self, point): 

58 self.bezier.out_tangents[self.index] = point 

59 

60 def relative(self): 

61 return self 

62 

63 

64class AbsoluteBezierPointView(BezierPointView): 

65 @property 

66 def in_tangent(self): 

67 return self.bezier.in_tangents[self.index] + self.vertex 

68 

69 @in_tangent.setter 

70 def in_tangent(self, point): 

71 self.bezier.in_tangents[self.index] = point - self.vertex 

72 

73 @property 

74 def out_tangent(self): 

75 return self.bezier.out_tangents[self.index] + self.vertex 

76 

77 @out_tangent.setter 

78 def out_tangent(self, point): 

79 self.bezier.out_tangents[self.index] = point - self.vertex 

80 

81 def relative(self): 

82 return BezierPointView(self.bezier, self.index) 

83 

84 

85class BezierView: 

86 def __init__(self, bezier, absolute=False): 

87 self.bezier = bezier 

88 self.is_absolute = absolute 

89 

90 def point(self, index): 

91 if self.is_absolute: 

92 return AbsoluteBezierPointView(self.bezier, index) 

93 return BezierPointView(self.bezier, index) 

94 

95 def __len__(self): 

96 return len(self.bezier.vertices) 

97 

98 def __getitem__(self, key): 

99 if isinstance(key, slice): 

100 return [ 

101 self.point(i) 

102 for i in key 

103 ] 

104 return self.point(key) 

105 

106 def __iter__(self): 

107 for i in range(len(self)): 

108 yield self.point(i) 

109 

110 def append(self, point): 

111 if isinstance(point, NVector): 

112 self.bezier.add_point(point.clone()) 

113 else: 

114 bpt = point.relative() 

115 self.bezier.add_point(bpt.vertex.clone(), bpt.in_tangent.clone(), bpt.out_tangent.clone()) 

116 

117 @property 

118 def absolute(self): 

119 return BezierView(self.bezier, True) 

120 

121 

122## @ingroup Lottie 

123class Bezier(LottieObject): 

124 """! 

125 Single bezier curve 

126 """ 

127 _props = [ 

128 LottieProp("closed", "c", bool, False), 

129 LottieProp("in_tangents", "i", NVector, True), 

130 LottieProp("out_tangents", "o", NVector, True), 

131 LottieProp("vertices", "v", NVector, True), 

132 ] 

133 

134 def __init__(self): 

135 ## Closed property of shape 

136 self.closed = False 

137 ## Cubic bezier handles for the segments before each vertex 

138 self.in_tangents = [] 

139 ## Cubic bezier handles for the segments after each vertex 

140 self.out_tangents = [] 

141 ## Bezier curve vertices. 

142 self.vertices = [] 

143 #self.rel_tangents = rel_tangents 

144 ## More convent way to access points 

145 self.points = BezierView(self) 

146 

147 def clone(self): 

148 clone = Bezier() 

149 clone.closed = self.closed 

150 clone.in_tangents = [p.clone() for p in self.in_tangents] 

151 clone.out_tangents = [p.clone() for p in self.out_tangents] 

152 clone.vertices = [p.clone() for p in self.vertices] 

153 #clone.rel_tangents = self.rel_tangents 

154 return clone 

155 

156 def insert_point(self, index, pos, inp=NVector(0, 0), outp=NVector(0, 0)): 

157 """! 

158 Inserts a point at the given index 

159 @param index Index to insert the point at 

160 @param pos Point to add 

161 @param inp Tangent entering the point, as a vector relative to @p pos 

162 @param outp Tangent exiting the point, as a vector relative to @p pos 

163 @returns @c self, for easy chaining 

164 """ 

165 self.vertices.insert(index, pos) 

166 self.in_tangents.insert(index, inp.clone()) 

167 self.out_tangents.insert(index, outp.clone()) 

168 #if not self.rel_tangents: 

169 #self.in_tangents[-1] += pos 

170 #self.out_tangents[-1] += pos 

171 return self 

172 

173 def add_point(self, pos, inp=NVector(0, 0), outp=NVector(0, 0)): 

174 """! 

175 Appends a point to the curve 

176 @see insert_point 

177 """ 

178 self.insert_point(len(self.vertices), pos, inp, outp) 

179 return self 

180 

181 def add_smooth_point(self, pos, inp): 

182 """! 

183 Appends a point with symmetrical tangents 

184 @see insert_point 

185 """ 

186 self.add_point(pos, inp, -inp) 

187 return self 

188 

189 def close(self, closed=True): 

190 """! 

191 Updates self.closed 

192 @returns @c self, for easy chaining 

193 """ 

194 self.closed = closed 

195 return self 

196 

197 def point_at(self, t): 

198 """! 

199 @param t A value between 0 and 1, percentage along the length of the curve 

200 @returns The point at @p t in the curve 

201 """ 

202 i, t = self._index_t(t) 

203 points = self._bezier_points(i, True) 

204 return self._solve_bezier(t, points) 

205 

206 def tangent_angle_at(self, t): 

207 i, t = self._index_t(t) 

208 points = self._bezier_points(i, True) 

209 

210 n = len(points) - 1 

211 if n > 0: 

212 delta = sum(( 

213 (points[i+1] - points[i]) * n * self._solve_bezier_coeff(i, n - 1, t) 

214 for i in range(n) 

215 ), NVector(0, 0)) 

216 return math.atan2(delta.y, delta.x) 

217 

218 return 0 

219 

220 def _split(self, t): 

221 i, t = self._index_t(t) 

222 cub = self._bezier_points(i, True) 

223 split1, split2 = self._split_segment(t, cub) 

224 return i, split1, split2 

225 

226 def _split_segment(self, t, cub): 

227 if len(cub) == 2: 

228 k = self._solve_bezier_step(t, cub)[0] 

229 split1 = [cub[0], NVector(0, 0), NVector(0, 0), k] 

230 split2 = [k, NVector(0, 0), NVector(0, 0), cub[-1]] 

231 return split1, split2 

232 

233 if len(cub) == 3: 

234 quad = cub 

235 else: 

236 quad = self._solve_bezier_step(t, cub) 

237 lin = self._solve_bezier_step(t, quad) 

238 k = self._solve_bezier_step(t, lin)[0] 

239 split1 = [cub[0], quad[0]-cub[0], lin[0]-k, k] 

240 split2 = [k, lin[-1]-k, quad[-1]-cub[-1], cub[-1]] 

241 return split1, split2 

242 

243 def split_at(self, t): 

244 """! 

245 Get two pieces out of a Bezier curve 

246 @param t A value between 0 and 1, percentage along the length of the curve 

247 @returns Two Bezier objects that correspond to self, but split at @p t 

248 """ 

249 if len(self.vertices) < 2: 

250 return Bezier(), self.clone() 

251 

252 i, split1, split2 = self._split(t) 

253 

254 seg1 = Bezier() 

255 seg2 = Bezier() 

256 for j in range(i): 

257 seg1.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone()) 

258 for j in range(i+2, len(self.vertices)): 

259 seg2.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone()) 

260 

261 seg1.add_point(split1[0], self.in_tangents[i].clone(), split1[1]) 

262 seg1.add_point(split1[3], split1[2], split2[1]) 

263 

264 seg2.insert_point(0, split2[0], split1[2], split2[1]) 

265 seg2.insert_point(1, split2[3], split2[2], self.out_tangents[i+1].clone()) 

266 

267 return seg1, seg2 

268 

269 def segment(self, t1, t2): 

270 """! 

271 Splits a Bezier in two points and returns the segment between the 

272 @param t1 A value between 0 and 1, percentage along the length of the curve 

273 @param t2 A value between 0 and 1, percentage along the length of the curve 

274 @returns Bezier object that correspond to the segment between @p t1 and @p t2 

275 """ 

276 if self.closed and self.vertices and self.vertices[-1] != self.vertices[0]: 

277 copy = self.clone() 

278 copy.add_point(self.vertices[0]) 

279 copy.closed = False 

280 return copy.segment(t1, t2) 

281 

282 if t1 > 1: 

283 t1 = 1 

284 if t2 > 1: 

285 t2 = 1 

286 

287 if t1 > t2: 

288 t1, t2 = t2, t1 

289 elif t1 == t2: 

290 seg = Bezier() 

291 p = self.point_at(t1) 

292 seg.add_point(p) 

293 seg.add_point(p) 

294 return seg 

295 

296 seg1, seg2 = self.split_at(t1) 

297 t2p = (t2-t1) / (1-t1) 

298 seg3, seg4 = seg2.split_at(t2p) 

299 return seg3 

300 

301 def split_self_multi(self, positions): 

302 """! 

303 Adds more points to the Bezier 

304 @param positions list of percentages along the curve 

305 """ 

306 if not len(positions): 

307 return 

308 t1 = positions[0] 

309 seg1, seg2 = self.split_at(t1) 

310 self.vertices = [] 

311 self.in_tangents = [] 

312 self.out_tangents = [] 

313 

314 self.vertices = seg1.vertices[:-1] 

315 self.in_tangents = seg1.in_tangents[:-1] 

316 self.out_tangents = seg1.out_tangents[:-1] 

317 

318 for t2 in positions[1:]: 

319 t = (t2-t1) / (1-t1) 

320 seg1, seg2 = seg2.split_at(t) 

321 t1 = t 

322 self.vertices += seg1.vertices[:-1] 

323 self.in_tangents += seg1.in_tangents[:-1] 

324 self.out_tangents += seg1.out_tangents[:-1] 

325 

326 self.vertices += seg2.vertices 

327 self.in_tangents += seg2.in_tangents 

328 self.out_tangents += seg2.out_tangents 

329 

330 def split_each_segment(self): 

331 """! 

332 Adds a point in the middle of the segment between every pair of points in the Bezier 

333 """ 

334 vertices = self.vertices 

335 in_tangents = self.in_tangents 

336 out_tangents = self.out_tangents 

337 

338 self.vertices = [] 

339 self.in_tangents = [] 

340 self.out_tangents = [] 

341 

342 for i in range(len(vertices)-1): 

343 tocut = [vertices[i], out_tangents[i]+vertices[i], in_tangents[i+1]+vertices[i+1], vertices[i+1]] 

344 split1, split2 = self._split_segment(0.5, tocut) 

345 if i: 

346 self.out_tangents[-1] = split1[1] 

347 else: 

348 self.add_point(vertices[0], in_tangents[0], split1[1]) 

349 self.add_point(split1[3], split1[2], split2[1]) 

350 self.add_point(vertices[i+1], split2[2], NVector(0, 0)) 

351 

352 def split_self_chunks(self, n_chunks): 

353 """! 

354 Adds points the Bezier, splitting it into @p n_chunks additional chunks. 

355 """ 

356 splits = [i/n_chunks for i in range(1, n_chunks)] 

357 return self.split_self_multi(splits) 

358 

359 def _bezier_points(self, i, optimize): 

360 v1 = self.vertices[i].clone() 

361 v2 = self.vertices[i+1].clone() 

362 points = [v1] 

363 t1 = self.out_tangents[i].clone() 

364 if not optimize or t1.length != 0: 

365 points.append(t1+v1) 

366 t2 = self.in_tangents[i+1].clone() 

367 if not optimize or t1.length != 0: 

368 points.append(t2+v2) 

369 points.append(v2) 

370 return points 

371 

372 def _solve_bezier_step(self, t, points): 

373 next = [] 

374 p1 = points[0] 

375 for p2 in points[1:]: 

376 next.append(p1 * (1-t) + p2 * t) 

377 p1 = p2 

378 return next 

379 

380 def _solve_bezier_coeff(self, i, n, t): 

381 return ( 

382 math.factorial(n) / (math.factorial(i) * math.factorial(n - i)) # (n choose i) 

383 * (t ** i) * ((1 - t) ** (n-i)) 

384 ) 

385 

386 def _solve_bezier(self, t, points): 

387 n = len(points) - 1 

388 if n > 0: 

389 return sum(( 

390 points[i] * self._solve_bezier_coeff(i, n, t) 

391 for i in range(n+1) 

392 ), NVector(0, 0)) 

393 

394 #while len(points) > 1: 

395 #points = self._solve_bezier_step(t, points) 

396 return points[0] 

397 

398 def _index_t(self, t): 

399 if t <= 0: 

400 return 0, 0 

401 

402 if t >= 1: 

403 return len(self.vertices)-2, 1 

404 

405 n = len(self.vertices)-1 

406 for i in range(n): 

407 if (i+1) / n > t: 

408 break 

409 

410 return i, (t - (i/n)) * n 

411 

412 def reverse(self): 

413 """! 

414 Reverses the Bezier curve 

415 """ 

416 self.vertices = list(reversed(self.vertices)) 

417 out_tangents = list(reversed(self.in_tangents)) 

418 in_tangents = list(reversed(self.out_tangents)) 

419 self.in_tangents = in_tangents 

420 self.out_tangents = out_tangents 

421 

422 """def to_absolute(self): 

423 if self.rel_tangents: 

424 self.rel_tangents = False 

425 for i in range(len(self.vertices)): 

426 p = self.vertices[i] 

427 self.in_tangents[i] += p 

428 self.out_tangents[i] += p 

429 return self""" 

430 

431 def rounded(self, round_distance): 

432 cloned = Bezier() 

433 cloned.closed = self.closed 

434 # value from https://spencermortensen.com/articles/bezier-circle/ 

435 round_corner = 0.5519 

436 

437 def _get_vt(closest_index): 

438 closer_v = self.vertices[closest_index] 

439 distance = (current - closer_v).length 

440 new_pos_perc = min(distance/2, round_distance) / distance if distance else 0 

441 vert = current + (closer_v - current) * new_pos_perc 

442 tan = - (vert - current) * round_corner 

443 return vert, tan 

444 

445 for i, current in enumerate(self.vertices): 

446 if not self.closed and (i == 0 or i == len(self.points) - 1): 

447 cloned.points.append(self.points[i]) 

448 else: 

449 vert1, out_t = _get_vt(i - 1) 

450 cloned.add_point(vert1, NVector(0, 0), out_t) 

451 vert2, in_t = _get_vt((i+1) % len(self.points)) 

452 cloned.add_point(vert2, in_t, NVector(0, 0)) 

453 

454 return cloned 

455 

456 def scale(self, amount): 

457 for vl in (self.vertices, self.in_tangents, self.out_tangents): 

458 for v in vl: 

459 v *= amount 

460 

461 def lerp(self, other, t): 

462 if len(other.vertices) != len(self.vertices): 

463 if t < 1: 

464 return self.clone() 

465 return other.clone() 

466 

467 bez = Bezier() 

468 bez.closed = self.closed 

469 

470 for vlist_name in ["vertices", "in_tangents", "out_tangents"]: 

471 vlist = getattr(self, vlist_name) 

472 olist = getattr(other, vlist_name) 

473 out = getattr(bez, vlist_name) 

474 for v, o in zip(vlist, olist): 

475 out.append(v.lerp(o, t)) 

476 

477 return bez 

478 

479 def rough_length(self): 

480 if len(self.vertices) < 2: 

481 return 0 

482 last = self.vertices[0] 

483 length = 0 

484 for v in self.vertices[1:]: 

485 length += (v-last).length 

486 last = v 

487 if self.closed: 

488 length += (last-self.vertices[0]).length 

489 return length