|
| 1 | +""" |
| 2 | +Mechanical movements e.g. hammer, robot arm, etc. |
| 3 | +""" |
| 4 | + |
| 5 | +__all__ = ( |
| 6 | + "Hammer", |
| 7 | +) |
| 8 | + |
| 9 | +import bpy |
| 10 | +import numpy as np |
| 11 | + |
| 12 | +from .procedure import Procedure |
| 13 | + |
| 14 | + |
| 15 | +class Hammer(Procedure): |
| 16 | + """ |
| 17 | + Hammer movement: Resting, preparing, hitting, recoil, wobbling. |
| 18 | +
|
| 19 | + Keyframe types: |
| 20 | + - JITTER: Resting. |
| 21 | + - BREAKDOWN: Preparing to hit. |
| 22 | + - EXTREME: Hitting. |
| 23 | + - KEYFRAME: Recoil and wobbling. |
| 24 | +
|
| 25 | + Parameters |
| 26 | + ---------- |
| 27 | +
|
| 28 | + animkey: Animation key: |
| 29 | + - hit: Hitting. |
| 30 | + - prepare: Preparing to hit. |
| 31 | + - recoil: Bounce back after hit. |
| 32 | +
|
| 33 | + prepare_dur: Duration (sec) of rest to prepare. |
| 34 | + Default: 0.18 |
| 35 | +
|
| 36 | + hit_dur: Duration (sec) of prepare to hit movement. |
| 37 | + Default: 0.1 |
| 38 | +
|
| 39 | + recoil_dur: Duration (sec) of hit to recoil movement. |
| 40 | + Default: 0.13 |
| 41 | +
|
| 42 | + wobble_period: Duration (sec) of each wobble. |
| 43 | + Default: 0.35 |
| 44 | +
|
| 45 | + wobble_count: Number of wobbles to perform. |
| 46 | + Default: 4 |
| 47 | +
|
| 48 | + wobble_decay: Factor by which wobble intensity decays each time. |
| 49 | + Default: 0.5 |
| 50 | + """ |
| 51 | + |
| 52 | + def __init__(self, **kwargs): |
| 53 | + super().__init__(**kwargs) |
| 54 | + self.animkey = kwargs.get("animkey") |
| 55 | + self.prepare_dur = kwargs.get("prepare_dur", 0.18) |
| 56 | + self.hit_dur = kwargs.get("hit_dur", 0.1) |
| 57 | + self.recoil_dur = kwargs.get("recoil_dur", 0.13) |
| 58 | + self.wobble_period = kwargs.get("wobble_period", 0.35) |
| 59 | + self.wobble_count = kwargs.get("wobble_count", 4) |
| 60 | + self.wobble_decay = kwargs.get("wobble_decay", 0.5) |
| 61 | + |
| 62 | + def animate(self): |
| 63 | + fps = bpy.context.scene.render.fps |
| 64 | + hit_dur = self.hit_dur * fps |
| 65 | + prepare_dur = self.prepare_dur * fps |
| 66 | + recoil_dur = self.recoil_dur * fps |
| 67 | + wobble_period = self.wobble_period * fps |
| 68 | + wobble_count = self.wobble_count |
| 69 | + wobble_decay = self.wobble_decay |
| 70 | + |
| 71 | + before_dur = prepare_dur + hit_dur |
| 72 | + wobble_dur = wobble_period * wobble_count |
| 73 | + after_dur = wobble_dur + recoil_dur |
| 74 | + total_dur = before_dur + after_dur |
| 75 | + |
| 76 | + for i, note in enumerate(self.midi): |
| 77 | + last = note.prev.start if note.prev else -1e9 |
| 78 | + next = note.next_start |
| 79 | + |
| 80 | + hit_intensity = np.interp(note.velocity, [0, 127], [0, 1]) |
| 81 | + |
| 82 | + # Prepare to hit |
| 83 | + if note.start - last > total_dur: |
| 84 | + # Long time since, so reset to resting position. |
| 85 | + self.animkey.animate(note.start-before_dur, type="JITTER") |
| 86 | + |
| 87 | + prepare_frame = max(note.start-hit_dur, (note.start+last)/2) |
| 88 | + self.animkey.animate(prepare_frame, type="BREAKDOWN", prepare=hit_intensity) |
| 89 | + |
| 90 | + # Hit |
| 91 | + self.animkey.animate(note.start, type="EXTREME", handle="VECTOR", hit=hit_intensity) |
| 92 | + |
| 93 | + # Wobble |
| 94 | + dur_limit = min(after_dur, next-note.start-before_dur) |
| 95 | + offset = recoil_dur |
| 96 | + |
| 97 | + for j in range(wobble_count): |
| 98 | + if offset >= dur_limit: |
| 99 | + break |
| 100 | + intensity = hit_intensity * wobble_decay ** (j+1) |
| 101 | + |
| 102 | + name = "prepare" if j % 2 == 0 else "hit" |
| 103 | + kwargs = {name: intensity} |
| 104 | + self.animkey.animate(note.start+offset, type="KEYFRAME", **kwargs) |
| 105 | + |
| 106 | + offset += wobble_period |
| 107 | + |
| 108 | + if next - before_dur > note.start + offset: |
| 109 | + # Long time until, so reset to resting position. |
| 110 | + self.animkey.animate(note.start+offset, type="JITTER") |
0 commit comments