diff --git a/src/Animation/Spr.luau b/src/Animation/Spr.luau deleted file mode 100644 index b92c608..0000000 --- a/src/Animation/Spr.luau +++ /dev/null @@ -1,838 +0,0 @@ ---!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 8476e3b..c6b1609 100644 --- a/src/Animation/Spring.luau +++ b/src/Animation/Spring.luau @@ -40,8 +40,8 @@ function Class._Bind(self: Types.Spring, prop: string, instance: Instance) local Mode = RunService:IsClient() and "Client" or "Server" local Connection: ((number) -> ())? - local TargetLinearPosition = self._type.ToLinear(self._State:Get()) - local CurrentValue = self._State:Get() + local TargetLinearPosition = self._DataType.ToLinear((self._State :: any):Get()) + local CurrentValue = (self._State :: any):Get() if CurrentValue ~= nil then (instance :: any)[prop] = CurrentValue @@ -49,7 +49,7 @@ function Class._Bind(self: Types.Spring, prop: string, instance: Instance) self._CurrentLinearPosition = TargetLinearPosition ; (self._State :: any):Listen(function(newValue) - TargetLinearPosition = self._type.ToLinear(newValue) + TargetLinearPosition = self._DataType.ToLinear(newValue) if Connection ~= nil then return -- The value changed when the spring was playing; don't create another connection, let the new one play @@ -59,7 +59,7 @@ function Class._Bind(self: Types.Spring, prop: string, instance: Instance) Connection = Scheduler:Add(Mode, function(delta: number) if self and (self :: any)._Update then (self :: any):_Update(TargetLinearPosition, delta); - (instance :: any)[prop] = self._type.FromLinear(self._CurrentLinearPosition) + (instance :: any)[prop] = self._DataType.FromLinear(self._CurrentLinearPosition) else if Connection then Scheduler:Remove(Mode, Connection) @@ -134,36 +134,19 @@ function Class._Update(self: Types.Spring, targetLinearPosition: { number }, del end --[[ - ### `Spring:Get()` - Returns the runtime position of the spring object. - - #### Returns - - `Position` - - #### Examples - ```luau - local MyState = Aegis.state(Vector3.new(0, 5, 0)) - local MySpring = Aegis.spring({ State = MyState }) - print(MySpring:Get()) -- 0, 5, 0 - ``` + Gets the end goal of the spring object. + + [Learn More](https://luminlabsdev.github.io/ui-framework/api/spring/#get) ]] function Class.Get(self: Types.Spring): Types.Animatable -- Return the unpacked format for improved clarity - return self._type.FromLinear(self._CurrentLinearPosition) + return self._DataType.FromLinear(self._CurrentLinearPosition) end --[[ - ### `Spring:Destroy()` - Destroys and removes the spring. - - #### Returns - - `nil` + Destroys the spring object. - #### Example - ```luau - local MySpring = Aegis.spring({ ... }) - MySpring:Destroy() -- spring no longer valid. - ``` + [Learn More](https://luminlabsdev.github.io/ui-framework/api/spring/#destroy) ]] function Class.Destroy(self: Types.Spring): nil (self :: any)._State:Destroy() @@ -172,34 +155,26 @@ function Class.Destroy(self: Types.Spring): nil end --[=[ - Creates a new spring. + Creates a new spring with a set goal. [Learn More](https://luminlabsdev.github.io/ui-framework/api/#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)) - end - - local Type = Storage.AnimatableDataTypes[typeof(springInfo.State:Get())] - - if not Type then - Debugger.Error("NotAnimatable", typeof(springInfo.State:Get())) - end - - local self = setmetatable({} :: Types.SpringProps, { __index = Class } :: { __index: Types.SpringFunc }) +return function(goal: Types.State, damping: number?, frequency: number?): Types.SpringExport + Debugger.Assert(Is.Constructor(goal :: any), "InvalidType", "State", type(goal)) + local Type = Storage.AnimatableDataTypes[typeof((goal :: any):Get())] + Debugger.Assert(Type, "NotAnimatable", type((goal :: any):Get())) - self.ConstructorClass = "spring" + local self = setmetatable({}, { __index = Class }) - self._Damping = springInfo.Damping or 1 - self._Frequency = springInfo.Frequency or 1 - self._State = springInfo.State + self._Type = "spring" + self._Damping = damping or 1 + self._Frequency = frequency or 1 + self._State = goal - self._type = Type - self._CurrentLinearPosition = self._type.ToLinear(springInfo.State:Get()) + self._DataType = Type + self._CurrentLinearPosition = Type.ToLinear((goal :: any):Get()) self._Velocity = table.create(#self._CurrentLinearPosition, 0) - return self + return self :: any end diff --git a/src/Animation/Tween.luau b/src/Animation/Tween.luau index e69de29..e0d7096 100644 --- a/src/Animation/Tween.luau +++ b/src/Animation/Tween.luau @@ -0,0 +1,11 @@ +-- Variables +local Root = script.Parent.Parent +local Utility = require(Root.Utility) +local Types = require(Root.Types) + +-- Functions + +-- Module +return function(goal: Types.State, tweenInfo: TweenInfo) + +end diff --git a/src/Keys/Change.luau b/src/Keys/Change.luau index 74d30cf..0502444 100644 --- a/src/Keys/Change.luau +++ b/src/Keys/Change.luau @@ -17,7 +17,7 @@ return function(prop: string) Apply = function(instance: Instance, callback: (changed: any) -> ()) local Success, Event = pcall(instance.GetPropertyChangedSignal, instance :: any, prop :: any) if not Success or type(callback) ~= "function" then - Debugger.Fatal("") + Debugger.Fatal("InvalidPropOrEvent", prop) end Event:Connect(function() callback((instance :: any)[prop]) diff --git a/src/Keys/Clean.luau b/src/Keys/Clean.luau index 3466dd4..61298d2 100644 --- a/src/Keys/Clean.luau +++ b/src/Keys/Clean.luau @@ -1,6 +1,7 @@ -- Variables local Root = script.Parent.Parent local Keys = require(script.Parent.List) +local Is = require(Root.Is) local Debugger = require(Root.Parent.debugger) -- Module @@ -16,7 +17,15 @@ return function() Name = "Clean", Apply = function(instance: Instance, values: { any }) Debugger.Assert(type(values) == "table", "InvalidType", "table", type(values)) - instance.Destroying:Once(function() end) + instance.Destroying:Once(function() + for _, value in values do + if Is.Constructor(value) or typeof(value) == "Instance" then + value:Destroy() + elseif typeof(value) == "RBXScriptConnection" then + value:Disconnect() + end + end + end) end, } end diff --git a/src/Keys/Event.luau b/src/Keys/Event.luau index a50b35f..994590a 100644 --- a/src/Keys/Event.luau +++ b/src/Keys/Event.luau @@ -19,10 +19,11 @@ return function(event: string) if not Keys["Event"] then Keys["Event"] = { Name = "Event", + Connection = nil, Apply = function(instance: Instance, callback: (...any) -> ()) local Success, Event = pcall(Find, instance :: any, event :: any) if not Success or type(callback) ~= "function" then - Debugger.Fatal("") + Debugger.Fatal("InvalidPropOrEvent", event) end Event:Connect(callback) end, diff --git a/src/Logs.luau b/src/Logs.luau index d9e9619..45b5209 100644 --- a/src/Logs.luau +++ b/src/Logs.luau @@ -1,9 +1,10 @@ return { IncompatibleType = "Data type '%s' is incompatible", - NotAnimatable = "%s is not an animatable object", + NotAnimatable = "%s is not an animatable object type", FailedCreation = "%s could not be created; '%s'", InvalidType = "Expected type %s; got '%s'", InvalidKey = "Expected valid key type; got '%s'", InvalidClass = "'%s' is not an instance class name", + InvalidPropOrEvent = "'%s' is not a property/event of the %s class" } diff --git a/src/State/Value.luau b/src/State/State.luau similarity index 85% rename from src/State/Value.luau rename to src/State/State.luau index 2594caf..de57666 100644 --- a/src/State/Value.luau +++ b/src/State/State.luau @@ -11,7 +11,7 @@ local Class = {} --[=[ Returns the current value of the object. - [Learn More](https://luminlabsdev.github.io/ui-framework/api/value/#get) + [Learn More](https://luminlabsdev.github.io/ui-framework/api/state/#get) ]=] function Class.Get(self: Types.State): any return self._State @@ -20,7 +20,7 @@ end --[=[ Sets the current value of the object. - [Learn More](https://luminlabsdev.github.io/ui-framework/api/value/#set) + [Learn More](https://luminlabsdev.github.io/ui-framework/api/state/#set) ]=] function Class.Set(self: Types.State, newValue: any): Types.State local Type = type(newValue) @@ -51,7 +51,7 @@ end --[=[ Listens to changes of state within the object. - [Learn More](https://luminlabsdev.github.io/ui-framework/api/value/#listen) + [Learn More](https://luminlabsdev.github.io/ui-framework/api/state/#listen) ]=] function Class.Listen(self: Types.State, listener: (new: any, old: any) -> ()): () -> () table.insert(self._Listeners, listener) -- Add a listener @@ -80,7 +80,7 @@ end --[=[ Destroys the value object. - [Learn More](https://luminlabsdev.github.io/ui-framework/api/value/#destroy) + [Learn More](https://luminlabsdev.github.io/ui-framework/api/state/#destroy) ]=] function Class.Destroy(self: Types.State): nil Utility.CleanMetatable(self :: any) @@ -92,7 +92,7 @@ end --[=[ Creates a new value/state object that dynamically changes in UI when changed itself. - [Learn More](https://luminlabsdev.github.io/ui-framework/api/#value) + [Learn More](https://luminlabsdev.github.io/ui-framework/api/#state) ]=] return function(initial: any): Types.StateExport local Type = type(initial) diff --git a/src/Types.luau b/src/Types.luau index 92108aa..b159ede 100644 --- a/src/Types.luau +++ b/src/Types.luau @@ -60,7 +60,7 @@ export type Spring = typeof(setmetatable( _Frequency: number, _Velocity: { number }, _CurrentLinearPosition: { number }, - _type: { + _DataType: { ToLinear: (value: Animatable) -> { number }, FromLinear: (value: { number }) -> Animatable, }, @@ -70,6 +70,19 @@ export type Spring = typeof(setmetatable( } )) +export type TweenExport = { + +} & Constructor + +export type Tween = typeof(setmetatable( + {} :: { + + }, + {} :: { + __index: TweenExport + } +)) + export type ComputeExport = Constructor export type Compute = typeof(setmetatable( diff --git a/src/Utility.luau b/src/Utility.luau index 2b8ce6c..f2edd2a 100644 --- a/src/Utility.luau +++ b/src/Utility.luau @@ -25,7 +25,7 @@ local function CheckTypeAndCall(expected: any, received: any, interaction: () -> if expected == nil or received == nil then interaction(...) -- Let the value `nil` to be set to anything else - Debugger.Warn("TypeMismatch", typeof(expected), typeof(received)) + Debugger.Warn("InvalidType", typeof(expected), typeof(received)) return end end diff --git a/src/init.luau b/src/init.luau index 7ffea54..84fd892 100644 --- a/src/init.luau +++ b/src/init.luau @@ -26,11 +26,11 @@ return table.freeze({ New = require(Instances.New), Update = require(Instances.Update), - Value = require(State.Value), + State = require(State.State), Compute = require(State.Compute), Spring = require(Animation.Spring), - -- Tween = require(Animation.Tween), + Tween = require(Animation.Tween), Event = require(Keys.Event), Change = require(Keys.Change),