File indexing completed on 2025-01-19 03:59:54

0001 import random
0002 import math
0003 from ..nvector import NVector
0004 from ..objects.shapes import Path
0005 from .. import objects
0006 from ..objects import easing
0007 from ..objects import properties
0008 
0009 
0010 def shake(position_prop, x_radius, y_radius, start_time, end_time, n_frames, interp=easing.Linear()):
0011     if not isinstance(position_prop, list):
0012         position_prop = [position_prop]
0013 
0014     n_frames = int(round(n_frames))
0015     frame_time = (end_time - start_time) / n_frames
0016     startpoints = list(map(
0017         lambda pp: pp.get_value(start_time),
0018         position_prop
0019     ))
0020 
0021     for i in range(n_frames):
0022         x = (random.random() * 2 - 1) * x_radius
0023         y = (random.random() * 2 - 1) * y_radius
0024         for pp, start in zip(position_prop, startpoints):
0025             px = start[0] + x
0026             py = start[1] + y
0027             pp.add_keyframe(start_time + i * frame_time, NVector(px, py), interp)
0028 
0029     for pp, start in zip(position_prop, startpoints):
0030         pp.add_keyframe(end_time, start, interp)
0031 
0032 
0033 def rot_shake(rotation_prop, angles, start_time, end_time, n_frames):
0034     frame_time = (end_time - start_time) / n_frames
0035     start = rotation_prop.get_value(start_time)
0036 
0037     for i in range(0, n_frames):
0038         a = angles[i % len(angles)] * math.sin(i/n_frames * math.pi)
0039         rotation_prop.add_keyframe(start_time + i * frame_time, start + a)
0040     rotation_prop.add_keyframe(end_time, start)
0041 
0042 
0043 def spring_pull(position_prop, point, start_time, end_time, falloff=15, oscillations=7):
0044     start = position_prop.get_value(start_time)
0045     d = start-point
0046 
0047     delta = (end_time - start_time) / oscillations
0048 
0049     for i in range(oscillations):
0050         time_x = i / oscillations
0051         factor = math.cos(time_x * math.pi * oscillations) * (1-time_x**(1/falloff))
0052         p = point + d * factor
0053         position_prop.add_keyframe(start_time + delta * i, p)
0054 
0055     position_prop.add_keyframe(end_time, point)
0056 
0057 
0058 def follow_path(position_prop, bezier, start_time, end_time, n_keyframes,
0059                 reverse=False, offset=NVector(0, 0), start_t=0, rotation_prop=None, rotation_offset=0):
0060     delta = (end_time - start_time) / (n_keyframes-1)
0061     fact = start_t
0062     factd = 1 / (n_keyframes-1)
0063 
0064     if rotation_prop:
0065         start_rot = rotation_prop.get_value(start_time) if rotation_offset is None else rotation_offset
0066 
0067     for i in range(n_keyframes):
0068         time = start_time + i * delta
0069 
0070         if fact > 1 + factd/2:
0071             fact -= 1
0072             if time != start_time:
0073                 easing.Jump()(position_prop.keyframes[-1])
0074                 if rotation_prop:
0075                     easing.Jump()(rotation_prop.keyframes[-1])
0076 
0077         f = 1 - fact if reverse else fact
0078         position_prop.add_keyframe(time, bezier.point_at(f)+offset)
0079 
0080         if rotation_prop:
0081             rotation_prop.add_keyframe(time, bezier.tangent_angle_at(f) / math.pi * 180 + start_rot)
0082 
0083         fact += factd
0084 
0085 
0086 def generate_path_appear(bezier, appear_start, appear_end, n_keyframes, reverse=False):
0087     obj = Path()
0088     beziers = []
0089     maxp = 0
0090 
0091     time_delta = (appear_end - appear_start) / n_keyframes
0092     for i in range(n_keyframes+1):
0093         time = appear_start + i * time_delta
0094         t2 = (time - appear_start) / (appear_end - appear_start)
0095 
0096         if reverse:
0097             t2 = 1 - t2
0098             segment = bezier.segment(t2, 1)
0099             segment.reverse()
0100         else:
0101             segment = bezier.segment(0, t2)
0102 
0103         beziers.append(segment)
0104         if len(segment.vertices) > maxp:
0105             maxp = len(segment.vertices)
0106 
0107         obj.shape.add_keyframe(time, segment)
0108 
0109     for segment in beziers:
0110         deltap = maxp - len(segment.vertices)
0111         if deltap > 0:
0112             segment.vertices += [segment.vertices[-1]] * deltap
0113             segment.in_tangents += [NVector(0, 0)] * deltap
0114             segment.out_tangents += [NVector(0, 0)] * deltap
0115 
0116     return obj
0117 
0118 
0119 def generate_path_disappear(bezier, disappear_start, disappear_end, n_keyframes, reverse=False):
0120     obj = Path()
0121     beziers = []
0122     maxp = 0
0123 
0124     time_delta = (disappear_end - disappear_start) / n_keyframes
0125     for i in range(n_keyframes+1):
0126         time = disappear_start + i * time_delta
0127         t1 = (time - disappear_start) / (disappear_end - disappear_start)
0128         if reverse:
0129             t1 = 1 - t1
0130             segment = bezier.segment(0, t1)
0131         else:
0132             segment = bezier.segment(1, t1)
0133             segment.reverse()
0134 
0135         beziers.append(segment)
0136         if len(segment.vertices) > maxp:
0137             maxp = len(segment.vertices)
0138 
0139         obj.shape.add_keyframe(time, segment)
0140 
0141     for segment in beziers:
0142         deltap = maxp - len(segment.vertices)
0143         if deltap > 0:
0144             segment.vertices += [segment.vertices[-1]] * deltap
0145             segment.in_tangents += [NVector(0, 0)] * deltap
0146             segment.out_tangents += [NVector(0, 0)] * deltap
0147 
0148     return obj
0149 
0150 
0151 def generate_path_segment(bezier, appear_start, appear_end, disappear_start, disappear_end, n_keyframes, reverse=False):
0152     obj = Path()
0153     beziers = []
0154     maxp = 0
0155 
0156     # HACK: For some reson reversed works better
0157     if not reverse:
0158         bezier.reverse()
0159 
0160     time_delta = (appear_end - appear_start) / n_keyframes
0161     for i in range(n_keyframes+1):
0162         time = appear_start + i * time_delta
0163         t1 = (time - disappear_start) / (disappear_end - disappear_start)
0164         t2 = (time - appear_start) / (appear_end - appear_start)
0165 
0166         t1 = max(0, min(1, t1))
0167         t2 = max(0, min(1, t2))
0168 
0169         #if reverse:
0170         if True:
0171             t1 = 1 - t1
0172             t2 = 1 - t2
0173             segment = bezier.segment(t2, t1)
0174             segment.reverse()
0175         #else:
0176             #segment = bezier.segment(t1, t2)
0177             #segment.reverse()
0178 
0179         beziers.append(segment)
0180         if len(segment.vertices) > maxp:
0181             maxp = len(segment.vertices)
0182 
0183         obj.shape.add_keyframe(time, segment)
0184 
0185     for segment in beziers:
0186         deltap = maxp - len(segment.vertices)
0187         if deltap > 0:
0188             segment.split_self_chunks(deltap+1)
0189 
0190     # HACK: Restore
0191     if not reverse:
0192         bezier.reverse()
0193     return obj
0194 
0195 
0196 class PointDisplacer:
0197     def __init__(self, time_start, time_end, n_frames):
0198         """!
0199         @param time_start   When the animation shall start
0200         @param time_end     When the animation shall end
0201         @param n_frames     Number of frames in the animation
0202         """
0203         ## When the animation shall start
0204         self.time_start = time_start
0205         ## When the animation shall end
0206         self.time_end = time_end
0207         ## Number of frames in the animation
0208         self.n_frames = n_frames
0209         ## Length of a frame
0210         self.time_delta = (time_end - time_start) / n_frames
0211 
0212     def animate_point(self, prop):
0213         startpos = prop.get_value(self.time_start)
0214         for f in range(self.n_frames+1):
0215             p = self._on_displace(startpos, f)
0216             prop.add_keyframe(self.frame_time(f), startpos+p)
0217 
0218     def _on_displace(self, startpos, f):
0219         raise NotImplementedError()
0220 
0221     def animate_bezier(self, prop):
0222         initial = prop.get_value(self.time_start)
0223 
0224         for f in range(self.n_frames+1):
0225             bezier = objects.Bezier()
0226             bezier.closed = initial.closed
0227 
0228             for pi in range(len(initial.vertices)):
0229                 startpos = initial.vertices[pi]
0230                 dp = self._on_displace(startpos, f)
0231                 t1sp = initial.in_tangents[pi] + startpos
0232                 t1fin = initial.in_tangents[pi] + self._on_displace(t1sp, f) - dp
0233                 t2sp = initial.out_tangents[pi] + startpos
0234                 t2fin = initial.out_tangents[pi] + self._on_displace(t2sp, f) - dp
0235 
0236                 bezier.add_point(dp + startpos, t1fin, t2fin)
0237 
0238             prop.add_keyframe(self.frame_time(f), bezier)
0239 
0240     def frame_time(self, f):
0241         return f * self.time_delta + self.time_start
0242 
0243     def _init_lerp(self, val_from, val_to, easing):
0244         self._kf = properties.OffsetKeyframe(0, NVector(val_from), NVector(val_to), easing)
0245 
0246     def _lerp_get(self, offset):
0247         return self._kf.interpolated_value(offset / self.n_frames)[0]
0248 
0249 
0250 class SineDisplacer(PointDisplacer):
0251     def __init__(
0252         self,
0253         wavelength,
0254         amplitude,
0255         time_start,
0256         time_end,
0257         n_frames,
0258         speed=1,
0259         axis=90,
0260     ):
0261         """!
0262         Displaces points as if they were following a sine wave
0263 
0264         @param wavelength  Distance between consecutive peaks
0265         @param amplitude   Distance from a peak to the original position
0266         @param time_start  When the animation shall start
0267         @param time_end    When the animation shall end
0268         @param n_frames    Number of keyframes to add
0269         @param speed       Number of peaks a point will go through in the given time
0270                            If negative, it will go the other way
0271         @param axis        Wave peak direction
0272         """
0273         super().__init__(time_start, time_end, n_frames)
0274 
0275         self.wavelength = wavelength
0276         self.amplitude = amplitude
0277         self.speed_f = math.pi * 2 * speed
0278         self.axis = axis / 180 * math.pi
0279 
0280     def _on_displace(self, startpos, f):
0281         off = -math.sin(startpos[0]/self.wavelength*math.pi*2-f*self.speed_f/self.n_frames) * self.amplitude
0282         return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
0283 
0284 
0285 class MultiSineDisplacer(PointDisplacer):
0286     def __init__(
0287         self,
0288         waves,
0289         time_start,
0290         time_end,
0291         n_frames,
0292         speed=1,
0293         axis=90,
0294         amplitude_scale=1,
0295     ):
0296         """!
0297         Displaces points as if they were following a sine wave
0298 
0299         @param waves       List of tuples (wavelength, amplitude)
0300         @param time_start  When the animation shall start
0301         @param time_end    When the animation shall end
0302         @param n_frames    Number of keyframes to add
0303         @param speed       Number of peaks a point will go through in the given time
0304                            If negative, it will go the other way
0305         @param axis        Wave peak direction
0306         @param amplitude_scale  Multiplies the resulting amplitude by this factor
0307         """
0308         super().__init__(time_start, time_end, n_frames)
0309 
0310         self.waves = waves
0311         self.speed_f = math.pi * 2 * speed
0312         self.axis = axis / 180 * math.pi
0313         self.amplitude_scale = amplitude_scale
0314 
0315     def _on_displace(self, startpos, f):
0316         off = 0
0317         for wavelength, amplitude in self.waves:
0318             off -= math.sin(startpos[0]/wavelength*math.pi*2-f*self.speed_f/self.n_frames) * amplitude
0319 
0320         off *= self.amplitude_scale
0321         return NVector(off * math.cos(self.axis), off * math.sin(self.axis))
0322 
0323 
0324 class DepthRotationAxis:
0325     def __init__(self, x, y, keep):
0326         self.x = x / x.length
0327         self.y = y / y.length
0328         self.keep = keep / keep.length # should be the cross product
0329 
0330     def rot_center(self, center, point):
0331         return (
0332             self.x * self.x.dot(center) +
0333             self.y * self.y.dot(center) +
0334             self.keep * self.keep.dot(point)
0335         )
0336 
0337     def extract_component(self, vector, axis):
0338         return sum(vector.element_scaled(axis).components)
0339 
0340     @classmethod
0341     def from_points(cls, keep_point, center=NVector(0, 0, 0)):
0342         keep = keep_point - center
0343         keep /= keep.length
0344         # Hughes-Moller to find x and y
0345         if abs(keep.x) > abs(keep.z):
0346             y = NVector(-keep.y, keep.x, 0)
0347         else:
0348             y = NVector(0, -keep.z, keep.y)
0349         y /= y.length
0350         x = y.cross(keep)
0351         return cls(x, y, keep)
0352 
0353 
0354 class DepthRotation:
0355     axis_x = DepthRotationAxis(NVector(0, 0, 1), NVector(0, 1, 0), NVector(1, 0, 0))
0356     axis_y = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 0, 1), NVector(0, 1, 0))
0357     axis_z = DepthRotationAxis(NVector(1, 0, 0), NVector(0, 1, 0), NVector(0, 0, 1))
0358 
0359     def __init__(self, center):
0360         self.center = center
0361 
0362     def rotate3d_y(self, point, angle):
0363         return self.rotate3d(point, angle, self.axis_y)
0364         # Hard-coded version:
0365         #c = NVector(self.center.x, point.y, self.center.z)
0366         #rad = angle * math.pi / 180
0367         #delta = point - c
0368         #pol_l = delta.length
0369         #pol_a = math.atan2(delta.z, delta.x)
0370         #dest_a = pol_a + rad
0371         #return NVector(
0372         #    c.x + pol_l * math.cos(dest_a),
0373         #    point.y,
0374         #    c.z + pol_l * math.sin(dest_a)
0375         #)
0376 
0377     def rotate3d_x(self, point, angle):
0378         return self.rotate3d(point, angle, self.axis_x)
0379         # Hard-coded version:
0380         #c = NVector(point.x, self.center.y, self.center.z)
0381         #rad = angle * math.pi / 180
0382         #delta = point - c
0383         #pol_l = delta.length
0384         #pol_a = math.atan2(delta.y, delta.z)
0385         #dest_a = pol_a + rad
0386         #return NVector(
0387         #    point.x,
0388         #    c.y + pol_l * math.sin(dest_a),
0389         #    c.z + pol_l * math.cos(dest_a),
0390         #)
0391 
0392     def rotate3d_z(self, point, angle):
0393         return self.rotate3d(point, angle, self.axis_z)
0394 
0395     def rotate3d(self, point, angle, axis):
0396         c = axis.rot_center(self.center, point)
0397         rad = angle * math.pi / 180
0398         delta = point - c
0399         pol_l = delta.length
0400         pol_a = math.atan2(
0401             axis.extract_component(delta, axis.y),
0402             axis.extract_component(delta, axis.x)
0403         )
0404         dest_a = pol_a + rad
0405         return c + axis.x * pol_l * math.cos(dest_a) + axis.y * pol_l * math.sin(dest_a)
0406 
0407 
0408 class DepthRotationDisplacer(PointDisplacer):
0409     axis_x = DepthRotation.axis_x
0410     axis_y = DepthRotation.axis_y
0411     axis_z = DepthRotation.axis_z
0412 
0413     def __init__(self, center, time_start, time_end, n_frames, axis,
0414                  depth=0, angle=360, anglestart=0, ease=easing.Linear()):
0415         super().__init__(time_start, time_end, n_frames)
0416         self.rotation = DepthRotation(center)
0417         if isinstance(axis, NVector):
0418             axis = DepthRotationAxis.from_points(axis)
0419         self.axis = axis
0420         self.depth = depth
0421         self._angle = angle
0422         self.anglestart = anglestart
0423         self.ease = ease
0424         self._init_lerp(0, angle, ease)
0425 
0426     @property
0427     def angle(self):
0428         return self._angle
0429 
0430     @angle.setter
0431     def angle(self, value):
0432         self._angle = value
0433         self._init_lerp(0, value, self.ease)
0434 
0435     def _on_displace(self, startpos, f):
0436         angle = self.anglestart + self._lerp_get(f)
0437         if len(startpos) < 3:
0438             startpos = NVector(*(startpos.components + [self.depth]))
0439         return self.rotation.rotate3d(startpos, angle, self.axis) - startpos
0440 
0441 
0442 class EnvelopeDeformation(PointDisplacer):
0443     def __init__(self, topleft, bottomright):
0444         self.topleft = topleft
0445         self.size = bottomright - topleft
0446         self.keyframes = []
0447 
0448     @property
0449     def time_start(self):
0450         return self.keyframes[0][0]
0451 
0452     def add_reset_keyframe(self, time):
0453         self.add_keyframe(
0454             time,
0455             self.topleft.clone(),
0456             NVector(self.topleft.x + self.size.x, self.topleft.y),
0457             NVector(self.topleft.x + self.size.x, self.topleft.y + self.size.y),
0458             NVector(self.topleft.x, self.topleft.y + self.size.y),
0459         )
0460 
0461     def add_keyframe(self, time, tl, tr, br, bl):
0462         self.keyframes.append([
0463             time,
0464             tl.clone(),
0465             tr.clone(),
0466             br.clone(),
0467             bl.clone()
0468         ])
0469 
0470     def _on_displace(self, startpos, f):
0471         _, tl, tr, br, bl = self.keyframes[f]
0472         relp = startpos - self.topleft
0473         relp.x /= self.size.x
0474         relp.y /= self.size.y
0475 
0476         x1 = tl.lerp(tr, relp.x)
0477         x2 = bl.lerp(br, relp.x)
0478 
0479         #return x1.lerp(x2, relp.y)
0480         return x1.lerp(x2, relp.y) - startpos
0481 
0482     @property
0483     def n_frames(self):
0484         return len(self.keyframes)-1
0485 
0486     def frame_time(self, f):
0487         return self.keyframes[f][0]
0488 
0489 
0490 class DisplacerDampener(PointDisplacer):
0491     """!
0492     Given a displacer and a function that returns a factor for a point,
0493     multiplies the effect of the displacer by the factor
0494     """
0495     def __init__(self, displacer, dampener):
0496         self.displacer = displacer
0497         self.dampener = dampener
0498 
0499     @property
0500     def time_start(self):
0501         return self.displacer.time_start
0502 
0503     def _on_displace(self, startpos, f):
0504         disp = self.displacer._on_displace(startpos, f)
0505         damp = self.dampener(startpos)
0506         return disp * damp
0507 
0508     @property
0509     def n_frames(self):
0510         return self.displacer.n_frames
0511 
0512     def frame_time(self, f):
0513         return self.displacer.frame_time(f)
0514 
0515 
0516 class FollowDisplacer(PointDisplacer):
0517     def __init__(
0518         self,
0519         origin,
0520         range,
0521         offset_func,
0522         time_start, time_end, n_frames,
0523         falloff_exp=1,
0524     ):
0525         """!
0526         @brief Uses a custom offset function, and applies a falloff to the displacement
0527 
0528         @param origin       Origin point for the falloff
0529         @param range        Radius after which the points will not move
0530         @param offset_func  Function returning an offset given a ratio of the time
0531         @param time_start   When the animation shall start
0532         @param time_end     When the animation shall end
0533         @param n_frames     Number of frames in the animation
0534         @param falloff_exp  Exponent for the falloff
0535         """
0536         super().__init__(time_start, time_end, n_frames)
0537         self.origin = origin
0538         self.range = range
0539         self.offset_func = offset_func
0540         self.falloff_exp = falloff_exp
0541 
0542     def _on_displace(self, startpos, f):
0543         influence = 1 - min(1, (startpos - self.origin).length / self.range) ** self.falloff_exp
0544         return self.offset_func(f / self.n_frames) * influence