diff --git a/src/Animation/Spr.luau b/src/Animation/Spr.luau new file mode 100644 index 0000000..b92c608 --- /dev/null +++ b/src/Animation/Spr.luau @@ -0,0 +1,838 @@ +--!strict +--!native +--------------------------------------------------------------------- +-- spr - Spring-driven motion library +-- +-- Copyright (c) 2024 Fractality. All rights reserved. +-- Released under the MIT license. +-- +-- Docs & license can be found at https://github.com/Fraktality/spr +-- +-- API Summary: +-- +-- spr.target( +-- Instance obj, +-- number dampingRatio, +-- number undampedFrequency, +-- dict targetProperties) +-- +-- Animates the given properties towardes the target values, +-- given damping ratio and undamped frequency. +-- +-- +-- spr.stop( +-- Instance obj[, +-- string property]) +-- +-- Stops the specified property on an Instance from animating. +-- If no property is specified, all properties of the Instance +-- will stop animating. +-- +-- Visualizer: https://www.desmos.com/calculator/rzvw27ljh9 +--------------------------------------------------------------------- + +local STRICT_RUNTIME_TYPES = true -- assert on parameter and property type mismatch +local SLEEP_OFFSET_SQ_LIMIT = (1/3840)^2 -- square of the offset sleep limit +local SLEEP_VELOCITY_SQ_LIMIT = 1e-2^2 -- square of the velocity sleep limit +local SLEEP_ROTATION_DIFF = math.rad(0.01) -- rad +local SLEEP_ROTATION_VELOCITY = math.rad(0.1) -- rad/s +local EPS = 1e-5 -- epsilon for stability checks around pathological frequency/damping values + +local RunService: RunService = game:GetService("RunService") + +local pi = math.pi +local exp = math.exp +local sin = math.sin +local cos = math.cos +local min = math.min +local max = math.max +local sqrt = math.sqrt +local atan2 = math.atan2 +local round = math.round + +local function magnitudeSq(vec: {number}) + local out = 0 + for _, v in vec do + out += v^2 + end + return out +end + +local function distanceSq(vec0: {number}, vec1: {number}) + local out = 0 + for i0, v0 in vec0 do + out += (vec1[i0] - v0)^2 + end + return out +end + +type TypeMetadata = { + springType: (dampingRatio: number, frequency: number, pos: number, typedat: TypeMetadata, rawTarget: T) -> LinearSpring, + toIntermediate: (T) -> {number}, + fromIntermediate: ({number}) -> T, +} + +-- Spring for an array of linear values +local LinearSpring = {} + +type LinearSpring = typeof(setmetatable({} :: { + d: number, + f: number, + g: {number}, + p: {number}, + v: {number}, + typedat: TypeMetadata, + rawTarget: T, +}, LinearSpring)) + +do + LinearSpring.__index = LinearSpring + + function LinearSpring.new(dampingRatio: number, frequency: number, pos: T, rawGoal: T, typedat) + local linearPos = typedat.toIntermediate(pos) + return setmetatable( + { + d = dampingRatio, + f = frequency, + g = linearPos, + p = linearPos, + v = table.create(#linearPos, 0), + typedat = typedat, + rawGoal = rawGoal + }, + LinearSpring + ) + end + + function LinearSpring.setGoal(self, goal: T) + self.rawGoal = goal + self.g = self.typedat.toIntermediate(goal) + end + + function LinearSpring.setDampingRatio(self: LinearSpring, dampingRatio: number) + self.d = dampingRatio + end + + function LinearSpring.setFrequency(self: LinearSpring, frequency: number) + self.f = frequency + end + + function LinearSpring.canSleep(self) + if magnitudeSq(self.v) > SLEEP_VELOCITY_SQ_LIMIT then + return false + end + + if distanceSq(self.p, self.g) > SLEEP_OFFSET_SQ_LIMIT then + return false + end + + return true + end + + function LinearSpring.step(self: LinearSpring, dt: number) + -- Advance the spring simulation by dt seconds. + -- Take the damped harmonic oscillator ODE: + -- f^2*(X[t] - g) + 2*d*f*X'[t] + X''[t] = 0 + -- Where X[t] is position at time t, g is target position, + -- f is undamped angular frequency, and d is damping ratio. + -- Apply constant initial conditions: + -- X[0] = p0 + -- X'[0] = v0 + -- Solve the IVP to get analytic expressions for X[t] and X'[t]. + -- The solution takes one of three forms for 0<=d<1, d=1, and d>1 + + local d = self.d + local f = self.f*(2*pi) -- Hz -> Rad/s + local g = self.g + local p = self.p + local v = self.v + + if d == 1 then -- critically damped + local q = exp(-f*dt) + local w = dt*q + + local c0 = q + w*f + local c2 = q - w*f + local c3 = w*f*f + + for idx = 1, #p do + local o = p[idx] - g[idx] + p[idx] = o*c0 + v[idx]*w + g[idx] + v[idx] = v[idx]*c2 - o*c3 + end + + elseif d < 1 then -- underdamped + local q = exp(-d*f*dt) + local c = sqrt(1 - d*d) + + local i = cos(dt*f*c) + local j = sin(dt*f*c) + + -- Damping ratios approaching 1 can cause division by very small numbers. + -- To mitigate that, group terms around z=j/c and find an approximation for z. + -- Start with the definition of z: + -- z = sin(dt*f*c)/c + -- Substitute a=dt*f: + -- z = sin(a*c)/c + -- Take the Maclaurin expansion of z with respect to c: + -- z = a - (a^3*c^2)/6 + (a^5*c^4)/120 + O(c^6) + -- z ≈ a - (a^3*c^2)/6 + (a^5*c^4)/120 + -- Rewrite in Horner form: + -- z ≈ a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6 + + local z + if c > EPS then + z = j/c + else + local a = dt*f + z = a + ((a*a)*(c*c)*(c*c)/20 - c*c)*(a*a*a)/6 + end + + -- Frequencies approaching 0 present a similar problem. + -- We want an approximation for y as f approaches 0, where: + -- y = sin(dt*f*c)/(f*c) + -- Substitute b=dt*c: + -- y = sin(b*c)/b + -- Now reapply the process from z. + + local y + if f*c > EPS then + y = j/(f*c) + else + local b = f*c + y = dt + ((dt*dt)*(b*b)*(b*b)/20 - b*b)*(dt*dt*dt)/6 + end + + for idx = 1, #p do + local o = p[idx] - g[idx] + p[idx] = (o*(i + z*d) + v[idx]*y)*q + g[idx] + v[idx] = (v[idx]*(i - z*d) - o*(z*f))*q + end + + else -- overdamped + local c = sqrt(d*d - 1) + + local r1 = -f*(d + c) + local r2 = -f*(d - c) + + local ec1 = exp(r1*dt) + local ec2 = exp(r2*dt) + + for idx = 1, #p do + local o = p[idx] - g[idx] + local co2 = (v[idx] - o*r1)/(2*f*c) + local co1 = ec1*(o - co2) + + p[idx] = co1 + co2*ec2 + g[idx] + v[idx] = co1*r1 + co2*ec2*r2 + end + end + + return self.typedat.fromIntermediate(self.p) + end +end + +local RotationSpring = {} + +type RotationSpring = typeof(setmetatable({} :: { + d: number, + f: number, + g: CFrame, + p: CFrame, + v: Vector3, +}, RotationSpring)) + +do + RotationSpring.__index = RotationSpring + + function RotationSpring.new(d: number, f: number, p: CFrame, g: CFrame) + return setmetatable( + { + d = d, + f = f, + g = g:Orthonormalize(), + p = p:Orthonormalize(), + v = Vector3.zero + }, + RotationSpring + ) + end + + function RotationSpring.setGoal(self: RotationSpring, value: CFrame) + self.g = value:Orthonormalize() + end + + function RotationSpring.setDampingRatio(self: RotationSpring, dampingRatio: number) + self.d = dampingRatio + end + + function RotationSpring.setFrequency(self: RotationSpring, frequency: number) + self.f = frequency + end + + -- evaluate dot products in high precision + local function dot(v0: Vector3, v1: Vector3) + return v0.X*v1.X + v0.Y*v1.Y + v0.Z*v1.Z + end + + local function areRotationsClose(c0: CFrame, c1: CFrame) + local rx = dot(c0.XVector, c1.XVector) + local ry = dot(c0.YVector, c1.YVector) + local rz = dot(c0.ZVector, c1.ZVector) + local trace = rx + ry + rz + return trace > 1 + 2*cos(SLEEP_ROTATION_DIFF) + end + + local function angleDiff(c0: CFrame, c1: CFrame) + local x = dot(c0.XVector, c1.XVector) + local y = dot(c0.YVector, c1.YVector) + local z = dot(c0.ZVector, c1.ZVector) + local w = x + y + z - 1 + return atan2(sqrt(max(0, 1 - w*w*0.25)), w*0.5) + end + + -- gives approx. 21% accuracy improvement over CFrame.fromAxisAngle near poles + local function fromAxisAngle(axis: Vector3, angle: number) + local c = cos(angle) + local s = sin(angle) + local x, y, z = axis.X, axis.Y, axis.Z + + local mxy = x*y*(1 - c) + local myz = y*z*(1 - c) + local mzx = z*x*(1 - c) + + local rx = Vector3.new(x*x*(1 - c) + c, mxy + z*s, mzx - y*s) + local ry = Vector3.new(mxy - z*s, y*y*(1 - c) + c, myz + x*s) + local rz = Vector3.new(mzx + y*s, myz - x*s, z*z*(1 - c) + c) + + return CFrame.fromMatrix(Vector3.zero, rx, ry, rz):Orthonormalize() + end + + local function rotateAxis(r0: Vector3, c1: CFrame) + local c0 = CFrame.identity + local mag = r0.Magnitude + if mag > 1e-6 then + c0 = fromAxisAngle(r0.Unit, mag) + end + return c0 * c1 + end + + -- axis*angle difference between two cframes + local function axisAngleDiff(c0: CFrame, c1: CFrame) + -- use native axis (stable enough) + local axis = (c0*c1:Inverse()):ToAxisAngle() + + -- use full-precision angle calculation to minimize truncation + local angle = angleDiff(c0, c1) + return axis.Unit*angle + end + + function RotationSpring.canSleep(self: RotationSpring) + local sleepP = areRotationsClose(self.p, self.g) + local sleepV = self.v.Magnitude < SLEEP_ROTATION_VELOCITY + return sleepP and sleepV + end + + function RotationSpring.step(self: RotationSpring, dt: number): CFrame + local d = self.d + local f = self.f*(2*pi) + local g = self.g + local p0 = self.p + local v0 = self.v + + local offset = axisAngleDiff(p0, g) + local decay = exp(-d*f*dt) + + local pt: CFrame + local vt: Vector3 + + if d == 1 then -- critically damped + pt = rotateAxis((offset*(1 + f*dt) + v0*dt)*decay, g) + vt = (v0*(1 - dt*f) - offset*(dt*f*f))*decay + + elseif d < 1 then -- underdamped + local c = sqrt(1 - d*d) + + local i = cos(dt*f*c) + local j = sin(dt*f*c) + + local y = j/(f*c) + local z = j/c + + pt = rotateAxis((offset*(i + z*d) + v0*y)*decay, g) + vt = (v0*(i - z*d) - offset*(z*f))*decay + + else -- overdamped + local c = sqrt(d*d - 1) + + local r1 = -f*(d + c) + local r2 = -f*(d - c) + + local co2 = (v0 - offset*r1)/(2*f*c) + local co1 = offset - co2 + + local e1 = co1*exp(r1*dt) + local e2 = co2*exp(r2*dt) + + pt = rotateAxis(e1 + e2, g) + vt = e1*r1 + e2*r2 + end + + self.p = pt + self.v = vt + + return pt + end +end + +-- Defined early to be used by CFrameSpring +local typeMetadata_Vector3 = { + springType = LinearSpring.new, + + toIntermediate = function(value) + return {value.X, value.Y, value.Z} + end, + + fromIntermediate = function(value: {number}) + return Vector3.new(value[1], value[2], value[3]) + end, +} + +-- Encapsulates a CFrame - Separates translation from rotation +local CFrameSpring = {} +do + CFrameSpring.__index = CFrameSpring + + function CFrameSpring.new( + dampingRatio: number, + frequency: number, + valueCurrent: CFrame, + valueGoal: CFrame, + _: any + ) + return setmetatable( + { + rawGoal = valueGoal, + _position = LinearSpring.new(dampingRatio, frequency, valueCurrent.Position, valueGoal.Position, typeMetadata_Vector3), + _rotation = RotationSpring.new(dampingRatio, frequency, valueCurrent.Rotation, valueGoal.Rotation) + }, + CFrameSpring + ) + end + + function CFrameSpring:setGoal(value: CFrame) + self.rawGoal = value + self._position:setGoal(value.Position) + self._rotation:setGoal(value.Rotation) + end + + function CFrameSpring:setDampingRatio(value: number) + self._position.d = value + self._rotation.d = value + end + + function CFrameSpring:setFrequency(value: number) + self._position.f = value + self._rotation.f = value + end + + function CFrameSpring:canSleep() + return self._position:canSleep() and self._rotation:canSleep() + end + + function CFrameSpring:step(dt): CFrame + local p: Vector3 = self._position:step(dt) + local r: CFrame = self._rotation:step(dt) + return r + p + end +end + +-- Color conversions +local rgbToLuv +local luvToRgb +do + local function inverseGammaCorrectD65(c) + return c < 0.0404482362771076 and c/12.92 or 0.87941546140213*(c + 0.055)^2.4 + end + + local function gammaCorrectD65(c) + return c < 3.1306684425e-3 and 12.92*c or 1.055*c^(1/2.4) - 0.055 + end + + function rgbToLuv(value: Color3): {number} + -- convert RGB to a variant of cieluv space + local r, g, b = value.R, value.G, value.B + + -- D65 sRGB inverse gamma correction + r = inverseGammaCorrectD65(r) + g = inverseGammaCorrectD65(g) + b = inverseGammaCorrectD65(b) + + -- sRGB -> xyz + local x = 0.9257063972951867*r - 0.8333736323779866*g - 0.09209820666085898*b + local y = 0.2125862307855956*r + 0.71517030370341085*g + 0.0722004986433362*b + local z = 3.6590806972265883*r + 11.4426895800574232*g + 4.1149915024264843*b + + -- xyz -> scaled cieluv + local l = y > 0.008856451679035631 and 116*y^(1/3) - 16 or 903.296296296296*y + + local u, v + if z > 1e-14 then + u = l*x/z + v = l*(9*y/z - 0.46832) + else + u = -0.19783*l + v = -0.46832*l + end + + return {l, u, v} + end + + function luvToRgb(value: {number}): Color3 + -- convert back from modified cieluv to rgb space + local l = value[1] + if l < 0.0197955 then + return Color3.new(0, 0, 0) + end + local u = value[2]/l + 0.19783 + local v = value[3]/l + 0.46832 + + -- cieluv -> xyz + local y = (l + 16)/116 + y = y > 0.206896551724137931 and y*y*y or 0.12841854934601665*y - 0.01771290335807126 + local x = y*u/v + local z = y*((3 - 0.75*u)/v - 5) + + -- xyz -> D65 sRGB + local r = 7.2914074*x - 1.5372080*y - 0.4986286*z + local g = -2.1800940*x + 1.8757561*y + 0.0415175*z + local b = 0.1253477*x - 0.2040211*y + 1.0569959*z + + -- clamp minimum sRGB component + if r < 0 and r < g and r < b then + r, g, b = 0, g - r, b - r + elseif g < 0 and g < b then + r, g, b = r - g, 0, b - g + elseif b < 0 then + r, g, b = r - b, g - b, 0 + end + + -- gamma correction from D65 + -- clamp to avoid undesirable overflow wrapping behavior on certain properties (e.g. BasePart.Color) + return Color3.new( + min(gammaCorrectD65(r), 1), + min(gammaCorrectD65(g), 1), + min(gammaCorrectD65(b), 1) + ) + end +end + +-- Type definitions +-- Transforms Roblox types into intermediate types, converting +-- between spaces as necessary to preserve perceptual linearity +local typeMetadata = { + boolean = { + springType = LinearSpring.new, + + toIntermediate = function(value) + return {value and 1 or 0} + end, + + fromIntermediate = function(value) + return value[1] >= 0.5 + end, + }, + + number = { + springType = LinearSpring.new, + + toIntermediate = function(value) + return {value} + end, + + fromIntermediate = function(value) + return value[1] + end, + }, + + NumberRange = { + springType = LinearSpring.new, + + toIntermediate = function(value) + return {value.Min, value.Max} + end, + + fromIntermediate = function(value) + return NumberRange.new(value[1], value[2]) + end, + }, + + UDim = { + springType = LinearSpring.new, + + toIntermediate = function(value) + return {value.Scale, value.Offset} + end, + + fromIntermediate = function(value: {number}) + return UDim.new(value[1], round(value[2])) + end, + }, + + UDim2 = { + springType = LinearSpring.new, + + toIntermediate = function(value) + local x = value.X + local y = value.Y + return {x.Scale, x.Offset, y.Scale, y.Offset} + end, + + fromIntermediate = function(value: {number}) + return UDim2.new(value[1], round(value[2]), value[3], round(value[4])) + end, + }, + + Vector2 = { + springType = LinearSpring.new, + + toIntermediate = function(value) + return {value.X, value.Y} + end, + + fromIntermediate = function(value: {number}) + return Vector2.new(value[1], value[2]) + end, + }, + + Vector3 = typeMetadata_Vector3, + + Color3 = { + springType = LinearSpring.new, + toIntermediate = rgbToLuv, + fromIntermediate = luvToRgb, + }, + + -- Only interpolates start and end keypoints + ColorSequence = { + springType = LinearSpring.new, + + toIntermediate = function(value) + local keypoints = value.Keypoints + + local luv0 = rgbToLuv(keypoints[1].Value) + local luv1 = rgbToLuv(keypoints[#keypoints].Value) + + return { + luv0[1], luv0[2], luv0[3], + luv1[1], luv1[2], luv1[3], + } + end, + + fromIntermediate = function(value: {}) + return ColorSequence.new( + luvToRgb{value[1], value[2], value[3]}, + luvToRgb{value[4], value[5], value[6]} + ) + end, + }, + + CFrame = { + springType = CFrameSpring.new, + toIntermediate = error, -- custom (CFrameSpring) + fromIntermediate = error, -- custom (CFrameSpring) + } +} + +type PropertyOverride = { + [string]: { + class: string, + get: (any)->(), + set: (any, any)->(), + } +} + +local PSEUDO_PROPERTIES: PropertyOverride = { + Pivot = { + class = "PVInstance", + get = function(inst: PVInstance) + return inst:GetPivot() + end, + set = function(inst: PVInstance, value: CFrame) + inst:PivotTo(value) + end + }, + Scale = { + class = "Model", + get = function(inst: Model) + return inst:GetScale() + end, + set = function(inst: Model, value: number) + local FLOAT_MANTISSA_MIN = 1.402e-45 + local FLOAT_MANTISSA_MAX = 2^24 + value = math.clamp(value, FLOAT_MANTISSA_MIN, FLOAT_MANTISSA_MAX) + inst:ScaleTo(value) + end + } +} + +local function getProperty(instance: Instance, property: string): any + local override = PSEUDO_PROPERTIES[property] + if override and instance:IsA(override.class) then + return override.get(instance) + else + return (instance :: any)[property] + end +end + +local function setProperty(instance: Instance, property: string, value: unknown) + local override = PSEUDO_PROPERTIES[property] + if override and instance:IsA(override.class) then + override.set(instance, value) + else + (instance :: any)[property] = value + end +end + +-- Frame loop +local springStates_other: {[Instance]: {[string]: any}} = {} -- {[instance] = {[property] = spring} +local springStates_render: {[Instance]: {[string]: any}} = {} -- {[instance] = {[property] = spring} +local completedCallbacks: {[Instance]: {()->()}} = {} + +local function processSprings(springStates: typeof(springStates_other), dt: number) + for instance, state in springStates do + for propName, spring in state do + if spring:canSleep() then + state[propName] = nil + setProperty(instance, propName, spring.rawGoal) + else + setProperty(instance, propName, spring:step(dt)) + end + end + + if not next(state) then + springStates[instance] = nil + + -- trigger completed callbacks when all properties finish animating + local callbackList = completedCallbacks[instance] + if callbackList then + -- flush callback list before we run any callbacks in case + -- one of the callbacks recursively adds another callback + completedCallbacks[instance] = nil + + for _, callback in callbackList do + task.spawn(callback) + end + end + end + end +end + +RunService.PreSimulation:Connect(function(dt) + processSprings(springStates_other, dt) +end) + +RunService.PostSimulation:Connect(function(dt) + processSprings(springStates_render, dt) +end) + +local function assertType(argNum: number, fnName: string, expectedType: string, value: unknown) + if not expectedType:find(typeof(value)) then + error(`bad argument #{argNum} to {fnName} ({expectedType} expected, got {typeof(value)})`, 3) + end +end + +-- API +local spr = {} + +function spr.target(instance: Instance, dampingRatio: number, frequency: number, properties: {[string]: any}) + if STRICT_RUNTIME_TYPES then + assertType(1, "spr.target", "Instance", instance) + assertType(2, "spr.target", "number", dampingRatio) + assertType(3, "spr.target", "number", frequency) + assertType(4, "spr.target", "table", properties) + end + + if dampingRatio ~= dampingRatio or dampingRatio < 0 then + error(("expected damping ratio >= 0; got %.2f"):format(dampingRatio), 2) + end + + if frequency ~= frequency or frequency < 0 then + error(("expected undamped frequency >= 0; got %.2f"):format(frequency), 2) + end + + local targetRecord = (if instance:IsA("Camera") then springStates_render else springStates_other) :: {[Instance]: {[string]: any}} + + local state = targetRecord[instance] + if not state then + state = {} + targetRecord[instance] = state + end + + for propName, propTarget in properties do + local propValue = getProperty(instance, propName) + + if STRICT_RUNTIME_TYPES and typeof(propTarget) ~= typeof(propValue) then + error(`bad property {propName} to spr.target ({typeof(propValue)} expected, got {typeof(propTarget)})`, 2) + end + + -- Special case infinite frequency for an instantaneous change + if frequency == math.huge then + setProperty(instance, propName, propTarget) + state[propName] = nil + continue + end + + local spring = state[propName] + if not spring then + local md = typeMetadata[typeof(propTarget)] + if not md then + error("unsupported type: " .. typeof(propTarget), 2) + end + + spring = md.springType(dampingRatio, frequency, propValue, propTarget, md) + state[propName] = spring + end + + spring:setGoal(propTarget) + spring:setDampingRatio(dampingRatio) + spring:setFrequency(frequency) + end + + if not next(state) then + targetRecord[instance] = nil + end +end + +function spr.stop(instance: Instance, property: string?) + if STRICT_RUNTIME_TYPES then + assertType(1, "spr.stop", "Instance", instance) + assertType(2, "spr.stop", "string|nil", property) + end + + if property then + local state = springStates_other[instance] or springStates_render[instance] + if state then + state[property] = nil + end + else + springStates_other[instance] = nil + springStates_render[instance] = nil + end +end + +function spr.completed(instance: Instance, callback: ()->()) + if STRICT_RUNTIME_TYPES then + assertType(1, "spr.completed", "Instance", instance) + assertType(2, "spr.completed", "function", callback) + end + + local callbackList = completedCallbacks[instance] + if callbackList then + table.insert(callbackList, callback) + else + completedCallbacks[instance] = {callback} + end +end + +return table.freeze(spr) diff --git a/src/Animation/Spring.luau b/src/Animation/Spring.luau index 18996e6..8476e3b 100644 --- a/src/Animation/Spring.luau +++ b/src/Animation/Spring.luau @@ -47,8 +47,8 @@ function Class._Bind(self: Types.Spring, prop: string, instance: Instance) (instance :: any)[prop] = CurrentValue end self._CurrentLinearPosition = TargetLinearPosition - - ;(self._State :: any):Listen(function(newValue) +; + (self._State :: any):Listen(function(newValue) TargetLinearPosition = self._type.ToLinear(newValue) if Connection ~= nil then @@ -176,11 +176,7 @@ end [Learn More](https://luminlabsdev.github.io/ui-framework/api/#spring) ]=] -return function(springInfo: { - State: Types.State, - Damping: number?, - Frequency: number?, -}): Types.Spring +return function(state: Types.State, damping: number?, frequency: number?): Types.Spring -- Checks if Is.Constructor(springInfo.State :: any) == false then Debugger.Error("TypeMismatch", "Aegis.State", typeof(springInfo.State)) diff --git a/src/State/Compute.luau b/src/State/Compute.luau index 3f3a4e2..e0d7f86 100644 --- a/src/State/Compute.luau +++ b/src/State/Compute.luau @@ -80,7 +80,7 @@ end return function( processor: (get: typeof(use)) -> (), dependencies: { Types.State | Types.Spring | Types.Constructor }? -): Types.Compute +): Types.ComputeExport local Dependencies = nil :: { [Types.State | Types.Spring | Types.Constructor]: () -> () }? local self = setmetatable({}, { __index = Class }) diff --git a/src/State/Value.luau b/src/State/Value.luau index f52ffbc..2594caf 100644 --- a/src/State/Value.luau +++ b/src/State/Value.luau @@ -94,7 +94,7 @@ end [Learn More](https://luminlabsdev.github.io/ui-framework/api/#value) ]=] -return function(initial: any): Types.State +return function(initial: any): Types.StateExport local Type = type(initial) if Type == "table" then diff --git a/src/Types.luau b/src/Types.luau index 5cf532f..92108aa 100644 --- a/src/Types.luau +++ b/src/Types.luau @@ -32,6 +32,11 @@ export type Constructor = { Destroy: (self: T) -> (), } +export type StateExport = { + Set: (self: StateExport, value: any) -> State, + Listen: (self: StateExport, listener: (new: any, old: any) -> ()) -> () -> (), +} & Constructor + export type State = typeof(setmetatable( {} :: { _Type: "state", @@ -39,13 +44,14 @@ export type State = typeof(setmetatable( _Listeners: { (newValue: any, oldValue: any) -> () }, }, {} :: { - __index: { - Set: (self: State, value: any) -> State, - Listen: (self: State, listener: (new: any, old: any) -> ()) -> () -> (), - } & Constructor, + __index: StateExport, } )) +export type SpringExport = { + _Update: (self: SpringExport, linearTargetPosition: { number }, delta: number) -> (), +} & Constructor + export type Spring = typeof(setmetatable( {} :: { _Type: "spring", @@ -60,12 +66,12 @@ export type Spring = typeof(setmetatable( }, }, {} :: { - __index: { - _Update: (self: Spring, linearTargetPosition: { number }, delta: number) -> (), - } & Constructor, + __index: SpringExport, } )) +export type ComputeExport = Constructor + export type Compute = typeof(setmetatable( {} :: { _Type: "compute", @@ -74,7 +80,7 @@ export type Compute = typeof(setmetatable( _Result: any, _Instances: { [string]: Instance }?, }, - {} :: { __index: Constructor } + {} :: { __index: ComputeExport } )) return {}