2013/Libraries/Fusion/Animation/Spring.luau

223 lines
5.9 KiB
Plaintext

--!nonstrict
--[[
Constructs a new computed state object, which follows the value of another
state object using a spring simulation.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
local logError = require "../Logging/logError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local unpackType = require "../Animation/unpackType"
local SpringScheduler = require "../Animation/SpringScheduler"
local updateAll = require "../State/updateAll"
local xtypeof = require "../Utility/xtypeof"
local peek = require "../State/peek"
local typeof = require "../../../Modules/Polyfill/typeof"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Sets the position of the internal springs, meaning the value of this
Spring will jump to the given value. This doesn't affect velocity.
If the type doesn't match the current type of the spring, an error will be
thrown.
]]
function class:setPosition(newValue: PubTypes.Animatable)
local newType = typeof(newValue)
if newType ~= self._currentType then
logError("springTypeMismatch", nil, newType, self._currentType)
end
self._springPositions = unpackType(newValue, newType)
self._currentValue = newValue
SpringScheduler.add(self)
updateAll(self)
end
--[[
Sets the velocity of the internal springs, overwriting the existing velocity
of this Spring. This doesn't affect position.
If the type doesn't match the current type of the spring, an error will be
thrown.
]]
function class:setVelocity(newValue: PubTypes.Animatable)
local newType = typeof(newValue)
if newType ~= self._currentType then
logError("springTypeMismatch", nil, newType, self._currentType)
end
self._springVelocities = unpackType(newValue, newType)
SpringScheduler.add(self)
end
--[[
Adds to the velocity of the internal springs, on top of the existing
velocity of this Spring. This doesn't affect position.
If the type doesn't match the current type of the spring, an error will be
thrown.
]]
function class:addVelocity(deltaValue: PubTypes.Animatable)
local deltaType = typeof(deltaValue)
if deltaType ~= self._currentType then
logError("springTypeMismatch", nil, deltaType, self._currentType)
end
local springDeltas = unpackType(deltaValue, deltaType)
for index, delta in ipairs(springDeltas) do
self._springVelocities[index] += delta
end
SpringScheduler.add(self)
end
--[[
Called when the goal state changes value, or when the speed or damping has
changed.
]]
function class:update(): boolean
local goalValue = peek(self._goalState)
-- figure out if this was a goal change or a speed/damping change
if goalValue == self._goalValue then
-- speed/damping change
local damping = peek(self._damping)
if typeof(damping) ~= "number" then
logErrorNonFatal("mistypedSpringDamping", nil, typeof(damping))
elseif damping < 0 then
logErrorNonFatal("invalidSpringDamping", nil, damping)
else
self._currentDamping = damping
end
local speed = peek(self._speed)
if typeof(speed) ~= "number" then
logErrorNonFatal("mistypedSpringSpeed", nil, typeof(speed))
elseif speed < 0 then
logErrorNonFatal("invalidSpringSpeed", nil, speed)
else
self._currentSpeed = speed
end
return false
else
-- goal change - reconfigure spring to target new goal
self._goalValue = goalValue
local oldType = self._currentType
local newType = typeof(goalValue)
self._currentType = newType
local springGoals = unpackType(goalValue, newType)
local numSprings = #springGoals
self._springGoals = springGoals
if newType ~= oldType then
-- if the type changed, snap to the new value and rebuild the
-- position and velocity tables
self._currentValue = self._goalValue
-- local springPositions = (numSprings, 0)
local springPositions = {}
for i = 1, numSprings do
springPositions[i] = 0
end
-- local springVelocities = (numSprings, 0)
local springVelocities = {}
for i = 1, numSprings do
springVelocities[i] = 0
end
for index, springGoal in ipairs(springGoals) do
springPositions[index] = springGoal
end
self._springPositions = springPositions
self._springVelocities = springVelocities
-- the spring may have been animating before, so stop that
SpringScheduler.remove(self)
return true
-- otherwise, the type hasn't changed, just the goal...
elseif numSprings == 0 then
-- if the type isn't animatable, snap to the new value
self._currentValue = self._goalValue
return true
else
-- if it's animatable, let it animate to the goal
SpringScheduler.add(self)
return false
end
end
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._currentValue
end
function class:get()
logError "stateGetWasRemoved"
end
local function Spring<T>(
goalState: PubTypes.Value<T>,
speed: PubTypes.CanBeState<number>?,
damping: PubTypes.CanBeState<number>?
): Types.Spring<T>
-- apply defaults for speed and damping
if speed == nil then
speed = 10
end
if damping == nil then
damping = 1
end
local dependencySet = { [goalState] = true }
if xtypeof(speed) == "State" then
dependencySet[speed] = true
end
if xtypeof(damping) == "State" then
dependencySet[damping] = true
end
local self = setmetatable({
type = "State",
kind = "Spring",
dependencySet = dependencySet,
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_speed = speed,
_damping = damping,
_goalState = goalState,
_goalValue = nil,
_currentType = nil,
_currentValue = nil,
_currentSpeed = peek(speed),
_currentDamping = peek(damping),
_springPositions = nil,
_springGoals = nil,
_springVelocities = nil,
}, CLASS_METATABLE)
-- add this object to the goal state's dependent set
goalState.dependentSet[self] = true
self:update()
return self
end
return Spring