aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorthetarnav <gthetarnav@gmail.com>2025-11-01 21:11:32 +0100
committerthetarnav <gthetarnav@gmail.com>2025-11-01 21:11:32 +0100
commit78f9f8bbce0cb09f14de41db6846674e1e782dbe (patch)
tree7118e86ddcff1aa7f02481a0d89e82cfc3bf951d /core
parent6ea7bdbbe588a935e731f96f7d75e1c91c3238a3 (diff)
Add ease inverse procedures
Diffstat (limited to 'core')
-rw-r--r--core/math/ease/ease.odin242
-rw-r--r--core/math/ease/ease_inverse.odin248
-rw-r--r--core/math/ease/flux.odin177
3 files changed, 459 insertions, 208 deletions
diff --git a/core/math/ease/ease.odin b/core/math/ease/ease.odin
index c6dd56dbc..98d549c7a 100644
--- a/core/math/ease/ease.odin
+++ b/core/math/ease/ease.odin
@@ -1,9 +1,8 @@
-// Easing procedures and flux easing used for animations.
+// Easing procedures used for animations.
package ease
-import "core:math"
+@require import "core:math"
import "base:intrinsics"
-import "core:time"
@(private) PI_2 :: math.PI / 2
@@ -174,7 +173,7 @@ exponential_in_out :: proc "contextless" (p: $T) -> T where intrinsics.type_is_f
if p == 0.0 || p == 1.0 {
return p
}
-
+
if p < 0.5 {
return 0.5 * math.pow(2, (20 * p) - 10)
} else {
@@ -307,224 +306,51 @@ Ease :: enum {
}
@(require_results)
-ease :: proc "contextless" (type: Ease, p: $T) -> T
- where intrinsics.type_is_float(T) {
+ease :: proc "contextless" (type: Ease, p: $T) -> T where intrinsics.type_is_float(T) {
switch type {
- case .Linear: return p
+ case .Linear: return p
- case .Quadratic_In: return quadratic_in(p)
- case .Quadratic_Out: return quadratic_out(p)
- case .Quadratic_In_Out: return quadratic_in_out(p)
+ case .Quadratic_In: return quadratic_in(p)
+ case .Quadratic_Out: return quadratic_out(p)
+ case .Quadratic_In_Out: return quadratic_in_out(p)
- case .Cubic_In: return cubic_in(p)
- case .Cubic_Out: return cubic_out(p)
- case .Cubic_In_Out: return cubic_in_out(p)
+ case .Cubic_In: return cubic_in(p)
+ case .Cubic_Out: return cubic_out(p)
+ case .Cubic_In_Out: return cubic_in_out(p)
- case .Quartic_In: return quartic_in(p)
- case .Quartic_Out: return quartic_out(p)
- case .Quartic_In_Out: return quartic_in_out(p)
+ case .Quartic_In: return quartic_in(p)
+ case .Quartic_Out: return quartic_out(p)
+ case .Quartic_In_Out: return quartic_in_out(p)
- case .Quintic_In: return quintic_in(p)
- case .Quintic_Out: return quintic_out(p)
- case .Quintic_In_Out: return quintic_in_out(p)
+ case .Quintic_In: return quintic_in(p)
+ case .Quintic_Out: return quintic_out(p)
+ case .Quintic_In_Out: return quintic_in_out(p)
- case .Sine_In: return sine_in(p)
- case .Sine_Out: return sine_out(p)
- case .Sine_In_Out: return sine_in_out(p)
+ case .Sine_In: return sine_in(p)
+ case .Sine_Out: return sine_out(p)
+ case .Sine_In_Out: return sine_in_out(p)
- case .Circular_In: return circular_in(p)
- case .Circular_Out: return circular_out(p)
- case .Circular_In_Out: return circular_in_out(p)
+ case .Circular_In: return circular_in(p)
+ case .Circular_Out: return circular_out(p)
+ case .Circular_In_Out: return circular_in_out(p)
- case .Exponential_In: return exponential_in(p)
- case .Exponential_Out: return exponential_out(p)
+ case .Exponential_In: return exponential_in(p)
+ case .Exponential_Out: return exponential_out(p)
case .Exponential_In_Out: return exponential_in_out(p)
- case .Elastic_In: return elastic_in(p)
- case .Elastic_Out: return elastic_out(p)
- case .Elastic_In_Out: return elastic_in_out(p)
+ case .Elastic_In: return elastic_in(p)
+ case .Elastic_Out: return elastic_out(p)
+ case .Elastic_In_Out: return elastic_in_out(p)
- case .Back_In: return back_in(p)
- case .Back_Out: return back_out(p)
- case .Back_In_Out: return back_in_out(p)
+ case .Back_In: return back_in(p)
+ case .Back_Out: return back_out(p)
+ case .Back_In_Out: return back_in_out(p)
- case .Bounce_In: return bounce_in(p)
- case .Bounce_Out: return bounce_out(p)
- case .Bounce_In_Out: return bounce_in_out(p)
+ case .Bounce_In: return bounce_in(p)
+ case .Bounce_Out: return bounce_out(p)
+ case .Bounce_In_Out: return bounce_in_out(p)
}
// in case type was invalid
return 0
}
-Flux_Map :: struct($T: typeid) {
- values: map[^T]Flux_Tween(T),
- keys_to_be_deleted: [dynamic]^T,
-}
-
-Flux_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,
-
- inited: bool,
-
- // callbacks, data can be set, will be pushed to callback
- data: rawptr, // by default gets set to value input
- on_start: proc(flux: ^Flux_Map(T), data: rawptr),
- on_update: proc(flux: ^Flux_Map(T), data: rawptr),
- on_complete: proc(flux: ^Flux_Map(T), data: rawptr),
-}
-
-// init flux map to a float type and a wanted cap
-@(require_results)
-flux_init :: proc($T: typeid, value_capacity := 8) -> Flux_Map(T) where intrinsics.type_is_float(T) {
- return {
- values = make(map[^T]Flux_Tween(T), value_capacity),
- keys_to_be_deleted = make([dynamic]^T, 0, value_capacity),
- }
-}
-
-// delete map content
-flux_destroy :: proc(flux: Flux_Map($T)) where intrinsics.type_is_float(T) {
- delete(flux.values)
- delete(flux.keys_to_be_deleted)
-}
-
-// clear map content, stops all animations
-flux_clear :: proc(flux: ^Flux_Map($T)) where intrinsics.type_is_float(T) {
- clear(&flux.values)
-}
-
-// append / overwrite existing tween value to parameters
-// rest is initialized in flux_tween_init, inside update
-// return value can be used to set callbacks
-@(require_results)
-flux_to :: proc(
- flux: ^Flux_Map($T),
- value: ^T,
- goal: T,
- type: Ease = .Quadratic_Out,
- duration: time.Duration = time.Second,
- delay: f64 = 0,
-) -> (tween: ^Flux_Tween(T)) where intrinsics.type_is_float(T) {
- if res, ok := &flux.values[value]; ok {
- tween = res
- } else {
- flux.values[value] = {}
- tween = &flux.values[value]
- }
-
- tween^ = {
- value = value,
- goal = goal,
- duration = duration,
- delay = delay,
- type = type,
- data = value,
- }
-
- return
-}
-
-// init internal properties
-flux_tween_init :: proc(tween: ^Flux_Tween($T), duration: time.Duration) where intrinsics.type_is_float(T) {
- 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
-}
-
-// 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
-flux_update :: proc(flux: ^Flux_Map($T), dt: f64) where intrinsics.type_is_float(T) {
- clear(&flux.keys_to_be_deleted)
-
- for key, &tween in flux.values {
- 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 {
- flux_tween_init(&tween, tween.duration)
-
- if tween.on_start != nil {
- tween.on_start(flux, 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.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(flux, tween.data)
- }
-
- if tween.progress >= 1 {
- // append keys to array that will be deleted after the loop
- append(&flux.keys_to_be_deleted, key)
-
- if tween.on_complete != nil {
- tween.on_complete(flux, tween.data)
- }
- }
- }
- }
-
- // loop through keys that should be deleted from the map
- if len(flux.keys_to_be_deleted) != 0 {
- for key in flux.keys_to_be_deleted {
- delete_key(&flux.values, key)
- }
- }
-}
-
-// stop a specific key inside the map
-// returns true when it successfully removed the key
-@(require_results)
-flux_stop :: proc(flux: ^Flux_Map($T), key: ^T) -> bool where intrinsics.type_is_float(T) {
- if key in flux.values {
- delete_key(&flux.values, key)
- return true
- }
-
- return false
-}
-
-// returns the amount of time left for the tween animation, if the key exists in the map
-// returns 0 if the tween doesnt exist on the map
-@(require_results)
-flux_tween_time_left :: proc(flux: Flux_Map($T), key: ^T) -> f64 {
- if tween, ok := flux.values[key]; ok {
- return ((1 - tween.progress) * tween.rate) + tween.delay
- } else {
- return 0
- }
-}
diff --git a/core/math/ease/ease_inverse.odin b/core/math/ease/ease_inverse.odin
new file mode 100644
index 000000000..d1cb1f698
--- /dev/null
+++ b/core/math/ease/ease_inverse.odin
@@ -0,0 +1,248 @@
+// Inverse easing procedures
+// These are the mathematical inverses of the corresponding easing functions,
+// allowing you to reverse the transformation:
+// if y = ease_fn(x), then x = ease_fn_inverse(y) + some_imprecision
+package ease
+
+@require import "core:math"
+import "base:intrinsics"
+
+// Helper for handling negative bases with fractional exponents
+// since math.pow(negative, fraction) returns NaN
+@(private="file")
+_signed_pow :: proc "contextless" (x, exp: $T) -> T where intrinsics.type_is_float(T) {
+ if x >= 0 {
+ return math.pow(x, exp)
+ } else {
+ return -math.pow(-x, exp)
+ }
+}
+
+// Inverse of quadratic_in
+// x = sqrt(y)
+@(require_results)
+quadratic_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.sqrt(p)
+}
+
+// Inverse of quadratic_out
+// x = 1 - sqrt(1 - y)
+@(require_results)
+quadratic_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return 1 - math.sqrt(1 - p)
+}
+
+// Inverse of quadratic_in_out
+// x = sqrt(y/2) ; [0, 0.5)
+// x = 1 - sqrt((1-y)/2) ; [0.5, 1]
+@(require_results)
+quadratic_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ if p < 0.5 {
+ return math.sqrt(p / 2)
+ } else {
+ return 1 - math.sqrt((1 - p) / 2)
+ }
+}
+
+// Inverse of cubic_in
+// x = y^(1/3)
+@(require_results)
+cubic_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.pow(p, 1.0/3.0)
+}
+
+// Inverse of cubic_out
+// x = (y - 1)^(1/3) + 1
+@(require_results)
+cubic_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return _signed_pow(p - 1, 1.0/3.0) + 1
+}
+
+// Inverse of cubic_in_out
+// x = (y/4)^(1/3) ; [0, 0.5)
+// x = ((y-1)*2)^(1/3)/2 + 1 ; [0.5, 1]
+@(require_results)
+cubic_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ if p < 0.5 {
+ return math.pow(p / 4, 1.0/3.0)
+ } else {
+ return _signed_pow((p - 1) * 2, 1.0/3.0) / 2 + 1
+ }
+}
+
+// Inverse of quartic_in
+// x = y^(1/4)
+@(require_results)
+quartic_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.pow(p, 0.25)
+}
+
+// Inverse of quartic_out
+// x = 1 - (1 - y)^(1/4)
+@(require_results)
+quartic_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return 1 - math.pow(1 - p, 0.25)
+}
+
+// Inverse of quartic_in_out
+// x = (y/8)^(1/4) ; [0, 0.5)
+// x = 1 - ((1-y)/8)^(1/4) ; [0.5, 1]
+@(require_results)
+quartic_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ if p < 0.5 {
+ return math.pow(p / 8, 0.25)
+ } else {
+ return 1 - math.pow((1 - p) / 8, 0.25)
+ }
+}
+
+// Inverse of quintic_in
+// x = y^(1/5)
+@(require_results)
+quintic_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.pow(p, 0.2)
+}
+
+// Inverse of quintic_out
+// x = (y - 1)^(1/5) + 1
+@(require_results)
+quintic_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return _signed_pow(p - 1, 0.2) + 1
+}
+
+// Inverse of quintic_in_out
+// x = (y/16)^(1/5) ; [0, 0.5)
+// x = ((y-1)*2)^(1/5)/2 + 1 ; [0.5, 1]
+@(require_results)
+quintic_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ if p < 0.5 {
+ return math.pow(p / 16, 0.2)
+ } else {
+ return _signed_pow((p - 1) * 2, 0.2) / 2 + 1
+ }
+}
+
+// Inverse of sine_in
+// x = asin(y - 1) * 2/π + 1
+@(require_results)
+sine_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.asin(p - 1) * 2/math.PI + 1
+}
+
+// Inverse of sine_out
+// x = asin(y) * 2/π
+@(require_results)
+sine_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.asin(p) * 2/math.PI
+}
+
+// Inverse of sine_in_out
+// x = acos(1 - 2y) / π
+@(require_results)
+sine_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.acos(1 - 2*p) / math.PI
+}
+
+// Inverse of circular_in
+// x = sqrt(2y - y²)
+@(require_results)
+circular_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return math.sqrt(2*p - p*p)
+}
+
+// Inverse of circular_out
+// x = 1 - sqrt(1 - y²)
+@(require_results)
+circular_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return 1 - math.sqrt(1 - p*p)
+}
+
+// Inverse of circular_in_out
+// x = sqrt(1 - (1-2y)²) / 2 ; [0, 0.5)
+// x = 1 - sqrt(1 - (2y-1)²) / 2 ; [0.5, 1]
+@(require_results)
+circular_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ if p < 0.5 {
+ q := 1 - 2*p
+ return math.sqrt(1 - q*q) / 2
+ } else {
+ q := 2*p - 1
+ return 1 - math.sqrt(1 - q*q) / 2
+ }
+}
+
+// Inverse of exponential_in
+// x = log₂(y) / 10 + 1
+@(require_results)
+exponential_in_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return p == 0.0 ? 0.0 : math.log2(p) / 10 + 1
+}
+
+// Inverse of exponential_out
+// x = -log₂(1 - y) / 10
+@(require_results)
+exponential_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ return p == 1.0 ? 1.0 : -math.log2(1 - p) / 10
+}
+
+// Inverse of exponential_in_out
+// x = (log₂(2y) + 10) / 20 ; [0, 0.5)
+// x = (10 - log₂(2(1-y))) / 20 ; [0.5, 1]
+@(require_results)
+exponential_in_out_inverse :: proc "contextless" (p: $T) -> T where intrinsics.type_is_float(T) {
+ if p == 0.0 || p == 1.0 {
+ return p
+ }
+
+ if p < 0.5 {
+ return (math.log2(2*p) + 10) / 20
+ } else {
+ return (10 - math.log2(2*(1-p))) / 20
+ }
+}
+
+// Additional enum variant
+
+@(require_results)
+ease_inverse :: proc "contextless" (type: Ease, p: $T) -> T where intrinsics.type_is_float(T) {
+ switch type {
+ case .Linear: return p
+
+ case .Quadratic_In: return quadratic_in_inverse(p)
+ case .Quadratic_Out: return quadratic_out_inverse(p)
+ case .Quadratic_In_Out: return quadratic_in_out_inverse(p)
+
+ case .Cubic_In: return cubic_in_inverse(p)
+ case .Cubic_Out: return cubic_out_inverse(p)
+ case .Cubic_In_Out: return cubic_in_out_inverse(p)
+
+ case .Quartic_In: return quartic_in_inverse(p)
+ case .Quartic_Out: return quartic_out_inverse(p)
+ case .Quartic_In_Out: return quartic_in_out_inverse(p)
+
+ case .Quintic_In: return quintic_in_inverse(p)
+ case .Quintic_Out: return quintic_out_inverse(p)
+ case .Quintic_In_Out: return quintic_in_out_inverse(p)
+
+ case .Sine_In: return sine_in_inverse(p)
+ case .Sine_Out: return sine_out_inverse(p)
+ case .Sine_In_Out: return sine_in_out_inverse(p)
+
+ case .Circular_In: return circular_in_inverse(p)
+ case .Circular_Out: return circular_out_inverse(p)
+ case .Circular_In_Out: return circular_in_out_inverse(p)
+
+ case .Exponential_In: return exponential_in_inverse(p)
+ case .Exponential_Out: return exponential_out_inverse(p)
+ case .Exponential_In_Out: return exponential_in_out_inverse(p)
+
+ case .Elastic_In, .Elastic_Out, .Elastic_In_Out,
+ .Back_In, .Back_Out, .Back_In_Out,
+ .Bounce_In, .Bounce_Out, .Bounce_In_Out:
+ // These do not have simple closed-form inverses
+ return 0
+ }
+
+ // In case type was invalid
+ return 0
+}
diff --git a/core/math/ease/flux.odin b/core/math/ease/flux.odin
new file mode 100644
index 000000000..137f3dff6
--- /dev/null
+++ b/core/math/ease/flux.odin
@@ -0,0 +1,177 @@
+// Flux easing used for animations
+package ease
+
+import "core:time"
+
+Flux_Map :: struct($T: typeid) {
+ values: map[^T]Flux_Tween(T),
+ keys_to_be_deleted: [dynamic]^T,
+}
+
+Flux_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,
+
+ inited: bool,
+
+ // callbacks, data can be set, will be pushed to callback
+ data: rawptr, // by default gets set to value input
+ on_start: proc(flux: ^Flux_Map(T), data: rawptr),
+ on_update: proc(flux: ^Flux_Map(T), data: rawptr),
+ on_complete: proc(flux: ^Flux_Map(T), data: rawptr),
+}
+
+// init flux map to a float type and a wanted cap
+@(require_results)
+flux_init :: proc($T: typeid, value_capacity := 8, allocator := context.allocator, loc := #caller_location) -> Flux_Map(T) where intrinsics.type_is_float(T) {
+ return {
+ values = make(map[^T]Flux_Tween(T), value_capacity, allocator, loc),
+ keys_to_be_deleted = make([dynamic]^T, 0, value_capacity, allocator, loc),
+ }
+}
+
+// delete map content
+flux_destroy :: proc(flux: Flux_Map($T), allocator := context.allocator, loc := #caller_location) where intrinsics.type_is_float(T) {
+ delete(flux.values, allocator, loc)
+ delete(flux.keys_to_be_deleted, allocator, loc)
+}
+
+// clear map content, stops all animations
+flux_clear :: proc(flux: ^Flux_Map($T)) where intrinsics.type_is_float(T) {
+ clear(&flux.values)
+}
+
+// append / overwrite existing tween value to parameters
+// rest is initialized in flux_tween_init, inside update
+// return value can be used to set callbacks
+@(require_results)
+flux_to :: proc(
+ flux: ^Flux_Map($T),
+ value: ^T,
+ goal: T,
+ type: Ease = .Quadratic_Out,
+ duration: time.Duration = time.Second,
+ delay: f64 = 0,
+) -> (tween: ^Flux_Tween(T)) where intrinsics.type_is_float(T) {
+ if res, ok := &flux.values[value]; ok {
+ tween = res
+ } else {
+ flux.values[value] = {}
+ tween = &flux.values[value]
+ }
+
+ tween^ = {
+ value = value,
+ goal = goal,
+ duration = duration,
+ delay = delay,
+ type = type,
+ data = value,
+ }
+
+ return
+}
+
+// init internal properties
+flux_tween_init :: proc(tween: ^Flux_Tween($T), duration: time.Duration) where intrinsics.type_is_float(T) {
+ 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
+}
+
+// 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
+flux_update :: proc(flux: ^Flux_Map($T), dt: f64) where intrinsics.type_is_float(T) {
+ clear(&flux.keys_to_be_deleted)
+
+ for key, &tween in flux.values {
+ 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 {
+ flux_tween_init(&tween, tween.duration)
+
+ if tween.on_start != nil {
+ tween.on_start(flux, 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.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(flux, tween.data)
+ }
+
+ if tween.progress >= 1 {
+ // append keys to array that will be deleted after the loop
+ append(&flux.keys_to_be_deleted, key)
+
+ if tween.on_complete != nil {
+ tween.on_complete(flux, tween.data)
+ }
+ }
+ }
+ }
+
+ // loop through keys that should be deleted from the map
+ if len(flux.keys_to_be_deleted) != 0 {
+ for key in flux.keys_to_be_deleted {
+ delete_key(&flux.values, key)
+ }
+ }
+}
+
+// stop a specific key inside the map
+// returns true when it successfully removed the key
+@(require_results)
+flux_stop :: proc(flux: ^Flux_Map($T), key: ^T) -> bool where intrinsics.type_is_float(T) {
+ if key in flux.values {
+ delete_key(&flux.values, key)
+ return true
+ }
+
+ return false
+}
+
+// returns the amount of time left for the tween animation, if the key exists in the map
+// returns 0 if the tween doesn't exist on the map
+@(require_results)
+flux_tween_time_left :: proc(flux: Flux_Map($T), key: ^T) -> f64 {
+ if tween, ok := flux.values[key]; ok {
+ return ((1 - tween.progress) * tween.rate) + tween.delay
+ } else {
+ return 0
+ }
+}