arcadia/tween/tween.odin

239 lines
5.7 KiB
Odin

package tween
import "base:intrinsics"
import "core:math/ease"
import "core:time"
TweenContext :: struct($T: typeid) {
tweens: [dynamic]Tween(T),
}
Tween :: struct($T: typeid) {
value: ^T,
start: T,
diff: T,
goal: T,
delay: f64, // in seconds
duration: time.Duration,
progress: f64,
rate: f64,
type: ease.Ease,
inited: bool,
id: int,
// callbacks, data can be set, will be pushed to callback
data: rawptr, // by default gets set to value input
on_start: proc(ctx: ^TweenContext(T), data: rawptr),
on_update: proc(ctx: ^TweenContext(T), data: rawptr),
on_complete: proc(ctx: ^TweenContext(T), data: rawptr),
}
@(require_results)
context_init_f :: proc(
$T: typeid,
) -> TweenContext(T) where intrinsics.type_is_float(T) {
return {tweens = make([dynamic]Tween(T))}
}
@(require_results)
context_init_v :: proc(
$T: typeid/[$N]$E,
) -> TweenContext(T) where intrinsics.type_is_float(E) {
return {tweens = make([dynamic]Tween(T))}
}
context_init :: proc {
context_init_f,
context_init_v,
}
// delete map content
context_destroy :: proc(ctx: TweenContext($T)) {
delete(ctx.tweens)
}
// clear map content, stops all animations
context_clear :: proc(ctx: ^TweenContext($T)) {
clear(&ctx.tweens)
}
// append / overwrite existing tween value to parameters
// rest is initialized in tween_init, inside update
// return value can be used to set callbacks
@(require_results)
tween_to :: proc(
ctx: ^TweenContext($T),
value: ^T,
goal: T,
type: ease.Ease = .Quadratic_Out,
duration: time.Duration = time.Second,
delay: f64 = 0,
id: int = 0,
) -> (
tween: ^Tween(T),
) {
append(&ctx.tweens, Tween(T){})
n := len(ctx.tweens) - 1
tween = &ctx.tweens[n]
tween^ = {
value = value,
goal = goal,
duration = duration,
delay = delay,
type = type,
data = value,
id = id,
}
return
}
// init internal properties
_tween_init :: proc(tween: ^Tween($T), duration: time.Duration) {
tween.inited = true
tween.start = tween.value^
tween.diff = tween.goal - tween.value^
s := time.duration_seconds(duration)
tween.rate = duration > 0 ? 1.0 / s : 0
tween.progress = duration > 0 ? 0 : 1
}
_tween_init_f :: proc(
tween: ^Tween($T),
duration: time.Duration,
) where intrinsics.type_is_float(T) {
_tween_init(tween, duration)
}
_tween_init_v :: proc(
tween: ^Tween($T/[$N]$E),
duration: time.Duration,
) where intrinsics.type_is_float(E) {
_tween_init(tween, duration)
}
tween_init :: proc {
_tween_init_f,
_tween_init_v,
}
_tween_interpolate :: proc(tween: ^Tween($T), dt, delay_remainder: f64) {
tween.progress += tween.rate * (dt + delay_remainder)
x := tween.progress >= 1 ? 1 : ease.ease(tween.type, tween.progress)
tween.value^ = tween.start + tween.diff * T(x)
}
tween_interpolate_f :: proc(
tween: ^Tween($T),
dt, delay_remainder: f64,
) where intrinsics.type_is_float(T) {
_tween_interpolate(tween, dt, delay_remainder)
}
tween_interpolate_v :: proc(
tween: ^Tween($T/[$N]$E),
dt, delay_remainder: f64,
) where intrinsics.type_is_float(E) {
_tween_interpolate(tween, dt, delay_remainder)
}
tween_interpolate :: proc {
tween_interpolate_f,
tween_interpolate_v,
}
// update all tweens, wait for their delay if one exists
// calls callbacks in all stages, when they're filled
// deletes tween from the map after completion
_tween_update :: proc(ctx: ^TweenContext($T), dt: f64) {
for &tween in ctx.tweens {
delay_remainder := f64(0)
// Update delay if necessary.
if tween.delay > 0 {
tween.delay -= dt
if tween.delay < 0 {
// We finished the delay, but in doing so consumed part of this frame's `dt` budget.
// Keep track of it so we can apply it to this tween without affecting others.
delay_remainder = tween.delay
// We're done with this delay.
tween.delay = 0
}
}
// We either had no delay, or the delay has been consumed.
if tween.delay <= 0 {
if !tween.inited {
tween_init(&tween, tween.duration)
if tween.on_start != nil {
tween.on_start(ctx, tween.data)
}
}
// If part of the `dt` budget was consumed this frame, then `delay_remainder` will be
// that remainder, a negative value. Adding it to `dt` applies what's left of the `dt`
// to the tween so it advances properly, instead of too much or little.
tween_interpolate(&tween, dt, delay_remainder)
// tween.progress += tween.rate * (dt + delay_remainder)
// x := tween.progress >= 1 ? 1 : ease(tween.type, tween.progress)
// tween.value^ = tween.start + tween.diff * T(x)
if tween.on_update != nil {
tween.on_update(ctx, tween.data)
}
if tween.progress >= 1 {
// append keys to array that will be deleted after the loop
if tween.on_complete != nil {
tween.on_complete(ctx, tween.data)
}
}
}
}
}
tween_update_f :: proc(ctx: ^TweenContext($T), dt: f64) where intrinsics.type_is_float(T) {
_tween_update(ctx, dt)
}
tween_update_v :: proc(ctx: ^TweenContext($T/[$N]$E), dt: f64) where intrinsics.type_is_float(E) {
_tween_update(ctx, dt)
}
tween_update :: proc {
tween_update_v,
tween_update_f,
}
// stop a specific key inside the map
// returns true when it successfully removed the key
// @(require_results)
tween_stop :: proc(ctx: ^TweenContext($T), id: int) -> bool {
removed_tweens := false
for i := 0; i < len(ctx.tweens); {
if tween.id == id {
unordered_remove(&ctx.tweens, i)
removed_tweens = true
} else {
i += 1
}
}
return false
}
// Returns the first tween animation with that id
// If no tween exists with the given id, will return 0
@(require_results)
tween_time_left :: proc(flux: TweenContext($T), id: int) -> f64 {
for tween in ctx.tweens {
if tween.id == id {
return ((1 - tween.progress) * tween.rate) + tween.delay
}
}
return 0
}