local scheduler = require("./utils/scheduler") local signal = require("./utils/signal") local spring = require("./spring") local tween = require("./tween") local types = require("./types") type Animatable = types.Animatable type SpringOptions = spring.SpringOptions type TweenOptions = tween.TweenOptions export type MotionLike = { step: (self: MotionLike, dt: number) -> Value, idle: (self: MotionLike) -> boolean, getPosition: (self: MotionLike) -> Value, getGoal: (self: MotionLike) -> Value, setPosition: (self: MotionLike, value: Value) -> (), setGoal: (self: MotionLike, value: Value) -> (), } export type MotionState = { spring: spring.Spring, tween: tween.Tween, current: MotionLike, started: boolean, complete: boolean, onChange: signal.SubscribeSignal<(Value, number)>, onComplete: signal.SubscribeSignal, fireComplete: signal.FireSignal, } export type MotionOptions = { start: boolean?, spring: SpringOptions?, tween: TweenOptions?, } export type Motion = { state: MotionState, setPosition: (self: Motion, value: Value) -> (), setVelocity: (self: Motion, value: Value) -> (), setGoal: (self: Motion, value: Value, options: MotionOptions?) -> (), getPosition: (self: Motion) -> Value, getVelocity: (self: Motion) -> Value, getGoal: (self: Motion) -> Value, onChange: (self: Motion, callback: (value: Value, deltaTime: number) -> ()) -> () -> (), onComplete: (self: Motion, callback: (value: Value) -> ()) -> () -> (), step: (self: Motion, deltaTime: number) -> Value, spring: (self: Motion, goal: Value, options: SpringOptions?) -> (), tween: (self: Motion, goal: Value, options: TweenOptions?) -> (), idle: (self: Motion) -> boolean, configure: (self: Motion, options: MotionOptions) -> (), start: (self: Motion) -> (), stop: (self: Motion) -> (), destroy: (self: Motion) -> (), scheduleUpdate: (self: Motion) -> (), prepareSpring: (self: Motion) -> (), prepareTween: (self: Motion) -> (), } local Motion = {} :: Motion (Motion :: any).__index = Motion local motionScheduler = scheduler("Motion", function(state: MotionState, deltaTime, remove): T if state.complete then remove(state) return state.current:getPosition() end local position = state.current:step(deltaTime) if state.current:idle() then remove(state) state.complete = true state.fireComplete(position) end return position end) local function omitStartOption(inputOptions: T): T local options: any = inputOptions if options == nil or options.start ~= true then return options else local result: any = table.clone(options) result.start = nil return result end end local function createMotion(initialValue: Value, inputOptions: MotionOptions?): Motion local options: MotionOptions = inputOptions or {} local motionSpring = spring.createSpring(initialValue, omitStartOption(options.spring)) local motionTween = tween.createTween(initialValue, omitStartOption(options.tween)) local current: MotionLike = if not options.tween then motionSpring else motionTween local onChange, fireChange = signal() local onComplete, fireComplete = signal() local state: MotionState = { spring = motionSpring, tween = motionTween, current = current, started = false, complete = true, onChange = onChange, onComplete = onComplete, fireComplete = fireComplete, } local self: Motion = setmetatable({ state = state, }, Motion) :: any if options.start then self:start() end motionSpring:onChange(function(value: Value, deltaTime: number) if state.current == motionSpring then fireChange(value, deltaTime) end end) motionTween:onChange(function(value: Value, deltaTime: number) if state.current == motionTween then fireChange(value, deltaTime) end end) return self end function Motion:scheduleUpdate() self.state.complete = false if self.state.started then motionScheduler.add(self.state) end end function Motion:prepareSpring() local state = self.state if state.current ~= state.spring then state.spring:setPosition(state.tween:getPosition()) state.current = state.spring end end function Motion:prepareTween() local state = self.state if state.current ~= state.tween then state.tween:setPosition(state.spring:getPosition()) state.spring:halt() state.current = state.tween end end function Motion:start() self.state.started = true if not self.state.complete then motionScheduler.add(self.state) end end function Motion:stop() self.state.started = false motionScheduler.remove(self.state) end function Motion:idle(): boolean return self.state.complete end function Motion:step(dt: number): Animatable return motionScheduler.update(self.state, dt) end function Motion:configure(options: MotionOptions) if options.spring then self.state.spring:configure(options.spring) end if options.tween then self.state.tween:configure(options.tween) end if not self.state.current:idle() then self:scheduleUpdate() end end function Motion:getPosition(): Animatable return self.state.current:getPosition() end function Motion:getVelocity(): Animatable return self.state.spring:getVelocity() end function Motion:getGoal(): Animatable return self.state.current:getGoal() end function Motion:setPosition(value: Animatable) self.state.current:setPosition(value) if not self.state.current:idle() then self:scheduleUpdate() end end function Motion:setVelocity(value: Animatable) if self.state.current == self.state.spring then self.state.spring:setVelocity(value) if not self.state.current:idle() then self:scheduleUpdate() end end end function Motion:setGoal(value: Animatable, options: MotionOptions?) if options then if options.spring then return self:spring(value, options.spring) elseif options.tween then return self:tween(value, options.tween) end end self.state.current:setGoal(value) if not self.state.current:idle() then self:scheduleUpdate() end end function Motion:spring(value: Animatable, options: SpringOptions?) self:prepareSpring() self.state.spring:setGoal(value, options) if not self.state.spring:idle() then self:scheduleUpdate() end end function Motion:tween(value: Animatable, options: TweenOptions?) self:prepareTween() self.state.tween:setGoal(value, options) if not self.state.tween:idle() then self:scheduleUpdate() end end function Motion:onChange(callback: (value: Animatable, deltaTime: number) -> ()): () -> () return self.state.onChange(callback) end function Motion:onComplete(callback: (value: Animatable) -> ()): () -> () return self.state.onComplete(callback) end function Motion:destroy() self:stop() end return { scheduler = motionScheduler, createMotion = createMotion, }