Finish animation update loop with loop/pingpong/reverse/stop flags, clean up keyframe sampling, add coverage
- 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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
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
|
||||
);
|
||||
@@ -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));
|
||||
|
||||
@@ -7,3 +7,4 @@ include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_keyframe.c)
|
||||
dusktest(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);
|
||||
}
|
||||
Reference in New Issue
Block a user