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