This commit is contained in:
spider 2026-08-06 11:42:43 -05:00 committed by GitHub
commit e5036d04bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 614 additions and 67 deletions

213
src/easings.luau Normal file
View file

@ -0,0 +1,213 @@
--!native
--!optimize 2
export type EasingFunction = (t: number) -> number
local function lerp(t: number): number
return t
end
local function sine_ease_in(t: number): number
return 1 - math.cos((math.pi * t) / 2)
end
local function sine_ease_out(t: number): number
return math.sin((math.pi * t) / 2)
end
local function sine_ease_inout(t: number): number
return -(math.cos(math.pi * t) - 1) / 2
end
local function quad_ease_in(t: number): number
return t^2
end
local function quad_ease_out(t: number): number
return 1 - (1 - t)^2
end
local function quad_ease_inout(t: number): number
return if t < 0.5 then 2 * t^2 else 1 - math.pow(-2 * t + 2, 2) / 2
end
local function cubic_ease_in(t: number): number
return t^3
end
local function cubic_ease_out(t: number): number
return 1 - math.pow(1 - t, 3);
end
local function cubic_ease_inout(t: number): number
return if t < 0.5 then 4 * t^3 else 1 - math.pow(-2 * t + 2, 3) / 2
end
local function quart_ease_in(t: number): number
return t^4
end
local function quart_ease_out(t: number): number
return 1 - math.pow(1 - t, 4)
end
local function quart_ease_inout(t: number): number
return if t < 0.5 then 8 * t^4 else 1 - math.pow(-2 * t + 2, 4) / 2
end
local function quint_ease_in(t: number): number
return t^5
end
local function quint_ease_out(t: number): number
return 1 - math.pow(1 - t, 5)
end
local function quint_ease_inout(t: number): number
return if t < 0.5 then 16 * t^5 else 1 - math.pow(-2 * t + 2, 5) / 2
end
local function expo_ease_in(t: number): number
return if t == 0 then 0 else math.pow(2, 10 * t - 10)
end
local function expo_ease_out(t: number): number
return if t == 1 then 1 else 1 - math.pow(2, -10 * t)
end
local function expo_ease_inout(t: number): number
if t == 0 then
return 0
elseif t == 1 then
return 1
end
return if t < 0.5
then math.pow(2, 20 * t - 10) / 2
else (2 - math.pow(2, -20 * t + 10)) / 2
end
local function circ_ease_in(t: number): number
return 1 - math.sqrt(1 - math.pow(t, 2))
end
local function circ_ease_out(t: number): number
return math.sqrt(1 - math.pow(t - 1, 2))
end
local function circ_ease_inout(t: number): number
return if t < 0.5
then (1 - math.sqrt(1 - math.pow(2 * t, 2))) / 2
else (math.sqrt(1 - math.pow(-2 * t + 2, 2)) + 1) / 2
end
local _BACK_C1 = 1.70158
local _BACK_C2 = _BACK_C1 * 1.525
local _BACK_C3 = _BACK_C1 + 1
local function back_ease_in(t: number): number
return _BACK_C3 * t^3 - _BACK_C1 * t^2
end
local function back_ease_out(t: number): number
return 1 + _BACK_C3 * math.pow(t - 1, 3) + _BACK_C1 * math.pow(t - 1, 2)
end
local function back_ease_inout(t: number): number
return if t < 0.5
then (math.pow(2 * t, 2) * ((_BACK_C2 + 1) * 2 * t - _BACK_C2)) / 2
else (math.pow(2 * t - 2, 2) * ((_BACK_C2 + 1) * (t * 2 - 2) + _BACK_C2) + 2) / 2
end
local _ELASTIC_C4 = (2 * math.pi) / 3
local _ELASTIC_C5 = (2 * math.pi) / 4.5
local function elastic_ease_in(t: number): number
if t == 0 then
return 0
elseif t == 1 then
return 1
end
return -math.pow(2, 10 * t - 10) * math.sin((t * 10 - 10.75) * _ELASTIC_C4)
end
local function elastic_ease_out(t: number): number
if t == 0 then
return 0
elseif t == 1 then
return 1
end
return math.pow(2, -10 * t) * math.sin((t * 10 - 0.75) * _ELASTIC_C4) + 1
end
local function elastic_ease_inout(t: number): number
if t == 0 then
return 0
elseif t == 1 then
return 1
end
return if t < 0.5
then -(math.pow(2, 20 * t - 10) * math.sin((20 * t - 11.125) * _ELASTIC_C5)) / 2
else (math.pow(2, -20 * t + 10) * math.sin((20 * t - 11.125) * _ELASTIC_C5)) / 2 + 1
end
local _BOUNCE_N1 = 7.5625
local _BOUNCE_D1 = 2.75
local function bounce_ease_out(t: number): number
if t < (1 / _BOUNCE_D1) then
return _BOUNCE_N1 * t^2
elseif t < (2 / _BOUNCE_D1) then
return _BOUNCE_N1 * (t - (1.5 / _BOUNCE_D1))^2 + 0.75
elseif t < (2.5 / _BOUNCE_D1) then
return _BOUNCE_N1 * (t - (2.25 / _BOUNCE_D1))^2 + 0.9375
else
return _BOUNCE_N1 * (t - (2.625 / _BOUNCE_D1))^2 + 0.984375
end
end
local function bounce_ease_in(t: number): number
return 1 - bounce_ease_out(1 - t)
end
local function bounce_ease_inout(t: number): number
return if t < 0.5
then (1 - bounce_ease_out(1 - 2 * t)) / 2
else (1 + bounce_ease_out(2 * t - 1)) / 2
end
local easings = {
lerp = lerp,
sine_ease_in = sine_ease_in,
sine_ease_out = sine_ease_out,
sine_ease_inout = sine_ease_inout,
quad_ease_in = quad_ease_in,
quad_ease_out = quad_ease_out,
quad_ease_inout = quad_ease_inout,
cubic_ease_in = cubic_ease_in,
cubic_ease_out = cubic_ease_out,
cubic_ease_inout = cubic_ease_inout,
quart_ease_in = quart_ease_in,
quart_ease_out = quart_ease_out,
quart_ease_inout = quart_ease_inout,
quint_ease_in = quint_ease_in,
quint_ease_out = quint_ease_out,
quint_ease_inout = quint_ease_inout,
expo_ease_in = expo_ease_in,
expo_ease_out = expo_ease_out,
expo_ease_inout = expo_ease_inout,
circ_ease_in = circ_ease_in,
circ_ease_out = circ_ease_out,
circ_ease_inout = circ_ease_inout,
back_ease_in = back_ease_in,
back_ease_out = back_ease_out,
back_ease_inout = back_ease_inout,
elastic_ease_in = elastic_ease_in,
elastic_ease_out = elastic_ease_out,
elastic_ease_inout = elastic_ease_inout,
bounce_ease_in = bounce_ease_in,
bounce_ease_out = bounce_ease_out,
bounce_ease_inout = bounce_ease_inout
}
return table.freeze(easings)

