--!strict --[[ 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( goalState: PubTypes.Value, speed: PubTypes.CanBeState?, damping: PubTypes.CanBeState? ): Types.Spring -- 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