summaryrefslogtreecommitdiffstats
path: root/src/animation
diff options
context:
space:
mode:
authorUlrich-Matthias Schäfer <ulima.ums@googlemail.com>2018-11-06 13:48:05 +0100
committerUlrich-Matthias Schäfer <ulima.ums@googlemail.com>2018-11-06 13:48:05 +0100
commita0b13ebcacfd74b9f521110c7225bb404325bcd3 (patch)
treea07c5cc422645e31d7dfef81ce4e54f03f0945f6 /src/animation
parent9f2696e8a2cf7e4eebc1cc7e31027fe2070094fa (diff)
downloadsvg.js-a0b13ebcacfd74b9f521110c7225bb404325bcd3.tar.gz
svg.js-a0b13ebcacfd74b9f521110c7225bb404325bcd3.zip
reordered modules, add es6 build
Diffstat (limited to 'src/animation')
-rw-r--r--src/animation/Animator.js85
-rw-r--r--src/animation/Controller.js173
-rw-r--r--src/animation/Queue.js59
-rw-r--r--src/animation/Runner.js928
-rw-r--r--src/animation/Timeline.js271
5 files changed, 1516 insertions, 0 deletions
diff --git a/src/animation/Animator.js b/src/animation/Animator.js
new file mode 100644
index 0000000..fdb2326
--- /dev/null
+++ b/src/animation/Animator.js
@@ -0,0 +1,85 @@
+import Queue from './Queue.js'
+
+const Animator = {
+ nextDraw: null,
+ frames: new Queue(),
+ timeouts: new Queue(),
+ timer: window.performance || window.Date,
+ transforms: [],
+
+ frame (fn) {
+ // Store the node
+ var node = Animator.frames.push({ run: fn })
+
+ // Request an animation frame if we don't have one
+ if (Animator.nextDraw === null) {
+ Animator.nextDraw = window.requestAnimationFrame(Animator._draw)
+ }
+
+ // Return the node so we can remove it easily
+ return node
+ },
+
+ transform_frame (fn, id) {
+ Animator.transforms[id] = fn
+ },
+
+ timeout (fn, delay) {
+ delay = delay || 0
+
+ // Work out when the event should fire
+ var time = Animator.timer.now() + delay
+
+ // Add the timeout to the end of the queue
+ var node = Animator.timeouts.push({ run: fn, time: time })
+
+ // Request another animation frame if we need one
+ if (Animator.nextDraw === null) {
+ Animator.nextDraw = window.requestAnimationFrame(Animator._draw)
+ }
+
+ return node
+ },
+
+ cancelFrame (node) {
+ Animator.frames.remove(node)
+ },
+
+ clearTimeout (node) {
+ Animator.timeouts.remove(node)
+ },
+
+ _draw (now) {
+ // Run all the timeouts we can run, if they are not ready yet, add them
+ // to the end of the queue immediately! (bad timeouts!!! [sarcasm])
+ var nextTimeout = null
+ var lastTimeout = Animator.timeouts.last()
+ while ((nextTimeout = Animator.timeouts.shift())) {
+ // Run the timeout if its time, or push it to the end
+ if (now >= nextTimeout.time) {
+ nextTimeout.run()
+ } else {
+ Animator.timeouts.push(nextTimeout)
+ }
+
+ // If we hit the last item, we should stop shifting out more items
+ if (nextTimeout === lastTimeout) break
+ }
+
+ // Run all of the animation frames
+ var nextFrame = null
+ var lastFrame = Animator.frames.last()
+ while ((nextFrame !== lastFrame) && (nextFrame = Animator.frames.shift())) {
+ nextFrame.run()
+ }
+
+ Animator.transforms.forEach(function (el) { el() })
+
+ // If we have remaining timeouts or frames, draw until we don't anymore
+ Animator.nextDraw = Animator.timeouts.first() || Animator.frames.first()
+ ? window.requestAnimationFrame(Animator._draw)
+ : null
+ }
+}
+
+export default Animator
diff --git a/src/animation/Controller.js b/src/animation/Controller.js
new file mode 100644
index 0000000..1716545
--- /dev/null
+++ b/src/animation/Controller.js
@@ -0,0 +1,173 @@
+import { timeline } from '../modules/core/defaults.js'
+import { extend } from '../utils/adopter.js'
+
+/***
+Base Class
+==========
+The base stepper class that will be
+***/
+
+function makeSetterGetter (k, f) {
+ return function (v) {
+ if (v == null) return this[v]
+ this[k] = v
+ if (f) f.call(this)
+ return this
+ }
+}
+
+export let easing = {
+ '-': function (pos) { return pos },
+ '<>': function (pos) { return -Math.cos(pos * Math.PI) / 2 + 0.5 },
+ '>': function (pos) { return Math.sin(pos * Math.PI / 2) },
+ '<': function (pos) { return -Math.cos(pos * Math.PI / 2) + 1 },
+ bezier: function (t0, x0, t1, x1) {
+ return function (t) {
+ // TODO: FINISH
+ }
+ }
+}
+
+export class Stepper {
+ done () { return false }
+}
+
+/***
+Easing Functions
+================
+***/
+
+export class Ease extends Stepper {
+ constructor (fn) {
+ super()
+ this.ease = easing[fn || timeline.ease] || fn
+ }
+
+ step (from, to, pos) {
+ if (typeof from !== 'number') {
+ return pos < 1 ? from : to
+ }
+ return from + (to - from) * this.ease(pos)
+ }
+}
+
+/***
+Controller Types
+================
+***/
+
+export class Controller extends Stepper {
+ constructor (fn) {
+ super()
+ this.stepper = fn
+ }
+
+ step (current, target, dt, c) {
+ return this.stepper(current, target, dt, c)
+ }
+
+ done (c) {
+ return c.done
+ }
+}
+
+function recalculate () {
+ // Apply the default parameters
+ var duration = (this._duration || 500) / 1000
+ var overshoot = this._overshoot || 0
+
+ // Calculate the PID natural response
+ var eps = 1e-10
+ var pi = Math.PI
+ var os = Math.log(overshoot / 100 + eps)
+ var zeta = -os / Math.sqrt(pi * pi + os * os)
+ var wn = 3.9 / (zeta * duration)
+
+ // Calculate the Spring values
+ this.d = 2 * zeta * wn
+ this.k = wn * wn
+}
+
+export class Spring extends Controller {
+ constructor (duration, overshoot) {
+ super()
+ this.duration(duration || 500)
+ .overshoot(overshoot || 0)
+ }
+
+ step (current, target, dt, c) {
+ if (typeof current === 'string') return current
+ c.done = dt === Infinity
+ if (dt === Infinity) return target
+ if (dt === 0) return current
+
+ if (dt > 100) dt = 16
+
+ dt /= 1000
+
+ // Get the previous velocity
+ var velocity = c.velocity || 0
+
+ // Apply the control to get the new position and store it
+ var acceleration = -this.d * velocity - this.k * (current - target)
+ var newPosition = current +
+ velocity * dt +
+ acceleration * dt * dt / 2
+
+ // Store the velocity
+ c.velocity = velocity + acceleration * dt
+
+ // Figure out if we have converged, and if so, pass the value
+ c.done = Math.abs(target - newPosition) + Math.abs(velocity) < 0.002
+ return c.done ? target : newPosition
+ }
+}
+
+extend(Spring, {
+ duration: makeSetterGetter('_duration', recalculate),
+ overshoot: makeSetterGetter('_overshoot', recalculate)
+})
+
+export class PID extends Controller {
+ constructor (p, i, d, windup) {
+ super()
+
+ p = p == null ? 0.1 : p
+ i = i == null ? 0.01 : i
+ d = d == null ? 0 : d
+ windup = windup == null ? 1000 : windup
+ this.p(p).i(i).d(d).windup(windup)
+ }
+
+ step (current, target, dt, c) {
+ if (typeof current === 'string') return current
+ c.done = dt === Infinity
+
+ if (dt === Infinity) return target
+ if (dt === 0) return current
+
+ var p = target - current
+ var i = (c.integral || 0) + p * dt
+ var d = (p - (c.error || 0)) / dt
+ var windup = this.windup
+
+ // antiwindup
+ if (windup !== false) {
+ i = Math.max(-windup, Math.min(i, windup))
+ }
+
+ c.error = p
+ c.integral = i
+
+ c.done = Math.abs(p) < 0.001
+
+ return c.done ? target : current + (this.P * p + this.I * i + this.D * d)
+ }
+}
+
+extend(PID, {
+ windup: makeSetterGetter('windup'),
+ p: makeSetterGetter('P'),
+ i: makeSetterGetter('I'),
+ d: makeSetterGetter('D')
+})
diff --git a/src/animation/Queue.js b/src/animation/Queue.js
new file mode 100644
index 0000000..14b92b4
--- /dev/null
+++ b/src/animation/Queue.js
@@ -0,0 +1,59 @@
+export default class Queue {
+ constructor () {
+ this._first = null
+ this._last = null
+ }
+
+ push (value) {
+ // An item stores an id and the provided value
+ var item = value.next ? value : { value: value, next: null, prev: null }
+
+ // Deal with the queue being empty or populated
+ if (this._last) {
+ item.prev = this._last
+ this._last.next = item
+ this._last = item
+ } else {
+ this._last = item
+ this._first = item
+ }
+
+ // Update the length and return the current item
+ return item
+ }
+
+ shift () {
+ // Check if we have a value
+ var remove = this._first
+ if (!remove) return null
+
+ // If we do, remove it and relink things
+ this._first = remove.next
+ if (this._first) this._first.prev = null
+ this._last = this._first ? this._last : null
+ return remove.value
+ }
+
+ // Shows us the first item in the list
+ first () {
+ return this._first && this._first.value
+ }
+
+ // Shows us the last item in the list
+ last () {
+ return this._last && this._last.value
+ }
+
+ // Removes the item that was returned from the push
+ remove (item) {
+ // Relink the previous item
+ if (item.prev) item.prev.next = item.next
+ if (item.next) item.next.prev = item.prev
+ if (item === this._last) this._last = item.prev
+ if (item === this._first) this._first = item.next
+
+ // Invalidate item
+ item.prev = null
+ item.next = null
+ }
+}
diff --git a/src/animation/Runner.js b/src/animation/Runner.js
new file mode 100644
index 0000000..f752185
--- /dev/null
+++ b/src/animation/Runner.js
@@ -0,0 +1,928 @@
+import { Controller, Ease, Stepper } from './Controller.js'
+import { extend } from '../utils/adopter.js'
+import { getOrigin } from '../utils/utils.js'
+import { noop, timeline } from '../modules/core/defaults.js'
+import { registerMethods } from '../utils/methods.js'
+import Animator from './Animator.js'
+import Box from '../types/Box.js'
+import EventTarget from '../types/EventTarget.js'
+import Matrix from '../types/Matrix.js'
+import Morphable, { TransformBag } from '../types/Morphable.js'
+import Point from '../types/Point.js'
+import SVGNumber from '../types/SVGNumber.js'
+import Timeline from './Timeline.js'
+
+export default class Runner extends EventTarget {
+ constructor (options) {
+ super()
+
+ // Store a unique id on the runner, so that we can identify it later
+ this.id = Runner.id++
+
+ // Ensure a default value
+ options = options == null
+ ? timeline.duration
+ : options
+
+ // Ensure that we get a controller
+ options = typeof options === 'function'
+ ? new Controller(options)
+ : options
+
+ // Declare all of the variables
+ this._element = null
+ this._timeline = null
+ this.done = false
+ this._queue = []
+
+ // Work out the stepper and the duration
+ this._duration = typeof options === 'number' && options
+ this._isDeclarative = options instanceof Controller
+ this._stepper = this._isDeclarative ? options : new Ease()
+
+ // We copy the current values from the timeline because they can change
+ this._history = {}
+
+ // Store the state of the runner
+ this.enabled = true
+ this._time = 0
+ this._last = 0
+
+ // Save transforms applied to this runner
+ this.transforms = new Matrix()
+ this.transformId = 1
+
+ // Looping variables
+ this._haveReversed = false
+ this._reverse = false
+ this._loopsDone = 0
+ this._swing = false
+ this._wait = 0
+ this._times = 1
+ }
+
+ /*
+ Runner Definitions
+ ==================
+ These methods help us define the runtime behaviour of the Runner or they
+ help us make new runners from the current runner
+ */
+
+ element (element) {
+ if (element == null) return this._element
+ this._element = element
+ element._prepareRunner()
+ return this
+ }
+
+ timeline (timeline) {
+ // check explicitly for undefined so we can set the timeline to null
+ if (typeof timeline === 'undefined') return this._timeline
+ this._timeline = timeline
+ return this
+ }
+
+ animate (duration, delay, when) {
+ var o = Runner.sanitise(duration, delay, when)
+ var runner = new Runner(o.duration)
+ if (this._timeline) runner.timeline(this._timeline)
+ if (this._element) runner.element(this._element)
+ return runner.loop(o).schedule(delay, when)
+ }
+
+ schedule (timeline, delay, when) {
+ // The user doesn't need to pass a timeline if we already have one
+ if (!(timeline instanceof Timeline)) {
+ when = delay
+ delay = timeline
+ timeline = this.timeline()
+ }
+
+ // If there is no timeline, yell at the user...
+ if (!timeline) {
+ throw Error('Runner cannot be scheduled without timeline')
+ }
+
+ // Schedule the runner on the timeline provided
+ timeline.schedule(this, delay, when)
+ return this
+ }
+
+ unschedule () {
+ var timeline = this.timeline()
+ timeline && timeline.unschedule(this)
+ return this
+ }
+
+ loop (times, swing, wait) {
+ // Deal with the user passing in an object
+ if (typeof times === 'object') {
+ swing = times.swing
+ wait = times.wait
+ times = times.times
+ }
+
+ // Sanitise the values and store them
+ this._times = times || Infinity
+ this._swing = swing || false
+ this._wait = wait || 0
+ return this
+ }
+
+ delay (delay) {
+ return this.animate(0, delay)
+ }
+
+ /*
+ Basic Functionality
+ ===================
+ These methods allow us to attach basic functions to the runner directly
+ */
+
+ queue (initFn, runFn, isTransform) {
+ this._queue.push({
+ initialiser: initFn || noop,
+ runner: runFn || noop,
+ isTransform: isTransform,
+ initialised: false,
+ finished: false
+ })
+ var timeline = this.timeline()
+ timeline && this.timeline()._continue()
+ return this
+ }
+
+ during (fn) {
+ return this.queue(null, fn)
+ }
+
+ after (fn) {
+ return this.on('finish', fn)
+ }
+
+ /*
+ Runner animation methods
+ ========================
+ Control how the animation plays
+ */
+
+ time (time) {
+ if (time == null) {
+ return this._time
+ }
+ let dt = time - this._time
+ this.step(dt)
+ return this
+ }
+
+ duration () {
+ return this._times * (this._wait + this._duration) - this._wait
+ }
+
+ loops (p) {
+ var loopDuration = this._duration + this._wait
+ if (p == null) {
+ var loopsDone = Math.floor(this._time / loopDuration)
+ var relativeTime = (this._time - loopsDone * loopDuration)
+ var position = relativeTime / this._duration
+ return Math.min(loopsDone + position, this._times)
+ }
+ var whole = Math.floor(p)
+ var partial = p % 1
+ var time = loopDuration * whole + this._duration * partial
+ return this.time(time)
+ }
+
+ position (p) {
+ // Get all of the variables we need
+ var x = this._time
+ var d = this._duration
+ var w = this._wait
+ var t = this._times
+ var s = this._swing
+ var r = this._reverse
+ var position
+
+ if (p == null) {
+ /*
+ This function converts a time to a position in the range [0, 1]
+ The full explanation can be found in this desmos demonstration
+ https://www.desmos.com/calculator/u4fbavgche
+ The logic is slightly simplified here because we can use booleans
+ */
+
+ // Figure out the value without thinking about the start or end time
+ const f = function (x) {
+ var swinging = s * Math.floor(x % (2 * (w + d)) / (w + d))
+ var backwards = (swinging && !r) || (!swinging && r)
+ var uncliped = Math.pow(-1, backwards) * (x % (w + d)) / d + backwards
+ var clipped = Math.max(Math.min(uncliped, 1), 0)
+ return clipped
+ }
+
+ // Figure out the value by incorporating the start time
+ var endTime = t * (w + d) - w
+ position = x <= 0 ? Math.round(f(1e-5))
+ : x < endTime ? f(x)
+ : Math.round(f(endTime - 1e-5))
+ return position
+ }
+
+ // Work out the loops done and add the position to the loops done
+ var loopsDone = Math.floor(this.loops())
+ var swingForward = s && (loopsDone % 2 === 0)
+ var forwards = (swingForward && !r) || (r && swingForward)
+ position = loopsDone + (forwards ? p : 1 - p)
+ return this.loops(position)
+ }
+
+ progress (p) {
+ if (p == null) {
+ return Math.min(1, this._time / this.duration())
+ }
+ return this.time(p * this.duration())
+ }
+
+ step (dt) {
+ // If we are inactive, this stepper just gets skipped
+ if (!this.enabled) return this
+
+ // Update the time and get the new position
+ dt = dt == null ? 16 : dt
+ this._time += dt
+ var position = this.position()
+
+ // Figure out if we need to run the stepper in this frame
+ var running = this._lastPosition !== position && this._time >= 0
+ this._lastPosition = position
+
+ // Figure out if we just started
+ var duration = this.duration()
+ var justStarted = this._lastTime < 0 && this._time > 0
+ var justFinished = this._lastTime < this._time && this.time > duration
+ this._lastTime = this._time
+ if (justStarted) {
+ this.fire('start', this)
+ }
+
+ // Work out if the runner is finished set the done flag here so animations
+ // know, that they are running in the last step (this is good for
+ // transformations which can be merged)
+ var declarative = this._isDeclarative
+ this.done = !declarative && !justFinished && this._time >= duration
+
+ // Call initialise and the run function
+ if (running || declarative) {
+ this._initialise(running)
+
+ // clear the transforms on this runner so they dont get added again and again
+ this.transforms = new Matrix()
+ var converged = this._run(declarative ? dt : position)
+ this.fire('step', this)
+ }
+ // correct the done flag here
+ // declaritive animations itself know when they converged
+ this.done = this.done || (converged && declarative)
+ if (this.done) {
+ this.fire('finish', this)
+ }
+ return this
+ }
+
+ finish () {
+ return this.step(Infinity)
+ }
+
+ reverse (reverse) {
+ this._reverse = reverse == null ? !this._reverse : reverse
+ return this
+ }
+
+ ease (fn) {
+ this._stepper = new Ease(fn)
+ return this
+ }
+
+ active (enabled) {
+ if (enabled == null) return this.enabled
+ this.enabled = enabled
+ return this
+ }
+
+ /*
+ Private Methods
+ ===============
+ Methods that shouldn't be used externally
+ */
+
+ // Save a morpher to the morpher list so that we can retarget it later
+ _rememberMorpher (method, morpher) {
+ this._history[method] = {
+ morpher: morpher,
+ caller: this._queue[this._queue.length - 1]
+ }
+ }
+
+ // Try to set the target for a morpher if the morpher exists, otherwise
+ // do nothing and return false
+ _tryRetarget (method, target) {
+ if (this._history[method]) {
+ // if the last method wasnt even initialised, throw it away
+ if (!this._history[method].caller.initialised) {
+ let index = this._queue.indexOf(this._history[method].caller)
+ this._queue.splice(index, 1)
+ return false
+ }
+
+ // for the case of transformations, we use the special retarget function
+ // which has access to the outer scope
+ if (this._history[method].caller.isTransform) {
+ this._history[method].caller.isTransform(target)
+ // for everything else a simple morpher change is sufficient
+ } else {
+ this._history[method].morpher.to(target)
+ }
+
+ this._history[method].caller.finished = false
+ var timeline = this.timeline()
+ timeline && timeline._continue()
+ return true
+ }
+ return false
+ }
+
+ // Run each initialise function in the runner if required
+ _initialise (running) {
+ // If we aren't running, we shouldn't initialise when not declarative
+ if (!running && !this._isDeclarative) return
+
+ // Loop through all of the initialisers
+ for (var i = 0, len = this._queue.length; i < len; ++i) {
+ // Get the current initialiser
+ var current = this._queue[i]
+
+ // Determine whether we need to initialise
+ var needsIt = this._isDeclarative || (!current.initialised && running)
+ running = !current.finished
+
+ // Call the initialiser if we need to
+ if (needsIt && running) {
+ current.initialiser.call(this)
+ current.initialised = true
+ }
+ }
+ }
+
+ // Run each run function for the position or dt given
+ _run (positionOrDt) {
+ // Run all of the _queue directly
+ var allfinished = true
+ for (var i = 0, len = this._queue.length; i < len; ++i) {
+ // Get the current function to run
+ var current = this._queue[i]
+
+ // Run the function if its not finished, we keep track of the finished
+ // flag for the sake of declarative _queue
+ var converged = current.runner.call(this, positionOrDt)
+ current.finished = current.finished || (converged === true)
+ allfinished = allfinished && current.finished
+ }
+
+ // We report when all of the constructors are finished
+ return allfinished
+ }
+
+ addTransform (transform, index) {
+ this.transforms.lmultiplyO(transform)
+ return this
+ }
+
+ clearTransform () {
+ this.transforms = new Matrix()
+ return this
+ }
+
+ static sanitise (duration, delay, when) {
+ // Initialise the default parameters
+ var times = 1
+ var swing = false
+ var wait = 0
+ duration = duration || timeline.duration
+ delay = delay || timeline.delay
+ when = when || 'last'
+
+ // If we have an object, unpack the values
+ if (typeof duration === 'object' && !(duration instanceof Stepper)) {
+ delay = duration.delay || delay
+ when = duration.when || when
+ swing = duration.swing || swing
+ times = duration.times || times
+ wait = duration.wait || wait
+ duration = duration.duration || timeline.duration
+ }
+
+ return {
+ duration: duration,
+ delay: delay,
+ swing: swing,
+ times: times,
+ wait: wait,
+ when: when
+ }
+ }
+}
+
+Runner.id = 0
+
+class FakeRunner {
+ constructor (transforms = new Matrix(), id = -1, done = true) {
+ this.transforms = transforms
+ this.id = id
+ this.done = done
+ }
+}
+
+extend([Runner, FakeRunner], {
+ mergeWith (runner) {
+ return new FakeRunner(
+ runner.transforms.lmultiply(this.transforms),
+ runner.id
+ )
+ }
+})
+
+// FakeRunner.emptyRunner = new FakeRunner()
+
+const lmultiply = (last, curr) => last.lmultiplyO(curr)
+const getRunnerTransform = (runner) => runner.transforms
+
+function mergeTransforms () {
+ // Find the matrix to apply to the element and apply it
+ let runners = this._transformationRunners.runners
+ let netTransform = runners
+ .map(getRunnerTransform)
+ .reduce(lmultiply, new Matrix())
+
+ this.transform(netTransform)
+
+ this._transformationRunners.merge()
+
+ if (this._transformationRunners.length() === 1) {
+ this._frameId = null
+ }
+}
+
+class RunnerArray {
+ constructor () {
+ this.runners = []
+ this.ids = []
+ }
+
+ add (runner) {
+ if (this.runners.includes(runner)) return
+
+ let id = runner.id + 1
+
+ let leftSibling = this.ids.reduce((last, curr) => {
+ if (curr > last && curr < id) return curr
+ return last
+ }, 0)
+
+ let index = this.ids.indexOf(leftSibling) + 1
+
+ this.ids.splice(index, 0, id)
+ this.runners.splice(index, 0, runner)
+
+ return this
+ }
+
+ getByID (id) {
+ return this.runners[this.ids.indexOf(id + 1)]
+ }
+
+ remove (id) {
+ let index = this.ids.indexOf(id + 1)
+ this.ids.splice(index, 1)
+ this.runners.splice(index, 1)
+ return this
+ }
+
+ merge () {
+ let lastRunner = null
+ this.runners.forEach((runner, i) => {
+ if (lastRunner && runner.done && lastRunner.done) {
+ this.remove(runner.id)
+ this.edit(lastRunner.id, runner.mergeWith(lastRunner))
+ }
+
+ lastRunner = runner
+ })
+
+ return this
+ }
+
+ edit (id, newRunner) {
+ let index = this.ids.indexOf(id + 1)
+ this.ids.splice(index, 1, id)
+ this.runners.splice(index, 1, newRunner)
+ return this
+ }
+
+ length () {
+ return this.ids.length
+ }
+
+ clearBefore (id) {
+ let deleteCnt = this.ids.indexOf(id + 1) || 1
+ this.ids.splice(0, deleteCnt, 0)
+ this.runners.splice(0, deleteCnt, new FakeRunner())
+ return this
+ }
+}
+
+let frameId = 0
+registerMethods({
+ Element: {
+ animate (duration, delay, when) {
+ var o = Runner.sanitise(duration, delay, when)
+ var timeline = this.timeline()
+ return new Runner(o.duration)
+ .loop(o)
+ .element(this)
+ .timeline(timeline)
+ .schedule(delay, when)
+ },
+
+ delay (by, when) {
+ return this.animate(0, by, when)
+ },
+
+ // this function searches for all runners on the element and deletes the ones
+ // which run before the current one. This is because absolute transformations
+ // overwfrite anything anyway so there is no need to waste time computing
+ // other runners
+ _clearTransformRunnersBefore (currentRunner) {
+ this._transformationRunners.clearBefore(currentRunner.id)
+ },
+
+ _currentTransform (current) {
+ return this._transformationRunners.runners
+ // we need the equal sign here to make sure, that also transformations
+ // on the same runner which execute before the current transformation are
+ // taken into account
+ .filter((runner) => runner.id <= current.id)
+ .map(getRunnerTransform)
+ .reduce(lmultiply, new Matrix())
+ },
+
+ addRunner (runner) {
+ this._transformationRunners.add(runner)
+
+ Animator.transform_frame(
+ mergeTransforms.bind(this), this._frameId
+ )
+ },
+
+ _prepareRunner () {
+ if (this._frameId == null) {
+ this._transformationRunners = new RunnerArray()
+ .add(new FakeRunner(new Matrix(this)))
+
+ this._frameId = frameId++
+ }
+ }
+ }
+})
+
+extend(Runner, {
+ attr (a, v) {
+ return this.styleAttr('attr', a, v)
+ },
+
+ // Add animatable styles
+ css (s, v) {
+ return this.styleAttr('css', s, v)
+ },
+
+ styleAttr (type, name, val) {
+ // apply attributes individually
+ if (typeof name === 'object') {
+ for (var key in val) {
+ this.styleAttr(type, key, val[key])
+ }
+ }
+
+ var morpher = new Morphable(this._stepper).to(val)
+
+ this.queue(function () {
+ morpher = morpher.from(this.element()[type](name))
+ }, function (pos) {
+ this.element()[type](name, morpher.at(pos))
+ return morpher.done()
+ })
+
+ return this
+ },
+
+ zoom (level, point) {
+ var morpher = new Morphable(this._stepper).to(new SVGNumber(level))
+
+ this.queue(function () {
+ morpher = morpher.from(this.zoom())
+ }, function (pos) {
+ this.element().zoom(morpher.at(pos), point)
+ return morpher.done()
+ })
+
+ return this
+ },
+
+ /**
+ ** absolute transformations
+ **/
+
+ //
+ // M v -----|-----(D M v = F v)------|-----> T v
+ //
+ // 1. define the final state (T) and decompose it (once)
+ // t = [tx, ty, the, lam, sy, sx]
+ // 2. on every frame: pull the current state of all previous transforms
+ // (M - m can change)
+ // and then write this as m = [tx0, ty0, the0, lam0, sy0, sx0]
+ // 3. Find the interpolated matrix F(pos) = m + pos * (t - m)
+ // - Note F(0) = M
+ // - Note F(1) = T
+ // 4. Now you get the delta matrix as a result: D = F * inv(M)
+
+ transform (transforms, relative, affine) {
+ // If we have a declarative function, we should retarget it if possible
+ relative = transforms.relative || relative
+ if (this._isDeclarative && !relative && this._tryRetarget('transform', transforms)) {
+ return this
+ }
+
+ // Parse the parameters
+ var isMatrix = Matrix.isMatrixLike(transforms)
+ affine = transforms.affine != null
+ ? transforms.affine
+ : (affine != null ? affine : !isMatrix)
+
+ // Create a morepher and set its type
+ const morpher = new Morphable()
+ .type(affine ? TransformBag : Matrix)
+ .stepper(this._stepper)
+
+ let origin
+ let element
+ let current
+ let currentAngle
+ let startTransform
+
+ function setup () {
+ // make sure element and origin is defined
+ element = element || this.element()
+ origin = origin || getOrigin(transforms, element)
+
+ startTransform = new Matrix(relative ? undefined : element)
+
+ // add the runner to the element so it can merge transformations
+ element.addRunner(this)
+
+ // Deactivate all transforms that have run so far if we are absolute
+ if (!relative) {
+ element._clearTransformRunnersBefore(this)
+ }
+ }
+
+ function run (pos) {
+ // clear all other transforms before this in case something is saved
+ // on this runner. We are absolute. We dont need these!
+ if (!relative) this.clearTransform()
+
+ let { x, y } = new Point(origin).transform(element._currentTransform(this))
+
+ let target = new Matrix({ ...transforms, origin: [x, y] })
+ let start = this._isDeclarative && current
+ ? current
+ : startTransform
+
+ if (affine) {
+ target = target.decompose(x, y)
+ start = start.decompose(x, y)
+
+ // Get the current and target angle as it was set
+ const rTarget = target.rotate
+ const rCurrent = start.rotate
+
+ // Figure out the shortest path to rotate directly
+ const possibilities = [rTarget - 360, rTarget, rTarget + 360]
+ const distances = possibilities.map(a => Math.abs(a - rCurrent))
+ const shortest = Math.min(...distances)
+ const index = distances.indexOf(shortest)
+ target.rotate = possibilities[index]
+ }
+
+ if (relative) {
+ // we have to be careful here not to overwrite the rotation
+ // with the rotate method of Matrix
+ if (!isMatrix) {
+ target.rotate = transforms.rotate || 0
+ }
+ if (this._isDeclarative && currentAngle) {
+ start.rotate = currentAngle
+ }
+ }
+
+ morpher.from(start)
+ morpher.to(target)
+
+ let affineParameters = morpher.at(pos)
+ currentAngle = affineParameters.rotate
+ current = new Matrix(affineParameters)
+
+ this.addTransform(current)
+ return morpher.done()
+ }
+
+ function retarget (newTransforms) {
+ // only get a new origin if it changed since the last call
+ if (
+ (newTransforms.origin || 'center').toString() !==
+ (transforms.origin || 'center').toString()
+ ) {
+ origin = getOrigin(transforms, element)
+ }
+
+ // overwrite the old transformations with the new ones
+ transforms = { ...newTransforms, origin }
+ }
+
+ this.queue(setup, run, retarget)
+ this._isDeclarative && this._rememberMorpher('transform', morpher)
+ return this
+ },
+
+ // Animatable x-axis
+ x (x, relative) {
+ return this._queueNumber('x', x)
+ },
+
+ // Animatable y-axis
+ y (y) {
+ return this._queueNumber('y', y)
+ },
+
+ dx (x) {
+ return this._queueNumberDelta('dx', x)
+ },
+
+ dy (y) {
+ return this._queueNumberDelta('dy', y)
+ },
+
+ _queueNumberDelta (method, to) {
+ to = new SVGNumber(to)
+
+ // Try to change the target if we have this method already registerd
+ if (this._tryRetargetDelta(method, to)) return this
+
+ // Make a morpher and queue the animation
+ var morpher = new Morphable(this._stepper).to(to)
+ this.queue(function () {
+ var from = this.element()[method]()
+ morpher.from(from)
+ morpher.to(from + to)
+ }, function (pos) {
+ this.element()[method](morpher.at(pos))
+ return morpher.done()
+ })
+
+ // Register the morpher so that if it is changed again, we can retarget it
+ this._rememberMorpher(method, morpher)
+ return this
+ },
+
+ _queueObject (method, to) {
+ // Try to change the target if we have this method already registerd
+ if (this._tryRetarget(method, to)) return this
+
+ // Make a morpher and queue the animation
+ var morpher = new Morphable(this._stepper).to(to)
+ this.queue(function () {
+ morpher.from(this.element()[method]())
+ }, function (pos) {
+ this.element()[method](morpher.at(pos))
+ return morpher.done()
+ })
+
+ // Register the morpher so that if it is changed again, we can retarget it
+ this._rememberMorpher(method, morpher)
+ return this
+ },
+
+ _queueNumber (method, value) {
+ return this._queueObject(method, new SVGNumber(value))
+ },
+
+ // Animatable center x-axis
+ cx (x) {
+ return this._queueNumber('cx', x)
+ },
+
+ // Animatable center y-axis
+ cy (y) {
+ return this._queueNumber('cy', y)
+ },
+
+ // Add animatable move
+ move (x, y) {
+ return this.x(x).y(y)
+ },
+
+ // Add animatable center
+ center (x, y) {
+ return this.cx(x).cy(y)
+ },
+
+ // Add animatable size
+ size (width, height) {
+ // animate bbox based size for all other elements
+ var box
+
+ if (!width || !height) {
+ box = this._element.bbox()
+ }
+
+ if (!width) {
+ width = box.width / box.height * height
+ }
+
+ if (!height) {
+ height = box.height / box.width * width
+ }
+
+ return this
+ .width(width)
+ .height(height)
+ },
+
+ // Add animatable width
+ width (width) {
+ return this._queueNumber('width', width)
+ },
+
+ // Add animatable height
+ height (height) {
+ return this._queueNumber('height', height)
+ },
+
+ // Add animatable plot
+ plot (a, b, c, d) {
+ // Lines can be plotted with 4 arguments
+ if (arguments.length === 4) {
+ return this.plot([a, b, c, d])
+ }
+
+ // FIXME: this needs to be rewritten such that the element is only accesed
+ // in the init function
+ return this._queueObject('plot', new this._element.MorphArray(a))
+
+ /*
+ var morpher = this._element.morphArray().to(a)
+
+ this.queue(function () {
+ morpher.from(this._element.array())
+ }, function (pos) {
+ this._element.plot(morpher.at(pos))
+ })
+
+ return this
+ */
+ },
+
+ // Add leading method
+ leading (value) {
+ return this._queueNumber('leading', value)
+ },
+
+ // Add animatable viewbox
+ viewbox (x, y, width, height) {
+ return this._queueObject('viewbox', new Box(x, y, width, height))
+ },
+
+ update (o) {
+ if (typeof o !== 'object') {
+ return this.update({
+ offset: arguments[0],
+ color: arguments[1],
+ opacity: arguments[2]
+ })
+ }
+
+ if (o.opacity != null) this.attr('stop-opacity', o.opacity)
+ if (o.color != null) this.attr('stop-color', o.color)
+ if (o.offset != null) this.attr('offset', o.offset)
+
+ return this
+ }
+})
diff --git a/src/animation/Timeline.js b/src/animation/Timeline.js
new file mode 100644
index 0000000..619e50a
--- /dev/null
+++ b/src/animation/Timeline.js
@@ -0,0 +1,271 @@
+import { registerMethods } from '../utils/methods.js'
+import Animator from './Animator.js'
+
+var time = window.performance || Date
+
+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 {
+ // Construct a new timeline on the given element
+ constructor () {
+ this._timeSource = function () {
+ return time.now()
+ }
+
+ this._dispatcher = document.createElement('div')
+
+ // 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
+ }
+
+ getEventTarget () {
+ return this._dispatcher
+ }
+
+ /**
+ *
+ */
+
+ // schedules a runner on the timeline
+ schedule (runner, delay, when) {
+ 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
+ // TODO: Deal with window.blur window.focus to pause animations
+ 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
+
+ // TODO: Figure out end time of runner
+ 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
+ }
+ }
+})