1 // Copyright 2023 Alexandros F. G. Kapretsos
2 // SPDX-License-Identifier: Apache-2.0
3 
4 // NOTE(AlexandrosKap): Maybe add splines.
5 
6 module deetween;
7 
8 import math = core.math;
9 import expo = std.math.exponential;
10 
11 private enum {
12     PI = 3.141592f,
13 }
14 
15 /// A tween mode describes how an animation should update.
16 enum TweenMode {
17     bomb, /// It stops updating when it reaches the beginning or end of the animation.
18     loop, /// It returns to the beginning or end of the animation when it reaches the beginning or end of the animation.
19     yoyo, /// It reverses the given delta time when it reaches the beginning or end of the animation.
20 }
21 
22 /// A representation of an easing function.
23 alias EasingFunc = float function(float x) pure nothrow @nogc @safe;
24 
25 /// A tween handles the transition from one value to another value based on a transition duration.
26 struct Tween {
27     EasingFunc f = &easeLinear;      /// The function used to ease from the first to the last value.
28     TweenMode mode = TweenMode.bomb; /// The mode of the animation.
29     float a = 0.0f;                  /// The first animation value.
30     float b = 0.0f;                  /// The last animation value.
31     float time = 0.0f;               /// The current time of the animation.
32     float duration = 0.0f;           /// The duration of the animation.
33     bool isYoyoing;                  /// Controls if the delta time given to the update function is reversed.
34 
35 pure nothrow @nogc @safe:
36 
37     /// Creates a new tween.
38     this(float a, float b, float duration, TweenMode mode = TweenMode.bomb, EasingFunc f = &easeLinear) {
39         this.f = f;
40         this.mode = mode;
41         this.a = a;
42         this.b = b;
43         this.duration = duration;
44     }
45 
46     /// Returns true if the animation has started.
47     /// This function makes sense when the tween mode is set to bomb.
48     bool hasStarted() const {
49         return time > 0.0f;
50     }
51 
52     /// Returns true if the animation has finished.
53     /// This function makes sense when the tween mode is set to bomb.
54     bool hasFinished() const {
55         return time >= duration;
56     }
57 
58     /// Returns the current animation progress.
59     /// The progress is between 0.0 and 1.0.
60     float progress() const {
61         if (duration == 0.0f) {
62             return 0.0f;
63         }
64         return clamp(time / duration, 0.0f, 1.0f);
65     }
66 
67     /// Sets the current animation progress to a specific value.
68     /// The progress is between 0.0 and 1.0.
69     void progress(float value) {
70         time = clamp(value * duration, 0.0f, 1.0f);
71     }
72 
73     /// Returns the current animation value.
74     /// The value is between the first and the last animation value.
75     float now() const {
76         if (time <= 0.0f) {
77             return a;
78         } else if (time >= duration) {
79             return b;
80         } else {
81             return ease(a, b, progress, f);
82         }
83     }
84 
85     /// Returns the current animation time.
86     float elapsedTime() const {
87         return time;
88     }
89 
90     /// Sets the current animation time to a specific value and returns the current animation value.
91     float elapsedTime(float time) {
92         final switch (mode) {
93         case TweenMode.bomb:
94             this.time = clamp(time, 0.0f, duration);
95             return now;
96         case TweenMode.loop:
97             this.time = loopClamp(time, 0.0f, duration);
98             return now;
99         case TweenMode.yoyo:
100             if (time < 0.0f) {
101                 isYoyoing = false;
102             } else if (time > duration) {
103                 isYoyoing = true;
104             }
105             this.time = clamp(time, 0.0f, duration);
106             return now;
107         }
108     }
109 
110     /// Updates the current animation time by the given delta time and returns the current animation value.
111     float update(float dt) {
112         if (isYoyoing) {
113             return elapsedTime(time - dt);
114         } else {
115             return elapsedTime(time + dt);
116         }
117     }
118 
119     /// Resets the current animation time.
120     void reset() {
121         time = 0.0f;
122     }
123 }
124 
125 /// A keyframe is a data type that has a value and a time.
126 struct Keyframe {
127     float value = 0.0f; /// The current value.
128     float time = 0.0f;  /// The current time.
129 
130 pure nothrow @nogc @safe:
131 
132     /// Creates a new keyframe.
133     this(float value, float time) {
134         this.value = value;
135         this.time = time;
136     }
137 }
138 
139 /// A keyframe group handles the transition from one keyframe to another keyframe.
140 struct KeyframeGroup {
141     Keyframe[] keys;                 /// The keyframes of the animation.
142     EasingFunc f = &easeLinear;      /// The function used to ease from one keyframe to another keyframe.
143     TweenMode mode = TweenMode.bomb; /// The mode of the animation.
144     float time = 0.0f;               /// The current time of the animation.
145     float duration = 0.0f;           /// The duration of the animation
146     bool isYoyoing;                  /// Controls if the delta time given to the update function is reversed.
147 
148 pure nothrow @safe:
149 
150     /// Creates a new keyframe group.
151     this(float duration, TweenMode mode = TweenMode.bomb, EasingFunc f = &easeLinear) {
152         this.f = f;
153         this.mode = mode;
154         this.duration = duration;
155     }
156 
157     /// Returns true if the animation has started.
158     /// This function makes sense when the tween mode is set to bomb.
159     @nogc
160     bool hasStarted() const {
161         return time > 0.0f;
162     }
163 
164     /// Returns true if the animation has finished.
165     /// This function makes sense when the tween mode is set to bomb.
166     @nogc
167     bool hasFinished() const {
168         return time >= duration;
169     }
170 
171     /// Returns the current animation progress.
172     /// The progress is between 0.0 and 1.0.
173     @nogc
174     float progress() const {
175         if (duration == 0.0f) {
176             return 0.0f;
177         }
178         return clamp(time / duration, 0.0f, 1.0f);
179     }
180 
181     /// Sets the current animation progress to a specific value.
182     /// The progress is between 0.0 and 1.0.
183     @nogc
184     void progress(float value) {
185         time = clamp(value * duration, 0.0f, 1.0f);
186     }
187 
188     /// Returns the current animation value.
189     /// The value is between the current keyframe and the next keyframe.
190     @nogc
191     float now() const {
192         if (keys.length == 0) {
193             return 0.0f;
194         } else if (time <= 0.0f) {
195             return keys[0].value;
196         } else if (time >= duration) {
197             return keys[$ - 1].value;
198         } else {
199             foreach (i; 0 .. keys.length) {
200                 if (time <= keys[i].time) {
201                     const a = keys[i - 1];
202                     const b = keys[i];
203                     const weight = (time - a.time) / (b.time - a.time);
204                     return ease(a.value, b.value, weight, f);
205                 }
206             }
207             return 0.0f;
208         }
209     }
210 
211     /// Returns the current animation time.
212     @nogc
213     float elapsedTime() const {
214         return time;
215     }
216 
217     /// Sets the current animation time to a specific value and returns the current animation value.
218     @nogc
219     float elapsedTime(float time) {
220         final switch (mode) {
221         case TweenMode.bomb:
222             this.time = clamp(time, 0.0f, duration);
223             return now;
224         case TweenMode.loop:
225             this.time = loopClamp(time, 0.0f, duration);
226             return now;
227         case TweenMode.yoyo:
228             if (time < 0.0f) {
229                 isYoyoing = false;
230             } else if (time > duration) {
231                 isYoyoing = true;
232             }
233             this.time = clamp(time, 0.0f, duration);
234             return now;
235         }
236     }
237 
238     /// Updates the current animation time by the given delta time and returns the current animation value.
239     @nogc
240     float update(float dt) {
241         if (isYoyoing) {
242             return elapsedTime(time - dt);
243         } else {
244             return elapsedTime(time + dt);
245         }
246     }
247 
248     /// Resets the current animation time.
249     @nogc
250     void reset() {
251         time = 0.0f;
252     }
253 
254     /// Returns the keyframe count of the animation.
255     @nogc
256     size_t length() const {
257         return keys.length;
258     }
259 
260     /// Returns the keyframe count of the animation.
261     @nogc
262     size_t opDollar() const {
263         return length;
264     }
265 
266     /// Returns the keyframe at the given index.
267     @nogc
268     Keyframe opIndex(size_t index) const {
269         return keys[index];
270     }
271 
272     /// Sets the keyframe at the given index to the given keyframe.
273     @nogc
274     void opIndexAssign(Keyframe value, size_t index) {
275         keys[index] = value;
276     }
277 
278     /// Appends the keyframe to the animation.
279     void append(Keyframe[] items...) {
280         foreach (item; items) {
281             if (keys.length == 0 || keys[$ - 1].time <= item.time) {
282                 keys ~= item;
283             } else {
284                 keys ~= item;
285                 // Hehehe!
286                 foreach (i; 1 .. keys.length) {
287                     foreach_reverse (j; i .. keys.length) {
288                         if (keys[j - 1].time > keys[j].time) {
289                             Keyframe temp = keys[j - 1];
290                             keys[j - 1] = keys[j];
291                             keys[j] = temp;
292                         }
293                     }
294                 }
295             }
296         }
297     }
298 
299     /// A helper function for appending many keyframes evenly to the animation.
300     void appendEvenly(float[] values...) {
301         if (values.length == 0) {
302             return;
303         } else if (values.length == 1) {
304             append(Keyframe(values[0], 0.0f));
305             return;
306         }
307         const length = values.length - 1;
308         foreach (i; 0 .. length) {
309             append(Keyframe(values[i], duration * (cast(float) i / length)));
310         }
311         append(Keyframe(values[$ - 1], duration));
312     }
313 
314     /// Removes the keyframe at the given index.
315     @nogc
316     void remove(size_t index) {
317         if (keys.length == 0) {
318             return;
319         }
320         const length = keys.length - 1;
321         foreach (i; index .. length) {
322             keys[i] = keys[i + 1];
323         }
324         keys = keys[0 .. $ - 1];
325     }
326 
327     /// Removes all keyframes from the animation.
328     @nogc
329     void clear() {
330         keys.length = 0;
331     }
332 }
333 
334 /// A value sequence handles the transition from one value to another value based on a value duration.
335 struct ValueSequence {
336     TweenMode mode;             /// The mode of the animation.
337     int a;                      /// The first animation value.
338     int b;                      /// The last animation value.
339     int value;                  /// The current animation value.
340     float valueTime = 0.0f;     /// The current time of the current value.
341     float valueDuration = 0.0f; /// The duration of a value.
342     bool isYoyoing;             /// Controls if the delta time given to the update function is reversed.
343 
344 pure nothrow @nogc @safe:
345 
346     /// Creates a new value sequence.
347     this(int a, int b, float valueDuration, TweenMode mode = TweenMode.bomb) {
348         this.mode = mode;
349         this.a = a;
350         this.b = b;
351         this.value = a;
352         this.valueDuration = valueDuration;
353     }
354 
355     /// Returns true if the animation has started.
356     /// This function makes sense when the tween mode is set to bomb.
357     bool hasStarted() const {
358         return value > a;
359     }
360 
361     /// Returns true if the animation has finished.
362     /// This function makes sense when the tween mode is set to bomb.
363     bool hasFinished() const {
364         return value >= b;
365     }
366 
367     /// Returns the current animation value.
368     /// The value is between the first and the last animation value.
369     int now() const {
370         if (value < a) {
371             return a;
372         } else if (value > b) {
373             return b;
374         } else {
375             return value;
376         }
377     }
378 
379     /// Updates the current time of the current value by the given delta time and returns the current animation value.
380     int update(float dt) {
381         if (isYoyoing) {
382             valueTime -= dt;
383         } else {
384             valueTime += dt;
385         }
386         // NOTE(AlexandrosKap): Super bad part of code... Maybe change it if someone finds a bug.
387         final switch (mode) {
388         case TweenMode.bomb:
389             while (valueTime < 0.0f) {
390                 if (value > a) {
391                     value -= 1;
392                     valueTime += valueDuration;
393                 } else {
394                     valueTime = 0.0f;
395                 }
396             }
397             while (valueTime > valueDuration) {
398                 if (value < b) {
399                     value += 1;
400                     valueTime -= valueDuration;
401                 } else {
402                     valueTime = valueDuration;
403                 }
404             }
405             return now;
406         case TweenMode.loop:
407             while (valueTime < 0.0f) {
408                 if (value > a) {
409                     value -= 1;
410                 } else {
411                     value = b;
412                 }
413                 valueTime += valueDuration;
414             }
415             while (valueTime > valueDuration) {
416                 if (value < b) {
417                     value += 1;
418                 } else {
419                     value = a;
420                 }
421                 valueTime -= valueDuration;
422             }
423             return now;
424         case TweenMode.yoyo:
425             while (valueTime < 0.0f) {
426                 if (value > a) {
427                     value -= 1;
428                     valueTime += valueDuration;
429                 } else {
430                     valueTime = 0.0f;
431                     isYoyoing = false;
432                 }
433             }
434             while (valueTime > valueDuration) {
435                 if (value < b) {
436                     value += 1;
437                     valueTime -= valueDuration;
438                 } else {
439                     valueTime = valueDuration;
440                     isYoyoing = true;
441                 }
442             }
443             return now;
444         }
445     }
446 
447     /// Resets the current animation value.
448     void reset() {
449         value = a;
450         valueTime = 0.0f;
451     }
452 }
453 
454 pure nothrow @nogc @safe {
455     /// An easing function.
456     float easeNearest(float x) {
457         return 0.0f;
458     }
459 
460     /// An easing function.
461     float easeLinear(float x) {
462         return x;
463     }
464 
465     /// An easing function.
466     float easeInSine(float x) {
467         return 1.0f - math.cos((x * PI) / 2.0f);
468     }
469 
470     /// An easing function.
471     float easeOutSine(float x) {
472         return math.sin((x * PI) / 2.0f);
473     }
474 
475     /// An easing function.
476     float easeInOutSine(float x) {
477         return -(math.cos(PI * x) - 1.0f) / 2.0f;
478     }
479 
480     /// An easing function.
481     float easeInQuad(float x) {
482         return x * x;
483     }
484 
485     /// An easing function.
486     float easeOutQuad(float x) {
487         return 1.0f - (1.0f - x) * (1.0f - x);
488     }
489 
490     /// An easing function.
491     float easeInOutQuad(float x) {
492         if (x < 0.5f) {
493             return 2.0f * x * x;
494         } else {
495             return 1.0f - expo.pow(-2.0f * x + 2.0f, 2.0f) / 2.0f;
496         }
497     }
498 
499     /// An easing function.
500     float easeInCubic(float x) {
501         return x * x * x;
502     }
503 
504     /// An easing function.
505     float easeOutCubic(float x) {
506         return 1.0f - expo.pow(1.0f - x, 3.0f);
507     }
508 
509     /// An easing function.
510     float easeInOutCubic(float x) {
511         if (x < 0.5f) {
512             return 4.0f * x * x * x;
513         } else {
514             return 1.0f - expo.pow(-2.0f * x + 2.0f, 3.0f) / 2.0f;
515         }
516     }
517 
518     /// An easing function.
519     float easeInQuart(float x) {
520         return x * x * x * x;
521     }
522 
523     /// An easing function.
524     float easeOutQuart(float x) {
525         return 1.0f - expo.pow(1.0f - x, 4.0f);
526     }
527 
528     /// An easing function.
529     float easeInOutQuart(float x) {
530         if (x < 0.5f) {
531             return 8.0f * x * x * x * x;
532         } else {
533             return 1.0f - expo.pow(-2.0f * x + 2.0f, 4.0f) / 2.0f;
534         }
535     }
536 
537     /// An easing function.
538     float easeInQuint(float x) {
539         return x * x * x * x * x;
540     }
541 
542     /// An easing function.
543     float easeOutQuint(float x) {
544         return 1.0f - expo.pow(1.0f - x, 5.0f);
545     }
546 
547     /// An easing function.
548     float easeInOutQuint(float x) {
549         if (x < 0.5f) {
550             return 16.0f * x * x * x * x * x;
551         } else {
552             return 1.0f - expo.pow(-2.0f * x + 2.0f, 5.0f) / 2.0f;
553         }
554     }
555 
556     /// An easing function.
557     float easeInExpo(float x) {
558         if (x == 0.0f) {
559             return 0.0f;
560         } else {
561             return expo.pow(2.0f, 10.0f * x - 10.0f);
562         }
563     }
564 
565     /// An easing function.
566     float easeOutExpo(float x) {
567         if (x == 1.0f) {
568             return 1.0f;
569         } else {
570             return 1.0f - expo.pow(2.0f, -10.0f * x);
571         }
572     }
573 
574     /// An easing function.
575     float easeInOutExpo(float x) {
576         if (x == 0.0f) {
577             return 0.0f;
578         } else if (x == 1.0f) {
579             return 1.0f;
580         } else if (x < 0.5f) {
581             return expo.pow(2.0f, 20.0f * x - 10.0f) / 2.0f;
582         } else {
583             return (2.0f - expo.pow(2.0f, -20.0f * x + 10.0f)) / 2.0f;
584         }
585     }
586 
587     /// An easing function.
588     float easeInCirc(float x) {
589         return 1.0f - math.sqrt(1.0f - expo.pow(x, 2.0f));
590     }
591 
592     /// An easing function.
593     float easeOutCirc(float x) {
594         return math.sqrt(1.0f - expo.pow(x - 1.0f, 2.0f));
595     }
596 
597     /// An easing function.
598     float easeInOutCirc(float x) {
599         if (x < 0.5f) {
600             return (1.0f - math.sqrt(1.0f - expo.pow(2.0f * x, 2.0f))) / 2.0f;
601         } else {
602             return (math.sqrt(1.0f - expo.pow(-2.0f * x + 2.0f, 2.0f)) + 1.0f) / 2.0f;
603         }
604     }
605 
606     /// An easing function.
607     float easeInBack(float x) {
608         enum c1 = 1.70158f;
609         enum c3 = c1 + 1.0f;
610         return c3 * x * x * x - c1 * x * x;
611     }
612 
613     /// An easing function.
614     float easeOutBack(float x) {
615         enum c1 = 1.70158f;
616         enum c3 = c1 + 1.0f;
617         return 1.0f + c3 * expo.pow(x - 1.0f, 3.0f) + c1 * expo.pow(x - 1.0f, 2.0f);
618     }
619 
620     /// An easing function.
621     float easeInOutBack(float x) {
622         enum c1 = 1.70158f;
623         enum c2 = c1 * 1.525f;
624         if (x < 0.5f) {
625             return (expo.pow(2.0f * x, 2.0f) * ((c2 + 1.0f) * 2.0f * x - c2)) / 2.0f;
626         } else {
627             return (expo.pow(2.0f * x - 2.0f, 2.0f) * ((c2 + 1.0f) * (x * 2.0f - 2.0f) + c2) + 2.0f) / 2.0f;
628         }
629     }
630 
631     /// An easing function.
632     float easeInElastic(float x) {
633         enum c4 = (2.0f * PI) / 3.0f;
634         if (x == 0.0f) {
635             return 0.0f;
636         } else if (x == 1.0f) {
637             return 1.0f;
638         } else {
639             return -expo.pow(2.0f, 10.0f * x - 10.0f) * math.sin((x * 10.0f - 10.75f) * c4);
640         }
641     }
642 
643     /// An easing function.
644     float easeOutElastic(float x) {
645         enum c4 = (2.0f * PI) / 3.0f;
646         if (x == 0.0f) {
647             return 0.0f;
648         } else if (x == 1.0f) {
649             return 1.0f;
650         } else {
651             return expo.pow(2.0f, -10.0f * x) * math.sin((x * 10.0f - 0.75f) * c4) + 1.0f;
652         }
653     }
654 
655     /// An easing function.
656     float easeInOutElastic(float x) {
657         enum c5 = (2.0f * PI) / 4.5f;
658         if (x == 0.0f) {
659             return 0.0f;
660         } else if (x == 1.0f) {
661             return 1.0f;
662         } else if (x < 0.5f) {
663             return -(expo.pow(2.0f, 20.0f * x - 10.0f) * math.sin((20.0f * x - 11.125f) * c5)) / 2.0f;
664         } else {
665             return (expo.pow(2.0f, -20.0f * x + 10.0f) * math.sin((20.0f * x - 11.125f) * c5)) / 2.0f + 1.0f;
666         }
667     }
668 
669     /// An easing function.
670     float easeInBounce(float x) {
671         return 1.0f - easeOutBounce(1.0f - x);
672     }
673 
674     /// An easing function.
675     float easeOutBounce(float x) {
676         enum n1 = 7.5625f;
677         enum d1 = 2.75f;
678         if (x < 1.0f / d1) {
679             return n1 * x * x;
680         } else if (x < 2.0f / d1) {
681             return n1 * (x -= 1.5f / d1) * x + 0.75f;
682         } else if (x < 2.5f / d1) {
683             float xm = x - 2.25f / d1;
684             return n1 * xm * xm + 0.9375f;
685         } else {
686             float xm = x - 2.625f / d1;
687             return n1 * xm * xm + 0.984375f;
688         }
689     }
690 
691     /// An easing function.
692     float easeInOutBounce(float x) {
693         if (x < 0.5f) {
694             return (1.0f - easeOutBounce(1.0f - 2.0f * x)) / 2.0f;
695         } else {
696             return (1.0f + easeOutBounce(2.0f * x - 1.0f)) / 2.0f;
697         }
698     }
699 
700     /// Interpolates linearly between a and b by weight.
701     /// The weight should be between 0.0 and 1.0, but this is not mandatory.
702     float lerp(float a, float b, float weight) {
703         return a + (b - a) * weight;
704     }
705 
706     /// Interpolates between a and b by weight by using an easing function.
707     /// The weight should be between 0.0 and 1.0, but this is not mandatory.
708     float ease(float a, float b, float weight, EasingFunc f) {
709         return a + (b - a) * f(weight);
710     }
711 
712     /// Interpolates between a and b with smoothing at the limits by weight.
713     /// The weight should be between 0.0 and 1.0, but this is not mandatory.
714     float smoothStep(float a, float b, float weight) {
715         float v = weight * weight * (3.0f - 2.0f * weight);
716         return (b * v) + (a * (1.0f - v));
717     }
718 
719     /// Interpolates between a and b with smoothing at the limits by weight.
720     /// The weight should be between 0.0 and 1.0, but this is not mandatory.
721     float smootherStep(float a, float b, float weight) {
722         float v = weight * weight * weight * (weight * (weight * 6.0f - 15.0f) + 10.0f);
723         return (b * v) + (a * (1.0f - v));
724     }
725 
726     /// Interpolates linearly between a and b by delta time.
727     float moveTowards(float a, float b, float dt) {
728 	    if (abs(b - a) > abs(dt)) {
729 	        return a + sign(b - a) * dt;
730 	    } else {
731 	        return b;
732 	    }
733     }
734 
735     /// Interpolates smoothly between a and b by delta time by using a slowdown factor.
736     float smoothMoveTowards(float a, float b, float dt, float slowdown) {
737 	    float target = ((a * (slowdown - 1.0f)) + b) / slowdown;
738 	    return a + (target - a) * dt;
739     }
740 
741     /// Returns true if a is moving towards b by delta time.
742     bool isMovingTowards(float a, float b, float dt) {
743         return !(abs(b - a) <= dt);
744     }
745 
746     private float abs(float x) {
747         if (x < 0.0f) {
748             return -x;
749         } else {
750             return x;
751         }
752     }
753 
754     private float sign(float x) {
755         if (x < 0.0f) {
756             return -1.0f;
757         } else {
758             return 1.0f;
759         }
760     }
761 
762     private float clamp(float x, float min, float max) {
763         if (x < min) {
764             return min;
765         } else if (x > max) {
766             return max;
767         } else {
768             return x;
769         }
770     }
771 
772     private float loopClamp(float x, float min, float max) {
773         float result = x;
774         while (result < min) {
775             result += max;
776         }
777         while (result > max) {
778             result -= max;
779         }
780         return result;
781     }
782 }
783 
784 unittest {
785     const a = 9.0f;
786     const b = 20.0f;
787     const totalDuration = 1.0f;
788     const dt = 0.001f;
789 
790     auto tween = Tween(a, b, totalDuration, TweenMode.bomb);
791 
792     assert(tween.now == a);
793     while (!tween.hasFinished) {
794         float value = tween.update(dt);
795         assert(value >= a && value <= b);
796     }
797     assert(tween.now == b);
798 }
799 
800 unittest {
801     const a = 9.0f;
802     const b = 20.0f;
803     const totalDuration = 1.0f;
804     const dt = 0.001f;
805 
806     auto group = KeyframeGroup(totalDuration, TweenMode.bomb);
807     group.append(
808         Keyframe(a, 0.0f),
809         Keyframe(b, totalDuration),
810     );
811 
812     assert(group.now == a);
813     while (!group.hasFinished) {
814         float value = group.update(dt);
815         assert(value >= a && value <= b);
816     }
817     assert(group.now == b);
818 }
819 
820 unittest {
821     const a = 9;
822     const b = 20;
823     const valueDuration = 0.1f;
824     const dt = 0.001f;
825 
826     auto sequence = ValueSequence(a, b, valueDuration, TweenMode.bomb);
827 
828     assert(sequence.now == a);
829     while (!sequence.hasFinished) {
830         int value = sequence.update(dt);
831         assert(value >= a && value <= b);
832     }
833     assert(sequence.now == b);
834 }
835 
836 unittest {
837     const a = 69;
838     const b = 420;
839     const totalDuration = 1.0f;
840 
841     auto anim1 = Tween(a, b, totalDuration, TweenMode.loop);
842     auto anim2 = KeyframeGroup(totalDuration, TweenMode.loop);
843     auto anim3 = ValueSequence(a, b, totalDuration / (b - a), TweenMode.loop);
844     anim2.appendEvenly(a, b);
845 
846     assert(anim1.progress == 0.0f);
847     assert(anim2.progress == 0.0f);
848 
849     assert(anim1.update(0.0f) == a);
850     assert(anim2.update(0.0f) == a);
851     assert(anim3.update(0.0f) == a);
852 
853     assert(anim1.update(totalDuration) == b);
854     assert(anim2.update(totalDuration) == b);
855     assert(anim3.update(totalDuration) == b);
856 
857     assert(anim1.progress == 1.0f);
858     assert(anim2.progress == 1.0f);
859 
860     anim1.reset();
861     anim2.reset();
862     anim3.reset();
863 
864     assert(anim1.update(totalDuration + 0.1f) < b);
865     assert(anim2.update(totalDuration + 0.1f) < b);
866     assert(anim3.update(totalDuration + 0.1f) < b);
867 }
868 
869 unittest {
870     const a = 1;
871     const b = 2;
872     const c = 3;
873 
874     auto group = KeyframeGroup();
875     group.appendEvenly(a, b, c);
876 
877     assert(group.length == 3);
878     assert(group[0].value == a);
879     group.remove(1);
880     assert(group.length == 2);
881     assert(group[0].value == a);
882     assert(group[1].value == c);
883 }