Initial Commit

This commit is contained in:
OmniLottie
2026-03-01 21:36:54 +08:00
commit a386c803e1
199 changed files with 42253 additions and 0 deletions
+544
View File
@@ -0,0 +1,544 @@
import random
import math
from ..nvector import NVector
from ..objects.shapes import Path
from .. import objects
from ..objects import easing
from ..objects import properties
def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()):
if not isinstance(position_prop, list):
position_prop = [position_prop]
n_frames = int(round(n_frames))
frame_time = (end_time - start_time) / n_frames
startpoints = list(map(
lambda pp: pp.get_value(start_time),
position_prop
))
for i in range(n_frames):
x = (random.random() * 2 - 1) * x_radius
y = (random.random() * 2 - 1) * y_radius
for pp, start in zip(position_prop, startpoints):
px = start[0] + x
py = start[1] + y
pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp)
for pp, start in zip(position_prop, startpoints):
pp.add_keyframe(end_time, start, interp)
def rot_shake(rotation_prop, angles, start_time, end_time, n_frames):
frame_time = (end_time - start_time) / n_frames
start = rotation_prop.get_value(start_time)
for i in range(0, n_frames):
a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi)
rotation_prop.add_keyframe(start_time + i * frame_time, start + a)
rotation_prop.add_keyframe(end_time, start)
def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7):
start = position_prop.get_value(start_time)
d = start-point
delta = (end_time - start_time) / oscillations
for i in range(oscillations):
time_x = i / oscillations
factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff))
p = point + d * factor
position_prop.add_keyframe(start_time + delta * i, p)
position_prop.add_keyframe(end_time, point)
def follow_path(position_prop, bezier, start_time, end_time, n_keyframes,
reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0):
delta = (end_time - start_time) / (n_keyframes-1)
fact = start_t
factd = 1 / (n_keyframes-1)
if rotation_prop:
start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset
for i in range(n_keyframes):
time = start_time + i * delta
if fact > 1 + factd/2:
fact -= 1
if time != start_time:
easing.Jump()(position_prop.keyframes[-1])
if rotation_prop:
easing.Jump()(rotation_prop.keyframes[-1])
f = 1 - fact if reverse else fact
position_prop.add_keyframe(time, bezier.point_at(f)+offset)
if rotation_prop:
rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot)
fact += factd
def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False):
obj = Path()
beziers = []
maxp = 0
time_delta = (appear_end - appear_start) / n_keyframes
for i in range(n_keyframes+1):
time = appear_start + i * time_delta
t2 = (time - appear_start) / (appear_end - appear_start)
if reverse:
t2 = 1 - t2
segment = bezier.segment(t2, 1)
segment.reverse()
else:
segment = bezier.segment(0, t2)
beziers.append(segment)
if len(segment.vertices) > maxp:
maxp = len(segment.vertices)
obj.shape.add_keyframe(time, segment)
for segment in beziers:
deltap = maxp - len(segment.vertices)
if deltap > 0:
segment.vertices += [segment.vertices[-1]] * deltap
segment.in_tangents += [NVector(0, 0)] * deltap
segment.out_tangents += [NVector(0, 0)] * deltap
return obj
def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False):
obj = Path()
beziers = []
maxp = 0
time_delta = (disappear_end - disappear_start) / n_keyframes
for i in range(n_keyframes+1):
time = disappear_start + i * time_delta
t1 = (time - disappear_start) / (disappear_end - disappear_start)
if reverse:
t1 = 1 - t1
segment = bezier.segment(0, t1)
else:
segment = bezier.segment(1, t1)
segment.reverse()
beziers.append(segment)
if len(segment.vertices) > maxp:
maxp = len(segment.vertices)
obj.shape.add_keyframe(time, segment)
for segment in beziers:
deltap = maxp - len(segment.vertices)
if deltap > 0:
segment.vertices += [segment.vertices[-1]] * deltap
segment.in_tangents += [NVector(0, 0)] * deltap
segment.out_tangents += [NVector(0, 0)] * deltap
return obj
def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False):
obj = Path()
beziers = []
maxp = 0
# HACK: For some reson reversed works better
if not reverse:
bezier.reverse()
time_delta = (appear_end - appear_start) / n_keyframes
for i in range(n_keyframes+1):
time = appear_start + i * time_delta
t1 = (time - disappear_start) / (disappear_end - disappear_start)
t2 = (time - appear_start) / (appear_end - appear_start)
t1 = max(0, min(1, t1))
t2 = max(0, min(1, t2))
#if reverse:
if True:
t1 = 1 - t1
t2 = 1 - t2
segment = bezier.segment(t2, t1)
segment.reverse()
#else:
#segment = bezier.segment(t1, t2)
#segment.reverse()
beziers.append(segment)
if len(segment.vertices) > maxp:
maxp = len(segment.vertices)
obj.shape.add_keyframe(time, segment)
for segment in beziers:
deltap = maxp - len(segment.vertices)
if deltap > 0:
segment.split_self_chunks(deltap+1)
# HACK: Restore
if not reverse:
bezier.reverse()
return obj
class PointDisplacer:
def __init__(self, time_start, time_end, n_frames):
"""!
@param time_start When the animation shall start
@param time_end When the animation shall end
@param n_frames Number of frames in the animation
"""
## When the animation shall start
self.time_start = time_start
## When the animation shall end
self.time_end = time_end
## Number of frames in the animation
self.n_frames = n_frames
## Length of a frame
self.time_delta = (time_end - time_start) / n_frames
def animate_point(self, prop):
startpos = prop.get_value(self.time_start)
for f in range(self.n_frames+1):
p = self._on_displace(startpos, f)
prop.add_keyframe(self.frame_time(f), startpos+p)
def _on_displace(self, startpos, f):
raise NotImplementedError()
def animate_bezier(self, prop):
initial = prop.get_value(self.time_start)
for f in range(self.n_frames+1):
bezier = objects.Bezier()
bezier.closed = initial.closed
for pi in range(len(initial.vertices)):
startpos = initial.vertices[pi]
dp = self._on_displace(startpos, f)
t1sp = initial.in_tangents[pi] + startpos
t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp
t2sp = initial.out_tangents[pi] + startpos
t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp
bezier.add_point(dp + startpos, t1fin, t2fin)
prop.add_keyframe(self.frame_time(f), bezier)
def frame_time(self, f):
return f * self.time_delta + self.time_start
def _init_lerp(self, val_from, val_to, easing):
self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing)
def _lerp_get(self, offset):
return self._kf.interpolated_value(offset / self.n_frames)[0]
class SineDisplacer(PointDisplacer):
def __init__(
self,
wavelength,
amplitude,
time_start,
time_end,
n_frames,
speed=1,
axis=90,
):
"""!
Displaces points as if they were following a sine wave
@param wavelength Distance between consecutive peaks
@param amplitude Distance from a peak to the original position
@param time_start When the animation shall start
@param time_end When the animation shall end
@param n_frames Number of keyframes to add
@param speed Number of peaks a point will go through in the given time
If negative, it will go the other way
@param axis Wave peak direction
"""
super().__init__(time_start, time_end, n_frames)
self.wavelength = wavelength
self.amplitude = amplitude
self.speed_f = math.pi * 2 * speed
self.axis = axis / 180 * math.pi
def _on_displace(self, startpos, f):
off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude
return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
class MultiSineDisplacer(PointDisplacer):
def __init__(
self,
waves,
time_start,
time_end,
n_frames,
speed=1,
axis=90,
amplitude_scale=1,
):
"""!
Displaces points as if they were following a sine wave
@param waves List of tuples (wavelength, amplitude)
@param time_start When the animation shall start
@param time_end When the animation shall end
@param n_frames Number of keyframes to add
@param speed Number of peaks a point will go through in the given time
If negative, it will go the other way
@param axis Wave peak direction
@param amplitude_scale Multiplies the resulting amplitude by this factor
"""
super().__init__(time_start, time_end, n_frames)
self.waves = waves
self.speed_f = math.pi * 2 * speed
self.axis = axis / 180 * math.pi
self.amplitude_scale = amplitude_scale
def _on_displace(self, startpos, f):
off = 0
for wavelength, amplitude in self.waves:
off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude
off *= self.amplitude_scale
return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
class DepthRotationAxis:
def __init__(self, x, y, keep):
self.x = x / x.length
self.y = y / y.length
self.keep = keep / keep.length # should be the cross product
def rot_center(self, center, point):
return (
self.x * self.x.dot(center) +
self.y * self.y.dot(center) +
self.keep * self.keep.dot(point)
)
def extract_component(self, vector, axis):
return sum(vector.element_scaled(axis).components)
@classmethod
def from_points(cls, keep_point, center=NVector(0, 0, 0)):
keep = keep_point - center
keep /= keep.length
# Hughes-Moller to find x and y
if abs(keep.x) > abs(keep.z):
y = NVector(-keep.y, keep.x, 0)
else:
y = NVector(0, -keep.z, keep.y)
y /= y.length
x = y.cross(keep)
return cls(x, y, keep)
class DepthRotation:
axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0))
axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0))
axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1))
def __init__(self, center):
self.center = center
def rotate3d_y(self, point, angle):
return self.rotate3d(point, angle, self.axis_y)
# Hard-coded version:
#c = NVector(self.center.x, point.y, self.center.z)
#rad = angle * math.pi / 180
#delta = point - c
#pol_l = delta.length
#pol_a = math.atan2(delta.z, delta.x)
#dest_a = pol_a + rad
#return NVector(
# c.x + pol_l * math.cos(dest_a),
# point.y,
# c.z + pol_l * math.sin(dest_a)
#)
def rotate3d_x(self, point, angle):
return self.rotate3d(point, angle, self.axis_x)
# Hard-coded version:
#c = NVector(point.x, self.center.y, self.center.z)
#rad = angle * math.pi / 180
#delta = point - c
#pol_l = delta.length
#pol_a = math.atan2(delta.y, delta.z)
#dest_a = pol_a + rad
#return NVector(
# point.x,
# c.y + pol_l * math.sin(dest_a),
# c.z + pol_l * math.cos(dest_a),
#)
def rotate3d_z(self, point, angle):
return self.rotate3d(point, angle, self.axis_z)
def rotate3d(self, point, angle, axis):
c = axis.rot_center(self.center, point)
rad = angle * math.pi / 180
delta = point - c
pol_l = delta.length
pol_a = math.atan2(
axis.extract_component(delta, axis.y),
axis.extract_component(delta, axis.x)
)
dest_a = pol_a + rad
return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a)
class DepthRotationDisplacer(PointDisplacer):
axis_x = DepthRotation.axis_x
axis_y = DepthRotation.axis_y
axis_z = DepthRotation.axis_z
def __init__(self, center, time_start, time_end, n_frames, axis,
depth=0, angle=360, anglestart=0, ease=easing.Linear()):
super().__init__(time_start, time_end, n_frames)
self.rotation = DepthRotation(center)
if isinstance(axis, NVector):
axis = DepthRotationAxis.from_points(axis)
self.axis = axis
self.depth = depth
self._angle = angle
self.anglestart = anglestart
self.ease = ease
self._init_lerp(0, angle, ease)
@property
def angle(self):
return self._angle
@angle.setter
def angle(self, value):
self._angle = value
self._init_lerp(0, value, self.ease)
def _on_displace(self, startpos, f):
angle = self.anglestart + self._lerp_get(f)
if len(startpos) < 3:
startpos = NVector(*(startpos.components + [self.depth]))
return self.rotation.rotate3d(startpos, angle, self.axis) - startpos
class EnvelopeDeformation(PointDisplacer):
def __init__(self, topleft, bottomright):
self.topleft = topleft
self.size = bottomright - topleft
self.keyframes = []
@property
def time_start(self):
return self.keyframes[0][0]
def add_reset_keyframe(self, time):
self.add_keyframe(
time,
self.topleft.clone(),
NVector(self.topleft.x + self.size.x, self.topleft.y),
NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y),
NVector(self.topleft.x, self.topleft.y + self.size.y),
)
def add_keyframe(self, time, tl, tr, br, bl):
self.keyframes.append([
time,
tl.clone(),
tr.clone(),
br.clone(),
bl.clone()
])
def _on_displace(self, startpos, f):
_, tl, tr, br, bl = self.keyframes[f]
relp = startpos - self.topleft
relp.x /= self.size.x
relp.y /= self.size.y
x1 = tl.lerp(tr, relp.x)
x2 = bl.lerp(br, relp.x)
#return x1.lerp(x2, relp.y)
return x1.lerp(x2, relp.y) - startpos
@property
def n_frames(self):
return len(self.keyframes)-1
def frame_time(self, f):
return self.keyframes[f][0]
class DisplacerDampener(PointDisplacer):
"""!
Given a displacer and a function that returns a factor for a point,
multiplies the effect of the displacer by the factor
"""
def __init__(self, displacer, dampener):
self.displacer = displacer
self.dampener = dampener
@property
def time_start(self):
return self.displacer.time_start
def _on_displace(self, startpos, f):
disp = self.displacer._on_displace(startpos, f)
damp = self.dampener(startpos)
return disp * damp
@property
def n_frames(self):
return self.displacer.n_frames
def frame_time(self, f):
return self.displacer.frame_time(f)
class FollowDisplacer(PointDisplacer):
def __init__(
self,
origin,
range,
offset_func,
time_start, time_end, n_frames,
falloff_exp=1,
):
"""!
@brief Uses a custom offset function, and applies a falloff to the displacement
@param origin Origin point for the falloff
@param range Radius after which the points will not move
@param offset_func Function returning an offset given a ratio of the time
@param time_start When the animation shall start
@param time_end When the animation shall end
@param n_frames Number of frames in the animation
@param falloff_exp Exponent for the falloff
"""
super().__init__(time_start, time_end, n_frames)
self.origin = origin
self.range = range
self.offset_func = offset_func
self.falloff_exp = falloff_exp
def _on_displace(self, startpos, f):
influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp
return self.offset_func(f / self.n_frames) * influence