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 }