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*;