mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-16 23:26:26 +00:00
545 lines
17 KiB
Python
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
|