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(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t *keyframes,
|
||||||
uint16_t keyframeCount
|
uint16_t *keyframeCounts,
|
||||||
|
const uint16_t layerCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertNotNull(keyframes, "Keyframes 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->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, "Animation pointer cannot be null.");
|
||||||
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
|
||||||
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;
|
|
||||||
|
|
||||||
do {
|
uint16_t keyframeCount = anim->keyframeCounts[layer];
|
||||||
if(current->time > time) {
|
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
|
||||||
end = current;
|
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
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) {
|
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
|
||||||
end = start;
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while(true);
|
|
||||||
|
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
// Call onUpdate for each layer.
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
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
|
#pragma once
|
||||||
#include "keyframe.h"
|
#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 {
|
typedef struct {
|
||||||
keyframe_t *keyframes;
|
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;
|
} animation_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an animation.
|
* Initializes an animation with the given keyframes and layer count.
|
||||||
*
|
*
|
||||||
* @param anim The animation to initialize.
|
* @param anim Pointer to the animation to initialize.
|
||||||
* @param keyframes The keyframes to use for the animation.
|
* @param keyframes Pointer to the array of keyframes for each layer.
|
||||||
* @param keyframeCount The number of keyframes in the animation.
|
* @param keyframeCount Number of keyframes in each layer.
|
||||||
|
* @param layerCount Number of layers in the animation.
|
||||||
*/
|
*/
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
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 anim Pointer to the animation to set the time for.
|
||||||
* @param time The time at which to get the value, in seconds.
|
* @param time The new time to set for the animation.
|
||||||
* @return The value of the animation at the given time.
|
|
||||||
*/
|
*/
|
||||||
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
|
#endif
|
||||||
|
|
||||||
keyframe_t *start;
|
|
||||||
keyframe_t *end;
|
|
||||||
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
||||||
if(time >= last->time) return last->value;
|
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;
|
keyframe_t *current = (keyframe_t *)keyframes;
|
||||||
start = current;
|
keyframe_t *start = current;
|
||||||
|
while(current->time <= time) {
|
||||||
do {
|
|
||||||
if(current->time > time) {
|
|
||||||
end = current;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
start = current;
|
start = current;
|
||||||
current++;
|
current++;
|
||||||
|
}
|
||||||
if(current > last) {
|
keyframe_t *end = current;
|
||||||
end = start;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while(true);
|
|
||||||
|
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
float_t t = (time - start->time) / (end->time - start->time);
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ include(dusktest)
|
|||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
dusktest(test_keyframe.c)
|
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