Initial commit

This commit is contained in:
aaron 2023-08-08 21:33:11 +01:00
commit cb002f4f27
50 changed files with 4666 additions and 0 deletions

25
src/action.luau Normal file
View file

@ -0,0 +1,25 @@
type Action = {
priority: number,
callback: (Instance) -> ()
}
local ActionMT = {}
local function is_action(v: any)
return getmetatable(v) == ActionMT
end
local function action(callback: (Instance) -> (), priority: number?): Action
local t = {
priority = priority or 1,
callback = callback
}
setmetatable(t :: any, ActionMT)
return t
end
return function()
return action, is_action
end

83
src/apply.luau Normal file
View file

@ -0,0 +1,83 @@
if not game then
script = (require :: any) "test/wrap-require"
typeof = require "test/mock".typeof
end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind)
local _, is_action = require(script.Parent.action)()
local event_buffer: { [string]: () -> () } = {}
local action_buffers = {} :: { { () -> () } }
setmetatable(action_buffers :: any, {
__index = function(_, i: number)
action_buffers[i] = {}
return action_buffers[i]
end
})
local function recurse(instance: Instance, properties: { [unknown]: unknown })
for property, value in properties do
if type(value) == "table" then
if is_action(value) then
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ())
else
recurse(instance, value :: {})
end
elseif type(property) == "string" then
if type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
event_buffer[property] = value :: () -> ()
else
bind.property(instance, property, value :: () -> ())
end
else
(instance :: any)[property] = value
end
elseif type(property) == "number" then
if type(value) == "function" then
bind.children(instance, value :: () -> { Instance })
else
(value :: Instance).Parent = instance
end
end
end
end
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
local parent: unknown = properties.Parent
if parent then properties.Parent = nil end
table.clear(event_buffer)
for _, buffer in next, action_buffers do
table.clear(buffer)
end
recurse(instance, properties)
for event, fn in next, event_buffer do
(instance :: any)[event]:Connect(fn)
end
for _, buffer in next, action_buffers do
for _, callback in next, buffer do
callback()
end
end
if parent then
if type(parent) == "function" then
error("cannot set parent to state")
else
instance.Parent = parent :: Instance
end
end
return instance
end
return apply

125
src/bind.luau Normal file
View file

