Some animation work

This commit is contained in:
2026-07-21 09:29:14 -05:00
parent 0826908068
commit 1436fda858
12 changed files with 806 additions and 11 deletions
+3 -1
View File
@@ -16,7 +16,9 @@ void animationInit(
) {
assertNotNull(anim, "Animation pointer cannot be null.");
keyframeSetInit(&anim->keyframes, tracks, trackCounts, trackCount);
keyframeSetInit(
&anim->keyframes, tracks, trackCounts, NULL, NULL, trackCount, NULL
);
anim->time = 0.0f;
anim->speed = 1.0f;
anim->loop = false;
+17 -1
View File
@@ -10,7 +10,10 @@ void keyframeSetInit(
keyframeset_t *set,
keyframe_t **tracks,
uint16_t *trackCounts,
uint16_t trackCount
keyframesetcallback_t *callbacks,
void **users,
uint16_t trackCount,
void *user
) {
assertNotNull(set, "Keyframe set pointer cannot be null.");
assertNotNull(tracks, "Tracks pointer cannot be null.");
@@ -20,6 +23,9 @@ void keyframeSetInit(
set->tracks = tracks;
set->trackCounts = trackCounts;
set->trackCount = trackCount;
set->callbacks = callbacks;
set->users = users;
set->user = user;
}
float_t keyframeSetGetValue(
@@ -58,3 +64,13 @@ float_t keyframeSetGetDuration(keyframeset_t *set) {
}
return duration;
}
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex) {
assertNotNull(set, "Keyframe set pointer cannot be null.");
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
if(!set->callbacks || !set->callbacks[trackIndex]) return;
set->callbacks[trackIndex](
set, trackIndex, set->users ? set->users[trackIndex] : NULL
);
}
+47 -5
View File
@@ -6,35 +6,68 @@
#pragma once
#include "keyframe.h"
typedef struct keyframeset_t keyframeset_t;
/**
* Callback associated with one track of a keyframe set (see
* keyframeSetFireCallback()).
*
* @param set The keyframe set the track belongs to.
* @param trackIndex The track this callback is registered for.
* @param user The per-track user pointer passed to keyframeSetInit().
*/
typedef void (*keyframesetcallback_t)(
keyframeset_t *set,
const uint16_t trackIndex,
void *user
);
/**
* A group of N parallel keyframe tracks (e.g. one per animated channel --
* position.x/y/z, bone rotations, etc.) sharing a single timeline. Each
* track is independent: its own keyframe array and count, evaluated at
* whatever time is asked for.
*/
typedef struct {
typedef struct keyframeset_t {
/** Caller-owned array of trackCount keyframe_t arrays. */
keyframe_t **tracks;
/** Caller-owned array of trackCount counts, one per entry in tracks. */
uint16_t *trackCounts;
uint16_t trackCount;
/** Caller-owned array of trackCount callbacks, one per track. Entries
* (or the whole array) may be NULL for tracks with no callback. */
keyframesetcallback_t *callbacks;
/** Caller-owned array of trackCount user pointers, one per track,
* passed to that track's callbacks[] entry. May be NULL. */
void **users;
/** Extra user data for the set as a whole, not tied to any one track. */
void *user;
} keyframeset_t;
/**
* Initializes a keyframe set. Neither tracks, trackCounts, nor the
* keyframe_t arrays they point to are copied -- they must outlive this
* keyframeset_t.
* Initializes a keyframe set. None of the passed-in arrays (or the
* keyframe_t arrays tracks points to) are copied -- they must outlive
* this keyframeset_t.
*
* @param set The keyframe set to initialize.
* @param tracks Array of trackCount keyframe_t arrays.
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
* @param callbacks Array of trackCount callbacks, one per track, or NULL
* if no track has one.
* @param users Array of trackCount user pointers, matching callbacks, or
* NULL.
* @param trackCount The number of tracks.
* @param user Extra user data for the set as a whole; may be NULL.
*/
void keyframeSetInit(
keyframeset_t *set,
keyframe_t **tracks,
uint16_t *trackCounts,
uint16_t trackCount
keyframesetcallback_t *callbacks,
void **users,
uint16_t trackCount,
void *user
);
/**
@@ -72,3 +105,12 @@ void keyframeSetGetValues(
* @return The set's duration, in seconds.
*/
float_t keyframeSetGetDuration(keyframeset_t *set);
/**
* Invokes a track's callback, if it (and its callbacks array) is set.
* No-op otherwise.
*
* @param set The keyframe set the track belongs to.
* @param trackIndex The track whose callback to invoke.
*/
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex);
+2 -1
View File
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(dmf)
add_subdirectory(dmf)
add_subdirectory(animation)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetanimationloader.c
)
@@ -0,0 +1,234 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetanimationloader.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetAnimationLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(
loading->loading.animation.state != ASSET_ANIMATION_LOADING_STATE_READ_FILE
) {
errorOk();
}
assertNull(loading->loading.animation.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.animation.file;
assetLoaderErrorChain(
loading, assetFileInit(file, loading->entry->name, NULL, NULL)
);
if(file->size > ASSET_ANIMATION_FILE_SIZE_MAX) {
assetLoaderErrorThrow(
loading, "Animation JSON exceeds maximum allowed size"
);
}
uint8_t *buffer;
size_t size;
assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
assetLoaderErrorChain(loading, assetFileDispose(file));
loading->loading.animation.buffer = buffer;
loading->loading.animation.size = size;
loading->loading.animation.state = ASSET_ANIMATION_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetAnimationLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.animation.state) {
case ASSET_ANIMATION_LOADING_STATE_INITIAL:
loading->loading.animation.state =
ASSET_ANIMATION_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_ANIMATION_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *buffer = loading->loading.animation.buffer;
assertNotNull(buffer, "Animation buffer should have been loaded by now.");
yyjson_doc *doc = yyjson_read(
(char *)buffer,
loading->loading.animation.size,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
loading->loading.animation.buffer = NULL;
if(!doc) assetLoaderErrorThrow(loading, "Failed to parse animation JSON");
yyjson_val *root = yyjson_doc_get_root(doc);
yyjson_val *channelsVal = yyjson_obj_get(root, "channels");
if(!channelsVal || !yyjson_is_arr(channelsVal)) {
yyjson_doc_free(doc);
assetLoaderErrorThrow(loading, "Animation JSON missing 'channels' array");
}
uint16_t channelCount = (uint16_t)yyjson_arr_size(channelsVal);
if(channelCount == 0) {
yyjson_doc_free(doc);
assetLoaderErrorThrow(loading, "Animation must have at least one channel");
}
keyframe_t **tracks = memoryAllocate(channelCount * sizeof(keyframe_t *));
uint16_t *trackCounts = memoryAllocate(channelCount * sizeof(uint16_t));
size_t idx, max;
yyjson_val *channelJson;
uint16_t parsed = 0;
yyjson_arr_foreach(channelsVal, idx, max, channelJson) {
keyframe_t *keyframes;
uint16_t count;
errorret_t ret =
assetAnimationParseChannel(channelJson, &keyframes, &count);
if(errorIsNotOk(ret)) {
assetAnimationFreeChannels(tracks, parsed);
memoryFree(trackCounts);
yyjson_doc_free(doc);
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorChain(ret);
}
tracks[idx] = keyframes;
trackCounts[idx] = count;
parsed++;
}
animationInit(
&loading->entry->data.animation, tracks, trackCounts, channelCount
);
yyjson_val *loopVal = yyjson_obj_get(root, "loop");
if(loopVal) loading->entry->data.animation.loop = yyjson_get_bool(loopVal);
yyjson_val *speedVal = yyjson_obj_get(root, "speed");
if(speedVal) {
loading->entry->data.animation.speed = (float_t)yyjson_get_num(speedVal);
}
yyjson_doc_free(doc);
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetAnimationDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
// A load that failed before animationInit() ran (e.g. a parse error)
// never populated these -- nothing to free in that case.
keyframeset_t *set = &entry->data.animation.keyframes;
if(set->tracks) {
assetAnimationFreeChannels(set->tracks, set->trackCount);
memoryFree(set->trackCounts);
set->tracks = NULL;
set->trackCounts = NULL;
set->trackCount = 0;
}
errorOk();
}
errorret_t assetAnimationParseEasing(
const char_t *name,
easingtype_t *outEasing
) {
if(stringEquals(name, "LINEAR")) *outEasing = EASING_LINEAR;
else if(stringEquals(name, "IN_SINE")) *outEasing = EASING_IN_SINE;
else if(stringEquals(name, "OUT_SINE")) *outEasing = EASING_OUT_SINE;
else if(stringEquals(name, "IN_OUT_SINE")) *outEasing = EASING_IN_OUT_SINE;
else if(stringEquals(name, "IN_QUAD")) *outEasing = EASING_IN_QUAD;
else if(stringEquals(name, "OUT_QUAD")) *outEasing = EASING_OUT_QUAD;
else if(stringEquals(name, "IN_OUT_QUAD")) *outEasing = EASING_IN_OUT_QUAD;
else if(stringEquals(name, "IN_CUBIC")) *outEasing = EASING_IN_CUBIC;
else if(stringEquals(name, "OUT_CUBIC")) *outEasing = EASING_OUT_CUBIC;
else if(stringEquals(name, "IN_OUT_CUBIC")) *outEasing = EASING_IN_OUT_CUBIC;
else if(stringEquals(name, "IN_QUART")) *outEasing = EASING_IN_QUART;
else if(stringEquals(name, "OUT_QUART")) *outEasing = EASING_OUT_QUART;
else if(stringEquals(name, "IN_OUT_QUART")) *outEasing = EASING_IN_OUT_QUART;
else if(stringEquals(name, "IN_BACK")) *outEasing = EASING_IN_BACK;
else if(stringEquals(name, "OUT_BACK")) *outEasing = EASING_OUT_BACK;
else if(stringEquals(name, "IN_OUT_BACK")) *outEasing = EASING_IN_OUT_BACK;
else errorThrow("Unknown easing type '%s'", name);
errorOk();
}
errorret_t assetAnimationParseChannel(
yyjson_val *channelJson,
keyframe_t **outKeyframes,
uint16_t *outCount
) {
if(!yyjson_is_arr(channelJson)) {
errorThrow("Animation channel must be an array");
}
size_t count = yyjson_arr_size(channelJson);
if(count == 0) {
errorThrow("Animation channel must have at least one keyframe");
}
keyframe_t *keyframes = memoryAllocate(count * sizeof(keyframe_t));
size_t idx, max;
yyjson_val *keyframeJson;
yyjson_arr_foreach(channelJson, idx, max, keyframeJson) {
yyjson_val *timeVal = yyjson_obj_get(keyframeJson, "time");
yyjson_val *valueVal = yyjson_obj_get(keyframeJson, "value");
if(!timeVal || !valueVal) {
memoryFree(keyframes);
errorThrow("Animation keyframe missing 'time' or 'value'");
}
keyframes[idx].time = (float_t)yyjson_get_num(timeVal);
keyframes[idx].value = (float_t)yyjson_get_num(valueVal);
yyjson_val *easingVal = yyjson_obj_get(keyframeJson, "easing");
if(!easingVal) {
keyframes[idx].easing = EASING_LINEAR;
continue;
}
errorret_t ret = assetAnimationParseEasing(
yyjson_get_str(easingVal), &keyframes[idx].easing
);
if(errorIsNotOk(ret)) {
memoryFree(keyframes);
errorChain(ret);
}
}
*outKeyframes = keyframes;
*outCount = (uint16_t)count;
errorOk();
}
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count) {
for(uint16_t i = 0; i < count; i++) memoryFree(tracks[i]);
memoryFree(tracks);
}
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "asset/assetfile.h"
#include "animation/animation.h"
#include "yyjson.h"
#define ASSET_ANIMATION_FILE_SIZE_MAX 1024*256
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
/**
* JSON animation file format:
* {
* "loop": false,
* "speed": 1.0,
* "channels": [
* [
* { "time": 0.0, "value": 0.0, "easing": "LINEAR" },
* { "time": 1.0, "value": 10.0 }
* ]
* ]
* }
*
* "loop" and "speed" are optional, defaulting to false and 1.0 (see
* animationInit()). "channels" is required and must have at least one
* entry; each entry is itself a non-empty array of keyframes, ascending
* by "time", sharing one timeline with every other channel (see
* keyframeset_t). Each keyframe requires "time" and "value"; "easing" is
* optional and defaults to "LINEAR" (see easingtype_t for the full set of
* names, e.g. "IN_QUAD", "OUT_BACK", "IN_OUT_CUBIC").
*/
typedef enum {
ASSET_ANIMATION_LOADING_STATE_INITIAL,
ASSET_ANIMATION_LOADING_STATE_READ_FILE,
ASSET_ANIMATION_LOADING_STATE_PARSE,
ASSET_ANIMATION_LOADING_STATE_DONE
} assetanimationloadingstate_t;
typedef struct {
assetfile_t file;
assetanimationloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetanimationloaderloading_t;
typedef animation_t assetanimationoutput_t;
errorret_t assetAnimationLoaderAsync(assetloading_t *loading);
errorret_t assetAnimationLoaderSync(assetloading_t *loading);
errorret_t assetAnimationDispose(assetentry_t *entry);
/**
* Internal. Maps a JSON easing name (e.g. "IN_OUT_QUAD") to its
* easingtype_t.
*
* @param name The easing name to parse.
* @param outEasing Destination for the parsed easing type.
* @return Error state; fails if name doesn't match any easingtype_t.
*/
errorret_t assetAnimationParseEasing(
const char_t *name,
easingtype_t *outEasing
);
/**
* Internal. Parses one "channels" array entry into a heap-allocated
* keyframe_t array (memoryAllocate) -- freed later by
* assetAnimationFreeChannels() (on a parse failure) or
* assetAnimationDispose() (once loaded).
*
* @param channelJson The channel's JSON array of keyframe objects.
* @param outKeyframes Destination for the newly allocated keyframe array.
* @param outCount Destination for the number of keyframes parsed.
* @return Error state.
*/
errorret_t assetAnimationParseChannel(
yyjson_val *channelJson,
keyframe_t **outKeyframes,
uint16_t *outCount
);
/**
* Internal. Frees the first count entries of tracks (each a
* memoryAllocate'd keyframe_t array from assetAnimationParseChannel()),
* then frees tracks itself. Used both to unwind a partially-parsed
* channel list on failure and to free a fully-loaded animation's
* channels in assetAnimationDispose().
*
* @param tracks The channel array to free.
* @param count The number of entries in tracks to free.
*/
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count);
+6
View File
@@ -45,4 +45,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.loadAsync = assetJsonLoaderAsync,
.dispose = assetJsonDispose
},
[ASSET_LOADER_TYPE_ANIMATION] = {
.loadSync = assetAnimationLoaderSync,
.loadAsync = assetAnimationLoaderAsync,
.dispose = assetAnimationDispose
},
};
+4
View File
@@ -12,6 +12,7 @@
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/animation/assetanimationloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
@@ -22,6 +23,7 @@ typedef enum {
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_ANIMATION,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
@@ -33,6 +35,7 @@ typedef union {
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
assetanimationloaderloading_t animation;
} assetloaderloading_t;
typedef union {
@@ -42,6 +45,7 @@ typedef union {
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetanimationoutput_t animation;
} assetloaderoutput_t;
typedef union {