Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interpolateCubic, interpolateCubicClosed, interpolateMonotone, interpolateMonotoneClosed #87

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Expand Up @@ -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 [cubic Hermite splines](https://en.wikipedia.org/wiki/Cubic_Hermite_spline) and [monotone cubic splines](https://en.wikipedia.org/wiki/Monotone_cubic_interpolation).

<a href="#interpolateBasis" name="interpolateBasis">#</a> d3.<b>interpolateBasis</b>(<i>values</i>) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/basis.js), [Examples](https://observablehq.com/@d3/d3-interpolatebasis)

Expand All @@ -241,6 +241,24 @@ 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).

<a href="#interpolateCubic" name="interpolateCubic">#</a> d3.<b>interpolateCubic</b>(<i>values</i>) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/cubic.js)<!-- , [Examples](https://observablehq.com/@d3/d3-interpolatecubic) -->

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).

<a href="#interpolateCubicClosed" name="interpolateCubicClosed">#</a> d3.<b>interpolateCubicClosed</b>(<i>values</i>) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/cubic.js)<!-- , [Examples](https://observablehq.com/@d3/d3-interpolatecubic) -->

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*)).


<a href="#interpolateMonotone" name="interpolateMonotone">#</a> d3.<b>interpolateMonotone</b>(<i>values</i>) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/monotone.js)<!-- , [Examples](https://observablehq.com/@d3/d3-interpolatemonotone) -->

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).

<a href="#interpolateMonotoneClosed" name="interpolateMonotoneClosed">#</a> d3.<b>interpolateMonotoneClosed</b>(<i>values</i>) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/monotone.js)<!-- , [Examples](https://observablehq.com/@d3/d3-interpolatemonotone) -->

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

<a name="piecewise" href="#piecewise">#</a> d3.<b>piecewise</b>(<i>interpolate</i>, <i>values</i>) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/piecewise.js), [Examples](https://observablehq.com/@d3/d3-piecewise)
Expand Down
40 changes: 40 additions & 0 deletions src/cubic.js
@@ -0,0 +1,40 @@
import {clamp, floor, frac, min} from "./math.js";

export default function cubic(values, type) {
let n = values.length - 1, k;
values = values.slice();
switch (type) {
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');
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) {
const i = min(n - 1, 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");
}
2 changes: 2 additions & 0 deletions src/index.js
Expand Up @@ -5,6 +5,8 @@ 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 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";
Expand Down
11 changes: 11 additions & 0 deletions 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, lo, hi) {
return t < lo ? lo : t > hi ? hi : t;
}
42 changes: 42 additions & 0 deletions src/monotone.js
@@ -0,0 +1,42 @@
import {abs, clamp, frac, min, sign} from "./math.js";

export default function monotone(values, type) {
let n = values.length - 1, k;
values = values.slice();
switch (type) {
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');
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) {
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");
}
27 changes: 27 additions & 0 deletions 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();
});
37 changes: 37 additions & 0 deletions 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();
});
31 changes: 31 additions & 0 deletions 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();
});