@ -0,0 +1,125 @@
local warn = warn -- todo
if not game then
script = (require :: any) "test/wrap-require"
warn = print
end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local get = graph.get
local set_effect = graph.set_effect
local capture = graph.capture
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
local hold: { Instance? } = {}
local weak: { Instance? } = setmetatable({}, { __mode = "v" }) :: any
local bindcount = 0
local srcs do
local src1 = debug.info(1, "s")
local srctrunc = string.sub(src1, 1, #src1-4)
srcs = {
src1,
srctrunc .. "apply",
srctrunc .. "create",
}
end
local function traceback() -- ensures trace begins outside of any vide library file
local s = 1
repeat
s += 1
local src = debug.info(s, "s")
until not table.find(srcs, src)
return debug.traceback("", s)
end
function setup(instance: Instance, setter: (Instance) -> ())
if flags.strict then
local fn = setter
local trace = traceback()
setter = function(instance)
local ok, err: string? = pcall(fn, instance)
if not ok then warn(`error occured updating state binding:\n{err}\nset from:{trace}`) end
end
end
local nodes = (capture(setter :: () -> unknown, instance))
for _, node in next, nodes do
set_effect(node, setter, instance)
end
bindcount += 1
local key = bindcount
weak[key] = instance
local function ref()
local _ = setter
local instance = weak[key] :: Instance
hold[key] = instance.Parent and instance or nil -- prevent gc of instance while parented
end
ref()
instance:GetPropertyChangedSignal("Parent"):Connect(ref)
end
-- todo: move `fn` as arg?
local function bind_property(instance: Instance, property: string, fn: () -> unknown)
setup(instance, function(instance_weak: any)
instance_weak[property] = fn()
end)
end
local function bind_parent(instance: Instance, fn: () -> Instance?)
instance.Destroying:Connect(function()
instance= nil :: any -- allow gc when destroyed
end)
setup(instance, function(instance)
local _ = instance -- state will strongly reference instance when parent is bound
instance.Parent = fn()
end)
end
-- todo: could optimize, see: maps.luau values()
local function bind_children(parent: Instance, fn: () -> { Instance })
local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update
local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update
setup(parent, function(parent_weak)
local new_childs = fn() -- all (and only) children that should be parented after this update
if new_childs and type(new_childs) ~= "table" then
throw(`Cannot parent instance of type { type(new_childs) } `)
end
if new_childs then
for _, child in next, new_childs do
new_child_set[child] = true -- record child set from this update
if not current_child_set[child] then
child.Parent = parent_weak -- if child wasn't already parented then parent it
else
current_child_set[child] = nil -- remove child from cache if it was already in cache
end
end
end
for child in next, current_child_set do
child.Parent = nil -- unparent all children that weren't in the new children set
end
table.clear(current_child_set) -- clear cache, preserve capacity
current_child_set, new_child_set = new_child_set, current_child_set
end)
end
return {
property = bind_property,
parent = bind_parent,
children = bind_children,
}

42
src/cleanup.luau Normal file
View file

@ -0,0 +1,42 @@
if not game then script = require "test/wrap-require" end
-- todo: verify correct behavior in non-standard usage
local cleanup_callbacks = {} :: { [string]: () -> () }
local cleanup_callers = {} :: { [string]: () -> () }
setmetatable(cleanup_callers :: any, { __mode = "vs" })
-- todo: rare case where mem address is reused by another function on same line
local function cleanup(callback: () -> ())
local caller = debug.info(2, "f") :: () -> ()
local line = debug.info(2, "l") :: number
local ref = tostring(caller) .. "\0" .. line
local fn = cleanup_callbacks[ref]
if fn then
fn()
else
cleanup_callers[ref] = caller
end
cleanup_callbacks[ref] = callback
end
local buffer = {}
local function clean_garbage()
for ref, callback in next, cleanup_callbacks do
if cleanup_callers[ref] == nil then -- caller was garbage collected
callback()
table.insert(buffer, ref)
end
end
for _, ref in next, buffer do
cleanup_callbacks[ref] = nil
end
table.clear(buffer)
end
return function() return cleanup, clean_garbage end

75
src/create.luau Normal file
View file

@ -0,0 +1,75 @@
if not game then
script = (require :: any) "test/wrap-require"
Instance = require("test/mock").Instance
typeof = require("test/mock").typeof
end
local throw = require(script.Parent.throw)
local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply)
local memoize = require(script.Parent.memoize)
local function createInstance(className: string)
local success, instance: Instance = pcall(Instance.new, className :: any)
if success == false then throw(`invalid class name, could not create instance of class { className }`) end
local default: { [string]: unknown }? = defaults[className]
if default then
for i, v in next, default do
(instance :: any)[i] = v
end
end
return function(properties: { [any]: unknown }): Instance
return apply(instance:Clone(), properties)
end
end; createInstance = memoize(createInstance)
local function cloneInstance(instance: Instance)
return function(properties: { [any]: unknown }): Instance
local clone = instance:Clone()
if not clone then error("Attempt to clone a non-archivable instance", 3) end
return apply(clone, properties)
end
end
local function create(classNameOrInstance: string|Instance)
if type(classNameOrInstance) == "string" then
return createInstance(classNameOrInstance)
elseif typeof(classNameOrInstance) == "Instance" then
return cloneInstance(classNameOrInstance)
else
error("Bad argument #1, expected string or instance, got "..typeof(classNameOrInstance), 2)
end
end
type Props = { [any]: any }
return (create :: any) ::
( <T>(T & Instance) -> (Props) -> T ) &
( ("Folder") -> (Props) -> Folder ) &
( ("BillboardGui") -> (Props) -> BillboardGui ) &
( ("CanvasGroup") -> (Props) -> CanvasGroup ) &
( ("Frame") -> (Props) -> Frame ) &
( ("ImageButton") -> (Props) -> ImageButton ) &
( ("ImageLabel") -> (Props) -> ImageLabel ) &
( ("ScreenGui") -> (Props) -> ScreenGui ) &
( ("ScrollingFrame") -> (Props) -> ScrollingFrame ) &
( ("SurfaceGui") -> (Props) -> SurfaceGui ) &
( ("TextBox") -> (Props) -> TextBox ) &
( ("TextButton") -> (Props) -> TextButton ) &
( ("TextLabel") -> (Props) -> TextLabel ) &
( ("UIAspectRatioConstraint") -> (Props) -> UIAspectRatioConstraint ) &
( ("UICorner") -> (Props) -> UICorner ) &
( ("UIGradient") -> (Props) -> UIGradient ) &
( ("UIGridLayout") -> (Props) -> UIGridLayout ) &
( ("UIListLayout") -> (Props) -> UIListLayout ) &
( ("UIPadding") -> (Props) -> UIPadding ) &
( ("UIPageLayout") -> (Props) -> UIPageLayout ) &
( ("UIScale") -> (Props) -> UIScale ) &
( ("UISizeConstraint") -> (Props) -> UISizeConstraint ) &
( ("UIStroke") -> (Props) -> UIStroke ) &
( ("UITableLayout") -> (Props) -> UITableLayout ) &
( ("UITextSizeConstraint") -> (Props) -> UITextSizeConstraint ) &
( ("VideoFrame") -> (Props) -> VideoFrame ) &
( ("ViewportFrame") -> (Props) -> ViewportFrame ) &
( (string) -> (Props) -> Instance )

