From 629b0dc485e568b55c82454a68ee8978939bcf12 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 27 Aug 2016 15:39:22 -0700 Subject: [PATCH 01/14] Add interpolateZoom.rho. Fixes #25. --- src/zoom.js | 95 ++++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/src/zoom.js b/src/zoom.js index 3368b01..5f5bb1d 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -1,7 +1,4 @@ -var rho = Math.SQRT2, - rho2 = 2, - rho4 = 4, - epsilon2 = 1e-12; +var epsilon2 = 1e-12; function cosh(x) { return ((x = Math.exp(x)) + 1 / x) / 2; @@ -15,50 +12,60 @@ function tanh(x) { return ((x = Math.exp(2 * x)) - 1) / (x + 1); } -// p0 = [ux0, uy0, w0] -// p1 = [ux1, uy1, w1] -export default function(p0, p1) { - var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], - ux1 = p1[0], uy1 = p1[1], w1 = p1[2], - dx = ux1 - ux0, - dy = uy1 - uy0, - d2 = dx * dx + dy * dy, - i, - S; +export default (function zoomRho(rho, rho2, rho4) { - // Special case for u0 ≅ u1. - if (d2 < epsilon2) { - S = Math.log(w1 / w0) / rho; - i = function(t) { - return [ - ux0 + t * dx, - uy0 + t * dy, - w0 * Math.exp(rho * t * S) - ]; + // p0 = [ux0, uy0, w0] + // p1 = [ux1, uy1, w1] + function zoom(p0, p1) { + var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], + ux1 = p1[0], uy1 = p1[1], w1 = p1[2], + dx = ux1 - ux0, + dy = uy1 - uy0, + d2 = dx * dx + dy * dy, + i, + S; + + // Special case for u0 ≅ u1. + if (d2 < epsilon2) { + S = Math.log(w1 / w0) / rho; + i = function(t) { + return [ + ux0 + t * dx, + uy0 + t * dy, + w0 * Math.exp(rho * t * S) + ]; + } } - } - // General case. - else { - var d1 = Math.sqrt(d2), - b0 = (w1 * w1 - w0 * w0 + rho4 * d2) / (2 * w0 * rho2 * d1), - b1 = (w1 * w1 - w0 * w0 - rho4 * d2) / (2 * w1 * rho2 * d1), - r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), - r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1); - S = (r1 - r0) / rho; - i = function(t) { - var s = t * S, - coshr0 = cosh(r0), - u = w0 / (rho2 * d1) * (coshr0 * tanh(rho * s + r0) - sinh(r0)); - return [ - ux0 + u * dx, - uy0 + u * dy, - w0 * coshr0 / cosh(rho * s + r0) - ]; + // General case. + else { + var d1 = Math.sqrt(d2), + b0 = (w1 * w1 - w0 * w0 + rho4 * d2) / (2 * w0 * rho2 * d1), + b1 = (w1 * w1 - w0 * w0 - rho4 * d2) / (2 * w1 * rho2 * d1), + r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), + r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1); + S = (r1 - r0) / rho; + i = function(t) { + var s = t * S, + coshr0 = cosh(r0), + u = w0 / (rho2 * d1) * (coshr0 * tanh(rho * s + r0) - sinh(r0)); + return [ + ux0 + u * dx, + uy0 + u * dy, + w0 * coshr0 / cosh(rho * s + r0) + ]; + } } + + i.duration = S * 1000; + + return i; } - i.duration = S * 1000; + zoom.rho = function(_) { + var _1 = +_, _2 = _1 * _1, _4 = _2 * _2; + return zoomRho(_1, _2, _4); + }; - return i; -} + return zoom; +})(Math.SQRT2, 2, 4); From 372a62c1bb3bb663feee64b295d5b44154a62b78 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 24 Aug 2018 09:27:48 -0700 Subject: [PATCH 02/14] Use DOMMatrix to parse CSS transforms. --- src/transform/parse.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/transform/parse.js b/src/transform/parse.js index 8799bec..77ec290 100644 --- a/src/transform/parse.js +++ b/src/transform/parse.js @@ -1,18 +1,10 @@ import decompose, {identity} from "./decompose"; -var cssNode, - cssRoot, - cssView, - svgNode; +var svgNode; export function parseCss(value) { - if (value === "none") return identity; - if (!cssNode) cssNode = document.createElement("DIV"), cssRoot = document.documentElement, cssView = document.defaultView; - cssNode.style.transform = value; - value = cssView.getComputedStyle(cssRoot.appendChild(cssNode), null).getPropertyValue("transform"); - cssRoot.removeChild(cssNode); - value = value.slice(7, -1).split(","); - return decompose(+value[0], +value[1], +value[2], +value[3], +value[4], +value[5]); + const m = new (typeof DOMMatrix === "function" ? DOMMatrix : WebKitCSSMatrix)(value + ""); + return m.isIdentity ? identity : decompose(m.a, m.b, m.c, m.d, m.e, m.f); } export function parseSvg(value) { From 6c8db199f9991534e8c2948f7909ada4818f9604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 4 Jun 2020 14:42:30 +0200 Subject: [PATCH 03/14] disable eslint error on undefined DOMMatrix and WebKitCSSMatrix add disabled tests for transformCss (they might work if we had DOMMatrix in node-canvas, cf. https://github.com/Automattic/node-canvas/issues/1313) --- src/transform/parse.js | 1 + test/transformCss-test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 test/transformCss-test.js diff --git a/src/transform/parse.js b/src/transform/parse.js index df68285..c62088e 100644 --- a/src/transform/parse.js +++ b/src/transform/parse.js @@ -2,6 +2,7 @@ import decompose, {identity} from "./decompose.js"; var svgNode; +/* eslint-disable no-undef */ export function parseCss(value) { const m = new (typeof DOMMatrix === "function" ? DOMMatrix : WebKitCSSMatrix)(value + ""); return m.isIdentity ? identity : decompose(m.a, m.b, m.c, m.d, m.e, m.f); diff --git a/test/transformCss-test.js b/test/transformCss-test.js new file mode 100644 index 0000000..4fe0e2f --- /dev/null +++ b/test/transformCss-test.js @@ -0,0 +1,26 @@ +var tape = require("tape"), + interpolate = require("../"); + +/* + +// see https://github.com/d3/d3-interpolate/pull/83 +// and https://github.com/Automattic/node-canvas/issues/1313 +global.DOMMatrix = require("Canvas").DOMMatrix; + +tape("interpolateTransformCss(a, b) transforms as expected", function(test) { + test.equal(interpolate.interpolateTransformCss( + "translateY(12px) scale(2)", + "translateX(3em) rotate(5deg)" + )(0.5), "translate(24px, 6px) rotate(2.5deg) scale(1.5,1.5)"); + test.deepEqual(interpolate.interpolateTransformCss( + "matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)", + "translate(3px,90px)" + )(0.5), "translate(4px, 48px) rotate(-58.282525588538995deg) skewX(-39.847576765616985deg) scale(-0.6180339887498949,0.9472135954999579)"); + test.deepEqual(interpolate.interpolateTransformCss( + "skewX(-60)", + "skewX(60) translate(280,0)" + )(0.5), "translate(140, 0) skewX(0)"); + test.end(); +}); + +*/ From 098c2edd7047bb20e8ef1238322bfd9176939092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 5 Jun 2020 10:40:32 +0200 Subject: [PATCH 04/14] avoid crashing on low curvatures (rho=1e-3 gives a good approximation of a linear transform) --- src/zoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zoom.js b/src/zoom.js index 5f5bb1d..45aaa39 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -63,7 +63,7 @@ export default (function zoomRho(rho, rho2, rho4) { } zoom.rho = function(_) { - var _1 = +_, _2 = _1 * _1, _4 = _2 * _2; + var _1 = Math.max(1e-3, +_), _2 = _1 * _1, _4 = _2 * _2; return zoomRho(_1, _2, _4); }; From a4dd859cf08d4da5aa7a6b920558227f5b45b6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 5 Jun 2020 10:42:01 +0200 Subject: [PATCH 05/14] =?UTF-8?q?the=20movement=E2=80=99s=20duration=20sho?= =?UTF-8?q?uld=20not=20explode=20for=20small=20curvatures,=20and=20should?= =?UTF-8?q?=20be=20longer=20when=20the=20curvature=20is=20important?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/zoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zoom.js b/src/zoom.js index 45aaa39..f275602 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -57,7 +57,7 @@ export default (function zoomRho(rho, rho2, rho4) { } } - i.duration = S * 1000; + i.duration = S * 1000 * rho / Math.SQRT2; return i; } From aa991e4419f834a7e790f2c05d6697ace3e1a533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 5 Jun 2020 10:42:47 +0200 Subject: [PATCH 06/14] rho getter --- src/zoom.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zoom.js b/src/zoom.js index f275602..7810b42 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -63,6 +63,7 @@ export default (function zoomRho(rho, rho2, rho4) { } zoom.rho = function(_) { + if (arguments.length === 0) return rho; var _1 = Math.max(1e-3, +_), _2 = _1 * _1, _4 = _2 * _2; return zoomRho(_1, _2, _4); }; From 8b3594e648ae1e66450a3cc790fe8b8603e4ae7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 5 Jun 2020 10:43:06 +0200 Subject: [PATCH 07/14] unit tests --- test/inDelta.js | 22 +++++++++++++++++++--- test/zoom-test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/test/inDelta.js b/test/inDelta.js index f3def21..9df0b7f 100644 --- a/test/inDelta.js +++ b/test/inDelta.js @@ -1,10 +1,26 @@ var tape = require("tape"); -tape.Test.prototype.inDelta = function(actual, expected) { - this._assert(expected - 1e-6 < actual && actual < expected + 1e-6, { - message: "should be in delta", +tape.Test.prototype.inDelta = function(actual, expected, delta) { + delta = delta || 1e-6; + this._assert(inDelta(actual, expected, delta), { + message: "should be in delta " + delta, operator: "inDelta", actual: actual, expected: expected }); }; + +function inDelta(actual, expected, delta) { + return (Array.isArray(expected) ? inDeltaArray : inDeltaNumber)(actual, expected, delta); +} + +function inDeltaArray(actual, expected, delta) { + var n = expected.length, i = -1; + if (actual.length !== n) return false; + while (++i < n) if (!inDelta(actual[i], expected[i], delta)) return false; + return true; +} + +function inDeltaNumber(actual, expected, delta) { + return actual >= expected - delta && actual <= expected + delta; +} diff --git a/test/zoom-test.js b/test/zoom-test.js index dcb9692..49a5620 100644 --- a/test/zoom-test.js +++ b/test/zoom-test.js @@ -5,3 +5,33 @@ tape("interpolateZoom(a, b) handles nearly-coincident points", function(test) { test.deepEqual(interpolate.interpolateZoom([324.68721096803614, 59.43501602433761, 1.8827137399562621], [324.6872108946794, 59.43501601062763, 7.399052110984391])(0.5), [324.68721093135775, 59.43501601748262, 3.7323313186268305]); test.end(); }); + +tape("interpolateZoom returns the expected duration", function(test) { + test.inDelta(interpolate.interpolateZoom([0, 0, 1], [0, 0, 1.1]).duration, 67, 1); + test.inDelta(interpolate.interpolateZoom([0, 0, 1], [0, 0, 2]).duration, 490, 1); + test.inDelta(interpolate.interpolateZoom([0, 0, 1], [10, 0, 8]).duration, 2872.5, 1); + test.end(); +}); + +tape("interpolateZoom parameter rho() defaults to sqrt(2)", function(test) { + test.inDelta( + interpolate.interpolateZoom.rho(Math.sqrt(2))([0,0,1], [10, 10, 5])(0.5), + interpolate.interpolateZoom([0,0,1], [10, 10, 5])(0.5) + ); + test.inDelta(interpolate.interpolateZoom.rho(), Math.sqrt(2)); + test.end(); +}); + +tape("interpolateZoom.rho(0) is (almost) linear", function(test) { + const interp = interpolate.interpolateZoom.rho(0)([0, 0, 1], [10, 0, 8]); + test.inDelta(interp(0.5), [1.111, 0, Math.sqrt(8)], 1e-3); + test.equal(Math.round(interp.duration), 1470); + test.end(); +}); + +tape("interpolateZoom parameter rho(2) has a high curvature and takes more time", function(test) { + const interp = interpolate.interpolateZoom.rho(2)([0, 0, 1], [10, 0, 8]); + test.inDelta(interp(0.5), [1.111, 0, 12.885], 1e-3); + test.equal(Math.round(interp.duration), 3775); + test.end(); +}); From 794ce05052928134784f8c3c4589f24c55414a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 5 Jun 2020 10:54:26 +0200 Subject: [PATCH 08/14] document interpolateZoom.rho --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 046c3a6..ab8985f 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,10 @@ Returns an interpolator between the two views *a* and *b* of a two-dimensional p The returned interpolator exposes a *duration* property which encodes the recommended transition duration in milliseconds. This duration is based on the path length of the curved trajectory through *x,y* space. If you want a slower or faster transition, multiply this by an arbitrary scale factor (V as described in the original paper). +# *interpolateZoom*.rho([rho]) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/zoom.js) + +Given a [zoom interpolator](#interpolateZoom), returns a new zoom interpolator using the specified curvature *rho*. When *rho* is close to 0, the interpolator is almost linear. The default curvature is sqrt(2). If *rho* is not specified, returns the interpolator’s curvature. + # d3.interpolateDiscrete(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/discrete.js), [Examples](https://observablehq.com/@d3/d3-interpolatediscrete) Returns a discrete interpolator for the given array of *values*. The returned interpolator maps *t* in [0, 1 / *n*) to *values*[0], *t* in [1 / *n*, 2 / *n*) to *values*[1], and so on, where *n* = *values*.length. In effect, this is a lightweight [quantize scale](https://github.com/d3/d3-scale/blob/master/README.md#quantize-scales) with a fixed domain of [0, 1]. From 1954d05c0ba8475fe55600245eda4438cd9b65f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 5 Jun 2020 18:28:56 +0200 Subject: [PATCH 09/14] zoomInterpolate.rho() has no getter ref https://github.com/d3/d3-interpolate/pull/61#issuecomment-639606790 --- README.md | 4 ++-- src/zoom.js | 1 - test/zoom-test.js | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ab8985f..dc06831 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,9 @@ Returns an interpolator between the two views *a* and *b* of a two-dimensional p The returned interpolator exposes a *duration* property which encodes the recommended transition duration in milliseconds. This duration is based on the path length of the curved trajectory through *x,y* space. If you want a slower or faster transition, multiply this by an arbitrary scale factor (V as described in the original paper). -# *interpolateZoom*.rho([rho]) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/zoom.js) +# *interpolateZoom*.rho(rho) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/zoom.js) -Given a [zoom interpolator](#interpolateZoom), returns a new zoom interpolator using the specified curvature *rho*. When *rho* is close to 0, the interpolator is almost linear. The default curvature is sqrt(2). If *rho* is not specified, returns the interpolator’s curvature. +Given a [zoom interpolator](#interpolateZoom), returns a new zoom interpolator using the specified curvature *rho*. When *rho* is close to 0, the interpolator is almost linear. The default curvature is sqrt(2). # d3.interpolateDiscrete(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/discrete.js), [Examples](https://observablehq.com/@d3/d3-interpolatediscrete) diff --git a/src/zoom.js b/src/zoom.js index 7810b42..f275602 100644 --- a/src/zoom.js +++ b/src/zoom.js @@ -63,7 +63,6 @@ export default (function zoomRho(rho, rho2, rho4) { } zoom.rho = function(_) { - if (arguments.length === 0) return rho; var _1 = Math.max(1e-3, +_), _2 = _1 * _1, _4 = _2 * _2; return zoomRho(_1, _2, _4); }; diff --git a/test/zoom-test.js b/test/zoom-test.js index 49a5620..a4c19c5 100644 --- a/test/zoom-test.js +++ b/test/zoom-test.js @@ -15,10 +15,9 @@ tape("interpolateZoom returns the expected duration", function(test) { tape("interpolateZoom parameter rho() defaults to sqrt(2)", function(test) { test.inDelta( + interpolate.interpolateZoom([0,0,1], [10, 10, 5])(0.5), interpolate.interpolateZoom.rho(Math.sqrt(2))([0,0,1], [10, 10, 5])(0.5), - interpolate.interpolateZoom([0,0,1], [10, 10, 5])(0.5) ); - test.inDelta(interpolate.interpolateZoom.rho(), Math.sqrt(2)); test.end(); }); From 2b9596c4fb5b0c8a9342ff2948990ba900156202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 10 Jul 2020 19:50:59 +0200 Subject: [PATCH 10/14] version numbers --- README.md | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7374787..963420f 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ Note that the generic value interpolator detects not only nested objects and arr ## Installing -If you use NPM, `npm install d3-interpolate`. Otherwise, download the [latest release](https://github.com/d3/d3-interpolate/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-interpolate.v1.min.js) or as part of [D3](https://github.com/d3/d3). AMD, CommonJS, and vanilla environments are supported. +If you use NPM, `npm install d3-interpolate`. Otherwise, download the [latest release](https://github.com/d3/d3-interpolate/releases/latest). You can also load directly from [d3js.org](https://d3js.org), either as a [standalone library](https://d3js.org/d3-interpolate.v2.min.js) or as part of [D3](https://github.com/d3/d3). AMD, CommonJS, and vanilla environments are supported. In vanilla, a `d3` global is exported. (If using [color interpolation](#color-spaces), also load [d3-color](https://github.com/d3/d3-color).) ```html - - + +