Published unlisted
Edited
Oct 25, 2020
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
xx md`
### Hermite basis

If we want to interpolate between specified endpoints with specified tangents we
can use a [cubic Hermite spline][]. If we call the endpoint values ${$`x_0`} and
${$`x_1`}, and the tangent values ${$`x_0'`} and ${$`x_1'`}, our interpolating
cubic polynomials is:

${$$`
x(t) = h_{00}(t)x_0 + h_{10}(t)x_0' + h_{01}(t)x_1 + h_{11}(t)x_1'
`}
With basis functions (letting ${$`\bar{t} = 1-t`} be the [affine][affine combination] complement):
${$$`
\begin{aligned}
h_{00}(t) &= 2t^3 - 3t^2 + 1 = \bar{t} + t\bar{t}\kern2mu^2 - t^2\bar{t} \\
h_{10}(t) &= t^3 - 2t^2 + 1 = t\bar{t}\kern2mu^2 \\
h_{01}(t) &= -2t^3 + 3t^2 = t - t\bar{t}\kern2mu^2 + t^2\bar{t} \\
h_{11}(t) &= t^3 - t^2 = -t^2\bar{t} \\
\end{aligned}
`}

Or in keeping with the previous papers we could use an
alternative basis of trigonometric functions:

${$$`
x_\mathrm{trig}(t) = k_{00}(t)x_0 + k_{10}(t)x_0' + k_{01}(t)x_1 + k_{11}(t)x_1'
`}

${$$`
\begin{aligned}
k_{00}(t) &= \cos\left(\tfrac{\pi}{2} t\right)^2 \\[.3em]
k_{10}(t) &= \tfrac{2}{\pi}\!\left(
\sin\left(\tfrac{\pi}{2} t\right) -
\sin\left(\tfrac{\pi}{2} t\right)^2 \right) \\[.3em]
k_{01}(t) &= \sin\left(\tfrac{\pi}{2} t\right)^2 \\[.3em]
k_{11}(t) &= \tfrac{2}{\pi}\!\left(
-\cos\left(\tfrac{\pi}{2} t\right) +
\cos\left(\tfrac{\pi}{2} t\right)^2 \right)
\end{aligned}
`}

[cubic Hermite spline]: https://en.wikipedia.org/wiki/Cubic_Hermite_spline
[affine combination]: https://en.wikipedia.org/wiki/Affine_combination
`
Insert cell
Insert cell
circle_spline = function circle_spline(knots, segment) {
const npts = knots.length;
const segments = [];
for (let i = 0; i < npts; i++) {
const pm1 = knots[mod(i-1, npts)], p0 = knots[i];
const p1 = knots[mod(i+1, npts)], p2 = knots[mod(i+2, npts)];
const [a0, a1] = angles(pm1, p0, p1, p2);
segments.push(segment(p0, p1, a0, a1));
}
return segments;
}
Insert cell
apollo_spline = function apollo_spline(knots, correction) {
const npts = knots.length;
const astart = [], aend = []; // exterior angle
const kstart = [], kend = []; // half curvature = 1 / diameter
const dinv = []; // inverse distance between knots
// Loop through each interval, finding the angles and uncorrected
// curvature at the knots, and the inverse distance between knots.
for (let i = npts-1; i >= 0; i--) {
const pm1 = knots[mod(i-1, npts)], p0 = knots[i],
p1 = knots[mod(i+1, npts)], p2 = knots[mod(i+2, npts)];
const inv_dist = dinv[i] = 1 / cabs(csub(p1, p0));
const [a0, a1] = [astart[i], aend[i]] = angles(pm1, p0, p1, p2);
kstart[i] = inv_dist * (Math.sin(a0) - (a1 - a0));
kend[i] = inv_dist * (Math.sin(a1) + (a1 - a0));
}

// Loop through each interval again, this time calculating curvature
// adjustments, then producing a function that draws the curve segment.
const segments = [];
for (let i = npts-1; i >= 0; i--) {
const im1 = mod(i-1, npts), ip1 = mod(i+1, npts); // indices i-1, i+1
const a0 = astart[i], a1 = aend[i];
let kc0, kc1; // curvature correction
if (+correction == 1) {
const k0m = kend[im1], k0p = kstart[i],
k1m = kend[i], k1p = kstart[ip1],
dm1 = dinv[im1], d0 = dinv[i], d1 = dinv[ip1];
kc0 = (k0p - k0m) / (dm1 + d0);
kc1 = (k1p - k1m) / (d0 + d1);
} else if (+correction == 2) {
// set curvature at the endpoints equal to the curvature
// of the defining circular arcs.
kc0 = (a0 - a1), kc1 = (a0 - a1);
} else {
kc0 = kc1 = 0; // no curvature correction
}
segments[i] = apollo_segment_adjusted(
knots[i], knots[mod(i+1, npts)], a0, a1, kc0, kc1);
}
return segments;
}
Insert cell
x apollo_spline(closed_curve_points, 1).map((d, i, A) =>
[+curvature(d)(1).toFixed(5),
+curvature(A[mod(i+1, A.length)])(0).toFixed(5)])
Insert cell
x apollo_spline(closed_curve_points, 0).map((d, i, A) =>
[+curvature(d)(1).toFixed(5),
+curvature(A[mod(i+1, A.length)])(0).toFixed(5)])
Insert cell
apollo_segment_adjusted = function apollo_segment_adjusted(p0, p1, a0, a1, kc0, kc1) {
// kc0 and kc1 are the adjustments to make in curvature at the endpoints.
return function(t) {
const t_ = 1 - t, tt_ = t*t_;
const a = t_*(a0 + tt_*kc0) + t*(a1 - tt_*kc1);
const w = apollo_coords(a, t);
return cadd(p0, cmul(csub(p1, p0), w));
}
}
Insert cell
Insert cell
Insert cell
spline_plot = function spline_plot(segments, bounds, tolerance) {
const bezpts = [];
for (let i = 0; i < segments.length; i++) {
bezpts.push(...bezeval(segments[i], [0, 1], bounds, tolerance));
}
return bezpts_to_svgpath(bezpts)
}
Insert cell
Insert cell
angles = function angle(pm1, p0, p1, p2) {
const z0 = cdiv(csub(p1, pm1), csub(p0, pm1));
const z1 = cdiv(csub(p0, p2), csub(p1, p2));
return [imag(clog(z0)), -imag(clog(z1))];
}
Insert cell
Insert cell
blending = ({
linear: (t) => [t, t],
trig: (t) => [t, 0.5*(1 - Math.cos(Math.PI*t))],
cubic: (t) => [t, t * t * (3 - 2*t)],
})
Insert cell
lerp_segment = function lerp_segment(p0, p1, a0, a1, blend) {
if (blend == null) blend = (t) => [t, t];
return function(t) {
const [t1, t2] = blend(t);
const w0 = arclen_coords(a0, t1);
const w1 = arclen_coords(a1, t1);
const w = cadd(cmul([1-t2,0], w0), cmul([t2,0], w1));
return cadd(p0, cmul(csub(p1, p0), w));
}
}
Insert cell
arclen_segment = function arclen_segment(p0, p1, a0, a1, blend) {
if (blend == null) blend = (t) => [t, t];
return function(t) {
const [t1, t2] = blend(t);
const a = (1 - t2)*a0 + t2*a1;
const w = arclen_coords(a, t1);
return cadd(p0, cmul(csub(p1, p0), w));
}
}
Insert cell
apollo_segment = function apollo_segment(p0, p1, a0, a1, blend) {
if (blend == null) blend = (t) => [t, t];
return function(t) {
const [t1, t2] = blend(t);
const a = (1 - t2)*a0 + t2*a1;
const w = apollo_coords(a, t1);
return cadd(p0, cmul(csub(p1, p0), w));
}
}
Insert cell
wenz_bezier = function wenz_bezier(pm1, p0, p1, p2, bounds) {
const [a0, a1] = angles(pm1, p0, p1, p2);
return bezpts_to_svgpath(bezeval(
lerp_segment(p0, p1, a0, a1, blending.linear),
[0, 1], bounds, 0.01));
}
Insert cell
sv_bezier = function sv_bezier(pm1, p0, p1, p2, bounds) {
const [a0, a1] = angles(pm1, p0, p1, p2);
return bezpts_to_svgpath(bezeval(
lerp_segment(p0, p1, a0, a1, blending.trig),
[0, 1], bounds, 0.01));
}
Insert cell
Insert cell
sly_bezier = function sly_bezier(pm1, p0, p1, p2, bounds) {
const [a0, a1] = angles(pm1, p0, p1, p2);
return bezpts_to_svgpath(bezeval(
arclen_segment(p0, p1, a0, a1, blending.linear),
[0, 1], bounds, 0.01));
}
Insert cell
sly_trig_bezier = function sly_trig_bezier(pm1, p0, p1, p2, bounds) {
const [a0, a1] = angles(pm1, p0, p1, p2);
return bezpts_to_svgpath(bezeval(
arclen_segment(p0, p1, a0, a1, blending.trig),
[0, 1], bounds, 0.01));
}
Insert cell
Insert cell
apollo_bezier = function apollo_bezier(pm1, p0, p1, p2, bounds) {
const [a0, a1] = angles(pm1, p0, p1, p2);
return bezpts_to_svgpath(bezeval(
apollo_segment(p0, p1, a0, a1, blending.linear),
[0, 1], bounds, 0.01));
}
Insert cell
Insert cell
apollo_cubic_bezier = function apollo_cubic_bezier(pm1, p0, p1, p2, bounds) {
const [a0, a1] = angles(pm1, p0, p1, p2);
return bezpts_to_svgpath(bezeval(
apollo_segment(p0, p1, a0, a1, blending.cubic),
[0, 1], bounds, 0.01));
}
Insert cell
trig_blend_inv = {
const PI_INV = 1 / Math.PI;
return function trig_blendeasing_inv(t) {
return PI_INV*Math.acos(1 - 2*t);
}
}
Insert cell
Insert cell
arclen_coords = function arclen_coords(a, t) {
if (a == 0) return [t,0];
const r = Math.sin(a*t)/Math.sin(a);
const b = -a*(1-t);
return [r*Math.cos(b), r*Math.sin(b)];
}
Insert cell
Insert cell
// equivalent to above, modulo rounding errors
arclen_coords2 = function arclen_coords2(a, t) {
if (a == 0) return [t,0];
return cdiv(csub(cexp([0,2*a*t]), [1,0]),
csub(cexp([0,2*a]), [1,0]));
}
Insert cell
Insert cell
apollo_coords = function apollo_coords(a, t) {
const t1mz = cmul([1-t,0], cexp([0,a]));
return cdiv([t,0], cadd(t1mz, [t,0]));
}
Insert cell
Insert cell
latlong_coords = function latlong_coords(a, t) {
const b = Math.PI * t * 0.5;
const s = Math.sin(b), c = Math.cos(b);
const z = cexp([0,a]);
return cdiv([s, 0], cadd(cmul([c,0],z), [s,0]));
}
Insert cell
Insert cell
import {cadd, csub, cmul, cinv, cdiv, clog, cexp, imag, cabs, phase} from '@jrus/complex'
Insert cell
Insert cell
mod = function mod(x, y) {
return (x % y) + y * !!((x % y) && (x > 0 ^ y > 0));
}
Insert cell
viewof time = new View(0.4)
Insert cell
viewof curve_points = new View([[99,401],[361,483],[729,299],[614,476]])
Insert cell
viewof curve_points_smaller = new View([[156,419],[168,381],[371,355],[243,398]])
Insert cell
Insert cell
viewof closed_curve_points = new View([[87,119],[80,199],[107,59],[228,125],[192,137],[265,233],[34,223]])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more