Files
OmniLottie/lottie/objects/bezier.py
T
2026-03-01 21:36:54 +08:00

486 lines
15 KiB
Python

import math
from .base import LottieObject, LottieProp
from .nvector import NVector
class BezierPoint:
def __init__(self, vertex, in_tangent=None, out_tangent=None):
self.vertex = vertex
self.in_tangent = in_tangent or NVector(0, 0)
self.out_tangent = out_tangent or NVector(0, 0)
def relative(self):
return self
@classmethod
def smooth(cls, point, in_tangent):
return cls(point, in_tangent, -in_tangent)
@classmethod
def from_absolute(cls, point, in_tangent=None, out_tangent=None):
if not in_tangent:
in_tangent = point.clone()
if not out_tangent:
out_tangent = point.clone()
return BezierPoint(point, in_tangent, out_tangent)
class BezierPointView:
"""
View for bezier point
"""
def __init__(self, bezier, index):
self.bezier = bezier
self.index = index
@property
def vertex(self):
return self.bezier.vertices[self.index]
@vertex.setter
def vertex(self, point):
self.bezier.vertices[self.index] = point
@property
def in_tangent(self):
return self.bezier.in_tangents[self.index]
@in_tangent.setter
def in_tangent(self, point):
self.bezier.in_tangents[self.index] = point
@property
def out_tangent(self):
return self.bezier.out_tangents[self.index]
@out_tangent.setter
def out_tangent(self, point):
self.bezier.out_tangents[self.index] = point
def relative(self):
return self
class AbsoluteBezierPointView(BezierPointView):
@property
def in_tangent(self):
return self.bezier.in_tangents[self.index] + self.vertex
@in_tangent.setter
def in_tangent(self, point):
self.bezier.in_tangents[self.index] = point - self.vertex
@property
def out_tangent(self):
return self.bezier.out_tangents[self.index] + self.vertex
@out_tangent.setter
def out_tangent(self, point):
self.bezier.out_tangents[self.index] = point - self.vertex
def relative(self):
return BezierPointView(self.bezier, self.index)
class BezierView:
def __init__(self, bezier, absolute=False):
self.bezier = bezier
self.is_absolute = absolute
def point(self, index):
if self.is_absolute:
return AbsoluteBezierPointView(self.bezier, index)
return BezierPointView(self.bezier, index)
def __len__(self):
return len(self.bezier.vertices)
def __getitem__(self, key):
if isinstance(key, slice):
return [
self.point(i)
for i in key
]
return self.point(key)
def __iter__(self):
for i in range(len(self)):
yield self.point(i)
def append(self, point):
if isinstance(point, NVector):
self.bezier.add_point(point.clone())
else:
bpt = point.relative()
self.bezier.add_point(bpt.vertex.clone(), bpt.in_tangent.clone(), bpt.out_tangent.clone())
@property
def absolute(self):
return BezierView(self.bezier, True)
## @ingroup Lottie
class Bezier(LottieObject):
"""!
Single bezier curve
"""
_props = [
LottieProp("closed", "c", bool, False),
LottieProp("in_tangents", "i", NVector, True),
LottieProp("out_tangents", "o", NVector, True),
LottieProp("vertices", "v", NVector, True),
]
def __init__(self):
## Closed property of shape
self.closed = False
## Cubic bezier handles for the segments before each vertex
self.in_tangents = []
## Cubic bezier handles for the segments after each vertex
self.out_tangents = []
## Bezier curve vertices.
self.vertices = []
#self.rel_tangents = rel_tangents
## More convent way to access points
self.points = BezierView(self)
def clone(self):
clone = Bezier()
clone.closed = self.closed
clone.in_tangents = [p.clone() for p in self.in_tangents]
clone.out_tangents = [p.clone() for p in self.out_tangents]
clone.vertices = [p.clone() for p in self.vertices]
#clone.rel_tangents = self.rel_tangents
return clone
def insert_point(self, index, pos, inp=NVector(0, 0), outp=NVector(0, 0)):
"""!
Inserts a point at the given index
@param index Index to insert the point at
@param pos Point to add
@param inp Tangent entering the point, as a vector relative to @p pos
@param outp Tangent exiting the point, as a vector relative to @p pos
@returns @c self, for easy chaining
"""
self.vertices.insert(index, pos)
self.in_tangents.insert(index, inp.clone())
self.out_tangents.insert(index, outp.clone())
#if not self.rel_tangents:
#self.in_tangents[-1] += pos
#self.out_tangents[-1] += pos
return self
def add_point(self, pos, inp=NVector(0, 0), outp=NVector(0, 0)):
"""!
Appends a point to the curve
@see insert_point
"""
self.insert_point(len(self.vertices), pos, inp, outp)
return self
def add_smooth_point(self, pos, inp):
"""!
Appends a point with symmetrical tangents
@see insert_point
"""
self.add_point(pos, inp, -inp)
return self
def close(self, closed=True):
"""!
Updates self.closed
@returns @c self, for easy chaining
"""
self.closed = closed
return self
def point_at(self, t):
"""!
@param t A value between 0 and 1, percentage along the length of the curve
@returns The point at @p t in the curve
"""
i, t = self._index_t(t)
points = self._bezier_points(i, True)
return self._solve_bezier(t, points)
def tangent_angle_at(self, t):
i, t = self._index_t(t)
points = self._bezier_points(i, True)
n = len(points) - 1
if n > 0:
delta = sum((
(points[i+1] - points[i]) * n * self._solve_bezier_coeff(i, n - 1, t)
for i in range(n)
), NVector(0, 0))
return math.atan2(delta.y, delta.x)
return 0
def _split(self, t):
i, t = self._index_t(t)
cub = self._bezier_points(i, True)
split1, split2 = self._split_segment(t, cub)
return i, split1, split2
def _split_segment(self, t, cub):
if len(cub) == 2:
k = self._solve_bezier_step(t, cub)[0]
split1 = [cub[0], NVector(0, 0), NVector(0, 0), k]
split2 = [k, NVector(0, 0), NVector(0, 0), cub[-1]]
return split1, split2
if len(cub) == 3:
quad = cub
else:
quad = self._solve_bezier_step(t, cub)
lin = self._solve_bezier_step(t, quad)
k = self._solve_bezier_step(t, lin)[0]
split1 = [cub[0], quad[0]-cub[0], lin[0]-k, k]
split2 = [k, lin[-1]-k, quad[-1]-cub[-1], cub[-1]]
return split1, split2
def split_at(self, t):
"""!
Get two pieces out of a Bezier curve
@param t A value between 0 and 1, percentage along the length of the curve
@returns Two Bezier objects that correspond to self, but split at @p t
"""
i, split1, split2 = self._split(t)
seg1 = Bezier()
seg2 = Bezier()
for j in range(i):
seg1.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone())
for j in range(i+2, len(self.vertices)):
seg2.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone())
seg1.add_point(split1[0], self.in_tangents[i].clone(), split1[1])
seg1.add_point(split1[3], split1[2], split2[1])
seg2.insert_point(0, split2[0], split1[2], split2[1])
seg2.insert_point(1, split2[3], split2[2], self.out_tangents[i+1].clone())
return seg1, seg2
def segment(self, t1, t2):
"""!
Splits a Bezier in two points and returns the segment between the
@param t1 A value between 0 and 1, percentage along the length of the curve
@param t2 A value between 0 and 1, percentage along the length of the curve
@returns Bezier object that correspond to the segment between @p t1 and @p t2
"""
if self.closed and self.vertices and self.vertices[-1] != self.vertices[0]:
copy = self.clone()
copy.add_point(self.vertices[0])
copy.closed = False
return copy.segment(t1, t2)
if t1 > 1:
t1 = 1
if t2 > 1:
t2 = 1
if t1 > t2:
t1, t2 = t2, t1
elif t1 == t2:
seg = Bezier()
p = self.point_at(t1)
seg.add_point(p)
seg.add_point(p)
return seg
seg1, seg2 = self.split_at(t1)
t2p = (t2-t1) / (1-t1)
seg3, seg4 = seg2.split_at(t2p)
return seg3
def split_self_multi(self, positions):
"""!
Adds more points to the Bezier
@param positions list of percentages along the curve
"""
if not len(positions):
return
t1 = positions[0]
seg1, seg2 = self.split_at(t1)
self.vertices = []
self.in_tangents = []
self.out_tangents = []
self.vertices = seg1.vertices[:-1]
self.in_tangents = seg1.in_tangents[:-1]
self.out_tangents = seg1.out_tangents[:-1]
for t2 in positions[1:]:
t = (t2-t1) / (1-t1)
seg1, seg2 = seg2.split_at(t)
t1 = t
self.vertices += seg1.vertices[:-1]
self.in_tangents += seg1.in_tangents[:-1]
self.out_tangents += seg1.out_tangents[:-1]
self.vertices += seg2.vertices
self.in_tangents += seg2.in_tangents
self.out_tangents += seg2.out_tangents
def split_each_segment(self):
"""!
Adds a point in the middle of the segment between every pair of points in the Bezier
"""
vertices = self.vertices
in_tangents = self.in_tangents
out_tangents = self.out_tangents
self.vertices = []
self.in_tangents = []
self.out_tangents = []
for i in range(len(vertices)-1):
tocut = [vertices[i], out_tangents[i]+vertices[i], in_tangents[i+1]+vertices[i+1], vertices[i+1]]
split1, split2 = self._split_segment(0.5, tocut)
if i:
self.out_tangents[-1] = split1[1]
else:
self.add_point(vertices[0], in_tangents[0], split1[1])
self.add_point(split1[3], split1[2], split2[1])
self.add_point(vertices[i+1], split2[2], NVector(0, 0))
def split_self_chunks(self, n_chunks):
"""!
Adds points the Bezier, splitting it into @p n_chunks additional chunks.
"""
splits = [i/n_chunks for i in range(1, n_chunks)]
return self.split_self_multi(splits)
def _bezier_points(self, i, optimize):
v1 = self.vertices[i].clone()
v2 = self.vertices[i+1].clone()
points = [v1]
t1 = self.out_tangents[i].clone()
if not optimize or t1.length != 0:
points.append(t1+v1)
t2 = self.in_tangents[i+1].clone()
if not optimize or t1.length != 0:
points.append(t2+v2)
points.append(v2)
return points
def _solve_bezier_step(self, t, points):
next = []
p1 = points[0]
for p2 in points[1:]:
next.append(p1 * (1-t) + p2 * t)
p1 = p2
return next
def _solve_bezier_coeff(self, i, n, t):
return (
math.factorial(n) / (math.factorial(i) * math.factorial(n - i)) # (n choose i)
* (t ** i) * ((1 - t) ** (n-i))
)
def _solve_bezier(self, t, points):
n = len(points) - 1
if n > 0:
return sum((
points[i] * self._solve_bezier_coeff(i, n, t)
for i in range(n+1)
), NVector(0, 0))
#while len(points) > 1:
#points = self._solve_bezier_step(t, points)
return points[0]
def _index_t(self, t):
if t <= 0:
return 0, 0
if t >= 1:
return len(self.vertices)-2, 1
n = len(self.vertices)-1
for i in range(n):
if (i+1) / n > t:
break
return i, (t - (i/n)) * n
def reverse(self):
"""!
Reverses the Bezier curve
"""
self.vertices = list(reversed(self.vertices))
out_tangents = list(reversed(self.in_tangents))
in_tangents = list(reversed(self.out_tangents))
self.in_tangents = in_tangents
self.out_tangents = out_tangents
"""def to_absolute(self):
if self.rel_tangents:
self.rel_tangents = False
for i in range(len(self.vertices)):
p = self.vertices[i]
self.in_tangents[i] += p
self.out_tangents[i] += p
return self"""
def rounded(self, round_distance):
cloned = Bezier()
cloned.closed = self.closed
round_corner = 0.5519
def _get_vt(closest_index):
closer_v = self.vertices[closest_index]
distance = (current - closer_v).length
new_pos_perc = min(distance/2, round_distance) / distance if distance else 0
vert = current + (closer_v - current) * new_pos_perc
tan = - (vert - current) * round_corner
return vert, tan
for i, current in enumerate(self.vertices):
if not self.closed and (i == 0 or i == len(self.points) - 1):
cloned.points.append(self.points[i])
else:
vert1, out_t = _get_vt(i - 1)
cloned.add_point(vert1, NVector(0, 0), out_t)
vert2, in_t = _get_vt((i+1) % len(self.points))
cloned.add_point(vert2, in_t, NVector(0, 0))
return cloned
def scale(self, amount):
for vl in (self.vertices, self.in_tangents, self.out_tangents):
for v in vl:
v *= amount
def lerp(self, other, t):
if len(other.vertices) != len(self.vertices):
if t < 1:
return self.clone()
return other.clone()
bez = Bezier()
bez.closed = self.closed
for vlist_name in ["vertices", "in_tangents", "out_tangents"]:
vlist = getattr(self, vlist_name)
olist = getattr(other, vlist_name)
out = getattr(bez, vlist_name)
for v, o in zip(vlist, olist):
out.append(v.lerp(o, t))
return bez
def rough_length(self):
if len(self.vertices) < 2:
return 0
last = self.vertices[0]
length = 0
for v in self.vertices[1:]:
length += (v-last).length
last = v
if self.closed:
length += (last-self.vertices[0]).length
return length