Easing test
This commit is contained in:
@@ -1,31 +1,32 @@
|
||||
var PHASE_LEFT = 0;
|
||||
var PHASE_RIGHT = 1;
|
||||
|
||||
var SPEED = 3.0;
|
||||
var DURATION = 2.0;
|
||||
|
||||
function MoveCubeCutscene(params) {
|
||||
Cutscene.call(this);
|
||||
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.constructor = MoveCubeCutscene;
|
||||
|
||||
MoveCubeCutscene.prototype.update = function() {
|
||||
this.timer += TIME.delta;
|
||||
|
||||
if(this.phase === PHASE_LEFT) {
|
||||
this.cube.position.position.x -= SPEED * TIME.delta;
|
||||
if(this.timer >= DURATION) {
|
||||
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) {
|
||||
if(!this.animLeft.complete) {
|
||||
this.cube.position.position.x = this.animLeft.update(TIME.delta);
|
||||
} else {
|
||||
this.cube.position.position.x = this.animRight.update(TIME.delta);
|
||||
if(this.animRight.complete) {
|
||||
Cutscene.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(asset)
|
||||
add_subdirectory(cutscene)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "script/module/display/modulescreen.h"
|
||||
#include "script/module/display/modulespritebatch.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/cutscene/modulecutscene.h"
|
||||
#include "script/module/console/moduleconsole.h"
|
||||
@@ -36,6 +38,8 @@ static void moduleRegister(void) {
|
||||
moduleScreen();
|
||||
moduleSpriteBatch();
|
||||
moduleText();
|
||||
moduleEasing();
|
||||
moduleAnimation();
|
||||
moduleScene();
|
||||
moduleCutscene();
|
||||
moduleConsole();
|
||||
|
||||
Reference in New Issue
Block a user