diff options
author | Ulrich-Matthias Schäfer <ulima.ums@googlemail.com> | 2018-11-30 13:19:58 +0100 |
---|---|---|
committer | Ulrich-Matthias Schäfer <ulima.ums@googlemail.com> | 2018-11-30 13:19:58 +0100 |
commit | f4531868a190af69c4ecdcf6d7be6d3fc59f5d46 (patch) | |
tree | 1fc76780ad6918ef7b6eb0e302a55f1b043d8bc5 /src/animation | |
parent | d64b964d21e1399b198e44555be68a12378053e7 (diff) | |
parent | efc82b0eafa72902b35c2b22cd9e86bdbdd3edfb (diff) | |
download | svg.js-f4531868a190af69c4ecdcf6d7be6d3fc59f5d46.tar.gz svg.js-f4531868a190af69c4ecdcf6d7be6d3fc59f5d46.zip |
Merge branch '3.0.0' into 790-color-spaces
Diffstat (limited to 'src/animation')
-rw-r--r-- | src/animation/Animator.js | 8 | ||||
-rw-r--r-- | src/animation/Morphable.js | 260 | ||||
-rw-r--r-- | src/animation/Runner.js | 22 | ||||
-rw-r--r-- | src/animation/Timeline.js | 131 |
4 files changed, 358 insertions, 63 deletions
diff --git a/src/animation/Animator.js b/src/animation/Animator.js index cac0eb9..2786602 100644 --- a/src/animation/Animator.js +++ b/src/animation/Animator.js @@ -5,7 +5,7 @@ const Animator = { nextDraw: null, frames: new Queue(), timeouts: new Queue(), - timer: globals.window.performance || globals.window.Date, + timer: () => globals.window.performance || globals.window.Date, transforms: [], frame (fn) { @@ -29,7 +29,7 @@ const Animator = { delay = delay || 0 // Work out when the event should fire - var time = Animator.timer.now() + delay + var time = Animator.timer().now() + delay // Add the timeout to the end of the queue var node = Animator.timeouts.push({ run: fn, time: time }) @@ -43,11 +43,11 @@ const Animator = { }, cancelFrame (node) { - Animator.frames.remove(node) + node != null && Animator.frames.remove(node) }, clearTimeout (node) { - Animator.timeouts.remove(node) + node != null && Animator.timeouts.remove(node) }, _draw (now) { diff --git a/src/animation/Morphable.js b/src/animation/Morphable.js new file mode 100644 index 0000000..56ffe95 --- /dev/null +++ b/src/animation/Morphable.js @@ -0,0 +1,260 @@ +import { Ease } from './Controller.js' +import { + delimiter, + numberAndUnit, + pathLetters +} from '../modules/core/regex.js' +import { extend } from '../utils/adopter.js' +import Color from '../types/Color.js' +import PathArray from '../types/PathArray.js' +import SVGArray from '../types/SVGArray.js' +import SVGNumber from '../types/SVGNumber.js' + +export default class Morphable { + constructor (stepper) { + this._stepper = stepper || new Ease('-') + + this._from = null + this._to = null + this._type = null + this._context = null + this._morphObj = null + } + + from (val) { + if (val == null) { + return this._from + } + + this._from = this._set(val) + return this + } + + to (val) { + if (val == null) { + return this._to + } + + this._to = this._set(val) + return this + } + + type (type) { + // getter + if (type == null) { + return this._type + } + + // setter + this._type = type + return this + } + + _set (value) { + if (!this._type) { + var type = typeof value + + if (type === 'number') { + this.type(SVGNumber) + } else if (type === 'string') { + if (Color.isColor(value)) { + this.type(Color) + } else if (delimiter.test(value)) { + this.type(pathLetters.test(value) + ? PathArray + : SVGArray + ) + } else if (numberAndUnit.test(value)) { + this.type(SVGNumber) + } else { + this.type(NonMorphable) + } + } else if (morphableTypes.indexOf(value.constructor) > -1) { + this.type(value.constructor) + } else if (Array.isArray(value)) { + this.type(SVGArray) + } else if (type === 'object') { + this.type(ObjectBag) + } else { + this.type(NonMorphable) + } + } + + var result = (new this._type(value)) + if (this._type === Color) { + result = this._to ? result[this._to[4]]() + : this._from ? result[this._from[4]]() + : result + } + result = result.toArray() + + this._morphObj = this._morphObj || new this._type() + this._context = this._context + || Array.apply(null, Array(result.length)).map(Object) + return result + } + + stepper (stepper) { + if (stepper == null) return this._stepper + this._stepper = stepper + return this + } + + done () { + var complete = this._context + .map(this._stepper.done) + .reduce(function (last, curr) { + return last && curr + }, true) + return complete + } + + at (pos) { + var _this = this + + return this._morphObj.fromArray( + this._from.map(function (i, index) { + return _this._stepper.step(i, _this._to[index], pos, _this._context[index], _this._context) + }) + ) + } +} + +export class NonMorphable { + constructor (...args) { + this.init(...args) + } + + init (val) { + val = Array.isArray(val) ? val[0] : val + this.value = val + return this + } + + valueOf () { + return this.value + } + + toArray () { + return [ this.value ] + } +} + +export class TransformBag { + constructor (...args) { + this.init(...args) + } + + init (obj) { + if (Array.isArray(obj)) { + obj = { + scaleX: obj[0], + scaleY: obj[1], + shear: obj[2], + rotate: obj[3], + translateX: obj[4], + translateY: obj[5], + originX: obj[6], + originY: obj[7] + } + } + + Object.assign(this, TransformBag.defaults, obj) + return this + } + + toArray () { + var v = this + + return [ + v.scaleX, + v.scaleY, + v.shear, + v.rotate, + v.translateX, + v.translateY, + v.originX, + v.originY + ] + } +} + +TransformBag.defaults = { + scaleX: 1, + scaleY: 1, + shear: 0, + rotate: 0, + translateX: 0, + translateY: 0, + originX: 0, + originY: 0 +} + +export class ObjectBag { + constructor (...args) { + this.init(...args) + } + + init (objOrArr) { + this.values = [] + + if (Array.isArray(objOrArr)) { + this.values = objOrArr + return + } + + objOrArr = objOrArr || {} + var entries = [] + + for (let i in objOrArr) { + entries.push([i, objOrArr[i]]) + } + + entries.sort((a, b) => { + return a[0] - b[0] + }) + + this.values = entries.reduce((last, curr) => last.concat(curr), []) + return this + } + + valueOf () { + var obj = {} + var arr = this.values + + for (var i = 0, len = arr.length; i < len; i += 2) { + obj[arr[i]] = arr[i + 1] + } + + return obj + } + + toArray () { + return this.values + } +} + +const morphableTypes = [ + NonMorphable, + TransformBag, + ObjectBag +] + +export function registerMorphableType (type = []) { + morphableTypes.push(...[].concat(type)) +} + +export function makeMorphable () { + extend(morphableTypes, { + to (val) { + return new Morphable() + .type(this.constructor) + .from(this.valueOf()) + .to(val) + }, + fromArray (arr) { + this.init(arr) + return this + } + }) +} diff --git a/src/animation/Runner.js b/src/animation/Runner.js index 7e04c21..3af5823 100644 --- a/src/animation/Runner.js +++ b/src/animation/Runner.js @@ -9,7 +9,7 @@ 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 Morphable, { TransformBag } from './Morphable.js' import Point from '../types/Point.js' import SVGNumber from '../types/SVGNumber.js' import Timeline from './Timeline.js' @@ -48,7 +48,10 @@ export default class Runner extends EventTarget { // Store the state of the runner this.enabled = true this._time = 0 - this._last = 0 + this._lastTime = 0 + + // At creation, the runner is in reseted state + this._reseted = true // Save transforms applied to this runner this.transforms = new Matrix() @@ -261,7 +264,7 @@ export default class Runner extends EventTarget { // Figure out if we just started var duration = this.duration() - var justStarted = this._lastTime < 0 && this._time > 0 + var justStarted = this._lastTime <= 0 && this._time > 0 var justFinished = this._lastTime < this._time && this.time > duration this._lastTime = this._time if (justStarted) { @@ -274,6 +277,9 @@ export default class Runner extends EventTarget { var declarative = this._isDeclarative this.done = !declarative && !justFinished && this._time >= duration + // Runner is running. So its not in reseted state anymore + this._reseted = false + // Call initialise and the run function if (running || declarative) { this._initialise(running) @@ -281,6 +287,7 @@ export default class Runner extends EventTarget { // 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 @@ -292,6 +299,13 @@ export default class Runner extends EventTarget { return this } + reset () { + if (this._reseted) return this + this.loops(0) + this._reseted = true + return this + } + finish () { return this.step(Infinity) } @@ -564,7 +578,7 @@ registerMethods({ return new Runner(o.duration) .loop(o) .element(this) - .timeline(timeline) + .timeline(timeline.play()) .schedule(delay, when) }, diff --git a/src/animation/Timeline.js b/src/animation/Timeline.js index 6abcb80..c3ad07c 100644 --- a/src/animation/Timeline.js +++ b/src/animation/Timeline.js @@ -10,64 +10,58 @@ var makeSchedule = function (runnerInfo) { return { start: start, duration: duration, end: end, runner: runnerInfo.runner } } +const defaultSource = function () { + let w = globals.window + return (w.performance || w.Date).now() +} + export default class Timeline extends EventTarget { // Construct a new timeline on the given element - constructor () { + constructor (timeSource = defaultSource) { super() - this._timeSource = function () { - let w = globals.window - return (w.performance || w.Date).now() - } + this._timeSource = timeSource // Store the timing variables this._startTime = 0 this._speed = 1.0 - // Play control variables control how the animation proceeds - this._reverse = false + // 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 = false + this._paused = true this._runners = [] this._order = [] this._time = 0 this._lastSourceTime = 0 this._lastStepTime = 0 - } - /** - * - */ + // Make sure that step is always called in class context + this._step = this._step.bind(this) + } // 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) + return a.runner.id - b.runner.id }) } - 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 + var 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 = this._startTime + absoluteStartTime = endTime } else if (when === 'absolute' || when === 'start') { absoluteStartTime = delay delay = 0 @@ -86,21 +80,18 @@ export default class Timeline extends EventTarget { // Manage runner runner.unschedule() runner.timeline(this) - runner.time(-delay) - - // Save startTime for next runner - this._startTime = absoluteStartTime + runner.duration() + delay + // runner.time(-delay) // Save runnerInfo this._runners[runner.id] = { persist: this.persist(), runner: runner, - start: absoluteStartTime + start: absoluteStartTime + delay } - // Save order and continue + // Save order, update Time if needed and continue this._order.push(runner.id) - this._continue() + this.updateTime()._continue() return this } @@ -115,27 +106,42 @@ export default class Timeline extends EventTarget { return this } + // Calculates the end of the timeline + getEndTime () { + var lastRunnerInfo = this._runners[this._order[this._order.length - 1]] + var lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0 + var lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : 0 + return lastStartTime + lastDuration + } + + // Makes sure, that after pausing the time doesn't jump + updateTime () { + if (!this.active()) { + this._lastSourceTime = this._timeSource() + } + return this + } + play () { // Now make sure we are not paused and continue the animation this._paused = false - return this._continue() + return this.updateTime()._continue() } pause () { - // Cancel the next animation frame and pause - this._nextFrame = null this._paused = true - return this + return this._continue() } stop () { - // Cancel the next animation frame and go to start - this.seek(-this._time) + // Go to start and pause + this.time(0) return this.pause() } finish () { - this.seek(Infinity) + // Go to end and pause + this.time(this.getEndTime() + 1) return this.pause() } @@ -154,14 +160,13 @@ export default class Timeline extends EventTarget { } seek (dt) { - this._time += dt - return this._continue() + return this.time(this._time + dt) } time (time) { if (time == null) return this._time this._time = time - return this + return this._continue(true) } persist (dtOrForever) { @@ -176,20 +181,25 @@ export default class Timeline extends EventTarget { return this } - _step () { - // If the timeline is paused, just do nothing - if (this._paused) return - + _step (immediateStep = false) { // Get the time delta from the last time and update the time var time = this._timeSource() var dtSource = time - this._lastSourceTime + + if (immediateStep) dtSource = 0 + var dtTime = this._speed * dtSource + (this._time - this._lastStepTime) this._lastSourceTime = time - // Update the time - this._time += dtTime + // 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.fire('time', this._time) // Run all of the runners directly var runnersLeft = false @@ -204,8 +214,13 @@ export default class Timeline extends EventTarget { let dtToStart = this._time - runnerInfo.start // Dont run runner if not started yet - if (dtToStart < 0) { + if (dtToStart <= 0) { runnersLeft = true + + // This is for the case that teh timeline was seeked so that the time + // is now before the startTime of the runner. Thats why we need to set + // the runner to position 0 + runner.reset() continue } else if (dtToStart < dt) { // Adjust dt to make sure that animation is on point @@ -234,21 +249,27 @@ export default class Timeline extends EventTarget { } } - // Get the next animation frame to keep the simulation going - if (runnersLeft) { - this._nextFrame = Animator.frame(this._step.bind(this)) + // 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._order.length && this._speed < 0 && this._time > 0)) { + this._continue() } else { - this._nextFrame = null + this.fire('finished') + this.pause() } + return this } // Checks if we are running and continues the animation - _continue () { + _continue (immediateStep = false) { + Animator.cancelFrame(this._nextFrame) + this._nextFrame = null + + if (immediateStep) return this._step(true) if (this._paused) return this - if (!this._nextFrame) { - this._nextFrame = Animator.frame(this._step.bind(this)) - } + + this._nextFrame = Animator.frame(this._step) return this } |