summaryrefslogtreecommitdiffstats
path: root/src/animation
diff options
context:
space:
mode:
authorUlrich-Matthias Schäfer <ulima.ums@googlemail.com>2018-11-30 13:19:58 +0100
committerUlrich-Matthias Schäfer <ulima.ums@googlemail.com>2018-11-30 13:19:58 +0100
commitf4531868a190af69c4ecdcf6d7be6d3fc59f5d46 (patch)
tree1fc76780ad6918ef7b6eb0e302a55f1b043d8bc5 /src/animation
parentd64b964d21e1399b198e44555be68a12378053e7 (diff)
parentefc82b0eafa72902b35c2b22cd9e86bdbdd3edfb (diff)
downloadsvg.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.js8
-rw-r--r--src/animation/Morphable.js260
-rw-r--r--src/animation/Runner.js22
-rw-r--r--src/animation/Timeline.js131
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
}