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