109
src/defaults.luau Normal file
View file

@ -0,0 +1,109 @@
-- todo
local Enum = Enum
local Color3 = Color3
local Vector3 = Vector3
if not game then
local mock = require "test/mock"
Enum = mock.Enum :: any
Color3 = mock.Color3 :: any
Vector3 = mock.Vector3 :: any
end
return {
Part = {
Material = Enum.Material.SmoothPlastic,
Size = Vector3.new(1, 1, 1),
Anchored = true
},
BillboardGui = {
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
},
CanvasGroup = nil,
Frame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
},
ImageButton = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false
},
ImageLabel = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
},
ScreenGui = {
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling
},
ScrollingFrame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
ScrollBarImageColor3 = Color3.new(0, 0, 0)
},
SurfaceGui = {
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
PixelsPerStud = 50,
SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
},
TextBox = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
ClearTextOnFocus = false,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0)
},
TextButton = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0)
},
TextLabel = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0)
},
-- UIComponent instances
VideoFrame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
},
ViewportFrame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0
}
}

16
src/derive.luau Normal file
View file

@ -0,0 +1,16 @@
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
local create = graph.create
local get = graph.get
local capture_and_link = graph.capture_and_link
local function derive<T>(fn: () -> T): () -> T
local node, node_get = create((nil :: any) :: T)
node.cache = capture_and_link(node, fn)
return node_get
end
return derive

1
src/flags.luau Normal file
View file

@ -0,0 +1 @@
return { strict = false }

151
src/graph.luau Normal file
View file

@ -0,0 +1,151 @@
if not game then script = (require :: any) "test/wrap-require" end
local flags = require(script.Parent.flags)
export type Node<T> = {
cache: T,
derive: () -> T,
effects: { [(unknown) -> ()]: unknown }, -- weak values
children: { Node<T> } | false -- weak values
}
local reff = false
local refs = {} :: { Node<unknown> }
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
local EVALUATION_ERR = "error while evaluating node:\n\n"
setmetatable(refs :: any, WEAK_VALUES_RESIZABLE)
local check_for_yield do
local t = { __mode = "kv" }
setmetatable(t, t)
check_for_yield = function<T..., U...>(fn: (T...) -> (), ...: any)
local args = { ... }
t.__unm = function()
fn(unpack(args))
end
local ok, err = pcall(function()
return -t :: any
end)
if not ok then
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then
error(EVALUATION_ERR .. "cannot yield when deriving node in watcher", 3)
else
error(EVALUATION_ERR..err, 3)
end
end
end
end
local function set_effect<T>(node: Node<unknown>, fn: (T) -> (), key: T)
node.effects[fn :: () -> ()] = key
end
local function run_effects(node: Node<unknown>)
for effect, key in next, node.effects do
if flags.strict then effect(key) end
effect(key)
end
end
-- retrieves a node's cached value
-- recalculates value if an ancestor was updated
local function get<T>(node: Node<T>): T
if reff then table.insert(refs, node) end
return node.cache
end
local function set_child(parent: Node<unknown>, child: Node<unknown>)
if parent.children then
table.insert(parent.children, child)
else
parent.children = { child }
setmetatable(parent.children :: any, WEAK_VALUES_RESIZABLE)
end
end
-- runs node effects, recalculates descendants and runs descendant effects
local function update(node: Node<unknown>)
run_effects(node)
if node.children then
for _, child in node.children do
if flags.strict then check_for_yield(child.derive) end
child.cache = child.derive()
update(child)
end
end
end
-- sets a node's cached value and updates all descendants
local function set<T>(node: Node<T>, value: T)
node.cache = value
update(node)
end
-- links two nodes as parent-child with a function to compute a new value for child
local function link<T>(parent: Node<unknown>, child: Node<T>, derive: () -> T)
child.derive = derive
set_child(parent, child)
end
-- detect what nodes were referenced in the given callback and returns them in an array
local function capture<T, U>(fn: (U?) -> T, arg: U?): ({ Node<unknown> }, T)
if flags.strict then check_for_yield(fn, arg) end
table.clear(refs)
reff = true
local ok: boolean, result: T|string
if arg == nil then
ok, result = pcall(fn)
else
ok, result = pcall(fn, arg)
end
reff = false
if not ok then error("error while detecting watcher: " .. result :: string, 0) end
return refs, result :: T
end
-- captures and links any detected nodes
local function capture_and_link<T>(child: Node<T>, fn: () -> T): T
local nodes, value = capture(fn, nil)
child.derive = fn
for _, parent: Node<unknown> in next, nodes do
set_child(parent, child)
end
return value :: T
end
local function create<T>(value: T): (Node<T>, () -> T)
local node = {
cache = value,
derive = function() return nil :: any end,
effects = setmetatable({}, WEAK_VALUES_RESIZABLE) :: any,
children = false :: false
}
local function get_value()
return get(node)
end
return node, get_value
end
return table.freeze {
set_effect = set_effect,
get = get,
set = set,
link = link,
capture = capture,
capture_and_link = capture_and_link,
create = create :: (<T>(value: T) -> (Node<T>, () -> T)) & (<T>() -> (Node<T>, () -> T)),
}

71
src/init.luau Normal file
View file

@ -0,0 +1,71 @@
--------------------------------------------------------------------------------
-- vide.luau
-- v0.1.0
--------------------------------------------------------------------------------
if not game then script = (require :: any) "test/wrap-require" end
local create = require(script.create)
local source = require(script.source)
local watch = require(script.watch)
local cleanup, clean_garbage = require(script.cleanup)()
local derive = require(script.derive)
local indexes, values = require(script.maps)()
local spring, update_springs = require(script.spring)()
local action = require(script.action)()
local flags = require(script.flags)
local vide = {
-- core
create = create,
source = source,
watch = watch,
cleanup = cleanup,
derive = derive,
indexes = indexes,
values = values,
-- animations
spring = spring,
-- actions
action = action,
-- flags
strict = (nil :: any) :: boolean,
-- runtime
step = function(dt: number)
-- debug.profilebegin("VIDE STEP")
-- debug.profilebegin("VIDE SPRING")
update_springs(dt)
-- debug.profileend()
-- debug.profilebegin("VIDE GARBAGE CLEANUP")
clean_garbage()
-- debug.profileend()
-- debug.profileend()
end
}
setmetatable(vide :: any, {
__index = function(_, index: unknown)
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2)
end,
__newindex = function(_, index: unknown, value: unknown)
if index == "strict" then
flags.strict = if type(value) == "boolean" then value else error("strict must be a boolean", 2)
else
error(string.format("\"%s\" is not a valid member of vide", tostring(index)), 2)
end
end
})
if game then
game:GetService("RunService").Heartbeat:Connect(function(dt: number)
task.defer(vide.step, dt)
end)
end
return vide

145
src/maps.luau Normal file
View file

@ -0,0 +1,145 @@
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create = graph.create
local set = graph.set
local capture = graph.capture
local link = graph.link
type Map<K, V> = { [K]: V }
-- todo: optimize output array
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local input_cache = {} :: Map<K, VI>
local output_cache = {} :: Map<K, VO>
local input_nodes = {} :: Map<K, Node<VI>>
local remove_queue = {} :: { K }
local output_array = {} :: { VO }
local function recompute(data)
-- queue removed values
for k in next, input_cache do
if data[k] == nil then
table.insert(remove_queue, k)
end
end
-- remove queued values
for _, k in next, remove_queue do
input_cache[k] = nil
output_cache[k] = nil
input_nodes[k] = nil
end
table.clear(remove_queue)
-- process new or changed values
for k, v in next, data do
local cv = input_cache[k]
if cv == nil then
local node, get_value = create(v)
input_nodes[k] = node
output_cache[k] = transform(get_value, k)
input_cache[k] = v
elseif cv ~= v then
set(input_nodes[k], v)
input_cache[k] = v
end
end
-- output elements
table.clear(output_array)
for _, v in next, output_cache do
table.insert(output_array, v)
end
return output_array
end
local function derive()
return recompute(input())
end
local output, output_get = create(nil :: any)
local nodes, value = capture(input)
for _, node in next, nodes do
link(node, output, derive)
end
output.cache = recompute(value)
return output_get
end
-- todo: optimize output array
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
local cur_input_cache_up = {} :: Map<VI, K>
local new_input_cache_up = {} :: Map<VI, K>
local output_cache = {} :: Map<VI, VO>
local input_nodes = {} :: Map<VI, Node<K>>
local output_array = {} :: { VO }
local function recompute(data: Map<K, VI>)
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
-- process data
for i, v in next, data do
new_input_cache[v] = i
local cv = cur_input_cache[v]
if cv == nil then
local node, get_value = create(i)
input_nodes[v] = node
output_cache[v] = transform(v, get_value)
else
if cv ~= i then
set(input_nodes[v], i)
end
cur_input_cache[v] = nil
end
end
-- remove old values
for v in next, cur_input_cache do
output_cache[v] = nil
input_nodes[v] = nil
end
-- update buffer cache
table.clear(cur_input_cache)
cur_input_cache_up, new_input_cache_up = new_input_cache, cur_input_cache
-- output elements
table.clear(output_array)
for _, v in next, output_cache do
table.insert(output_array, v)
end
return output_array
end
local function derive()
return recompute(input())
end
local output, output_get = create(nil :: any)
local nodes, value = capture(input)
for _, node in next, nodes do
link(node, output, derive)
end
output.cache = recompute(value)
return output_get
end
return function() return indexes, values end

17
src/memoize.luau Normal file
View file

@ -0,0 +1,17 @@
local function memoize<X, Y>(f: (X) -> Y): ((X) -> Y, { [X]: Y })
local cache: { [X]: Y } = {}
return function(x: X): Y
local y: Y? = cache[x]
if not y then
y = f(x)
cache[x] = y
end
return y :: Y
end, cache
end
return memoize

25
src/source.luau Normal file
View file

@ -0,0 +1,25 @@
if not game then script = require "test/wrap-require" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create = graph.create
local set = graph.set
type Source<T> = (() -> T) & ((T) -> T)
local function source<T>(value: T): Source<T>
local node, get_value = create(value :: T)
return function(...): T
if select("#", ...) == 0 then return get_value() end
local v = ... :: T
if node.cache == v and type(v) ~= "table" then return v end
set(node, v)
return v
end
end
return source :: (<T>(value: T) -> Source<T>) & (<T>() -> Source<T>)

195
src/spring.luau Normal file
View file

@ -0,0 +1,195 @@
if not game then script = (require :: any) "test/wrap-require" end
--[[
Supported datatypes:
- number
- CFrame
- Color3
- UDim
- UDim2
- Vector2
- Vector3
Unsupported datatypes:
- bool
- Rect
- Vector2int16
- Vector3int16
- EnumItem
]]
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
local create = graph.create
local get = graph.get
local set = graph.set
local set_effect = graph.set_effect
local capture = graph.capture
type Node<T> = graph.Node<T>
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
type SpringData<T> = {
alpha: number,
duration: number,
period: number,
damping_ratio: number,
velocity: number,
initial_velocity: number,
initial_position: T,
target_position: T,
target_updated: boolean,
target: () -> T
}
type Lerp<T> = (initial: T, target: T, alpha: number) -> T
local function solve(T: number, z: number, u: number, t: number): number -- alpha
local wn = 2*math.pi / T
local wd = wn * math.sqrt(1 - z^2)
local a = z * wn
local s = math.exp(-a*t) * math.cos(wd*t)
local v = (u/wn) * math.exp(-a*t) * math.sin(wn*t)
return (1-s) + v
end
local lerpable: { [string]: Lerp<any> } = {
number = function(v1, v2, a)
return v1 + (v2 - v1)*a
end :: Lerp<number>,
CFrame = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<CFrame>,
Color3 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<Color3>,
UDim = function(v1, v2, a)
return UDim.new(
v1.Scale + (v2.Scale - v1.Scale)*a,
v1.Offset + (v2.Offset - v1.Offset)*a
)
end :: Lerp<UDim>,
UDim2 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<UDim2>,
Vector2 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<Vector2>,
Vector3 = function(v1, v2, a)
return v1:Lerp(v2, a)
end :: Lerp<Vector3>,
}
local springs: { [SpringData<any>]: Node<any> } = {}
setmetatable(springs, { __mode = "vs" })
local function spring<T>(target: () -> T, period: number?, damping_ratio: number?): () -> T
local inputs, initial_position = capture(target)
local output, output_get = create(initial_position)
local data: SpringData<T> = {
alpha = 0,
duration = 0,
period = period or 1,
damping_ratio = damping_ratio or 1,
velocity = 0,
initial_velocity = 0,
initial_position = initial_position,
target_position = initial_position,
target_updated = false,
target = target
}
local function input_changed(node)
data.target_updated = true
data.target_position = target()
springs[data] = node
end
for _, input in next, inputs do
set_effect(input, input_changed, output)
end
springs[data] = output
return output_get
end
local remove_queue = {}
local function update_springs(dt: number)
for data, output in next, springs do
if data.target_updated then
data.target_updated = false
data.target_position = data.target()
data.initial_position = get(output)
data.alpha = 0
data.duration = 0
data.initial_velocity = data.velocity
end
local initial_position = data.initial_position
local target_position = data.target_position
local target_type = typeof(target_position)
if target_type ~= typeof(initial_position) then
springs[data] = nil
warn(string.format(
"Mismatched state value types, cancelling state update (initial value: %s, target value: %s)",
typeof(initial_position),
target_type
))
throw(`Cannot tween type { typeof(initial_position) } and { target_type }`)
continue
end
local lerp: Lerp<Animatable> = lerpable[target_type]
if lerp == nil then
springs[data] = nil
throw(`Cannot animate type { target_type }`)
continue
end
local new_time = data.duration + dt
local new_alpha = solve(data.period, data.damping_ratio, data.initial_velocity, new_time)
local new_velocity = -(new_alpha - data.alpha)/dt
local acceleration = (new_velocity - data.velocity)/dt
data.velocity = new_velocity
data.alpha = new_alpha
data.duration = new_time
local value = lerp(initial_position, target_position, new_alpha)
if math.abs(acceleration) < 0.01 then
table.insert(remove_queue, data)
set(output, target_position)
else
set(output, value)
end
end
for _, data in next, remove_queue do
springs[data] = nil
end
table.clear(remove_queue)
end
return function() return spring, update_springs end

11
src/throw.luau Normal file
View file

@ -0,0 +1,11 @@
local function throw(msg: string)
local stack = 1
while debug.info(stack, "s") == debug.info(1, "s") do
stack += 1
end
error(msg, stack)
end
return throw

25
src/watch.luau Normal file
View file

@ -0,0 +1,25 @@
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
local set_effect = graph.set_effect
local capture = graph.capture
local function watch(effect: () -> ()): () -> ()
local nodes = capture(effect :: () -> nil)
nodes = table.clone(nodes)
for _, node in next, nodes do
set_effect(node, effect, true)
end
local function unwatch()
for _, node in next, nodes do
set_effect(node, effect, nil)
end
end
return unwatch
end
return watch