View file

@ -18,6 +18,7 @@ local show = require "./show"
local indexes = require "./indexes"
local values = require "./values"
local spring, update_springs = require "./spring"()
local tween, update_tweens = require "./tween"()
local action = require "./action"()
local changed = require "./changed"
local timeout, update_timeouts = require "./timeout"()
@ -31,92 +32,98 @@ export type Instances = create.Instances
export type Properties<T> = create.Properties<T>
local function step(dt: number)
if game then debug.profilebegin("VIDE STEP") end
if game then debug.profilebegin("VIDE STEP") end
if game then debug.profilebegin("VIDE SPRING") end
update_springs(dt)
if game then debug.profileend() end
if game then debug.profilebegin("VIDE SPRING") end
update_springs(dt)
if game then debug.profileend() end
if game then debug.profilebegin("VIDE SCHEDULER") end
update_timeouts(dt)
if game then debug.profileend() end
-- Added Tween update loop
if game then debug.profilebegin("VIDE TWEEN") end
update_tweens(dt)
if game then debug.profileend() end
if game then debug.profileend() end
if game then debug.profilebegin("VIDE SCHEDULER") end
update_timeouts(dt)
if game then debug.profileend() end
if game then debug.profileend() end
end
local stepped = game and game:GetService("RunService").Heartbeat:Connect(function(dt: number)
task.defer(step, dt)
task.defer(step, dt)
end)
local vide = {
version = version,
version = version,
-- core
root = root,
--branch = branch,
mount = mount,
create = create,
source = source,
effect = effect,
derive = derive,
switch = switch,
show = show,
indexes = indexes,
values = values,
-- core
root = root,
--branch = branch,
mount = mount,
create = create,
source = source,
effect = effect,
derive = derive,
switch = switch,
show = show,
indexes = indexes,
values = values,
-- util
cleanup = cleanup,
untrack = untrack,
read = read,
batch = batch,
context = context,
-- util
cleanup = cleanup,
untrack = untrack,
read = read,
batch = batch,
context = context,
-- animations
spring = spring,
-- animations
spring = spring,
tween = tween,
-- actions
action = action,
changed = changed,
-- actions
action = action,
changed = changed,
-- flags
strict = (nil :: any) :: boolean,
defaults = (nil :: any) :: boolean,
defer_nested_properties = (nil :: any) :: boolean,
-- flags
strict = (nil :: any) :: boolean,
defaults = (nil :: any) :: boolean,
defer_nested_properties = (nil :: any) :: boolean,
-- temporary
apply = function(instance: Instance)
return function(props: { [any]: any })
apply(instance, props)
return instance
end
end,
-- temporary
apply = function(instance: Instance)
return function(props: { [any]: any })
apply(instance, props)
return instance
end
end,
-- runtime
step = function(dt: number)
if stepped then
stepped:Disconnect()
stepped = nil
end
step(dt)
end
-- runtime
step = function(dt: number)
if stepped then
stepped:Disconnect()
stepped = nil
end
step(dt)
end
}
setmetatable(vide :: any, {
__index = function(_, index: unknown): ()
if flags[index] == nil then
error(`{tostring(index)} is not a valid member of vide`, 0)
else
return flags[index]
end
end,
__index = function(_, index: unknown): ()
if flags[index] == nil then
error(`{tostring(index)} is not a valid member of vide`, 0)
else
return flags[index]
end
end,
__newindex = function(_, index: unknown, value: unknown)
if flags[index] == nil then
error(`{tostring(index)} is not a valid member of vide, 0`)
else
flags[index] = value
end
end
__newindex = function(_, index: unknown, value: unknown)
if flags[index] == nil then
error(`{tostring(index)} is not a valid member of vide, 0`)
else
flags[index] = value
end
end
})
return vide

