From fee3c86f81c46c6ecc05f7b9519457381f3318e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 3 Jul 2020 13:02:25 +0200 Subject: [PATCH 1/7] interpolateBasis tests --- test/basis-test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/basis-test.js diff --git a/test/basis-test.js b/test/basis-test.js new file mode 100644 index 0000000..56dd034 --- /dev/null +++ b/test/basis-test.js @@ -0,0 +1,27 @@ +var tape = require("tape"), + interpolate = require("../"); + +require("./inDelta"); + +tape("interpolateBasis(values)(t) returns the expected values", function(test) { + var i = interpolate.interpolateBasis([0, 0, 3]); + test.equal(i(-1), 0); + test.equal(i(0), 0); + test.inDelta(i(0.19), 0.027436); + test.inDelta(i(0.21), 0.037044); + test.equal(i(1), 3); + test.equal(i(1.19), 3); + test.end(); +}); + +tape("interpolateBasisClosed(values)(t) returns the expected values", function(test) { + var i = interpolate.interpolateBasisClosed([0, 0, 3]); + test.equal(i(-1), 0.5); + test.equal(i(0), 0.5); + test.inDelta(i(0.19), 0.132350); + test.inDelta(i(0.21), 0.150350); + test.equal(i(1), 0.5); + test.inDelta(i(1.19), 0.132350); + test.inDelta(i(0.19 - 3), 0.132350); + test.end(); +}); From fdbd9c4b0fdbb8d1c74dbe0060b83b4a4bca866a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 3 Jul 2020 13:02:47 +0200 Subject: [PATCH 2/7] interpolateCubic, interpolateCubicClosed --- README.md | 11 ++++++++++- src/cubic.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 1 + test/cubic-test.js | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/cubic.js create mode 100644 test/cubic-test.js diff --git a/README.md b/README.md index 08bee8e..c14b0e5 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Returns an interpolator between the two hue angles *a* and *b*. If either hue is ### Splines -Whereas standard interpolators blend from a starting value *a* at *t* = 0 to an ending value *b* at *t* = 1, spline interpolators smoothly blend multiple input values for *t* in [0,1] using piecewise polynomial functions. Only cubic uniform nonrational [B-splines](https://en.wikipedia.org/wiki/B-spline) are currently supported, also known as basis splines. +Whereas standard interpolators blend from a starting value *a* at *t* = 0 to an ending value *b* at *t* = 1, spline interpolators smoothly blend multiple input values for *t* in [0,1] using piecewise polynomial functions. Cubic uniform nonrational [B-splines](https://en.wikipedia.org/wiki/B-spline), also known as basis splines, are supported, as well as the standard [cubic Hermite splines](https://en.wikipedia.org/wiki/Cubic_Hermite_spline). # d3.interpolateBasis(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/basis.js), [Examples](https://observablehq.com/@d3/d3-interpolatebasis) @@ -241,6 +241,15 @@ Returns a uniform nonrational B-spline interpolator through the specified array Returns a uniform nonrational B-spline interpolator through the specified array of *values*, which must be numbers. The control points are implicitly repeated such that the resulting one-dimensional spline has cyclical C² continuity when repeated around *t* in [0,1]. See also [d3.curveBasisClosed](https://github.com/d3/d3-shape/blob/master/README.md#curveBasisClosed). +# d3.interpolateCubic(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/cubic.js) + +Returns a cubic Hermite spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length - 1). + +# d3.interpolateCubicClosed(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/cubic.js) + +Returns a closed cubic Hermite spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length), and is cyclical (*f*(1 + *t*) = *f*(*t*)). + + ### Piecewise # d3.piecewise(interpolate, values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/piecewise.js), [Examples](https://observablehq.com/@d3/d3-piecewise) diff --git a/src/cubic.js b/src/cubic.js new file mode 100644 index 0000000..5a4e90a --- /dev/null +++ b/src/cubic.js @@ -0,0 +1,45 @@ +export default function cubic(values, type = "default") { + let n = values.length - 1, k; + values = values.slice(); + switch (type) { + case "default": + values.push(2 * values[n] - values[n - 1]); + values.unshift(2 * values[0] - values[1]); + return t => cubic(clamp(t, 0, 1)); + case "closed": + values.unshift(values[n]); + values.push(values[1]); + values.push(values[2]); + n += 2; + k = 1 - 1 / n; + return t => cubic(k * frac(t)); + case "open": + throw new Error('open cubic spline not implemented yet'); + } + + function cubic(t) { + const i = Math.min(n - 1, Math.floor(t * n)), + v0 = values[i], + v1 = values[i + 1], + v2 = values[i + 2], + v3 = values[i + 3], + d = t * n - i, + s20 = v2 - v0, + s31 = v3 - v1, + s21 = (v2 - v1) * 2; + return (((s20 + s31 - 2 * s21) * d + (3 * s21 - 2 * s20 - s31)) * d + s20) + * d / 2 + v1; + } +} + +export function closed (values) { + return cubic(values, "closed"); +} + +function frac(t) { + return t - Math.floor(t); +} + +function clamp(t, min, max) { + return Math.min(max, Math.max(min, t)); +} diff --git a/src/index.js b/src/index.js index b4dce7d..462fa21 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ export {default as interpolateBasisClosed} from "./basisClosed.js"; export {default as interpolateDate} from "./date.js"; export {default as interpolateDiscrete} from "./discrete.js"; export {default as interpolateHue} from "./hue.js"; +export {default as interpolateCubic, closed as interpolateCubicClosed} from "./cubic.js"; export {default as interpolateNumber} from "./number.js"; export {default as interpolateNumberArray} from "./numberArray.js"; export {default as interpolateObject} from "./object.js"; diff --git a/test/cubic-test.js b/test/cubic-test.js new file mode 100644 index 0000000..ca85508 --- /dev/null +++ b/test/cubic-test.js @@ -0,0 +1,37 @@ +var tape = require("tape"), + interpolate = require("../"); + +require("./inDelta"); + +tape("interpolateCubic(values)(t) returns the expected values", function(test) { + var i = interpolate.interpolateCubic([0, 0, 3, 4, 1]); + test.equal(i(-1), 0); + test.equal(i(0), 0); + test.equal(i(0.25), 0); + test.equal(i(0.5), 3); + test.equal(i(0.75), 4); + test.equal(i(1), 1); + test.inDelta(i(0.1), -0.144); + test.inDelta(i(0.19), -0.207936); + test.inDelta(i(0.21), -0.169344); + test.equal(i(2), 1); + test.end(); +}); + +tape("interpolateCubicClosed(values)(t) returns the expected values", function(test) { + var i = interpolate.interpolateCubicClosed([0, 0, 3, 4, 1]); + test.equal(i(0), 0); + test.equal(i(0.2), 0); + test.equal(i(0.4), 3); + test.equal(i(0.6), 4); + test.equal(i(0.8), 1); + test.equal(i(1), 0); + test.inDelta(i(0.1), -0.25); + test.inDelta(i(0.19), -0.068875); + test.inDelta(i(0.21), 0.0846875); + test.inDelta(i(1.1), -0.25); + test.inDelta(i(1.19), -0.068875); + test.equal(i(-1), 0); + test.equal(i(2), 0); + test.end(); +}); From 63333808dbc016d6e5f19297287a81c20292204e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 3 Jul 2020 20:15:43 +0200 Subject: [PATCH 3/7] monotone, monotoneClosed --- src/cubic.js | 12 +++--------- src/index.js | 1 + src/math.js | 11 +++++++++++ src/monotone.js | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 src/math.js create mode 100644 src/monotone.js diff --git a/src/cubic.js b/src/cubic.js index 5a4e90a..9e94c9f 100644 --- a/src/cubic.js +++ b/src/cubic.js @@ -1,3 +1,5 @@ +import {clamp, floor, frac, min} from "./math.js"; + export default function cubic(values, type = "default") { let n = values.length - 1, k; values = values.slice(); @@ -18,7 +20,7 @@ export default function cubic(values, type = "default") { } function cubic(t) { - const i = Math.min(n - 1, Math.floor(t * n)), + const i = min(n - 1, floor(t * n)), v0 = values[i], v1 = values[i + 1], v2 = values[i + 2], @@ -35,11 +37,3 @@ export default function cubic(values, type = "default") { export function closed (values) { return cubic(values, "closed"); } - -function frac(t) { - return t - Math.floor(t); -} - -function clamp(t, min, max) { - return Math.min(max, Math.max(min, t)); -} diff --git a/src/index.js b/src/index.js index 462fa21..7578b2d 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ export {default as interpolateDate} from "./date.js"; export {default as interpolateDiscrete} from "./discrete.js"; export {default as interpolateHue} from "./hue.js"; export {default as interpolateCubic, closed as interpolateCubicClosed} from "./cubic.js"; +export {default as interpolateMonotone, closed as interpolateMonotoneClosed} from "./monotone.js"; export {default as interpolateNumber} from "./number.js"; export {default as interpolateNumberArray} from "./numberArray.js"; export {default as interpolateObject} from "./object.js"; diff --git a/src/math.js b/src/math.js new file mode 100644 index 0000000..386d56f --- /dev/null +++ b/src/math.js @@ -0,0 +1,11 @@ +export var abs = Math.abs; +export var floor = Math.floor; +export var max = Math.max; +export var min = Math.min; +export var sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; }; +export function frac(t) { + return t - floor(t); +} +export function clamp(t, mi, ma) { + return min(ma, max(mi, t)); +} diff --git a/src/monotone.js b/src/monotone.js new file mode 100644 index 0000000..567bf24 --- /dev/null +++ b/src/monotone.js @@ -0,0 +1,41 @@ +import {abs, clamp, frac, min, sign} from "./math.js"; + +export default function monotone(values, type = "default") { + let n = values.length - 1, k; + values = values.slice(); + switch (type) { + case "default": + values.push(2 * values[n] - values[n - 1]); + values.unshift(2 * values[0] - values[1]); + return t => monotone(clamp(t, 0, 1)); + case "closed": + values.unshift(values[n]); + values.push(values[1]); + values.push(values[2]); + n += 2; + k = 1 - 1 / n; + return t => monotone(k * frac(t)); + case "open": + throw new Error('open monotone spline not implemented yet'); + } + + function monotone(t) { + const i = Math.min(n - 1, Math.floor(t * n)), + y_im1 = values[i], + y_i = values[i + 1], + y_ip1 = values[i + 2], + y_ip2 = values[i + 3], + d = t * n - i, + s_im1 = n * (y_i - y_im1), + s_i = n * (y_ip1 - y_i), + s_ip1 = n * (y_ip2 - y_ip1), + yp_i = (sign(s_im1) + sign(s_i)) * min(abs(s_im1), abs(s_i), 0.25 * n * abs(y_ip1 - y_im1)), + yp_ip1 = (sign(s_i) + sign(s_ip1)) * min(abs(s_i), abs(s_ip1), 0.25 * n * abs(y_ip2 - y_i)); + + return (((yp_i + yp_ip1 - 2 * s_i) * d + (3 * s_i - 2 * yp_i - yp_ip1)) * d + yp_i) * (d / n) + y_i; + } +} + +export function closed (values) { + return monotone(values, "closed"); +} From fd48a2f73b7f656d44aa9330d3052c58efb3b2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 3 Jul 2020 20:51:47 +0200 Subject: [PATCH 4/7] document monotone --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c14b0e5..4438a9d 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Returns an interpolator between the two hue angles *a* and *b*. If either hue is ### Splines -Whereas standard interpolators blend from a starting value *a* at *t* = 0 to an ending value *b* at *t* = 1, spline interpolators smoothly blend multiple input values for *t* in [0,1] using piecewise polynomial functions. Cubic uniform nonrational [B-splines](https://en.wikipedia.org/wiki/B-spline), also known as basis splines, are supported, as well as the standard [cubic Hermite splines](https://en.wikipedia.org/wiki/Cubic_Hermite_spline). +Whereas standard interpolators blend from a starting value *a* at *t* = 0 to an ending value *b* at *t* = 1, spline interpolators smoothly blend multiple input values for *t* in [0,1] using piecewise polynomial functions. Cubic uniform nonrational [B-splines](https://en.wikipedia.org/wiki/B-spline), also known as basis splines, are supported, as well as the [cubic Hermite splines](https://en.wikipedia.org/wiki/Cubic_Hermite_spline) and [monotone cubic splines](https://en.wikipedia.org/wiki/Monotone_cubic_interpolation). # d3.interpolateBasis(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/basis.js), [Examples](https://observablehq.com/@d3/d3-interpolatebasis) @@ -250,6 +250,15 @@ Returns a cubic Hermite spline interpolator through the specified array of *valu Returns a closed cubic Hermite spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length), and is cyclical (*f*(1 + *t*) = *f*(*t*)). +# d3.interpolateMonotone(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/monotone.js) + +Returns a monotone cubic spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length - 1). + +# d3.interpolateMonotoneClosed(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/monotone.js) + +Returns a closed monotone cubic spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length), and is cyclical (*f*(1 + *t*) = *f*(*t*)). + + ### Piecewise # d3.piecewise(interpolate, values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/piecewise.js), [Examples](https://observablehq.com/@d3/d3-piecewise) From 00d07845713fa42e36127dee3e6c9e062456e580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 3 Jul 2020 21:10:15 +0200 Subject: [PATCH 5/7] test monotone --- test/monotone-test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/monotone-test.js diff --git a/test/monotone-test.js b/test/monotone-test.js new file mode 100644 index 0000000..66031aa --- /dev/null +++ b/test/monotone-test.js @@ -0,0 +1,31 @@ +var tape = require("tape"), + interpolate = require("../"); + +require("./inDelta"); + +tape("interpolateMonotone(values)(t) returns the expected values", function(test) { + var i = interpolate.interpolateCubic([3, 2.8, 2.5, 1, 0.95, 0.8, 0.5, 0.1, 0.05]); + test.equal(i(-1), 3); + test.inDelta(i(0), 3); + test.inDelta(i(0.25), 2.5); + test.inDelta(i(0.5), 0.95); + test.inDelta(i(0.6), 0.8412); + test.inDelta(i(0.75), 0.5); + test.inDelta(i(1), 0.05); + test.inDelta(i(2), 0.05); + test.end(); +}); + +tape("interpolateMonotoneClosed(values)(t) returns the expected values", function(test) { + var i = interpolate.interpolateMonotoneClosed([0, 0, 3, 4, 1]); + test.equal(i(0), 0); + test.inDelta(i(0.2), 0); + test.inDelta(i(0.4), 3); + test.inDelta(i(0.5), 3.75); + test.inDelta(i(0.6), 4); + test.inDelta(i(0.8), 1); + test.inDelta(i(1), 0); + test.inDelta(i(-1), 0); + test.inDelta(i(2), 0); + test.end(); +}); From b9f494a49b8ff64159b9eef99c80c89edc19431a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Sat, 4 Jul 2020 23:29:45 +0200 Subject: [PATCH 6/7] default type is "clamped" https://github.com/Evercoder/culori/issues/91#issuecomment-653767693 --- src/cubic.js | 11 ++++++----- src/monotone.js | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cubic.js b/src/cubic.js index 9e94c9f..4564f5b 100644 --- a/src/cubic.js +++ b/src/cubic.js @@ -1,13 +1,9 @@ import {clamp, floor, frac, min} from "./math.js"; -export default function cubic(values, type = "default") { +export default function cubic(values, type) { let n = values.length - 1, k; values = values.slice(); switch (type) { - case "default": - values.push(2 * values[n] - values[n - 1]); - values.unshift(2 * values[0] - values[1]); - return t => cubic(clamp(t, 0, 1)); case "closed": values.unshift(values[n]); values.push(values[1]); @@ -17,6 +13,11 @@ export default function cubic(values, type = "default") { return t => cubic(k * frac(t)); case "open": throw new Error('open cubic spline not implemented yet'); + case "clamped": + default: + values.push(2 * values[n] - values[n - 1]); + values.unshift(2 * values[0] - values[1]); + return t => cubic(clamp(t, 0, 1)); } function cubic(t) { diff --git a/src/monotone.js b/src/monotone.js index 567bf24..95504bc 100644 --- a/src/monotone.js +++ b/src/monotone.js @@ -1,13 +1,9 @@ import {abs, clamp, frac, min, sign} from "./math.js"; -export default function monotone(values, type = "default") { +export default function monotone(values, type) { let n = values.length - 1, k; values = values.slice(); switch (type) { - case "default": - values.push(2 * values[n] - values[n - 1]); - values.unshift(2 * values[0] - values[1]); - return t => monotone(clamp(t, 0, 1)); case "closed": values.unshift(values[n]); values.push(values[1]); @@ -17,6 +13,11 @@ export default function monotone(values, type = "default") { return t => monotone(k * frac(t)); case "open": throw new Error('open monotone spline not implemented yet'); + case "clamped": + default: + values.push(2 * values[n] - values[n - 1]); + values.unshift(2 * values[0] - values[1]); + return t => monotone(clamp(t, 0, 1)); } function monotone(t) { From 41d47e1c56d50974a88afac5e22e29ebe5ed75c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 10 Jul 2020 19:46:55 +0200 Subject: [PATCH 7/7] hi/lo notation --- src/math.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/math.js b/src/math.js index 386d56f..cbee5e8 100644 --- a/src/math.js +++ b/src/math.js @@ -6,6 +6,6 @@ export var sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; export function frac(t) { return t - floor(t); } -export function clamp(t, mi, ma) { - return min(ma, max(mi, t)); +export function clamp(t, lo, hi) { + return t < lo ? lo : t > hi ? hi : t; }