351 lines
9.4 KiB
JavaScript
351 lines
9.4 KiB
JavaScript
import { globals } from '../utils/window.js'
|
|
import { registerMethods } from '../utils/methods.js'
|
|
import Animator from './Animator.js'
|
|
import EventTarget from '../types/EventTarget.js'
|
|
|
|
const makeSchedule = function (runnerInfo) {
|
|
const start = runnerInfo.start
|
|
const duration = runnerInfo.runner.duration()
|
|
const end = start + duration
|
|
return {
|
|
start: start,
|
|
duration: duration,
|
|
end: end,
|
|
runner: runnerInfo.runner
|
|
}
|
|
}
|
|
|
|
const defaultSource = function () {
|
|
const w = globals.window
|
|
return (w.performance || w.Date).now()
|
|
}
|
|
|
|
export default class Timeline extends EventTarget {
|
|
// Construct a new timeline on the given element
|
|
constructor(timeSource = defaultSource) {
|
|
super()
|
|
|
|
this._timeSource = timeSource
|
|
|
|
// terminate resets all variables to their initial state
|
|
this.terminate()
|
|
}
|
|
|
|
active() {
|
|
return !!this._nextFrame
|
|
}
|
|
|
|
finish() {
|
|
// Go to end and pause
|
|
this.time(this.getEndTimeOfTimeline() + 1)
|
|
return this.pause()
|
|
}
|
|
|
|
// Calculates the end of the timeline
|
|
getEndTime() {
|
|
const lastRunnerInfo = this.getLastRunnerInfo()
|
|
const lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0
|
|
const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
|
|
return lastStartTime + lastDuration
|
|
}
|
|
|
|
getEndTimeOfTimeline() {
|
|
const endTimes = this._runners.map((i) => i.start + i.runner.duration())
|
|
return Math.max(0, ...endTimes)
|
|
}
|
|
|
|
getLastRunnerInfo() {
|
|
return this.getRunnerInfoById(this._lastRunnerId)
|
|
}
|
|
|
|
getRunnerInfoById(id) {
|
|
return this._runners[this._runnerIds.indexOf(id)] || null
|
|
}
|
|
|
|
pause() {
|
|
this._paused = true
|
|
return this._continue()
|
|
}
|
|
|
|
persist(dtOrForever) {
|
|
if (dtOrForever == null) return this._persist
|
|
this._persist = dtOrForever
|
|
return this
|
|
}
|
|
|
|
play() {
|
|
// Now make sure we are not paused and continue the animation
|
|
this._paused = false
|
|
return this.updateTime()._continue()
|
|
}
|
|
|
|
reverse(yes) {
|
|
const currentSpeed = this.speed()
|
|
if (yes == null) return this.speed(-currentSpeed)
|
|
|
|
const positive = Math.abs(currentSpeed)
|
|
return this.speed(yes ? -positive : positive)
|
|
}
|
|
|
|
// schedules a runner on the timeline
|
|
schedule(runner, delay, when) {
|
|
if (runner == null) {
|
|
return this._runners.map(makeSchedule)
|
|
}
|
|
|
|
// The start time for the next animation can either be given explicitly,
|
|
// derived from the current timeline time or it can be relative to the
|
|
// last start time to chain animations directly
|
|
|
|
let absoluteStartTime = 0
|
|
const endTime = this.getEndTime()
|
|
delay = delay || 0
|
|
|
|
// Work out when to start the animation
|
|
if (when == null || when === 'last' || when === 'after') {
|
|
// Take the last time and increment
|
|
absoluteStartTime = endTime
|
|
} else if (when === 'absolute' || when === 'start') {
|
|
absoluteStartTime = delay
|
|
delay = 0
|
|
} else if (when === 'now') {
|
|
absoluteStartTime = this._time
|
|
} else if (when === 'relative') {
|
|
const runnerInfo = this.getRunnerInfoById(runner.id)
|
|
if (runnerInfo) {
|
|
absoluteStartTime = runnerInfo.start + delay
|
|
delay = 0
|
|
}
|
|
} else if (when === 'with-last') {
|
|
const lastRunnerInfo = this.getLastRunnerInfo()
|
|
const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
|
|
absoluteStartTime = lastStartTime
|
|
} else {
|
|
throw new Error('Invalid value for the "when" parameter')
|
|
}
|
|
|
|
// Manage runner
|
|
runner.unschedule()
|
|
runner.timeline(this)
|
|
|
|
const persist = runner.persist()
|
|
const runnerInfo = {
|
|
persist: persist === null ? this._persist : persist,
|
|
start: absoluteStartTime + delay,
|
|
runner
|
|
}
|
|
|
|
this._lastRunnerId = runner.id
|
|
|
|
this._runners.push(runnerInfo)
|
|
this._runners.sort((a, b) => a.start - b.start)
|
|
this._runnerIds = this._runners.map((info) => info.runner.id)
|
|
|
|
this.updateTime()._continue()
|
|
return this
|
|
}
|
|
|
|
seek(dt) {
|
|
return this.time(this._time + dt)
|
|
}
|
|
|
|
source(fn) {
|
|
if (fn == null) return this._timeSource
|
|
this._timeSource = fn
|
|
return this
|
|
}
|
|
|
|
speed(speed) {
|
|
if (speed == null) return this._speed
|
|
this._speed = speed
|
|
return this
|
|
}
|
|
|
|
stop() {
|
|
// Go to start and pause
|
|
this.time(0)
|
|
return this.pause()
|
|
}
|
|
|
|
time(time) {
|
|
if (time == null) return this._time
|
|
this._time = time
|
|
return this._continue(true)
|
|
}
|
|
|
|
// Remove the runner from this timeline
|
|
unschedule(runner) {
|
|
const index = this._runnerIds.indexOf(runner.id)
|
|
if (index < 0) return this
|
|
|
|
this._runners.splice(index, 1)
|
|
this._runnerIds.splice(index, 1)
|
|
|
|
runner.timeline(null)
|
|
return this
|
|
}
|
|
|
|
// Makes sure, that after pausing the time doesn't jump
|
|
updateTime() {
|
|
if (!this.active()) {
|
|
this._lastSourceTime = this._timeSource()
|
|
}
|
|
return this
|
|
}
|
|
|
|
// Checks if we are running and continues the animation
|
|
_continue(immediateStep = false) {
|
|
Animator.cancelFrame(this._nextFrame)
|
|
this._nextFrame = null
|
|
|
|
if (immediateStep) return this._stepImmediate()
|
|
if (this._paused) return this
|
|
|
|
this._nextFrame = Animator.frame(this._step)
|
|
return this
|
|
}
|
|
|
|
_stepFn(immediateStep = false) {
|
|
// Get the time delta from the last time and update the time
|
|
const time = this._timeSource()
|
|
let dtSource = time - this._lastSourceTime
|
|
|
|
if (immediateStep) dtSource = 0
|
|
|
|
const dtTime = this._speed * dtSource + (this._time - this._lastStepTime)
|
|
this._lastSourceTime = time
|
|
|
|
// Only update the time if we use the timeSource.
|
|
// Otherwise use the current time
|
|
if (!immediateStep) {
|
|
// Update the time
|
|
this._time += dtTime
|
|
this._time = this._time < 0 ? 0 : this._time
|
|
}
|
|
this._lastStepTime = this._time
|
|
this.fire('time', this._time)
|
|
|
|
// This is for the case that the timeline was seeked so that the time
|
|
// is now before the startTime of the runner. That is why we need to set
|
|
// the runner to position 0
|
|
|
|
// FIXME:
|
|
// However, resetting in insertion order leads to bugs. Considering the case,
|
|
// where 2 runners change the same attribute but in different times,
|
|
// resetting both of them will lead to the case where the later defined
|
|
// runner always wins the reset even if the other runner started earlier
|
|
// and therefore should win the attribute battle
|
|
// this can be solved by resetting them backwards
|
|
for (let k = this._runners.length; k--; ) {
|
|
// Get and run the current runner and ignore it if its inactive
|
|
const runnerInfo = this._runners[k]
|
|
const runner = runnerInfo.runner
|
|
|
|
// Make sure that we give the actual difference
|
|
// between runner start time and now
|
|
const dtToStart = this._time - runnerInfo.start
|
|
|
|
// Dont run runner if not started yet
|
|
// and try to reset it
|
|
if (dtToStart <= 0) {
|
|
runner.reset()
|
|
}
|
|
}
|
|
|
|
// Run all of the runners directly
|
|
let runnersLeft = false
|
|
for (let i = 0, len = this._runners.length; i < len; i++) {
|
|
// Get and run the current runner and ignore it if its inactive
|
|
const runnerInfo = this._runners[i]
|
|
const runner = runnerInfo.runner
|
|
let dt = dtTime
|
|
|
|
// Make sure that we give the actual difference
|
|
// between runner start time and now
|
|
const dtToStart = this._time - runnerInfo.start
|
|
|
|
// Dont run runner if not started yet
|
|
if (dtToStart <= 0) {
|
|
runnersLeft = true
|
|
continue
|
|
} else if (dtToStart < dt) {
|
|
// Adjust dt to make sure that animation is on point
|
|
dt = dtToStart
|
|
}
|
|
|
|
if (!runner.active()) continue
|
|
|
|
// If this runner is still going, signal that we need another animation
|
|
// frame, otherwise, remove the completed runner
|
|
const finished = runner.step(dt).done
|
|
if (!finished) {
|
|
runnersLeft = true
|
|
// continue
|
|
} else if (runnerInfo.persist !== true) {
|
|
// runner is finished. And runner might get removed
|
|
const endTime = runner.duration() - runner.time() + this._time
|
|
|
|
if (endTime + runnerInfo.persist < this._time) {
|
|
// Delete runner and correct index
|
|
runner.unschedule()
|
|
--i
|
|
--len
|
|
}
|
|
}
|
|
}
|
|
|
|
// Basically: we continue when there are runners right from us in time
|
|
// when -->, and when runners are left from us when <--
|
|
if (
|
|
(runnersLeft && !(this._speed < 0 && this._time === 0)) ||
|
|
(this._runnerIds.length && this._speed < 0 && this._time > 0)
|
|
) {
|
|
this._continue()
|
|
} else {
|
|
this.pause()
|
|
this.fire('finished')
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
terminate() {
|
|
// cleanup memory
|
|
|
|
// Store the timing variables
|
|
this._startTime = 0
|
|
this._speed = 1.0
|
|
|
|
// Determines how long a runner is hold in memory. Can be a dt or true/false
|
|
this._persist = 0
|
|
|
|
// Keep track of the running animations and their starting parameters
|
|
this._nextFrame = null
|
|
this._paused = true
|
|
this._runners = []
|
|
this._runnerIds = []
|
|
this._lastRunnerId = -1
|
|
this._time = 0
|
|
this._lastSourceTime = 0
|
|
this._lastStepTime = 0
|
|
|
|
// Make sure that step is always called in class context
|
|
this._step = this._stepFn.bind(this, false)
|
|
this._stepImmediate = this._stepFn.bind(this, true)
|
|
}
|
|
}
|
|
|
|
registerMethods({
|
|
Element: {
|
|
timeline: function (timeline) {
|
|
if (timeline == null) {
|
|
this._timeline = this._timeline || new Timeline()
|
|
return this._timeline
|
|
} else {
|
|
this._timeline = timeline
|
|
return this
|
|
}
|
|
}
|
|
}
|
|
})
|