251
src/tween.luau Normal file
View file

@ -0,0 +1,251 @@
local easings = require "./easings"
export type EasingStyle =
"lerp"
| "sine_ease_in" | "sine_ease_out" | "sine_ease_inout"
| "quad_ease_in" | "quad_ease_out" | "quad_ease_inout"
| "cubic_ease_in" | "cubic_ease_out" | "cubic_ease_inout"
| "quart_ease_in" | "quart_ease_out" | "quart_ease_inout"
| "quint_ease_in" | "quint_ease_out" | "quint_ease_inout"
| "expo_ease_in" | "expo_ease_out" | "expo_ease_inout"
| "circ_ease_in" | "circ_ease_out" | "circ_ease_inout"
| "back_ease_in" | "back_ease_out" | "back_ease_inout"
| "elastic_ease_in" | "elastic_ease_out" | "elastic_ease_inout"
| "bounce_ease_in" | "bounce_ease_out" | "bounce_ease_inout"
export type TweenConfig = {
time: number?,
style: EasingStyle?,
delay: number?,
reverses: boolean?,
repeat_count: number?,
}
local graph = require "./graph"
type Node<T> = graph.Node<T>
type SourceNode<T> = graph.SourceNode<T>
local create_node = graph.create_node
local create_source_node = graph.create_source_node
local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node
local update_descendants = graph.update_descendants
local push_scope_as_child_of = graph.push_scope_as_child_of
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect | { number }
--[[
Unsupported datatypes:
- bool
- Vector2int16
- Vector3int16
- EnumItem
]]
type TweenState<T> = {
time: number,
style: easings.EasingFunction,
delay: number,
reverses: boolean,
repeat_count: number,
time_passed: number,
x0_123: vector, x0_456: vector, -- start position
x1_123: vector, x1_456: vector, -- target position
source_value: T -- current value of tween input source
}
type TypeToVec6<T> = (T) -> (vector, vector)
type Vec6ToType<T> = (vector, vector) -> T
local type_to_vec6 = {
number = function(v)
return vector.create(v, 0, 0), vector.zero
end :: TypeToVec6<number>,
CFrame = function(v)
return v.Position, vector.create(v:ToEulerAnglesXYZ())
end :: TypeToVec6<CFrame>,
Color3 = function(v)
return vector.create(v.R, v.G, v.B), vector.zero
end :: TypeToVec6<Color3>,
UDim = function(v)
return vector.create(v.Scale, v.Offset, 0), vector.zero
end :: TypeToVec6<UDim>,
UDim2 = function(v)
return vector.create(v.X.Scale, v.X.Offset, v.Y.Scale), vector.create(v.Y.Offset, 0, 0)
end :: TypeToVec6<UDim2>,
Vector2 = function(v)
return vector.create(v.X, v.Y, 0), vector.zero
end :: TypeToVec6<Vector2>,
Vector3 = function(v)
return v, vector.zero
end :: TypeToVec6<Vector3>,
Rect = function(v)
return vector.create(v.Min.X, v.Min.Y, v.Max.X), vector.create(v.Max.Y, 0, 0)
end :: TypeToVec6<Rect>,
table = function(v)
return vector.create(v[1] or 0, v[2] or 0, v[3] or 0), vector.create(v[4] or 0, 0, 0)
end :: TypeToVec6<{ number }>
}
local vec6_to_type = {
number = function(a, b)
return a.X
end :: Vec6ToType<number>,
CFrame = function(a, b)
return CFrame.new(a) * CFrame.fromEulerAnglesXYZ(b.X, b.Y, b.Z)
end :: Vec6ToType<CFrame>,
Color3 = function(v)
return Color3.new(math.clamp(v.X, 0, 1), math.clamp(v.Y, 0, 1), math.clamp(v.Z, 0, 1))
end :: Vec6ToType<Color3>,
UDim = function(v)
return UDim.new(v.X, math.round(v.Y))
end :: Vec6ToType<UDim>,
UDim2 = function(a, b)
return UDim2.new(a.X, math.round(a.Y), a.Z, math.round(b.X))
end :: Vec6ToType<UDim2>,
Vector2 = function(v)
return Vector2.new(v.X, v.Y)
end :: Vec6ToType<Vector2>,
Vector3 = function(v)
return v
end :: Vec6ToType<Vector3>,
Rect = function(a, b)
return Rect.new(a.X, a.Y, a.Z, b.X)
end :: Vec6ToType<Rect>,
table = function(a, b)
return { a.X, a.Y, a.Z, b.X }
end :: Vec6ToType<{ number }>
}
local invalid_type = { __index = function(_, t: string) error(`cannot tween type {t}`, 0) end }
setmetatable(type_to_vec6, invalid_type)
setmetatable(vec6_to_type, invalid_type)
local tweens: { [TweenState<unknown>]: SourceNode<unknown> } = {}
setmetatable(tweens :: any, { __mode = "v" })
local function tween<T>(source: () -> T, config: TweenConfig?): () -> T
local owner = assert_stable_scope()
local data: TweenState<T> = {
time = (config and config.time) or 1,
style = easings[(config and config.style) or "quad_ease_out"],
delay = (config and config.delay) or 0,
reverses = (config and config.reverses) or false,
repeat_count = (config and config.repeat_count) or 0,
time_passed = 0,
x0_123 = vector.zero, x0_456 = vector.zero,
x1_123 = vector.zero, x1_456 = vector.zero,
source_value = false :: any,
}
local output = create_source_node(false :: any)
local function updater_effect()
local value = source()
local x1_123, x1_456 = type_to_vec6[typeof(value)](value)
local current_val = output.cache or value
local x0_123, x0_456 = type_to_vec6[typeof(current_val)](current_val)
data.x0_123, data.x0_456 = x0_123, x0_456
data.x1_123, data.x1_456 = x1_123, x1_456
data.time_passed = 0
data.source_value = value
tweens[data] = output
return value
end
local updater = create_node(owner, updater_effect, false :: any)
evaluate_node(updater)
output.cache = data.source_value
return function(...)
if select("#", ...) == 0 then
push_scope_as_child_of(output)
return output.cache
end
local v = ... :: T
data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v)
data.x0_123, data.x0_456 = data.x1_123, data.x1_456
tweens[data] = nil
output.cache = v
return v
end
end
local function update_tween_sources(dt: number)
for data, output in tweens do
data.time_passed += dt
local duration = data.time
local delay = data.delay
local reverses = data.reverses
local repeat_count = data.repeat_count
-- The duration of one full "cycle" (forward + optional back)
local cycle_duration = if reverses then duration * 2 else duration
local total_time = data.time_passed - delay
if total_time < 0 then continue end
-- Calculate how many cycles have finished
local current_cycle = math.floor(total_time / cycle_duration)
-- Check for completion (if not infinite repeat)
if repeat_count ~= -1 and current_cycle > repeat_count then
tweens[data] = nil
output.cache = data.source_value
else
-- Time relative to the start of the current cycle
local time_in_cycle = total_time % cycle_duration
local alpha
if reverses then
if time_in_cycle <= duration then
alpha = time_in_cycle / duration
else
alpha = 1 - ((time_in_cycle - duration) / duration)
end
else
alpha = time_in_cycle / duration
end
local eased_alpha = data.style(math.clamp(alpha, 0, 1))
local x_123 = data.x0_123 + (data.x1_123 - data.x0_123) * eased_alpha
local x_456 = data.x0_456 + (data.x1_456 - data.x0_456) * eased_alpha
output.cache = vec6_to_type[typeof(data.source_value)](x_123, x_456)
end
update_descendants(output)
end
end
return function()
return tween, function(dt: number)
update_tween_sources(dt)
end
end

