1 /**
2     Inochi2D Animation Player
3 
4     Copyright: 
5         Copyright © 2020-2026, Inochi2D Project
6     
7     License:
8         $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License)
9     
10     Authors:
11         Luna Nielsen
12 */
13 module inochi2d.animation.player;
14 import inochi2d.puppet;
15 import inochi2d.animation;
16 import inochi2d.param;
17 import nulib.collections;
18 import numath;
19 
20 class AnimationPlayer {
21 private:
22     Puppet puppet;
23     vector!AnimationPlaybackRef playingAnimations;
24 
25 public:
26     /**
27         Whether to snap to framerate
28     */
29     bool snapToFramerate = false;
30 
31     /**
32         Construct animation player
33     */
34     this(Puppet puppet) {
35         this.puppet = puppet;
36     }
37 
38     /**
39         Run an update step for the animation player
40     */
41     void update(float delta) {
42         foreach (ref anim; playingAnimations) {
43             if (anim.valid)
44                 anim.update(delta);
45         }
46     }
47 
48     /**
49         Gets an animation
50     */
51     AnimationPlaybackRef createOrGet(string name) {
52 
53         // Try fetching from pre-existing
54         foreach (ref AnimationPlaybackRef anim; playingAnimations) {
55             if (anim._name == name)
56                 return anim;
57         }
58 
59         // // Create new playback
60         // if (Animation* anim = name in puppet.animations()) {
61         //     playingAnimations ~= new AnimationPlayback(this, anim, name);
62         //     return playingAnimations[$-1];
63         // }
64 
65         // Invalid state
66         return null;
67     }
68 
69     /**
70         Convenience function which plays an animation
71     */
72     AnimationPlaybackRef play(string name) {
73         auto anim = createOrGet(name);
74         if (anim) {
75             anim.play();
76         }
77 
78         return anim;
79     }
80 
81     /**
82         pre-render one frame of all animations
83     */
84     void prerenderAll() {
85         foreach (anim; playingAnimations) {
86             anim.render();
87         }
88     }
89 
90     /**
91         Stop all animations
92     */
93     void stopAll(bool immediate = false) {
94         foreach (anim; playingAnimations) {
95             anim.stop(immediate);
96         }
97     }
98 
99     /**
100         Destroy all animations
101     */
102     void destroyAll() {
103         foreach (anim; playingAnimations) {
104             anim.valid = false;
105         }
106         playingAnimations.clear();
107     }
108 }
109 
110 struct AnimationPlayback {
111 private:
112     // Base Refs
113     AnimationPlayer player;
114     Animation* anim;
115     bool valid = true;
116     string _name;
117 
118     // Runtime
119     bool _playLeadOut = false;
120     bool _paused = false;
121     bool _playing = false;
122     bool _looping = false;
123     bool _stopping = false;
124     float _time = 0;
125     float _strength = 1;
126     float _speed = 1;
127     int _looped = 0;
128 
129     ref Puppet getPuppet() {
130         return player.puppet;
131     }
132 
133     this(AnimationPlayer player, Animation* anim, string name) {
134         this.player = player;
135         this.anim = anim;
136         this._name = name;
137         this._paused = false;
138         this._playing = false;
139         this._looping = false;
140         this._playLeadOut = false;
141     }
142 
143     // Internal functions
144 
145     void update(float delta) {
146         if (!valid || !isRunning)
147             return;
148         if (_paused) {
149             render();
150             return;
151         }
152 
153         // Time step
154         _time += delta;
155 
156         // Handle looping
157         if (!isPlayingLeadOut && looping && frame >= loopPointEnd) {
158             _time = cast(float)loopPointBegin * anim.timestep;
159             _looped++;
160         }
161 
162         // Always render the last frame
163         if (frame + 1 >= frames) {
164             _time = cast(float)(frames - 1) * anim.timestep;
165         }
166 
167         render();
168 
169         // Handle stopping animation completely on lead-out end
170         if (!_looping && isPlayingLeadOut()) {
171             if (frame + 1 >= anim.length) {
172                 _playing = false;
173                 _playLeadOut = false;
174                 _stopping = false;
175                 _time = 0;
176                 _looped = 0;
177             }
178         }
179     }
180 
181 public:
182     /// Gets the name of the animation
183     string name() {
184         return _name;
185     }
186 
187     /// Gets whether the animation has run to end
188     bool eof() {
189         return frame >= anim.length;
190     }
191 
192     /// Gets whether this instance is valid
193     bool isValid() {
194         return valid;
195     }
196 
197     /// Gets whether this instance is currently playing
198     bool playing() {
199         return _playing;
200     }
201 
202     /// Gets whether this instance is currently stopping
203     bool stopping() {
204         return _stopping;
205     }
206 
207     /// Gets whether this instance is currently paused
208     bool paused() {
209         return _paused;
210     }
211 
212     /// Gets or sets whether this instance is looping
213     bool looping() {
214         return _looping;
215     }
216 
217     bool looping(bool value) {
218         _looping = value;
219         return value;
220     }
221 
222     /// Gets how many times the animation has looped
223     int looped() {
224         return _looped;
225     }
226 
227     /// Gets or sets the speed multiplier for the animation
228     float speed() {
229         return _speed;
230     }
231 
232     float speed(bool value) {
233         _speed = clamp(value, 1, 10);
234         return value;
235     }
236 
237     /// Gets or sets the strength multiplier (0..1) for the animation
238     float strength() {
239         return _strength;
240     }
241 
242     float strength(float value) {
243         _strength = clamp(value, 0, 1);
244         return value;
245     }
246 
247     /// Gets the current frame of animation
248     int frame() {
249         return cast(int)round(_time / anim.timestep);
250     }
251 
252     /// Gets the current floating point (half-)frame of animation
253     float hframe() {
254         return _time / anim.timestep;
255     }
256 
257     /// Gets the frame looping ends at
258     int loopPointEnd() {
259         return hasLeadOut ? anim.leadOut : anim.length;
260     }
261 
262     /// Gets the frame looping begins at
263     int loopPointBegin() {
264         return hasLeadIn ? anim.leadIn : 0;
265     }
266 
267     /// Gets whether the animation has lead-in
268     bool hasLeadIn() {
269         return anim.leadIn > 0 && anim.leadIn + 1 < anim.length;
270     }
271 
272     /// Gets whether the animation has lead-out
273     bool hasLeadOut() {
274         return anim.leadOut > 0 && anim.leadOut + 1 < anim.length;
275     }
276 
277     /// Gets whether the animation is playing the leadout
278     bool isPlayingLeadOut() {
279         return ((_playing && !_looping) || _stopping) && _playLeadOut && frame < anim.length;
280     }
281 
282     /// Gets whether the animation is playing the main part or lead out
283     bool isRunning() {
284         return _playing || isPlayingLeadOut;
285     }
286 
287     /// Gets the framerate of the animation
288     int fps() {
289         return cast(int)(1000.0 / (anim.timestep * 1000.0));
290     }
291 
292     /// Gets playback seconds
293     int seconds() {
294         return cast(int)_time;
295     }
296 
297     /// Gets playback miliseconds
298     int miliseconds() {
299         return cast(int)((_time - cast(float)seconds) * 1000);
300     }
301 
302     /// Gets length in frames
303     int frames() {
304         return anim.length;
305     }
306 
307     /// Gets the backing animation for the current playback
308     Animation* animation() {
309         return anim;
310     }
311 
312     /// Gets the playback ID
313     ptrdiff_t playbackId() {
314         ptrdiff_t idx = -1;
315         foreach (i, ref sanim; player.playingAnimations)
316             if (sanim._name == this._name)
317                 idx = i;
318 
319         return idx;
320     }
321 
322     /**
323         Destroys this animation instance
324     */
325     void destroy() {
326         this.valid = false;
327         if (playbackId > -1)
328             player.playingAnimations.removeAt(playbackId);
329     }
330 
331     /**
332         Plays the animation
333     */
334     void play(bool loop = false, bool playLeadOut = true) {
335         if (_paused)
336             _paused = false;
337         else {
338             _looped = 0;
339             _time = 0;
340             _stopping = false;
341             _playing = true;
342             _looping = loop;
343             _playLeadOut = playLeadOut;
344         }
345     }
346 
347     /**
348         Pauses the animation
349     */
350     void pause() {
351         _paused = true;
352     }
353 
354     /**
355         Stops the animation
356     */
357     void stop(bool immediate = false) {
358         if (_stopping)
359             return;
360 
361         bool shouldStopImmediate = immediate || frame == 0 || _paused || !hasLeadOut;
362         _stopping = !shouldStopImmediate;
363         _looping = false;
364         _paused = false;
365         _playing = false;
366         _playLeadOut = !shouldStopImmediate;
367         if (shouldStopImmediate) {
368             _time = 0;
369             _looped = 0;
370         }
371     }
372 
373     /**
374         Seeks the animation
375     */
376     void seek(int frame) {
377         float frameTime = clamp(frame, 0, frames);
378         _time = frameTime * anim.timestep;
379         _looped = 0;
380     }
381 
382     /**
383         Renders the current frame of animation
384 
385         Called internally automatically by the animation player
386     */
387     void render() {
388         float realStrength = clamp(_strength, 0, 1);
389         foreach (lane; anim.lanes) {
390             if (auto param1d = cast(Parameter1D)lane.paramRef.targetParam) {
391 
392                 param1d.pushValue(lane.get(hframe, player.snapToFramerate) * realStrength);
393             } else if (auto param2d = cast(Parameter2D)lane.paramRef.targetParam) {
394 
395                 auto axis = cast(ParameterAxis)lane.paramRef.targetAxis;
396                 auto value = lane.get(hframe, player.snapToFramerate) * realStrength;
397                 param2d.pushValue(axis, value);
398             }
399         }
400     }
401 }
402 
403 alias AnimationPlaybackRef = AnimationPlayback*;