Files
2026-03-01 21:36:54 +08:00

545 lines
17 KiB
Python

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