76
test/tween-test.luau Normal file
View file

@ -0,0 +1,76 @@
local vide = require "../../vide"
local function system(): (number) -> number
local MAX = 40
local MIN = 10
local _, input, output = vide.root(function()
local input = vide.source(MAX)
local output = vide.tween(input, { time = 2, style = "quad_ease_inout" })
return input, output
end)
local T = 3
local t = 0
return function(dt)
t += dt
if t >= T then
t -= T
input(input() == MAX and MIN or MAX)
end
vide.step(dt)
return output()
end
end
--------------------------------------------------------------------------------
local function redraw_block(h: number)
local OFFSET = 70
local BLOCK = "█"
local function remainder_to_block(x)
return
if x > 7/8 then "█"
elseif x > 6/8 then "▇"
elseif x > 5/8 then "▆"
elseif x > 4/8 then "▅"
elseif x > 3/8 then "▄"
elseif x > 2/8 then "▃"
elseif x > 1/8 then "▂"
else "▁"
end
-- math.clamp added just in case an Elastic/Back easing style overshoots below 0
local h_f = math.max(0, math.floor(h))
local reset = "\27[H\27[2J" -- ANSI clear terminal
local offset = string.rep("\n", math.max(0, OFFSET - h_f))
local bar = remainder_to_block(h - h_f) .. "\n" .. string.rep(BLOCK .. "\n", h_f)
print(reset .. offset .. bar .. "\n" .. h)
end
local program_time = os.clock()
local function step(): number
local FPS = 30
local DT = 1/FPS
repeat until os.clock() - program_time >= DT
program_time += DT
return DT
end
local function loop()
local callback = system()
while true do
local dt = step()
local x = callback(dt)
redraw_block(x)
end
end
loop()