mirror of
https://github.com/centau/vide.git
synced 2026-08-20 23:01:37 +00:00
Initial commit
This commit is contained in:
commit
e76234feb5
52 changed files with 5235 additions and 0 deletions
55
src/Change.lua
Normal file
55
src/Change.lua
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/Change.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local memoize = require(script.Parent.memoize)
|
||||
local bind = require(script.Parent.bind)
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local link = graph.link
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
type Listener = (unknown) -> ()
|
||||
|
||||
local getChangeSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
|
||||
return {
|
||||
priority = 2,
|
||||
run = function(instance: Instance, listener: MaybeState<Listener>)
|
||||
local event: RBXScriptSignal<nil> = instance:GetPropertyChangedSignal(name)
|
||||
|
||||
if type(listener) == "function" then
|
||||
event:Connect(function()
|
||||
listener( (instance :: any)[name] )
|
||||
end)
|
||||
elseif wrapped(listener) then
|
||||
local state = create(nil)
|
||||
|
||||
link(listener :: State<Listener>, state, function()
|
||||
local newListener = get(listener :: State<Listener>)
|
||||
return newListener and function()
|
||||
newListener( (instance :: any)[name] )
|
||||
end
|
||||
end)
|
||||
|
||||
state.updated = true
|
||||
bind.event(state :: State<any>, instance, event)
|
||||
else
|
||||
error("Attempt to connect non-function to changed event", 2)
|
||||
end
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
local Changed = table.freeze(setmetatable({}, {__index = function(_, index: string)
|
||||
return getChangeSymbol(index)
|
||||
end})) :: any
|
||||
|
||||
return Changed :: { [string]: unknown }
|
||||
39
src/Children.lua
Normal file
39
src/Children.lua
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/Children.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then
|
||||
script = (require :: any) "test/wrap-require"
|
||||
typeof = require "test/mock".typeof
|
||||
end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
type Children = Types.Children
|
||||
|
||||
local function setChildren(instance: Instance, children: Children)
|
||||
if typeof(children) == "Instance" then
|
||||
if children.Parent then throw(`Cannot parent instance { instance.Name }, instance already parented`) end
|
||||
children.Parent = instance
|
||||
elseif wrapped(children) then
|
||||
bind.children(children :: any, instance )
|
||||
elseif type(children) == "table" then
|
||||
for _, child: Children in next, children do
|
||||
setChildren(instance, child)
|
||||
end
|
||||
else
|
||||
throw(`Cannot parent non-instance { typeof(children) }`)
|
||||
end
|
||||
end
|
||||
|
||||
local Children = {
|
||||
priority = 1,
|
||||
run = setChildren
|
||||
} :: Types.Symbol<Children>
|
||||
|
||||
return Children :: unknown
|
||||
16
src/Created.lua
Normal file
16
src/Created.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/Created.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
local Created = {
|
||||
priority = 3,
|
||||
run = function(instance: Instance, callback: (Instance) -> ())
|
||||
callback(instance)
|
||||
end
|
||||
} :: Types.Symbol<(Instance) -> ()>
|
||||
|
||||
return Created :: unknown
|
||||
41
src/Event.lua
Normal file
41
src/Event.lua
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/Event.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local memoize = require(script.Parent.memoize)
|
||||
local bind = require(script.Parent.bind)
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
type Listener = (unknown) -> ()
|
||||
|
||||
local getEventSymbol = memoize(function(name: string): Types.Symbol<MaybeState<Listener>>
|
||||
return {
|
||||
priority = 2,
|
||||
run = function(instance: Instance, listener: MaybeState<Listener>)
|
||||
local event: RBXScriptSignal<...unknown> = (instance :: any)[name]
|
||||
|
||||
if type(listener) == "function" then
|
||||
event:Connect(listener)
|
||||
elseif wrapped(listener) then
|
||||
bind.event(listener :: State<Listener>, instance, event)
|
||||
else
|
||||
error("Attempt to connect non-function to event", 2)
|
||||
end
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
local Event = table.freeze(setmetatable({}, {__index = function(_, index: string)
|
||||
return getEventSymbol(index)
|
||||
end})) :: any
|
||||
|
||||
return Event :: { [string]: unknown }
|
||||
|
||||
46
src/Layout.lua
Normal file
46
src/Layout.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/Layout.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local flags = require(script.Parent.flags)
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
local layoutProperties = {
|
||||
Parent = true,
|
||||
AnchorPoint = true,
|
||||
LayoutOrder = true,
|
||||
Position = true,
|
||||
Rotation = true,
|
||||
Size = true,
|
||||
SizeConstraint = true,
|
||||
Visible = true,
|
||||
ZIndex = true
|
||||
}
|
||||
|
||||
local function setLayout(instance: Instance, properties: { [string]: unknown })
|
||||
for property, value in next, properties do
|
||||
if flags.strict and not layoutProperties[property] then
|
||||
throw(`{ property } is not a valid layout property`)
|
||||
end
|
||||
|
||||
if wrapped(value) then
|
||||
bind.property(value :: graph.State<unknown>, instance, property)
|
||||
else
|
||||
(instance :: any)[property] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Layout = {
|
||||
priority = 1,
|
||||
run = setLayout
|
||||
} :: Types.Symbol<{ [string]: unknown }>
|
||||
|
||||
return Layout :: unknown
|
||||
19
src/Types.lua
Normal file
19
src/Types.lua
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/Types.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
|
||||
export type Symbol<T> = {
|
||||
priority: number,
|
||||
run: (Instance, T) -> ()
|
||||
}
|
||||
|
||||
export type Children = Instance | State<Children> | { Children }
|
||||
|
||||
return {}
|
||||
15
src/apply.lua
Normal file
15
src/apply.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/apply.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local applyProperties = require(script.Parent.applyProperties)
|
||||
|
||||
local function apply<T>(instance: T & Instance)
|
||||
return function(properties: { [any]: unknown }): T
|
||||
return applyProperties(instance, properties)
|
||||
end
|
||||
end
|
||||
|
||||
return apply
|
||||
66
src/applyProperties.lua
Normal file
66
src/applyProperties.lua
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/applyProperties.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local bind = require(script.Parent.bind)
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
local function applyProperty(instance: Instance, property: string, value: unknown)
|
||||
|
||||
end
|
||||
|
||||
local function applyProperties<T>(instance: T & Instance, properties: { [string|Types.Symbol<unknown> ]: unknown }): T
|
||||
local parent: unknown = properties.Parent
|
||||
if parent then properties.Parent = nil end
|
||||
|
||||
local eventBuffer: { [(Instance, () -> ()) -> ()]: () -> () } = {} -- connect events after setting properties
|
||||
local postCreation: (Instance) -> ()? = nil -- buffer for post-creation callback
|
||||
|
||||
for property, value in next, properties do
|
||||
if type(property) == "string" then
|
||||
if wrapped(value) then
|
||||
bind.property(value :: State<unknown>, instance, property)
|
||||
else
|
||||
(instance :: any)[property] = value
|
||||
end
|
||||
elseif type(property) == "table" then
|
||||
local priority = property.priority
|
||||
if priority == 1 then
|
||||
property.run(instance, value)
|
||||
elseif priority == 2 then
|
||||
eventBuffer[property.run] = value :: () -> ()
|
||||
elseif priority == 3 then
|
||||
assert(not postCreation)
|
||||
postCreation = value :: () -> ()
|
||||
else
|
||||
error("invalid priority")
|
||||
end
|
||||
else throw(`Invalid property { tostring(property) }, expected string or symbol`) end
|
||||
end
|
||||
|
||||
for fn, v in next, eventBuffer do
|
||||
fn(instance, v)
|
||||
end
|
||||
|
||||
if parent then
|
||||
applyProperty(instance, "Parent", parent)
|
||||
if wrapped(parent) then
|
||||
bind.parent(parent :: State<Instance?>, instance)
|
||||
else
|
||||
instance.Parent = parent :: Instance
|
||||
end
|
||||
end
|
||||
|
||||
if postCreation then postCreation(instance) end
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
return applyProperties
|
||||
139
src/bind.lua
Normal file
139
src/bind.lua
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/bind.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
local warn = warn -- todo
|
||||
|
||||
if not game then
|
||||
script = (require :: any) "test/wrap-require"
|
||||
warn = print
|
||||
end
|
||||
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
local get = graph.get
|
||||
local setEffect = graph.setEffect
|
||||
|
||||
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 .. "applyProperties",
|
||||
srctrunc .. "create",
|
||||
srctrunc .. "apply"
|
||||
}
|
||||
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(state: State<any>, instance: Instance, updateInstance: (Instance) -> ())
|
||||
if flags.strict then
|
||||
local fn = updateInstance
|
||||
local trace = traceback()
|
||||
updateInstance = 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
|
||||
|
||||
updateInstance(instance)
|
||||
setEffect(state, updateInstance, instance)
|
||||
|
||||
bindcount += 1
|
||||
local key = bindcount
|
||||
|
||||
weak[key] = instance
|
||||
|
||||
local function ref()
|
||||
local _ = state -- prevent gc of state while instance exists
|
||||
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
|
||||
|
||||
local function bindProperty(state: State<unknown>, instance_STRONG: Instance, property: string)
|
||||
setup(state, instance_STRONG, function(instance)
|
||||
(instance :: any)[property] = get(state)
|
||||
end)
|
||||
end
|
||||
|
||||
local function bindParent(state: State<Instance?>, instance_STRONG)
|
||||
instance_STRONG.Destroying:Connect(function()
|
||||
instance_STRONG = nil :: any -- allow gc when destroyed
|
||||
end)
|
||||
setup(state, instance_STRONG, function(instance)
|
||||
local _ = instance_STRONG -- state will strongly reference instance when parent is bound
|
||||
instance.Parent = get(state)
|
||||
end)
|
||||
end
|
||||
|
||||
local function bindChildren(state: State<{ Instance }?>, parent_STRONG: Instance)
|
||||
local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update
|
||||
local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update
|
||||
|
||||
setup(state, parent_STRONG, function(parent)
|
||||
local newChildren = get(state) -- all (and only) children that should be parented after this update
|
||||
if newChildren and type(newChildren) ~= "table" then
|
||||
throw(`Cannot parent instance of type { type(newChildren) } `)
|
||||
end
|
||||
|
||||
if newChildren then
|
||||
for _, child in next, newChildren do
|
||||
newChildrenSet[child] = true -- record child set from this update
|
||||
if not currentChildrenSet[child] then
|
||||
child.Parent = parent -- if child wasn't already parented then parent it
|
||||
else
|
||||
currentChildrenSet[child] = nil -- remove child from cache if it was already in cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for child in next, currentChildrenSet do
|
||||
child.Parent = nil -- unparent all children that weren't in the new children set
|
||||
end
|
||||
|
||||
table.clear(currentChildrenSet) -- clear cache, preserve capacity
|
||||
currentChildrenSet, newChildrenSet = newChildrenSet, currentChildrenSet
|
||||
end)
|
||||
end
|
||||
|
||||
local function bindEvent(state: State<() -> ()?>, instance_STRONG: Instance, event: RBXScriptSignal)
|
||||
local current: RBXScriptConnection? = nil
|
||||
setup(state, instance_STRONG, function(instance)
|
||||
if current then
|
||||
current:Disconnect()
|
||||
current = nil
|
||||
end
|
||||
local listener = get(state)
|
||||
if listener then
|
||||
current = event:Connect(listener)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
property = bindProperty,
|
||||
parent = bindParent,
|
||||
children = bindChildren,
|
||||
event = bindEvent
|
||||
}
|
||||
78
src/create.lua
Normal file
78
src/create.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/create.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then
|
||||
script = (require :: any) "test/wrap-require"
|
||||
Instance = require("test/mock").Instance
|
||||
end
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local defaults = require(script.Parent.defaults)
|
||||
local applyProperties = require(script.Parent.applyProperties)
|
||||
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 applyProperties(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 applyProperties(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 )
|
||||
112
src/defaults.lua
Normal file
112
src/defaults.lua
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/defaults.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- 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
|
||||
}
|
||||
}
|
||||
32
src/derive.lua
Normal file
32
src/derive.lua
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/derive.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type Unwrapper = graph.Unwrapper
|
||||
local create = graph.create
|
||||
local captureAndLink = graph.captureAndLink
|
||||
|
||||
local function derive<T>(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): State<T>
|
||||
local node = create((nil :: any) :: T)
|
||||
|
||||
if cleanup then
|
||||
local fn = deriveValue
|
||||
local last: T? = nil
|
||||
deriveValue = function(from)
|
||||
if last ~= nil then cleanup(last) end
|
||||
last = fn(from)
|
||||
return last :: T
|
||||
end
|
||||
end
|
||||
|
||||
local value: T = captureAndLink(node, deriveValue)
|
||||
node.cache = value
|
||||
|
||||
return node :: State<T>
|
||||
end
|
||||
|
||||
return derive
|
||||
24
src/each.lua
Normal file
24
src/each.lua
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/each.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type Node<T> = graph.Node<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local wrapped = graph.wrapped
|
||||
local link = graph.link
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local function each<K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
|
||||
|
||||
end
|
||||
|
||||
return (each :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
|
||||
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
|
||||
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
|
||||
-> State<Map<K, VO>> )
|
||||
5
src/flags.lua
Normal file
5
src/flags.lua
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
--------------------------------------------------------------------------------------------------------------------------------
|
||||
-- vide/flags.lua
|
||||
--------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
return { strict = false }
|
||||
231
src/graph.lua
Normal file
231
src/graph.lua
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/graph.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
export type State<T> = typeof(setmetatable(
|
||||
{} :: {
|
||||
cache: T,
|
||||
updated: boolean,
|
||||
derive: (any) -> T,
|
||||
effects: { [(unknown) -> ()]: unknown } | false, -- weak values
|
||||
children: { State<T> } | false -- weak values
|
||||
}, {} :: {
|
||||
__concat: (any, any) -> any,
|
||||
__add: (any, any) -> any,
|
||||
__sub: (any, any) -> any,
|
||||
__mul: (any, any) -> any,
|
||||
__div: (any, any) -> any,
|
||||
--__pow
|
||||
--__mod
|
||||
--__unm
|
||||
--__eq: (unknown, unknown) -> State<boolean>
|
||||
--__lt
|
||||
--__le
|
||||
}
|
||||
))
|
||||
|
||||
export type MaybeState<T> = State<T> | T
|
||||
export type Unwrapper = <T>(T) -> T
|
||||
|
||||
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
|
||||
local EVALUATION_ERR = "Error while evaluating state:\n\n"
|
||||
|
||||
local State = {}
|
||||
|
||||
local function wrapped(value: any): boolean
|
||||
return getmetatable(value) == State
|
||||
end
|
||||
|
||||
local unwrap: <T>(T) -> T;
|
||||
|
||||
local checkForYield do
|
||||
local t = { __mode = "kv" }
|
||||
setmetatable(t, t)
|
||||
|
||||
checkForYield = function(fn: (Unwrapper) -> ())
|
||||
t.__unm = function()
|
||||
fn(unwrap)
|
||||
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 state in watcher", 3)
|
||||
else
|
||||
error(EVALUATION_ERR..err, 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function setEffect<T>(state: State<unknown>, fn: (T) -> (), key: T)
|
||||
if not state.effects then
|
||||
state.effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any
|
||||
else
|
||||
state.effects[fn :: () -> ()] = key
|
||||
end
|
||||
end
|
||||
|
||||
local function runEffects(state: State<unknown>)
|
||||
if state.effects then
|
||||
for effect, key in next, state.effects do
|
||||
if flags.strict then effect(key) end
|
||||
effect(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- retrieves a state's cached value
|
||||
-- recalculates value if an ancestor was updated
|
||||
local function get<T>(state: State<T>): T
|
||||
if state.updated then
|
||||
state.updated = false
|
||||
|
||||
if flags.strict then checkForYield(state.derive) end
|
||||
|
||||
local ok, result: T|string? = pcall(state.derive, unwrap); if ok then
|
||||
rawset(state :: any, "cache", result :: T)
|
||||
else error(EVALUATION_ERR .. result :: string, 2) end
|
||||
end
|
||||
|
||||
return state.cache
|
||||
end
|
||||
|
||||
-- utility function for retrieving a value from state and allowing passthrough of non-state
|
||||
unwrap = function<T>(value: MaybeState<T>): T
|
||||
if wrapped(value) then
|
||||
return get(value :: State<T>)
|
||||
else
|
||||
return value :: T
|
||||
end
|
||||
end
|
||||
|
||||
local function addChild(parent: State<unknown>, child: State<unknown>)
|
||||
if parent.children then
|
||||
table.insert(parent.children, child)
|
||||
else
|
||||
parent.children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any
|
||||
end
|
||||
end
|
||||
|
||||
-- marks all state descendants for recalculation and runs effects
|
||||
local function update(state: State<unknown>)
|
||||
runEffects(state)
|
||||
if state.children then
|
||||
for _, child in state.children do
|
||||
if not child.updated then
|
||||
child.updated = true
|
||||
update(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- sets a state's cached value and updates all descendants
|
||||
local function set<T>(state: State<T>, value: T)
|
||||
state.cache = value
|
||||
update(state)
|
||||
end
|
||||
|
||||
-- links two states as parent-child
|
||||
local function link(parent: State<unknown>, child: State<unknown>, derive: () -> unknown)
|
||||
child.derive = derive
|
||||
addChild(parent, child)
|
||||
end
|
||||
|
||||
-- detect what states were referenced in the given callback and returns them in an array
|
||||
local function capture<T>(callback: (Unwrapper) -> T): ({ State<unknown> }, T)
|
||||
if flags.strict then checkForYield(callback) end
|
||||
|
||||
local states = table.create(2)
|
||||
|
||||
local ok: boolean, result: T|string = pcall(callback, function<T>(value: MaybeState<T>): T
|
||||
if wrapped(value) then
|
||||
table.insert(states, value :: State<T>)
|
||||
return get(value :: State<T>)
|
||||
else
|
||||
return value :: T
|
||||
end
|
||||
end)
|
||||
|
||||
if not ok then error("Error while detecting watcher: " .. result :: string, 2) end
|
||||
|
||||
return states, result :: T
|
||||
end
|
||||
|
||||
-- captures and links any detected states
|
||||
local function captureAndLink<T>(child: State<T>, callback: (Unwrapper) -> T): T
|
||||
local states, value = capture(callback)
|
||||
|
||||
child.derive = callback
|
||||
for _, parent: State<unknown> in next, states do
|
||||
addChild(parent, child)
|
||||
end
|
||||
|
||||
return value :: T
|
||||
end
|
||||
|
||||
local create: <T>(value: T) -> State<T>
|
||||
|
||||
-- factory function for creating operator overloads for shorthands to derive state
|
||||
local function overload(op: (unknown, unknown) -> unknown): (any, any) -> any
|
||||
return function(a: MaybeState<unknown>, b: MaybeState<unknown>): State<unknown>
|
||||
local derived: State<unknown> = create(nil :: any)
|
||||
|
||||
local aIsState = wrapped(a)
|
||||
local bIsState = wrapped(b)
|
||||
|
||||
if aIsState and bIsState then
|
||||
local function derive() return op(get(a :: State<unknown>), get(b :: State<unknown>)) end
|
||||
link(a :: State<unknown>, derived, derive)
|
||||
link(b :: State<unknown>, derived, derive)
|
||||
elseif aIsState then
|
||||
link(a :: State<unknown>, derived, function() return op(get(a :: State<unknown>), b) end)
|
||||
else--if bIsState then
|
||||
link(b :: State<unknown>, derived, function() return op(a, get(b :: State<unknown>)) end)
|
||||
end
|
||||
|
||||
derived.updated = true
|
||||
return derived
|
||||
end
|
||||
end
|
||||
|
||||
function State.__index(_, index)
|
||||
if index == "cache" then return nil end -- todo: better solution
|
||||
error("attempt to index state", 2)
|
||||
end
|
||||
|
||||
State.__concat = overload(function(a: any, b: any) return tostring(a) .. tostring(b) end)
|
||||
State.__add = overload(function(a: any, b: any) return a + b end)
|
||||
State.__sub = overload(function(a: any, b: any) return a - b end)
|
||||
State.__mul = overload(function(a: any, b: any) return a * b end)
|
||||
State.__div = overload(function(a: any, b: any) return a / b end)
|
||||
--State.__eq = overload(function(a: any, b: any) return a == b end)
|
||||
|
||||
function create<T>(value: T): State<T>
|
||||
return setmetatable({
|
||||
cache = value,
|
||||
updated = false,
|
||||
derive = function() return nil end :: any,
|
||||
effects = false :: false,
|
||||
children = false :: false
|
||||
}, State)
|
||||
end
|
||||
|
||||
return table.freeze {
|
||||
setEffect = setEffect,
|
||||
get = get,
|
||||
set = set,
|
||||
unwrap = unwrap,
|
||||
link = link,
|
||||
capture = capture,
|
||||
captureAndLink = captureAndLink,
|
||||
wrapped = wrapped,
|
||||
create = create,
|
||||
}
|
||||
84
src/init.lua
Normal file
84
src/init.lua
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide.lua
|
||||
-- v0.1.0
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local create = require(script.create)
|
||||
local apply = require(script.apply)
|
||||
local wrap = require(script.wrap)
|
||||
local watch = require(script.watch)
|
||||
local derive = require(script.derive)
|
||||
local map = require(script.map)
|
||||
|
||||
local unwrap = require(script.unwrap)
|
||||
local wrapped = require(script.wrapped)
|
||||
|
||||
local Layout = require(script.Layout)
|
||||
local Children = require(script.Children)
|
||||
local Event = require(script.Event)
|
||||
local Changed = require(script.Change)
|
||||
local Created = require(script.Created)
|
||||
|
||||
local spring, updateSprings = require(script.spring)()
|
||||
|
||||
local flags = require(script.flags)
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
type Unwrapper = <T>(T) -> T
|
||||
type Setter<T> = ( (new: T, force: true?) -> T ) & ( (update: (old: T) -> T, force: true?) -> T )
|
||||
|
||||
local vide = {
|
||||
-- core
|
||||
create = create,
|
||||
apply = apply,
|
||||
wrap = (wrap :: any) :: <T>(value: T?) -> (T, Setter<T>),
|
||||
derive = (derive :: any) :: <T>(deriver: (from: <U>(U) -> U) -> T, cleanup: (T) -> ()?) -> T,
|
||||
map = (map :: any) :: ( <V>(input: number, transform: (number) -> V, cleanup: (V) -> ()?) -> Map<number, V> ) & ( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?) -> Map<K, VO> ),
|
||||
watch = watch :: ((Unwrapper) -> ()) -> () -> (),
|
||||
|
||||
-- util
|
||||
unwrap = unwrap,
|
||||
wrapped = wrapped,
|
||||
|
||||
-- animations
|
||||
spring = (spring :: any) :: <T>(input: T, period: number, damping: number?) -> T,
|
||||
|
||||
-- symbols
|
||||
Event = Event,
|
||||
Changed = Changed,
|
||||
Layout = Layout,
|
||||
Children = Children,
|
||||
Created = Created,
|
||||
|
||||
-- flags
|
||||
strict = (nil :: any) :: boolean,
|
||||
|
||||
-- test
|
||||
step = function(dt: number)
|
||||
updateSprings(dt)
|
||||
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
|
||||
76
src/map.lua
Normal file
76
src/map.lua
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/map.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local wrapped = graph.wrapped
|
||||
local link = graph.link
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local function map<K, VI, VO>(input: unknown, transform: (K, VI) -> VO, cleanup: (VO) -> ()?): (MaybeState<Map<K, VO>>)
|
||||
if type(input) == "number" then
|
||||
local output: Map<K, VO> = table.create(input) :: {}
|
||||
|
||||
for i = 1, input do
|
||||
local v = transform(i :: any, nil :: any)
|
||||
output[i :: any] = v
|
||||
end
|
||||
|
||||
return output
|
||||
elseif wrapped(input) then
|
||||
local lastInput: Map<K, VI> = {}
|
||||
local lastOutput: Map<K, VO> = {}
|
||||
local output = create(lastOutput)
|
||||
|
||||
local function derive()
|
||||
local newInput = get(input :: State<Map<K, VI>>)
|
||||
if type(newInput) ~= "table" then error("Attempt to run map on a non-table state", 0) end
|
||||
|
||||
local lastInputClone = table.clone(lastInput)
|
||||
|
||||
for k, vi in next, newInput do
|
||||
if vi ~= lastInputClone[k] then
|
||||
local vo = transform(k, vi)
|
||||
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
|
||||
lastInput[k] = vi
|
||||
lastOutput[k] = vo
|
||||
end
|
||||
lastInputClone[k] = nil
|
||||
end
|
||||
|
||||
for k, v in next, lastInputClone do
|
||||
lastInput[k] = nil
|
||||
if cleanup and lastOutput[k] then cleanup(lastOutput[k]) end
|
||||
lastOutput[k] = nil
|
||||
end
|
||||
|
||||
return table.clone(lastOutput)
|
||||
end
|
||||
|
||||
link(input :: State<Map<K, VI>>, output, derive)
|
||||
output.updated = true
|
||||
|
||||
return output
|
||||
elseif type(input) == "table" then
|
||||
local output: Map<K, VO> = table.create(#input :: any) :: {}
|
||||
|
||||
for k, vi in input :: Map<K, VI> do
|
||||
local vo = transform(k, vi)
|
||||
output[k] = vo
|
||||
end
|
||||
|
||||
return output
|
||||
else error(string.format("Invalid type arg #1, expected number or table or state (got %s)", tostring(input)), 2) end
|
||||
end
|
||||
|
||||
return (map :: any) :: ( <V>(input: number, transform: (number) -> V) -> Map<number, V> ) &
|
||||
( <K, VI, VO>(input: Map<K, VI>, transform: (K, VI) -> VO) -> Map<K, VO> ) &
|
||||
( <K, VI, VO>(input: State<Map<K, VI>>, transform: (K, VI) -> VO, cleanup: (VO) -> ()?)
|
||||
-> State<Map<K, VO>> )
|
||||
21
src/memoize.lua
Normal file
21
src/memoize.lua
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/memoize.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
169
src/spring.lua
Normal file
169
src/spring.lua
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/spring.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
--[[
|
||||
Spring animation library adapted from RDL::spring v1.0
|
||||
|
||||
Supported datatypes:
|
||||
*number
|
||||
!bool
|
||||
*CFrame
|
||||
?Rect
|
||||
*Color3
|
||||
*UDim
|
||||
*UDim2
|
||||
*Vector2
|
||||
!Vector2int16
|
||||
*Vector3
|
||||
!Vector3int16
|
||||
!EnumItem
|
||||
]]
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local set = graph.set
|
||||
local unwrap = graph.unwrap
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
|
||||
|
||||
type SpringData<T> = {
|
||||
Alpha: number;
|
||||
Duration: number;
|
||||
Period: number;
|
||||
Damping: number;
|
||||
Velocity: number;
|
||||
InitialVelocity: number;
|
||||
Initial: T;
|
||||
Target: T;
|
||||
Input: State<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 activeSprings: { [State<any>]: SpringData<any> } = {}
|
||||
setmetatable(activeSprings, { __mode = "ks" })
|
||||
|
||||
local function spring<T>(input: MaybeState<T>, period: number, damping: number?): State<T>
|
||||
local initial = unwrap(input) :: T
|
||||
local output = create(initial)
|
||||
|
||||
local springData: SpringData<T> = {
|
||||
Alpha = 0,
|
||||
Duration = 0,
|
||||
Period = period,
|
||||
Damping = damping or 1,
|
||||
Velocity = 0,
|
||||
InitialVelocity = 0,
|
||||
Initial = initial,
|
||||
Target = initial,
|
||||
Input = input :: any,
|
||||
LastInput = initial,
|
||||
LastOutput = initial
|
||||
}
|
||||
|
||||
activeSprings[output] = springData
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
local function updateSprings(dt: number)
|
||||
for node, data in next, activeSprings do
|
||||
local currentTarget = get(data.Input)
|
||||
if currentTarget ~= data.Target then
|
||||
data.Target = currentTarget
|
||||
data.Initial = get(node)
|
||||
data.Target = get(data.Input)
|
||||
data.Alpha = 0
|
||||
data.Duration = 0
|
||||
data.InitialVelocity = data.Velocity
|
||||
end
|
||||
|
||||
local initial: Animatable = data.Initial
|
||||
local target: Animatable = data.Target
|
||||
local targetType: string = typeof(target)
|
||||
|
||||
if targetType ~= typeof(initial) then
|
||||
activeSprings[node] = nil
|
||||
warn(string.format(
|
||||
"Mismatched state value types, cancelling state update (initial value: %s, target value: %s)",
|
||||
typeof(initial),
|
||||
targetType
|
||||
))
|
||||
throw(`Cannot tween type { typeof(initial) } and { targetType }`)
|
||||
continue
|
||||
end
|
||||
|
||||
local lerp: Lerp<Animatable> = lerpable[targetType]
|
||||
|
||||
if lerp == nil then
|
||||
activeSprings[node] = nil
|
||||
throw(`Cannot animate type { targetType }`)
|
||||
continue
|
||||
end
|
||||
|
||||
local newTime = data.Duration + dt
|
||||
local newAlpha = solve(data.Period, data.Damping, data.InitialVelocity, newTime)
|
||||
|
||||
data.Velocity = -(newAlpha - data.Alpha)/dt
|
||||
data.Alpha = newAlpha
|
||||
data.Duration = newTime
|
||||
|
||||
local value = lerp(initial, target, newAlpha)
|
||||
|
||||
set(node, value)
|
||||
end
|
||||
end
|
||||
|
||||
return function() return spring, updateSprings end
|
||||
15
src/throw.lua
Normal file
15
src/throw.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/throw.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
9
src/unwrap.lua
Normal file
9
src/unwrap.lua
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/unwrap.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
return graph.unwrap
|
||||
36
src/watch.lua
Normal file
36
src/watch.lua
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
--------------------------------------------------------------------------------------------------------------
|
||||
-- vide/watch.lua
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type Unwrapper = graph.Unwrapper
|
||||
local setEffect = graph.setEffect
|
||||
local capture = graph.capture
|
||||
local unwrap = graph.unwrap
|
||||
|
||||
local function watch(effect: (Unwrapper) -> ()): () -> ()
|
||||
local states, cleanup = capture(effect :: () -> (() -> ()?))
|
||||
|
||||
local function fn()
|
||||
if cleanup then cleanup(); cleanup = nil end
|
||||
cleanup = effect(unwrap)
|
||||
end
|
||||
|
||||
for _, state in next, states do
|
||||
setEffect(state, fn, true)
|
||||
end
|
||||
|
||||
local function unwatch()
|
||||
for _, state in next, states do
|
||||
setEffect(state, fn, nil)
|
||||
end
|
||||
if cleanup then cleanup(); cleanup = nil end
|
||||
end
|
||||
|
||||
return unwatch
|
||||
end
|
||||
|
||||
return watch
|
||||
42
src/wrap.lua
Normal file
42
src/wrap.lua
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/wrap.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
type State<T> = graph.State<T>
|
||||
type MaybeState<T> = graph.MaybeState<T>
|
||||
local create = graph.create
|
||||
local get = graph.get
|
||||
local set = graph.set
|
||||
local wrapped = graph.wrapped
|
||||
|
||||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
type Setter<T> = (value: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?) -> T
|
||||
|
||||
local function wrap<T>(value: MaybeState<T>?): (State<T>, Setter<T>)
|
||||
local state = create(if wrapped(value) then get(value :: State<T>) else value :: T)
|
||||
|
||||
local function setter(vi: MaybeState<T> | (MaybeState<T>) -> MaybeState<T>, force: boolean?): T
|
||||
if type(vi) == "function" then
|
||||
vi = vi(get(state))
|
||||
end
|
||||
|
||||
local v = if wrapped(vi) then get(vi :: State<T>) else vi :: T
|
||||
|
||||
if v ~= state.cache or force then
|
||||
set(state, v)
|
||||
elseif flags.strict and type(v) == "table" then
|
||||
throw("attempt to set same table object")
|
||||
end
|
||||
|
||||
return v
|
||||
end
|
||||
|
||||
return state, setter
|
||||
end
|
||||
|
||||
return wrap
|
||||
9
src/wrapped.lua
Normal file
9
src/wrapped.lua
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
------------------------------------------------------------------------------------------
|
||||
-- vide/wrapped.lua
|
||||
------------------------------------------------------------------------------------------
|
||||
|
||||
if not game then script = (require :: any) "test/wrap-require" end
|
||||
|
||||
local graph = require(script.Parent.graph)
|
||||
|
||||
return graph.wrapped
|
||||
Loading…
Add table
Add a link
Reference in a new issue