223 lines
5.9 KiB
Plaintext
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
|