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:
2026-08-06 15:29:14 -05:00
parent 1bd73d69fe
commit 36fb359aa2
5 changed files with 603 additions and 53 deletions
+99 -26
View File
@@ -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);
}
+72 -12
View File
@@ -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
);
+6 -15
View File
@@ -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));
+1
View File
@@ -7,3 +7,4 @@ include(dusktest)
# Tests
dusktest(test_keyframe.c)
dusktest(test_animation.c)
+425
View File
@@ -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);
}