Easing test

This commit is contained in:
2026-05-05 22:24:25 -05:00
parent 6da02b25fa
commit 3695b10e4b
7 changed files with 470 additions and 16 deletions
+17 -16
View File
@@ -1,31 +1,32 @@
var PHASE_LEFT = 0;
var PHASE_RIGHT = 1;
var SPEED = 3.0; var SPEED = 3.0;
var DURATION = 2.0; var DURATION = 2.0;
function MoveCubeCutscene(params) { function MoveCubeCutscene(params) {
Cutscene.call(this); Cutscene.call(this);
this.cube = params.cube; this.cube = params.cube;
this.phase = PHASE_LEFT;
this.timer = 0.0; var startX = this.cube.position.position.x;
this.animLeft = new Animation([
{ time: 0.0, value: startX, easing: Easing.outQuad },
{ time: DURATION, value: startX - SPEED * DURATION }
]);
this.animRight = new Animation([
{ time: 0.0, value: startX - SPEED * DURATION, easing: Easing.inOutQuad },
{ time: DURATION, value: startX }
]);
} }
MoveCubeCutscene.prototype = Object.create(Cutscene.prototype); MoveCubeCutscene.prototype = Object.create(Cutscene.prototype);
MoveCubeCutscene.prototype.constructor = MoveCubeCutscene; MoveCubeCutscene.prototype.constructor = MoveCubeCutscene;
MoveCubeCutscene.prototype.update = function() { MoveCubeCutscene.prototype.update = function() {
this.timer += TIME.delta; if(!this.animLeft.complete) {
this.cube.position.position.x = this.animLeft.update(TIME.delta);
if(this.phase === PHASE_LEFT) { } else {
this.cube.position.position.x -= SPEED * TIME.delta; this.cube.position.position.x = this.animRight.update(TIME.delta);
if(this.timer >= DURATION) { if(this.animRight.complete) {
this.phase = PHASE_RIGHT;
this.timer = 0.0;
}
} else if(this.phase === PHASE_RIGHT) {
this.cube.position.position.x += SPEED * TIME.delta;
if(this.timer >= DURATION) {
Cutscene.finish(); Cutscene.finish();
} }
} }
+1
View File
@@ -54,6 +54,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
) )
# Subdirs # Subdirs
add_subdirectory(animation)
add_subdirectory(assert) add_subdirectory(assert)
add_subdirectory(asset) add_subdirectory(asset)
add_subdirectory(cutscene) add_subdirectory(cutscene)
+10
View File
@@ -0,0 +1,10 @@
# 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
easing.c
animation.c
)
+75
View File
@@ -0,0 +1,75 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "animation.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <math.h>
void animationInit(animation_t *anim) {
memoryZero(anim, sizeof(animation_t));
}
void animationAddKeyframe(
animation_t *anim,
float_t time,
float_t value,
easingtype_t easing
) {
assertTrue(
anim->keyframeCount < ANIMATION_KEYFRAME_COUNT_MAX,
"Keyframe count exceeds ANIMATION_KEYFRAME_COUNT_MAX"
);
uint8_t i = anim->keyframeCount++;
anim->keyframes[i].time = time;
anim->keyframes[i].value = value;
anim->keyframes[i].easing = easing;
if(time > anim->duration) anim->duration = time;
}
float_t animationGetValue(const animation_t *anim) {
if(anim->keyframeCount == 0) return 0.0f;
uint8_t last = anim->keyframeCount - 1;
if(anim->keyframeCount == 1) return anim->keyframes[0].value;
if(anim->time <= anim->keyframes[0].time) return anim->keyframes[0].value;
if(anim->time >= anim->keyframes[last].time) {
return anim->keyframes[last].value;
}
for(uint8_t i = 0; i < last; i++) {
const keyframe_t *a = &anim->keyframes[i];
const keyframe_t *b = &anim->keyframes[i + 1];
if(anim->time < a->time || anim->time >= b->time) continue;
float_t t = (anim->time - a->time) / (b->time - a->time);
t = easingApply(a->easing, t);
return a->value + (b->value - a->value) * t;
}
return anim->keyframes[last].value;
}
float_t animationUpdate(animation_t *anim, float_t delta) {
if(anim->complete && !anim->loop) return animationGetValue(anim);
anim->time += delta;
if(anim->duration > 0.0f && anim->time >= anim->duration) {
if(anim->loop) {
anim->time = fmodf(anim->time, anim->duration);
} else {
anim->time = anim->duration;
anim->complete = true;
}
}
return animationGetValue(anim);
}
void animationReset(animation_t *anim) {
anim->time = 0.0f;
anim->complete = false;
}
@@ -0,0 +1,293 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "animation/animation.h"
#include "util/memory.h"
static scriptproto_t MODULE_ANIMATION_PROTO;
static inline animation_t *moduleAnimationGet(
const jerry_call_info_t *callInfo
) {
return (animation_t *)scriptProtoGetValue(
&MODULE_ANIMATION_PROTO, callInfo->this_value
);
}
// Extracts an easingtype_t from a JS value. Accepts either a plain number
// or an Easing.xxx function (which has a .type numeric property).
static easingtype_t moduleAnimationReadEasing(
const jerry_value_t val
) {
if(jerry_value_is_number(val)) {
return (easingtype_t)(uint32_t)jerry_value_as_number(val);
}
if(jerry_value_is_object(val) || jerry_value_is_function(val)) {
jerry_value_t key = jerry_string_sz("type");
jerry_value_t typeVal = jerry_object_get(val, key);
jerry_value_free(key);
easingtype_t result = EASING_LINEAR;
if(jerry_value_is_number(typeVal)) {
result = (easingtype_t)(uint32_t)jerry_value_as_number(typeVal);
}
jerry_value_free(typeVal);
return result;
}
return EASING_LINEAR;
}
// Fires onReach callbacks for any keyframes crossed between prevTime and
// newTime. Returns an exception value on error, or jerry_undefined().
static jerry_value_t moduleAnimationFireKeyframes(
const jerry_value_t thisVal,
float_t prevTime,
float_t newTime
) {
jerry_value_t kfKey = jerry_string_sz("_keyframes");
jerry_value_t kfArr = jerry_object_get(thisVal, kfKey);
jerry_value_free(kfKey);
if(!jerry_value_is_array(kfArr)) {
jerry_value_free(kfArr);
return jerry_undefined();
}
jerry_length_t len = jerry_array_length(kfArr);
for(jerry_length_t i = 0; i < len; i++) {
jerry_value_t kfObj = jerry_object_get_index(kfArr, i);
jerry_value_t tKey = jerry_string_sz("time");
jerry_value_t tVal = jerry_object_get(kfObj, tKey);
jerry_value_free(tKey);
float_t kfTime = (float_t)jerry_value_as_number(tVal);
jerry_value_free(tVal);
if(prevTime < kfTime && newTime >= kfTime) {
jerry_value_t cbKey = jerry_string_sz("onReach");
jerry_value_t cb = jerry_object_get(kfObj, cbKey);
jerry_value_free(cbKey);
if(jerry_value_is_function(cb)) {
jerry_value_t r = jerry_call(cb, thisVal, NULL, 0);
jerry_value_free(cb);
if(jerry_value_is_exception(r)) {
jerry_value_free(kfObj);
jerry_value_free(kfArr);
return r;
}
jerry_value_free(r);
} else {
jerry_value_free(cb);
}
}
jerry_value_free(kfObj);
}
jerry_value_free(kfArr);
return jerry_undefined();
}
// Fires onComplete once. Returns an exception on error or jerry_undefined().
static jerry_value_t moduleAnimationFireComplete(
const jerry_value_t thisVal
) {
jerry_value_t firedKey = jerry_string_sz("_completeFired");
jerry_value_t firedVal = jerry_object_get(thisVal, firedKey);
bool_t alreadyFired = jerry_value_is_true(firedVal);
jerry_value_free(firedVal);
if(alreadyFired) {
jerry_value_free(firedKey);
return jerry_undefined();
}
jerry_value_t trueVal = jerry_boolean(true);
jerry_object_set(thisVal, firedKey, trueVal);
jerry_value_free(firedKey);
jerry_value_free(trueVal);
jerry_value_t cbKey = jerry_string_sz("onComplete");
jerry_value_t cb = jerry_object_get(thisVal, cbKey);
jerry_value_free(cbKey);
if(jerry_value_is_function(cb)) {
jerry_value_t r = jerry_call(cb, thisVal, NULL, 0);
jerry_value_free(cb);
if(jerry_value_is_exception(r)) return r;
jerry_value_free(r);
} else {
jerry_value_free(cb);
}
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationConstructor) {
animation_t *anim = (animation_t *)memoryAllocate(sizeof(animation_t));
animationInit(anim);
if(argc > 0 && jerry_value_is_boolean(args[argc - 1])) {
anim->loop = jerry_value_is_true(args[argc - 1]);
}
if(argc > 0 && jerry_value_is_array(args[0])) {
jerry_length_t len = jerry_array_length(args[0]);
for(jerry_length_t i = 0; i < len; i++) {
jerry_value_t kf = jerry_object_get_index(args[0], i);
jerry_value_t tKey = jerry_string_sz("time");
jerry_value_t vKey = jerry_string_sz("value");
jerry_value_t eKey = jerry_string_sz("easing");
float_t t = (float_t)jerry_value_as_number(
jerry_object_get(kf, tKey)
);
float_t v = (float_t)jerry_value_as_number(
jerry_object_get(kf, vKey)
);
jerry_value_t eVal = jerry_object_get(kf, eKey);
easingtype_t e = moduleAnimationReadEasing(eVal);
jerry_value_free(tKey);
jerry_value_free(vKey);
jerry_value_free(eKey);
jerry_value_free(eVal);
jerry_value_free(kf);
animationAddKeyframe(anim, t, v, e);
}
// Store the JS keyframes array for onReach callback detection.
jerry_value_t kfKey = jerry_string_sz("_keyframes");
jerry_object_set(callInfo->this_value, kfKey, args[0]);
jerry_value_free(kfKey);
}
jerry_value_t firedKey = jerry_string_sz("_completeFired");
jerry_value_t falseVal = jerry_boolean(false);
jerry_object_set(callInfo->this_value, firedKey, falseVal);
jerry_value_free(firedKey);
jerry_value_free(falseVal);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_ANIMATION_PROTO.info, anim
);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationUpdate) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
if(argc < 1 || !jerry_value_is_number(args[0])) {
return moduleBaseThrow("update() expects a number delta");
}
float_t prevTime = anim->time;
float_t delta = (float_t)jerry_value_as_number(args[0]);
float_t value = animationUpdate(anim, delta);
jerry_value_t kfResult = moduleAnimationFireKeyframes(
callInfo->this_value, prevTime, anim->time
);
if(jerry_value_is_exception(kfResult)) return kfResult;
jerry_value_free(kfResult);
if(anim->complete) {
jerry_value_t cResult = moduleAnimationFireComplete(
callInfo->this_value
);
if(jerry_value_is_exception(cResult)) return cResult;
jerry_value_free(cResult);
}
return jerry_number((double)value);
}
moduleBaseFunction(moduleAnimationGetValue) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
return jerry_number((double)animationGetValue(anim));
}
moduleBaseFunction(moduleAnimationReset) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
animationReset(anim);
jerry_value_t key = jerry_string_sz("_completeFired");
jerry_value_t falseVal = jerry_boolean(false);
jerry_object_set(callInfo->this_value, key, falseVal);
jerry_value_free(key);
jerry_value_free(falseVal);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationGetComplete) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_boolean(anim->complete) : jerry_boolean(false);
}
moduleBaseFunction(moduleAnimationGetLoop) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_boolean(anim->loop) : jerry_boolean(false);
}
moduleBaseFunction(moduleAnimationSetLoop) {
animation_t *anim = moduleAnimationGet(callInfo);
if(!anim) return moduleBaseThrow("Invalid Animation instance");
if(argc < 1) return moduleBaseThrow("Expected boolean");
anim->loop = jerry_value_is_true(args[0]);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationGetTime) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_number((double)anim->time) : jerry_number(0.0);
}
moduleBaseFunction(moduleAnimationGetDuration) {
animation_t *anim = moduleAnimationGet(callInfo);
return anim ? jerry_number((double)anim->duration) : jerry_number(0.0);
}
static void moduleAnimation(void) {
scriptProtoInit(
&MODULE_ANIMATION_PROTO,
"Animation",
sizeof(animation_t),
moduleAnimationConstructor
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "update", moduleAnimationUpdate
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "getValue", moduleAnimationGetValue
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "reset", moduleAnimationReset
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "complete",
moduleAnimationGetComplete, NULL
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "loop",
moduleAnimationGetLoop, moduleAnimationSetLoop
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "time",
moduleAnimationGetTime, NULL
);
scriptProtoDefineProp(
&MODULE_ANIMATION_PROTO, "duration",
moduleAnimationGetDuration, NULL
);
}
@@ -0,0 +1,70 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "animation/easing.h"
static scriptproto_t MODULE_EASING_PROTO;
// Generate one handler per easing curve.
#define EASING_MODULE_TABLE \
X(Linear, "linear", EASING_LINEAR, easingLinear) \
X(InSine, "inSine", EASING_IN_SINE, easingInSine) \
X(OutSine, "outSine", EASING_OUT_SINE, easingOutSine) \
X(InOutSine, "inOutSine", EASING_IN_OUT_SINE, easingInOutSine) \
X(InQuad, "inQuad", EASING_IN_QUAD, easingInQuad) \
X(OutQuad, "outQuad", EASING_OUT_QUAD, easingOutQuad) \
X(InOutQuad, "inOutQuad", EASING_IN_OUT_QUAD, easingInOutQuad) \
X(InCubic, "inCubic", EASING_IN_CUBIC, easingInCubic) \
X(OutCubic, "outCubic", EASING_OUT_CUBIC, easingOutCubic) \
X(InOutCubic, "inOutCubic", EASING_IN_OUT_CUBIC, easingInOutCubic) \
X(InQuart, "inQuart", EASING_IN_QUART, easingInQuart) \
X(OutQuart, "outQuart", EASING_OUT_QUART, easingOutQuart) \
X(InOutQuart, "inOutQuart", EASING_IN_OUT_QUART, easingInOutQuart) \
X(InBack, "inBack", EASING_IN_BACK, easingInBack) \
X(OutBack, "outBack", EASING_OUT_BACK, easingOutBack) \
X(InOutBack, "inOutBack", EASING_IN_OUT_BACK, easingInOutBack)
#define X(CName, jsName, type, fn) \
moduleBaseFunction(moduleEasing##CName) { \
if(argc < 1 || !jerry_value_is_number(args[0])) { \
return moduleBaseThrow("Expected number t"); \
} \
float_t t = (float_t)jerry_value_as_number(args[0]); \
return jerry_number((double)fn(t)); \
}
EASING_MODULE_TABLE
#undef X
// Adds a callable easing function with a .type property to the Easing object.
static void moduleEasingRegister(
const char_t *jsName,
uint8_t type,
jerry_external_handler_t fn
) {
jerry_value_t fnVal = jerry_function_external(fn);
jerry_value_t typeKey = jerry_string_sz("type");
jerry_value_t typeVal = jerry_number((double)type);
jerry_object_set(fnVal, typeKey, typeVal);
jerry_value_free(typeKey);
jerry_value_free(typeVal);
jerry_value_t nameKey = jerry_string_sz(jsName);
jerry_object_set(MODULE_EASING_PROTO.prototype, nameKey, fnVal);
jerry_value_free(nameKey);
jerry_value_free(fnVal);
}
static void moduleEasing(void) {
scriptProtoInit(&MODULE_EASING_PROTO, "Easing", sizeof(uint8_t), NULL);
#define X(CName, jsName, type, fn) \
moduleEasingRegister(jsName, type, moduleEasing##CName);
EASING_MODULE_TABLE
#undef X
}
+4
View File
@@ -17,6 +17,8 @@
#include "script/module/display/modulescreen.h" #include "script/module/display/modulescreen.h"
#include "script/module/display/modulespritebatch.h" #include "script/module/display/modulespritebatch.h"
#include "script/module/display/moduletext.h" #include "script/module/display/moduletext.h"
#include "script/module/animation/moduleeasing.h"
#include "script/module/animation/moduleanimation.h"
#include "script/module/scene/modulescene.h" #include "script/module/scene/modulescene.h"
#include "script/module/cutscene/modulecutscene.h" #include "script/module/cutscene/modulecutscene.h"
#include "script/module/console/moduleconsole.h" #include "script/module/console/moduleconsole.h"
@@ -36,6 +38,8 @@ static void moduleRegister(void) {
moduleScreen(); moduleScreen();
moduleSpriteBatch(); moduleSpriteBatch();
moduleText(); moduleText();
moduleEasing();
moduleAnimation();
moduleScene(); moduleScene();
moduleCutscene(); moduleCutscene();
moduleConsole(); moduleConsole();