From 7b02d60829d1151a9fd1e726a0a995b92b165328 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Fri, 7 Dec 2018 15:35:41 +0100 Subject: [PATCH] Release 3.0.4 --- CHANGELOG.md | 10 +++++ package.json | 2 +- spec/spec/types/Box.js | 19 +++++++++- src/animation/Animator.js | 21 +++++++++++ src/animation/Queue.js | 2 +- src/animation/Runner.js | 45 ++++++++++------------ src/animation/Timeline.js | 79 ++++++++++++++++++++++++++------------- src/elements/Dom.js | 3 -- src/elements/Svg.js | 4 ++ src/types/Box.js | 32 ++++++++++++++++ 10 files changed, 158 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3611fb8..b53f179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http: ==== +## [3.0.4] - 2018-12-07 + +### Fixed +- fixed `zoom` which was added correctly and is animatable now +- fixed `Runner` which merges transformations on the correct frame and in the correct way now +- fixed condition on which transforms get deleted from an element when animating +- fixed `Timeline` which executes Runner in the correct order now +- fixed `Svg` which correctly deletes the defs reference on `clear()` + ## [3.0.3] - 2018-12-05 ### Fixed @@ -745,6 +754,7 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http: +[3.0.4]: https://github.com/svgdotjs/svg.js/releases/tag/3.0.4 [3.0.3]: https://github.com/svgdotjs/svg.js/releases/tag/3.0.3 [3.0.2]: https://github.com/svgdotjs/svg.js/releases/tag/3.0.2 [3.0.1]: https://github.com/svgdotjs/svg.js/releases/tag/3.0.1 diff --git a/package.json b/package.json index 9e11632..6485a38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@svgdotjs/svg.js", - "version": "3.0.3", + "version": "3.0.4", "description": "A lightweight library for manipulating and animating SVG.", "url": "https://svgdotjs.github.io/", "homepage": "https://svgdotjs.github.io/", diff --git a/spec/spec/types/Box.js b/spec/spec/types/Box.js index 1e98982..56eb7da 100644 --- a/spec/spec/types/Box.js +++ b/spec/spec/types/Box.js @@ -8,7 +8,7 @@ import { import { getMethodsFor } from '../../../src/utils/methods.js' import { getWindow, withWindow } from '../../../src/utils/window.js' -const viewbox = getMethodsFor('viewbox').viewbox +const { zoom, viewbox} = getMethodsFor('viewbox') const { any, objectContaining, arrayContaining } = jasmine @@ -175,5 +175,22 @@ describe('Box.js', () => { expect(viewbox.call(canvas).toArray()).toEqual([10, 10, 200, 200]) }) }) + + describe('zoom()', () => { + it('zooms around the center by default', () => { + const canvas = zoom.call(SVG().size(100, 50).viewbox(0, 0, 100, 50).addTo(container), 2) + expect(canvas.attr('viewBox')).toEqual('25 12.5 50 25') + }) + + it('zooms around a point', () => { + const canvas = zoom.call(SVG().size(100, 50).viewbox(0, 0, 100, 50).addTo(container), 2, [0, 0]) + expect(canvas.attr('viewBox')).toEqual('0 0 50 25') + }) + + it('gets the zoom', () => { + const canvas = zoom.call(SVG().size(100, 50).viewbox(0, 0, 100, 50).addTo(container), 2) + expect(zoom.call(canvas)).toEqual(2) + }) + }) }) }) diff --git a/src/animation/Animator.js b/src/animation/Animator.js index 64570eb..390b200 100644 --- a/src/animation/Animator.js +++ b/src/animation/Animator.js @@ -5,6 +5,7 @@ const Animator = { nextDraw: null, frames: new Queue(), timeouts: new Queue(), + immediates: new Queue(), timer: () => globals.window.performance || globals.window.Date, transforms: [], @@ -38,6 +39,17 @@ const Animator = { return node }, + immediate (fn) { + // Add the immediate fn to the end of the queue + var node = Animator.immediates.push(fn) + // Request another animation frame if we need one + if (Animator.nextDraw === null) { + Animator.nextDraw = globals.window.requestAnimationFrame(Animator._draw) + } + + return node + }, + cancelFrame (node) { node != null && Animator.frames.remove(node) }, @@ -46,6 +58,10 @@ const Animator = { node != null && Animator.timeouts.remove(node) }, + cancelImmediate (node) { + node != null && Animator.immediates.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]) @@ -70,6 +86,11 @@ const Animator = { nextFrame.run() } + var nextImmediate = null + while ((nextImmediate = Animator.immediates.shift())) { + nextImmediate() + } + // If we have remaining timeouts or frames, draw until we don't anymore Animator.nextDraw = Animator.timeouts.first() || Animator.frames.first() ? globals.window.requestAnimationFrame(Animator._draw) diff --git a/src/animation/Queue.js b/src/animation/Queue.js index 14b92b4..0d3cdcd 100644 --- a/src/animation/Queue.js +++ b/src/animation/Queue.js @@ -18,7 +18,7 @@ export default class Queue { this._first = item } - // Update the length and return the current item + // Return the current item return item } diff --git a/src/animation/Runner.js b/src/animation/Runner.js index 6a085b6..8c26578 100644 --- a/src/animation/Runner.js +++ b/src/animation/Runner.js @@ -168,7 +168,7 @@ export default class Runner extends EventTarget { } after (fn) { - return this.on('finish', fn) + return this.on('finished', fn) } /* @@ -276,7 +276,8 @@ 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 justFinished = this._lastTime < this._time && this.time > duration + var justFinished = this._lastTime < duration && this._time >= duration + this._lastTime = this._time if (justStarted) { this.fire('start', this) @@ -304,15 +305,15 @@ export default class Runner extends EventTarget { // 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) + if (justFinished) { + this.fire('finished', this) } return this } reset () { if (this._reseted) return this - this.loops(0) + this.time(0) this._reseted = true return this } @@ -443,7 +444,7 @@ export default class Runner extends EventTarget { // TODO: Keep track of all transformations so that deletion is faster clearTransformsFromQueue () { - if (!this.done || !this._timeline || !this._timeline._order.includes(this)) { + if (!this.done || !this._timeline || !this._timeline._runnerIds.includes(this.id)) { this._queue = this._queue.filter((item) => { return !item.isTransform }) @@ -530,18 +531,10 @@ class RunnerArray { 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) + this.runners.push(runner) + this.ids.push(id) return this } @@ -564,10 +557,11 @@ class RunnerArray { const condition = lastRunner && runner.done && lastRunner.done // don't merge runner when persisted on timeline - && (!runner._timeline || !runner._timeline._order.includes(runner.id)) - && (!lastRunner._timeline || !lastRunner._timeline._order.includes(lastRunner.id)) + && (!runner._timeline || !runner._timeline._runnerIds.includes(runner.id)) + && (!lastRunner._timeline || !lastRunner._timeline._runnerIds.includes(lastRunner.id)) if (condition) { + // the +1 happens in the function this.remove(runner.id) this.edit(lastRunner.id, runner.mergeWith(lastRunner)) } @@ -580,7 +574,7 @@ class RunnerArray { edit (id, newRunner) { let index = this.ids.indexOf(id + 1) - this.ids.splice(index, 1, id) + this.ids.splice(index, 1, id + 1) this.runners.splice(index, 1, newRunner) return this } @@ -636,11 +630,10 @@ registerMethods({ this._transformationRunners.add(runner) // Make sure that the runner merge is executed at the very end of - // all Animator functions. Thats why we use setTimeout here - setTimeout(() => { - Animator.cancelFrame(this._frameId) - this._frameId = Animator.frame(mergeTransforms.bind(this)) - }, 0) + // all Animator functions. Thats why we use immediate here to execute + // the merge right after all frames are run + Animator.cancelImmediate(this._frameId) + this._frameId = Animator.immediate(mergeTransforms.bind(this)) }, _prepareRunner () { @@ -689,7 +682,7 @@ extend(Runner, { var morpher = new Morphable(this._stepper).to(new SVGNumber(level)) this.queue(function () { - morpher = morpher.from(this.zoom()) + morpher = morpher.from(this.element().zoom()) }, function (pos) { this.element().zoom(morpher.at(pos), point) return morpher.done() @@ -804,8 +797,8 @@ extend(Runner, { currentAngle = affineParameters.rotate current = new Matrix(affineParameters) - element._addRunner(this) this.addTransform(current) + element._addRunner(this) return morpher.done() } diff --git a/src/animation/Timeline.js b/src/animation/Timeline.js index 56198e0..f5460b3 100644 --- a/src/animation/Timeline.js +++ b/src/animation/Timeline.js @@ -33,7 +33,8 @@ export default class Timeline extends EventTarget { this._nextFrame = null this._paused = true this._runners = [] - this._order = [] + this._runnerIds = [] + this._lastRunnerId = -1 this._time = 0 this._lastSourceTime = 0 this._lastStepTime = 0 @@ -45,7 +46,7 @@ export default class Timeline extends EventTarget { // schedules a runner on the timeline schedule (runner, delay, when) { if (runner == null) { - return this._order.map((id) => makeSchedule(this._runners[id])) + return this._runners.map(makeSchedule) } // The start time for the next animation can either be given explicitly, @@ -80,34 +81,37 @@ export default class Timeline extends EventTarget { runner.timeline(this) const persist = runner.persist() - - // Save runnerInfo - this._runners[runner.id] = { + const runnerInfo = { persist: persist === null ? this._persist : persist, - runner: runner, - start: absoluteStartTime + delay + start: absoluteStartTime + delay, + runner } - // Save order, update Time if needed and continue - this._order.push(runner.id) + 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 } // Remove the runner from this timeline unschedule (runner) { - var index = this._order.indexOf(runner.id) + var index = this._runnerIds.indexOf(runner.id) if (index < 0) return this - delete this._runners[runner.id] - this._order.splice(index, 1) + this._runners.splice(index, 1) + this._runnerIds.splice(index, 1) + runner.timeline(null) return this } // Calculates the end of the timeline getEndTime () { - var lastRunnerInfo = this._runners[this._order[this._order.length - 1]] + var lastRunnerInfo = this._runners[this._runnerIds.indexOf(this._lastRunnerId)] var lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0 var lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : 0 return lastStartTime + lastDuration @@ -200,12 +204,39 @@ export default class Timeline extends EventTarget { 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. Thats why we need to set + // the runner to position 0 + + // FIXME: + // However, reseting in insertion order leads to bugs. Considering the case, + // where 2 runners change the same attriute but in different times, + // reseting 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 reseting them backwards + for (var k = this._runners.length; k--;) { + // Get and run the current runner and ignore it if its inactive + let runnerInfo = this._runners[k] + let runner = runnerInfo.runner + + // 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 + // and try to reset it + if (dtToStart <= 0) { + runner.reset() + } + } + // Run all of the runners directly var runnersLeft = false - for (var i = 0, len = this._order.length; i < len; i++) { + for (var i = 0, len = this._runners.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 runnerInfo = this._runners[i] + let runner = runnerInfo.runner let dt = dtTime // Make sure that we give the actual difference @@ -215,11 +246,6 @@ export default class Timeline extends EventTarget { // Dont run runner if not started yet if (dtToStart <= 0) { runnersLeft = true - - // This is for the case that the 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 @@ -236,21 +262,20 @@ export default class Timeline extends EventTarget { // 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) { + if (endTime + runnerInfo.persist < this._time) { // Delete runner and correct index - delete this._runners[this._order[i]] - this._order.splice(i--, 1) && --len - runner.timeline(null) + 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._order.length && this._speed < 0 && this._time > 0)) { + if ((runnersLeft && !(this._speed < 0 && this._time === 0)) || (this._runnerIds.length && this._speed < 0 && this._time > 0)) { this._continue() } else { this.fire('finished') diff --git a/src/elements/Dom.js b/src/elements/Dom.js index 7e22b05..458ebbc 100644 --- a/src/elements/Dom.js +++ b/src/elements/Dom.js @@ -58,9 +58,6 @@ export default class Dom extends EventTarget { this.node.removeChild(this.node.lastChild) } - // remove defs reference - delete this._defs - return this } diff --git a/src/elements/Svg.js b/src/elements/Svg.js index ab7d89f..53b488c 100644 --- a/src/elements/Svg.js +++ b/src/elements/Svg.js @@ -62,6 +62,10 @@ export default class Svg extends Container { while (this.node.hasChildNodes()) { this.node.removeChild(this.node.lastChild) } + + // remove defs reference + delete this._defs + return this } } diff --git a/src/types/Box.js b/src/types/Box.js index 3df1367..eb43d07 100644 --- a/src/types/Box.js +++ b/src/types/Box.js @@ -2,6 +2,7 @@ import { delimiter } from '../modules/core/regex.js' import { globals } from '../utils/window.js' import { register } from '../utils/adopter.js' import { registerMethods } from '../utils/methods.js' +import Matrix from './Matrix.js' import Point from './Point.js' import parser from '../modules/core/parser.js' @@ -150,6 +151,37 @@ registerMethods({ // act as setter return this.attr('viewBox', new Box(x, y, width, height)) + }, + + zoom (level, point) { + var style = window.getComputedStyle(this.node) + + var width = parseFloat(style.getPropertyValue('width')) + + var height = parseFloat(style.getPropertyValue('height')) + + var v = this.viewbox() + + var zoomX = width / v.width + + var zoomY = height / v.height + + var zoom = Math.min(zoomX, zoomY) + + if (level == null) { + return zoom + } + + var zoomAmount = zoom / level + if (zoomAmount === Infinity) zoomAmount = Number.MIN_VALUE + + point = point || new Point(width / 2 / zoomX + v.x, height / 2 / zoomY + v.y) + + var box = new Box(v).transform( + new Matrix({ scale: zoomAmount, origin: point }) + ) + + return this.viewbox(box) } } }) -- 2.39.5