import { globals } from '../utils/window.js' import { registerMethods } from '../utils/methods.js' import Animator from './Animator.js' import EventTarget from '../types/EventTarget.js' var makeSchedule = function ( runnerInfo ) { var start = runnerInfo.start var duration = runnerInfo.runner.duration() var end = start + duration return { start: start, duration: duration, end: end, runner: runnerInfo.runner } } export default class Timeline extends EventTarget { // Construct a new timeline on the given element constructor () { super() this._timeSource = function () { let w = globals.window return ( w.performance || w.Date ).now() } // Store the timing variables this._startTime = 0 this._speed = 1.0 // Play control variables control how the animation proceeds this._reverse = false this._persist = 0 // Keep track of the running animations and their starting parameters this._nextFrame = null this._paused = false this._runners = [] this._order = [] this._time = 0 this._lastSourceTime = 0 this._lastStepTime = 0 } /** * */ // schedules a runner on the timeline schedule ( runner, delay, when ) { // FIXME: how to sort? maybe by runner id? if ( runner == null ) { return this._runners.map( makeSchedule ).sort( function ( a, b ) { return ( a.start - b.start ) || ( a.duration - b.duration ) } ) } if ( !this.active() ) { this._step() if ( when == null ) { when = 'now' } } // 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 direclty var absoluteStartTime = 0 delay = delay || 0 // Work out when to start the animation if ( when == null || when === 'last' || when === 'after' ) { // Take the last time and increment absoluteStartTime = this._startTime } else if ( when === 'absolute' || when === 'start' ) { absoluteStartTime = delay delay = 0 } else if ( when === 'now' ) { absoluteStartTime = this._time } else if ( when === 'relative' ) { let runnerInfo = this._runners[runner.id] if ( runnerInfo ) { absoluteStartTime = runnerInfo.start + delay delay = 0 } } else { throw new Error( 'Invalid value for the "when" parameter' ) } // Manage runner runner.unschedule() runner.timeline( this ) runner.time( -delay ) // Save startTime for next runner this._startTime = absoluteStartTime + runner.duration() + delay // Save runnerInfo this._runners[runner.id] = { persist: this.persist(), runner: runner, start: absoluteStartTime } // Save order and continue this._order.push( runner.id ) this._continue() return this } // Remove the runner from this timeline unschedule ( runner ) { var index = this._order.indexOf( runner.id ) if ( index < 0 ) return this delete this._runners[runner.id] this._order.splice( index, 1 ) runner.timeline( null ) return this } play () { // Now make sure we are not paused and continue the animation this._paused = false return this._continue() } pause () { // Cancel the next animation frame and pause this._nextFrame = null this._paused = true return this } stop () { // Cancel the next animation frame and go to start this.seek( -this._time ) return this.pause() } finish () { this.seek( Infinity ) return this.pause() } speed ( speed ) { if ( speed == null ) return this._speed this._speed = speed return this } reverse ( yes ) { var currentSpeed = this.speed() if ( yes == null ) return this.speed( -currentSpeed ) var positive = Math.abs( currentSpeed ) return this.speed( yes ? positive : -positive ) } seek ( dt ) { this._time += dt return this._continue() } time ( time ) { if ( time == null ) return this._time this._time = time return this } persist ( dtOrForever ) { if ( dtOrForever == null ) return this._persist this._persist = dtOrForever return this } source ( fn ) { if ( fn == null ) return this._timeSource this._timeSource = fn return this } _step () { // If the timeline is paused, just do nothing if ( this._paused ) return // Get the time delta from the last time and update the time var time = this._timeSource() var dtSource = time - this._lastSourceTime var dtTime = this._speed * dtSource + ( this._time - this._lastStepTime ) this._lastSourceTime = time // Update the time this._time += dtTime this._lastStepTime = this._time // this.fire('time', this._time) // Run all of the runners directly var runnersLeft = false for ( var i = 0, len = this._order.length; i < len; i++ ) { // Get and run the current runner and ignore it if its inactive var runnerInfo = this._runners[this._order[i]] var runner = runnerInfo.runner let dt = dtTime // Make sure that we give the actual difference // between runner start time and now let 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 var finished = runner.step( dt ).done if ( !finished ) { runnersLeft = true // continue } else if ( runnerInfo.persist !== true ) { // runner is finished. And runner might get removed var endTime = runner.duration() - runner.time() + this._time if ( endTime + this._persist < this._time ) { // Delete runner and correct index delete this._runners[this._order[i]] this._order.splice( i--, 1 ) && --len runner.timeline( null ) } } } // Get the next animation frame to keep the simulation going if ( runnersLeft ) { this._nextFrame = Animator.frame( this._step.bind( this ) ) } else { this._nextFrame = null } return this } // Checks if we are running and continues the animation _continue () { if ( this._paused ) return this if ( !this._nextFrame ) { this._nextFrame = Animator.frame( this._step.bind( this ) ) } return this } active () { return !!this._nextFrame } } registerMethods( { Element: { timeline: function () { this._timeline = ( this._timeline || new Timeline() ) return this._timeline } } } )