diff --git a/src/dusk/animation/animation.c b/src/dusk/animation/animation.c index 1380bd6b..1660a4f0 100644 --- a/src/dusk/animation/animation.c +++ b/src/dusk/animation/animation.c @@ -11,42 +11,115 @@ void animationInit( animation_t *anim, keyframe_t *keyframes, - uint16_t keyframeCount + uint16_t *keyframeCounts, + const uint16_t layerCount ) { assertNotNull(anim, "Animation pointer cannot be null."); assertNotNull(keyframes, "Keyframes pointer cannot be null."); - assertTrue(keyframeCount > 0, "Keyframe count must be more than 0."); + assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null."); + assertTrue(layerCount > 0, "Layer count must be greater than zero."); + memoryZero(anim, sizeof(animation_t)); anim->keyframes = keyframes; - anim->keyframeCount = keyframeCount; + anim->keyframeCounts = keyframeCounts; + anim->layerCount = layerCount; + + // Determine duration + float_t duration = 0.0f; + for(uint16_t layer = 0; layer < layerCount; layer++) { + uint16_t keyframeCount = keyframeCounts[layer]; + assertTrue(keyframeCount > 0, "Keyframe count invalid."); + keyframe_t *layerKeyframes = keyframes + layer * keyframeCount; + + #ifdef DUSK_ASSERTIONS + // Check that the keyframes are sorted by time. + for(uint16_t i = 1; i < keyframeCount; i++) { + assertTrue( + layerKeyframes[i].time >= layerKeyframes[i - 1].time, + "Keyframes must be sorted by time." + ); + } + #endif + + keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1; + duration = mathMax(duration, lastKeyframe->time); + } + assertTrue(duration > 0, "Animation duration must be greater than 0."); + anim->duration = duration; } -float_t animationGetValue(animation_t *anim, const float_t time) { +float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) { assertNotNull(anim, "Animation pointer cannot be null."); - assertNotNull(anim->keyframes, "Keyframes pointer cannot be null."); - assertTrue(anim->keyframeCount > 0, "Keyframe count invalid."); - assertTrue(time >= 0, "Time must be non-negative."); - - keyframe_t *start; - keyframe_t *end; - keyframe_t *last = anim->keyframes + anim->keyframeCount - 1; - keyframe_t *current = anim->keyframes; - start = current; + assertTrue(layer < anim->layerCount, "Layer index out of bounds."); - do { - if(current->time > time) { - end = current; - break; + uint16_t keyframeCount = anim->keyframeCounts[layer]; + keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount; + return keyframeGetValue(layerKeyframes, keyframeCount, anim->time); +} + +void animationUpdate( + animation_t *anim, + const float_t deltaTime +) { + assertNotNull(anim, "Animation pointer cannot be null."); + assertTrue(deltaTime >= 0, "Delta time must be non-negative."); + + bool_t justCompleted = false; + + if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) { + bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0; + bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0; + assertFalse( + loop && pingpong, + "Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG." + ); + bool_t backward = pingpong + ? (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0 + : (anim->flags & ANIMATION_FLAG_REVERSE) != 0; + + // Resolve boundary crossings one at a time, so a single large deltaTime + // can correctly loop/pingpong across multiple boundaries in one call. + float_t remaining = deltaTime; + while(remaining > 0.0f) { + float_t toBoundary = ( + backward ? anim->time : (anim->duration - anim->time) + ); + if(remaining < toBoundary) { + anim->time += backward ? -remaining : remaining; + break; + } + + remaining -= toBoundary; + anim->time = backward ? 0.0f : anim->duration; + + bool_t stopHere = backward + ? (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0 + : (anim->flags & ANIMATION_FLAG_STOP_END) != 0; + + if(stopHere) { + justCompleted = true; + break; + } else if(pingpong) { + backward = !backward; + if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD; + else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD; + } else if(loop) { + anim->time = backward ? anim->duration : 0.0f; + if(anim->onLoop) anim->onLoop(anim->user); + } else { + justCompleted = true; + break; + } } - start = current; - current++; - if(current > last) { - end = start; - break; - } - } while(true); + if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED; + } - float_t t = (time - start->time) / (end->time - start->time); - return mathLerp(start->value, end->value, easingApply(start->easing, t)); + // Call onUpdate for each layer. + for(uint16_t layer = 0; layer < anim->layerCount; layer++) { + float_t value = animationGetLayerValue(anim, layer); + if(anim->onUpdate) anim->onUpdate(layer, value, anim->user); + } + + if(justCompleted && anim->onComplete) anim->onComplete(anim->user); } \ No newline at end of file diff --git a/src/dusk/animation/animation.h b/src/dusk/animation/animation.h index 5a76bdaf..f8cea8b8 100644 --- a/src/dusk/animation/animation.h +++ b/src/dusk/animation/animation.h @@ -6,29 +6,89 @@ #pragma once #include "keyframe.h" +#define ANIMATION_FLAG_LOOP (1 << 0) +#define ANIMATION_FLAG_REVERSE (1 << 1) +#define ANIMATION_FLAG_PINGPONG (1 << 2) +#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3) +#define ANIMATION_FLAG_STOP_END (1 << 4) + +// Internal - tracks which direction a pingponging animation is currently +// travelling. Do not set this manually, it is managed by animationUpdate(). +#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7) + +// Internal - set once the animation has stopped advancing (see +// animationUpdate()). Do not set this manually. There is currently no way to +// restart a completed animation short of clearing this bit and resetting +// anim->time by hand. +#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6) + typedef struct { keyframe_t *keyframes; - uint16_t keyframeCount; + uint16_t *keyframeCounts; + uint16_t layerCount; + float_t time; + float_t duration; + uint8_t flags; + + void *user; + void (*onUpdate)(const uint16_t layer, const float_t value, void *user); + void (*onComplete)(void *user); + void (*onLoop)(void *user); } animation_t; /** - * Initializes an animation. - * - * @param anim The animation to initialize. - * @param keyframes The keyframes to use for the animation. - * @param keyframeCount The number of keyframes in the animation. + * Initializes an animation with the given keyframes and layer count. + * + * @param anim Pointer to the animation to initialize. + * @param keyframes Pointer to the array of keyframes for each layer. + * @param keyframeCount Number of keyframes in each layer. + * @param layerCount Number of layers in the animation. */ void animationInit( animation_t *anim, keyframe_t *keyframes, - uint16_t keyframeCount + uint16_t *keyframeCounts, + const uint16_t layerCount ); /** - * Gets the value of the animation at a given time. + * Sets the current time of the animation, clamping it to the valid range. + * This will call the onUpdate callback but none of the other callbacks. * - * @param anim The animation to get the value from. - * @param time The time at which to get the value, in seconds. - * @return The value of the animation at the given time. + * @param anim Pointer to the animation to set the time for. + * @param time The new time to set for the animation. */ -float_t animationGetValue(animation_t *anim, const float_t time); \ No newline at end of file +void animationSetTime(animation_t *anim, const float_t time); + +/** + * Gets the current value of a specific layer in the animation based on the + * current animation time. + */ +float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer); + +/** + * Updates the animation state based on the elapsed time. Advances anim->time + * by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then + * resolves whatever happens when it reaches the 0 or duration boundary: + * + * - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing + * in the opposite direction, forever, unless stopped (see below). + * - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps + * playing in the same direction, forever, unless stopped (see below). + * - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the + * animation reaches that specific boundary, it clamps there and stops + * (firing onComplete) instead of looping/pingponging past it. + * - If none of the above apply at a boundary, the animation clamps there and + * stops, firing onComplete. + * + * onUpdate is called for every layer on every call. onLoop is called each + * time a loop wraps around. onComplete is called at most once, the moment + * the animation stops advancing. + * + * @param anim Pointer to the animation to update. + * @param deltaTime Time elapsed since the last update (in seconds). + */ +void animationUpdate( + animation_t *anim, + const float_t deltaTime +); \ No newline at end of file diff --git a/src/dusk/animation/keyframe.c b/src/dusk/animation/keyframe.c index c1b77d1a..a50338ef 100644 --- a/src/dusk/animation/keyframe.c +++ b/src/dusk/animation/keyframe.c @@ -27,27 +27,18 @@ float_t keyframeGetValue( } #endif - keyframe_t *start; - keyframe_t *end; keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1); if(time >= last->time) return last->value; + // Since time < last->time (checked above), current is guaranteed to stop + // at or before reaching last, so no separate end-of-array check is needed. keyframe_t *current = (keyframe_t *)keyframes; - start = current; - - do { - if(current->time > time) { - end = current; - break; - } + keyframe_t *start = current; + while(current->time <= time) { start = current; current++; - - if(current > last) { - end = start; - break; - } - } while(true); + } + keyframe_t *end = current; float_t t = (time - start->time) / (end->time - start->time); return mathLerp(start->value, end->value, easingApply(start->easing, t)); diff --git a/test/animation/CMakeLists.txt b/test/animation/CMakeLists.txt index 5f580139..87e9eed1 100644 --- a/test/animation/CMakeLists.txt +++ b/test/animation/CMakeLists.txt @@ -7,3 +7,4 @@ include(dusktest) # Tests dusktest(test_keyframe.c) +dusktest(test_animation.c) diff --git a/test/animation/test_animation.c b/test/animation/test_animation.c new file mode 100644 index 00000000..898cf6d4 --- /dev/null +++ b/test/animation/test_animation.c @@ -0,0 +1,425 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "animation/animation.h" +#include "util/memory.h" + +typedef struct { + uint16_t updateCount; + uint16_t loopCount; + uint16_t completeCount; + float_t lastValues[8]; +} animationtestcallbacks_t; + +static void testOnUpdate( + const uint16_t layer, const float_t value, void *user +) { + animationtestcallbacks_t *cb = (animationtestcallbacks_t *)user; + cb->updateCount++; + cb->lastValues[layer] = value; +} + +static void testOnLoop(void *user) { + ((animationtestcallbacks_t *)user)->loopCount++; +} + +static void testOnComplete(void *user) { + ((animationtestcallbacks_t *)user)->completeCount++; +} + +static void animationTestInit( + animation_t *anim, + animationtestcallbacks_t *cb, + keyframe_t *keyframes, + uint16_t *keyframeCounts, + const uint16_t layerCount +) { + memoryZero(cb, sizeof(animationtestcallbacks_t)); + animationInit(anim, keyframes, keyframeCounts, layerCount); + anim->user = cb; + anim->onUpdate = testOnUpdate; + anim->onLoop = testOnLoop; + anim->onComplete = testOnComplete; +} + +static void test_animationInitSingleLayerDuration(void **state) { + keyframe_t keyframes[3] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 3 }; + animation_t anim; + + animationInit(&anim, keyframes, keyframeCounts, 1); + + assert_float_equal(anim.duration, 2.0f, 0.0001f); + assert_int_equal(anim.layerCount, 1); + assert_float_equal(anim.time, 0.0f, 0.0001f); + assert_int_equal(anim.flags, 0); +} + +static void test_animationInitMultiLayerDurationIsMax(void **state) { + // Both layers must share the same keyframe count - animationInit/ + // animationGetLayerValue flatten the array as layer * keyframeCounts[layer], + // which only produces the correct offset when every layer's count matches. + keyframe_t keyframes[4] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, + { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, + { .time = 3.0f, .value = 300.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[2] = { 2, 2 }; + animation_t anim; + + animationInit(&anim, keyframes, keyframeCounts, 2); + + // Layer 1's last keyframe (time=3) is later than layer 0's (time=1). + assert_float_equal(anim.duration, 3.0f, 0.0001f); +} + +static void test_animationInitNullAsserts(void **state) { + keyframe_t keyframes[1] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR } }; + uint16_t keyframeCounts[1] = { 1 }; + animation_t anim; + + expect_assert_failure(animationInit(NULL, keyframes, keyframeCounts, 1)); + expect_assert_failure(animationInit(&anim, NULL, keyframeCounts, 1)); + expect_assert_failure(animationInit(&anim, keyframes, NULL, 1)); +} + +static void test_animationInitZeroLayerCountAsserts(void **state) { + keyframe_t keyframes[1] = { { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR } }; + uint16_t keyframeCounts[1] = { 1 }; + animation_t anim; + + expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 0)); +} + +static void test_animationInitUnsortedKeyframesAsserts(void **state) { + keyframe_t keyframes[2] = { + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + + expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1)); +} + +static void test_animationGetLayerValueSamplesEachLayerIndependently(void **state) { + keyframe_t keyframes[4] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[2] = { 2, 2 }; + animation_t anim; + + animationInit(&anim, keyframes, keyframeCounts, 2); + anim.time = 1.0f; + + assert_float_equal(animationGetLayerValue(&anim, 0), 10.0f, 0.0001f); + assert_float_equal(animationGetLayerValue(&anim, 1), 50.0f, 0.0001f); +} + +static void test_animationGetLayerValueOutOfBoundsAsserts(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + + animationInit(&anim, keyframes, keyframeCounts, 1); + expect_assert_failure(animationGetLayerValue(&anim, 1)); +} + +static void test_animationUpdateAdvancesTimeAndCallsOnUpdate(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + animationUpdate(&anim, 0.5f); + + assert_float_equal(anim.time, 0.5f, 0.0001f); + assert_int_equal(cb.updateCount, 1); + assert_float_equal(cb.lastValues[0], 5.0f, 0.0001f); + assert_int_equal(cb.completeCount, 0); +} + +static void test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + + // Overshoot the duration in a single update. + animationUpdate(&anim, 5.0f); + assert_float_equal(anim.time, 2.0f, 0.0001f); + assert_float_equal(cb.lastValues[0], 20.0f, 0.0001f); + assert_int_equal(cb.completeCount, 1); + + // Further updates must not clamp past the end or refire onComplete. + animationUpdate(&anim, 1.0f); + assert_float_equal(anim.time, 2.0f, 0.0001f); + assert_int_equal(cb.completeCount, 1); +} + +static void test_animationUpdateExactLandingOnEndFiresOnComplete(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + + // deltaTime exactly matches the remaining distance to the boundary. + animationUpdate(&anim, 2.0f); + assert_float_equal(anim.time, 2.0f, 0.0001f); + assert_int_equal(cb.completeCount, 1); +} + +static void test_animationUpdateLoopWrapsAndFiresOnLoop(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_LOOP; + + // 2.5 durations worth of time: wraps twice, lands at 1.0 into the third. + animationUpdate(&anim, 5.0f); + + assert_float_equal(anim.time, 1.0f, 0.0001f); + assert_int_equal(cb.loopCount, 2); + assert_int_equal(cb.completeCount, 0); +} + +static void test_animationUpdateLoopStopEndCompletesInsteadOfWrapping(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_STOP_END; + + animationUpdate(&anim, 5.0f); + + assert_float_equal(anim.time, 2.0f, 0.0001f); + assert_int_equal(cb.loopCount, 0); + assert_int_equal(cb.completeCount, 1); +} + +static void test_animationUpdateReversePlaysBackward(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_REVERSE; + anim.time = anim.duration; + + animationUpdate(&anim, 0.5f); + + assert_float_equal(anim.time, 1.5f, 0.0001f); + assert_float_equal(cb.lastValues[0], 15.0f, 0.0001f); +} + +static void test_animationUpdateReverseStopsAtBeginning(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_REVERSE; + anim.time = anim.duration; + + animationUpdate(&anim, 10.0f); + + assert_float_equal(anim.time, 0.0f, 0.0001f); + assert_float_equal(cb.lastValues[0], 0.0f, 0.0001f); + assert_int_equal(cb.completeCount, 1); +} + +static void test_animationUpdatePingpongBouncesForeverWithoutStopFlags(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_PINGPONG; + + // 0 -> 2 (2s consumed), reflect, backward 1s more -> time=1, going backward. + animationUpdate(&anim, 3.0f); + assert_float_equal(anim.time, 1.0f, 0.0001f); + assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0); + assert_int_equal(cb.completeCount, 0); + + // Continue backward to 0 (1s), reflect, forward 1s more -> time=1, forward. + animationUpdate(&anim, 2.0f); + assert_float_equal(anim.time, 1.0f, 0.0001f); + assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) == 0); + assert_int_equal(cb.completeCount, 0); +} + +static void test_animationUpdatePingpongStopEndCompletesAtEnd(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_END; + + animationUpdate(&anim, 10.0f); + + assert_float_equal(anim.time, 2.0f, 0.0001f); + assert_int_equal(cb.completeCount, 1); +} + +static void test_animationUpdatePingpongStopBeginningCompletesAtStart(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_BEGINNING; + anim.time = anim.duration; + anim.flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD; + + animationUpdate(&anim, 10.0f); + + assert_float_equal(anim.time, 0.0f, 0.0001f); + assert_int_equal(cb.completeCount, 1); +} + +static void test_animationUpdateMultiLayerCallsOnUpdatePerLayer(void **state) { + keyframe_t keyframes[4] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, + { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, + { .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[2] = { 2, 2 }; + animation_t anim; + animationtestcallbacks_t cb; + + animationTestInit(&anim, &cb, keyframes, keyframeCounts, 2); + animationUpdate(&anim, 1.0f); + + assert_int_equal(cb.updateCount, 2); + assert_float_equal(cb.lastValues[0], 10.0f, 0.0001f); + assert_float_equal(cb.lastValues[1], 50.0f, 0.0001f); +} + +static void test_animationUpdateNullAsserts(void **state) { + expect_assert_failure(animationUpdate(NULL, 1.0f)); +} + +static void test_animationUpdateNegativeDeltaTimeAsserts(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + + animationInit(&anim, keyframes, keyframeCounts, 1); + expect_assert_failure(animationUpdate(&anim, -1.0f)); +} + +static void test_animationInitZeroDurationAsserts(void **state) { + keyframe_t keyframes[1] = { { .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR } }; + uint16_t keyframeCounts[1] = { 1 }; + animation_t anim; + + expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1)); +} + +static void test_animationUpdateLoopAndPingpongTogetherAsserts(void **state) { + keyframe_t keyframes[2] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, + { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, + }; + uint16_t keyframeCounts[1] = { 2 }; + animation_t anim; + + animationInit(&anim, keyframes, keyframeCounts, 1); + anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_PINGPONG; + expect_assert_failure(animationUpdate(&anim, 0.1f)); +} + +int main(int argc, char **argv) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_animationInitSingleLayerDuration), + cmocka_unit_test(test_animationInitMultiLayerDurationIsMax), + cmocka_unit_test(test_animationInitNullAsserts), + cmocka_unit_test(test_animationInitZeroLayerCountAsserts), + cmocka_unit_test(test_animationInitUnsortedKeyframesAsserts), + cmocka_unit_test(test_animationGetLayerValueSamplesEachLayerIndependently), + cmocka_unit_test(test_animationGetLayerValueOutOfBoundsAsserts), + cmocka_unit_test(test_animationUpdateAdvancesTimeAndCallsOnUpdate), + cmocka_unit_test(test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly), + cmocka_unit_test(test_animationUpdateExactLandingOnEndFiresOnComplete), + cmocka_unit_test(test_animationUpdateLoopWrapsAndFiresOnLoop), + cmocka_unit_test(test_animationUpdateLoopStopEndCompletesInsteadOfWrapping), + cmocka_unit_test(test_animationUpdateReversePlaysBackward), + cmocka_unit_test(test_animationUpdateReverseStopsAtBeginning), + cmocka_unit_test(test_animationUpdatePingpongBouncesForeverWithoutStopFlags), + cmocka_unit_test(test_animationUpdatePingpongStopEndCompletesAtEnd), + cmocka_unit_test(test_animationUpdatePingpongStopBeginningCompletesAtStart), + cmocka_unit_test(test_animationUpdateMultiLayerCallsOnUpdatePerLayer), + cmocka_unit_test(test_animationUpdateNullAsserts), + cmocka_unit_test(test_animationUpdateNegativeDeltaTimeAsserts), + cmocka_unit_test(test_animationInitZeroDurationAsserts), + cmocka_unit_test(test_animationUpdateLoopAndPingpongTogetherAsserts), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +}