mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Initial commit
This commit is contained in:
commit
cb002f4f27
50 changed files with 4666 additions and 0 deletions
25
src/action.luau
Normal file
25
src/action.luau
Normal 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
83
src/apply.luau
Normal 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
125
src/bind.luau
Normal 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
42
src/cleanup.luau
Normal 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
75
src/create.luau
Normal 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
109
src/defaults.luau
Normal 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
16
src/derive.luau
Normal 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
1
src/flags.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
return { strict = false }
|
||||
151
src/graph.luau
Normal file
151
src/graph.luau
Normal 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
71
src/init.luau
Normal 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
145
src/maps.luau
Normal 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
17
src/memoize.luau
Normal 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
25
src/source.luau
Normal 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
195
src/spring.luau
Normal 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
11
src/throw.luau
Normal 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
25
src/watch.luau
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue