36fb359aa2
- animationUpdate now advances and resolves boundary crossings for ANIMATION_FLAG_LOOP, ANIMATION_FLAG_PINGPONG, ANIMATION_FLAG_REVERSE, and the STOP_BEGINNING/STOP_END flags, firing onLoop/onComplete appropriately; guards against LOOP+PINGPONG being set together and moves the duration-must-be-positive check into animationInit - keyframeGetValue clamps to the last keyframe's value instead of dividing by zero once time reaches it, and its keyframe walk drops a branch that's unreachable after that clamp - Adds test/animation/test_animation.c covering init, per-layer sampling, and the full animationUpdate flag matrix
426 lines
15 KiB
C
426 lines
15 KiB
C
/**
|
|
* 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);
|
|
}
|