File indexing completed on 2025-01-19 03:59:52
0001 import math 0002 from .base import LottieObject, LottieProp 0003 from ..nvector import NVector 0004 0005 0006 class BezierPoint: 0007 def __init__(self, vertex, in_tangent=None, out_tangent=None): 0008 self.vertex = vertex 0009 self.in_tangent = in_tangent or NVector(0, 0) 0010 self.out_tangent = out_tangent or NVector(0, 0) 0011 0012 def relative(self): 0013 return self 0014 0015 @classmethod 0016 def smooth(cls, point, in_tangent): 0017 return cls(point, in_tangent, -in_tangent) 0018 0019 @classmethod 0020 def from_absolute(cls, point, in_tangent=None, out_tangent=None): 0021 if not in_tangent: 0022 in_tangent = point.clone() 0023 if not out_tangent: 0024 out_tangent = point.clone() 0025 return BezierPoint(point, in_tangent, out_tangent) 0026 0027 0028 class BezierPointView: 0029 """ 0030 View for bezier point 0031 """ 0032 def __init__(self, bezier, index): 0033 self.bezier = bezier 0034 self.index = index 0035 0036 @property 0037 def vertex(self): 0038 return self.bezier.vertices[self.index] 0039 0040 @vertex.setter 0041 def vertex(self, point): 0042 self.bezier.vertices[self.index] = point 0043 0044 @property 0045 def in_tangent(self): 0046 return self.bezier.in_tangents[self.index] 0047 0048 @in_tangent.setter 0049 def in_tangent(self, point): 0050 self.bezier.in_tangents[self.index] = point 0051 0052 @property 0053 def out_tangent(self): 0054 return self.bezier.out_tangents[self.index] 0055 0056 @out_tangent.setter 0057 def out_tangent(self, point): 0058 self.bezier.out_tangents[self.index] = point 0059 0060 def relative(self): 0061 return self 0062 0063 0064 class AbsoluteBezierPointView(BezierPointView): 0065 @property 0066 def in_tangent(self): 0067 return self.bezier.in_tangents[self.index] + self.vertex 0068 0069 @in_tangent.setter 0070 def in_tangent(self, point): 0071 self.bezier.in_tangents[self.index] = point - self.vertex 0072 0073 @property 0074 def out_tangent(self): 0075 return self.bezier.out_tangents[self.index] + self.vertex 0076 0077 @out_tangent.setter 0078 def out_tangent(self, point): 0079 self.bezier.out_tangents[self.index] = point - self.vertex 0080 0081 def relative(self): 0082 return BezierPointView(self.bezier, self.index) 0083 0084 0085 class BezierView: 0086 def __init__(self, bezier, absolute=False): 0087 self.bezier = bezier 0088 self.is_absolute = absolute 0089 0090 def point(self, index): 0091 if self.is_absolute: 0092 return AbsoluteBezierPointView(self.bezier, index) 0093 return BezierPointView(self.bezier, index) 0094 0095 def __len__(self): 0096 return len(self.bezier.vertices) 0097 0098 def __getitem__(self, key): 0099 if isinstance(key, slice): 0100 return [ 0101 self.point(i) 0102 for i in key 0103 ] 0104 return self.point(key) 0105 0106 def __iter__(self): 0107 for i in range(len(self)): 0108 yield self.point(i) 0109 0110 def append(self, point): 0111 if isinstance(point, NVector): 0112 self.bezier.add_point(point.clone()) 0113 else: 0114 bpt = point.relative() 0115 self.bezier.add_point(bpt.vertex.clone(), bpt.in_tangent.clone(), bpt.out_tangent.clone()) 0116 0117 @property 0118 def absolute(self): 0119 return BezierView(self.bezier, True) 0120 0121 0122 ## @ingroup Lottie 0123 class Bezier(LottieObject): 0124 """! 0125 Single bezier curve 0126 """ 0127 _props = [ 0128 LottieProp("closed", "c", bool, False), 0129 LottieProp("in_tangents", "i", NVector, True), 0130 LottieProp("out_tangents", "o", NVector, True), 0131 LottieProp("vertices", "v", NVector, True), 0132 ] 0133 0134 def __init__(self): 0135 ## Closed property of shape 0136 self.closed = False 0137 ## Cubic bezier handles for the segments before each vertex 0138 self.in_tangents = [] 0139 ## Cubic bezier handles for the segments after each vertex 0140 self.out_tangents = [] 0141 ## Bezier curve vertices. 0142 self.vertices = [] 0143 #self.rel_tangents = rel_tangents 0144 ## More convent way to access points 0145 self.points = BezierView(self) 0146 0147 def clone(self): 0148 clone = Bezier() 0149 clone.closed = self.closed 0150 clone.in_tangents = [p.clone() for p in self.in_tangents] 0151 clone.out_tangents = [p.clone() for p in self.out_tangents] 0152 clone.vertices = [p.clone() for p in self.vertices] 0153 #clone.rel_tangents = self.rel_tangents 0154 return clone 0155 0156 def insert_point(self, index, pos, inp=NVector(0, 0), outp=NVector(0, 0)): 0157 """! 0158 Inserts a point at the given index 0159 @param index Index to insert the point at 0160 @param pos Point to add 0161 @param inp Tangent entering the point, as a vector relative to @p pos 0162 @param outp Tangent exiting the point, as a vector relative to @p pos 0163 @returns @c self, for easy chaining 0164 """ 0165 self.vertices.insert(index, pos) 0166 self.in_tangents.insert(index, inp.clone()) 0167 self.out_tangents.insert(index, outp.clone()) 0168 #if not self.rel_tangents: 0169 #self.in_tangents[-1] += pos 0170 #self.out_tangents[-1] += pos 0171 return self 0172 0173 def add_point(self, pos, inp=NVector(0, 0), outp=NVector(0, 0)): 0174 """! 0175 Appends a point to the curve 0176 @see insert_point 0177 """ 0178 self.insert_point(len(self.vertices), pos, inp, outp) 0179 return self 0180 0181 def add_smooth_point(self, pos, inp): 0182 """! 0183 Appends a point with symmetrical tangents 0184 @see insert_point 0185 """ 0186 self.add_point(pos, inp, -inp) 0187 return self 0188 0189 def close(self, closed=True): 0190 """! 0191 Updates self.closed 0192 @returns @c self, for easy chaining 0193 """ 0194 self.closed = closed 0195 return self 0196 0197 def point_at(self, t): 0198 """! 0199 @param t A value between 0 and 1, percentage along the length of the curve 0200 @returns The point at @p t in the curve 0201 """ 0202 i, t = self._index_t(t) 0203 points = self._bezier_points(i, True) 0204 return self._solve_bezier(t, points) 0205 0206 def tangent_angle_at(self, t): 0207 i, t = self._index_t(t) 0208 points = self._bezier_points(i, True) 0209 0210 n = len(points) - 1 0211 if n > 0: 0212 delta = sum(( 0213 (points[i+1] - points[i]) * n * self._solve_bezier_coeff(i, n - 1, t) 0214 for i in range(n) 0215 ), NVector(0, 0)) 0216 return math.atan2(delta.y, delta.x) 0217 0218 return 0 0219 0220 def _split(self, t): 0221 i, t = self._index_t(t) 0222 cub = self._bezier_points(i, True) 0223 split1, split2 = self._split_segment(t, cub) 0224 return i, split1, split2 0225 0226 def _split_segment(self, t, cub): 0227 if len(cub) == 2: 0228 k = self._solve_bezier_step(t, cub)[0] 0229 split1 = [cub[0], NVector(0, 0), NVector(0, 0), k] 0230 split2 = [k, NVector(0, 0), NVector(0, 0), cub[-1]] 0231 return split1, split2 0232 0233 if len(cub) == 3: 0234 quad = cub 0235 else: 0236 quad = self._solve_bezier_step(t, cub) 0237 lin = self._solve_bezier_step(t, quad) 0238 k = self._solve_bezier_step(t, lin)[0] 0239 split1 = [cub[0], quad[0]-cub[0], lin[0]-k, k] 0240 split2 = [k, lin[-1]-k, quad[-1]-cub[-1], cub[-1]] 0241 return split1, split2 0242 0243 def split_at(self, t): 0244 """! 0245 Get two pieces out of a Bezier curve 0246 @param t A value between 0 and 1, percentage along the length of the curve 0247 @returns Two Bezier objects that correspond to self, but split at @p t 0248 """ 0249 i, split1, split2 = self._split(t) 0250 0251 seg1 = Bezier() 0252 seg2 = Bezier() 0253 for j in range(i): 0254 seg1.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone()) 0255 for j in range(i+2, len(self.vertices)): 0256 seg2.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone()) 0257 0258 seg1.add_point(split1[0], self.in_tangents[i].clone(), split1[1]) 0259 seg1.add_point(split1[3], split1[2], split2[1]) 0260 0261 seg2.insert_point(0, split2[0], split1[2], split2[1]) 0262 seg2.insert_point(1, split2[3], split2[2], self.out_tangents[i+1].clone()) 0263 0264 return seg1, seg2 0265 0266 def segment(self, t1, t2): 0267 """! 0268 Splits a Bezier in two points and returns the segment between the 0269 @param t1 A value between 0 and 1, percentage along the length of the curve 0270 @param t2 A value between 0 and 1, percentage along the length of the curve 0271 @returns Bezier object that correspond to the segment between @p t1 and @p t2 0272 """ 0273 if self.closed and self.vertices and self.vertices[-1] != self.vertices[0]: 0274 copy = self.clone() 0275 copy.add_point(self.vertices[0]) 0276 copy.closed = False 0277 return copy.segment(t1, t2) 0278 0279 if t1 > 1: 0280 t1 = 1 0281 if t2 > 1: 0282 t2 = 1 0283 0284 if t1 > t2: 0285 t1, t2 = t2, t1 0286 elif t1 == t2: 0287 seg = Bezier() 0288 p = self.point_at(t1) 0289 seg.add_point(p) 0290 seg.add_point(p) 0291 return seg 0292 0293 seg1, seg2 = self.split_at(t1) 0294 t2p = (t2-t1) / (1-t1) 0295 seg3, seg4 = seg2.split_at(t2p) 0296 return seg3 0297 0298 def split_self_multi(self, positions): 0299 """! 0300 Adds more points to the Bezier 0301 @param positions list of percentages along the curve 0302 """ 0303 if not len(positions): 0304 return 0305 t1 = positions[0] 0306 seg1, seg2 = self.split_at(t1) 0307 self.vertices = [] 0308 self.in_tangents = [] 0309 self.out_tangents = [] 0310 0311 self.vertices = seg1.vertices[:-1] 0312 self.in_tangents = seg1.in_tangents[:-1] 0313 self.out_tangents = seg1.out_tangents[:-1] 0314 0315 for t2 in positions[1:]: 0316 t = (t2-t1) / (1-t1) 0317 seg1, seg2 = seg2.split_at(t) 0318 t1 = t 0319 self.vertices += seg1.vertices[:-1] 0320 self.in_tangents += seg1.in_tangents[:-1] 0321 self.out_tangents += seg1.out_tangents[:-1] 0322 0323 self.vertices += seg2.vertices 0324 self.in_tangents += seg2.in_tangents 0325 self.out_tangents += seg2.out_tangents 0326 0327 def split_each_segment(self): 0328 """! 0329 Adds a point in the middle of the segment between every pair of points in the Bezier 0330 """ 0331 vertices = self.vertices 0332 in_tangents = self.in_tangents 0333 out_tangents = self.out_tangents 0334 0335 self.vertices = [] 0336 self.in_tangents = [] 0337 self.out_tangents = [] 0338 0339 for i in range(len(vertices)-1): 0340 tocut = [vertices[i], out_tangents[i]+vertices[i], in_tangents[i+1]+vertices[i+1], vertices[i+1]] 0341 split1, split2 = self._split_segment(0.5, tocut) 0342 if i: 0343 self.out_tangents[-1] = split1[1] 0344 else: 0345 self.add_point(vertices[0], in_tangents[0], split1[1]) 0346 self.add_point(split1[3], split1[2], split2[1]) 0347 self.add_point(vertices[i+1], split2[2], NVector(0, 0)) 0348 0349 def split_self_chunks(self, n_chunks): 0350 """! 0351 Adds points the Bezier, splitting it into @p n_chunks additional chunks. 0352 """ 0353 splits = [i/n_chunks for i in range(1, n_chunks)] 0354 return self.split_self_multi(splits) 0355 0356 def _bezier_points(self, i, optimize): 0357 v1 = self.vertices[i].clone() 0358 v2 = self.vertices[i+1].clone() 0359 points = [v1] 0360 t1 = self.out_tangents[i].clone() 0361 if not optimize or t1.length != 0: 0362 points.append(t1+v1) 0363 t2 = self.in_tangents[i+1].clone() 0364 if not optimize or t1.length != 0: 0365 points.append(t2+v2) 0366 points.append(v2) 0367 return points 0368 0369 def _solve_bezier_step(self, t, points): 0370 next = [] 0371 p1 = points[0] 0372 for p2 in points[1:]: 0373 next.append(p1 * (1-t) + p2 * t) 0374 p1 = p2 0375 return next 0376 0377 def _solve_bezier_coeff(self, i, n, t): 0378 return ( 0379 math.factorial(n) / (math.factorial(i) * math.factorial(n - i)) # (n choose i) 0380 * (t ** i) * ((1 - t) ** (n-i)) 0381 ) 0382 0383 def _solve_bezier(self, t, points): 0384 n = len(points) - 1 0385 if n > 0: 0386 return sum(( 0387 points[i] * self._solve_bezier_coeff(i, n, t) 0388 for i in range(n+1) 0389 ), NVector(0, 0)) 0390 0391 #while len(points) > 1: 0392 #points = self._solve_bezier_step(t, points) 0393 return points[0] 0394 0395 def _index_t(self, t): 0396 if t <= 0: 0397 return 0, 0 0398 0399 if t >= 1: 0400 return len(self.vertices)-2, 1 0401 0402 n = len(self.vertices)-1 0403 for i in range(n): 0404 if (i+1) / n > t: 0405 break 0406 0407 return i, (t - (i/n)) * n 0408 0409 def reverse(self): 0410 """! 0411 Reverses the Bezier curve 0412 """ 0413 self.vertices = list(reversed(self.vertices)) 0414 out_tangents = list(reversed(self.in_tangents)) 0415 in_tangents = list(reversed(self.out_tangents)) 0416 self.in_tangents = in_tangents 0417 self.out_tangents = out_tangents 0418 0419 """def to_absolute(self): 0420 if self.rel_tangents: 0421 self.rel_tangents = False 0422 for i in range(len(self.vertices)): 0423 p = self.vertices[i] 0424 self.in_tangents[i] += p 0425 self.out_tangents[i] += p 0426 return self""" 0427 0428 def rounded(self, round_distance): 0429 cloned = Bezier() 0430 cloned.closed = self.closed 0431 round_corner = 0.5519 0432 0433 def _get_vt(closest_index): 0434 closer_v = self.vertices[closest_index] 0435 distance = (current - closer_v).length 0436 new_pos_perc = min(distance/2, round_distance) / distance if distance else 0 0437 vert = current + (closer_v - current) * new_pos_perc 0438 tan = - (vert - current) * round_corner 0439 return vert, tan 0440 0441 for i, current in enumerate(self.vertices): 0442 if not self.closed and (i == 0 or i == len(self.points) - 1): 0443 cloned.points.append(self.points[i]) 0444 else: 0445 vert1, out_t = _get_vt(i - 1) 0446 cloned.add_point(vert1, NVector(0, 0), out_t) 0447 vert2, in_t = _get_vt((i+1) % len(self.points)) 0448 cloned.add_point(vert2, in_t, NVector(0, 0)) 0449 0450 return cloned 0451 0452 def scale(self, amount): 0453 for vl in (self.vertices, self.in_tangents, self.out_tangents): 0454 for v in vl: 0455 v *= amount 0456 0457 def lerp(self, other, t): 0458 if len(other.vertices) != len(self.vertices): 0459 if t < 1: 0460 return self.clone() 0461 return other.clone() 0462 0463 bez = Bezier() 0464 bez.closed = self.closed 0465 0466 for vlist_name in ["vertices", "in_tangents", "out_tangents"]: 0467 vlist = getattr(self, vlist_name) 0468 olist = getattr(other, vlist_name) 0469 out = getattr(bez, vlist_name) 0470 for v, o in zip(vlist, olist): 0471 out.append(v.lerp(o, t)) 0472 0473 return bez 0474 0475 def rough_length(self): 0476 if len(self.vertices) < 2: 0477 return 0 0478 last = self.vertices[0] 0479 length = 0 0480 for v in self.vertices[1:]: 0481 length += (v-last).length 0482 last = v 0483 if self.closed: 0484 length += (last-self.vertices[0]).length 0485 return length