Published
Edited
Feb 4, 2021
Importers
14 stars
Insert cell
Insert cell
Insert cell
Insert cell
circleplot = html`
<svg width="200" height="200" fill="#f0f4ff" stroke="black">
<path d="${circlepath}"></path>
</svg>
`
Insert cell
Insert cell
tolerance = 0.001
Insert cell
circlefunc = t => [100 + 80*Math.cos(t), 100 + 80*Math.sin(t)]
Insert cell
circlebez = bezeval(circlefunc, [-Math.PI, Math.PI], [0, 200, 0, 200], tolerance)
Insert cell
circlepath = bezpts_to_svgpath(circlebez, 5) + 'Z'
Insert cell
Insert cell
Insert cell
bezeval = {
const {abs} = Math;
const onethird = 1/3;
const chebpt7_1 = 1/2 - Math.sqrt(3)/4;
const chebpt7_5 = 1/2 + Math.sqrt(3)/4;
// Internal recursive helper function:
const _bezeval = function _bezeval
(func, t0, t6, viewx0, viewx1, viewy0, viewy1, tolerance,
x0, x3, x6, y0, y3, y6) {
const td = (t6 - t0);
// Evaluate the function at the four new points for
// this section, and find polynomial coefficients:
const [x1, y1] = func(t0 + td * chebpt7_1);
const [x2, y2] = func(t0 + td * 0.25);
const [x4, y4] = func(t0 + td * 0.75);
const [x5, y5] = func(t0 + td * chebpt7_5);
const xcoeffs = vals2coeffs7([x0, x1, x2, x3, x4, x5, x6]);
const ycoeffs = vals2coeffs7([y0, y1, y2, y3, y4, y5, y6]);
let [xc0, xc1, xc2, xc3, xc4, xc5, xc6] = xcoeffs;
let [yc0, yc1, yc2, yc3, yc4, yc5, yc6] = ycoeffs;
const xresid = abs(xc4) + abs(xc5) + abs(xc6); // to compare against the tolerance
const yresid = abs(yc4) + abs(yc5) + abs(yc6);
const xspread = abs(xc1) + abs(xc2) + abs(xc3) + xresid; // for finding a bounding box
const yspread = abs(yc1) + abs(yc2) + abs(yc3) + yresid;
// If the curve is entirely outside the viewport, don't bother rendering:
if ((xc0 + xspread < viewx0) || (xc0 - xspread > viewx1) ||
(yc0 + yspread < viewy0) || (yc0 - yspread > viewy1)) {
return [];
}
// If the spread is very large in either dimension, we need
// to be more careful about detecting the edges of the viewport.
if ((xspread > viewx1 - viewx0) || (yspread > viewy1 - viewy0)) {
const xmax = Math.max(x0, x1, x2, x3, x4, x5, x6);
const xmin = Math.min(x0, x1, x2, x3, x4, x5, x6);
const ymax = Math.max(y0, y1, y2, y3, y4, y5, y6);
const ymin = Math.min(y0, y1, y2, y3, y4, y5, y6);
if ((xmax < viewx0) || (xmin > viewx1) ||
(ymax < viewy0) || (ymin > viewy1)) {
return [];
}
}
// If we hit the desired tolerance, return a single bezier segment:
if ((xresid < tolerance) && (yresid < tolerance)) {
// Alias degree 6 polynomial to degree 3:
xc0 += xc6, xc1 += xc5, xc2 += xc4;
yc0 += yc6, yc1 += yc5, yc2 += yc4;
// Convert from Chebyshev to Bernstein basis, and return:
const xt0 = (3*xc0 - 5*xc2) * onethird, xt1 = (15*xc3 - xc1) * onethird;
const yt0 = (3*yc0 - 5*yc2) * onethird, yt1 = (15*yc3 - yc1) * onethird;
return [x0, y0, xt0 + xt1, yt0 + yt1, xt0 - xt1, yt0 - yt1, x6, y6];
}
// If we don't hit the tolerance, recursively bisect the domain:
const left = _bezeval(
func, t0, 0.5*(t0 + t6), viewx0, viewx1, viewy0, viewy1, tolerance,
x0, x2, x3, y0, y2, y3);
const right = _bezeval(
func, 0.5*(t0 + t6), t6, viewx0, viewx1, viewy0, viewy1, tolerance,
x3, x4, x6, y3, y4, y6);
// Combine Bezier path sections from the left and right halves:
left.push(...right);
return left;
}
return function bezeval(func, domain, viewport, tolerance) {
const [t0, t6] = domain;
// Evaluate function at endpoints and midpoint:
const [x0, y0] = func(t0);
const [x3, y3] = func(0.5*(t0 + t6));
const [x6, y6] = func(t6);
const [viewx0, viewx1, viewy0, viewy1] = viewport;
return _bezeval(
func, t0, t6, viewx0, viewx1, viewy0, viewy1, tolerance,
x0, x3, x6, y0, y3, y6);
}
}
Insert cell
Insert cell
bezpts_to_svgpath = {
const r = (x, d) => (+x).toFixed(d).replace(/\.?0+$/,'') // round
return function bezpts_to_svgpath(pts, digits) {
const d = (digits == null) ? 4 : digits;
const output = [];
let x = 1/0, y = 1/0, n = pts.length;
for (let i = 0; i < n; i += 8) {
// If the endpoints of two adjacent segments are not close
// together, "move to" the startpoint of the next segment.
if (Math.abs(pts[i] - x) + Math.abs(pts[i+1] - y) > 1e-12)
output.push(`M${r(pts[i],d)},${r(pts[i+1],d)}`);
output.push(
`C${r(pts[i+2],d)},${r(pts[i+3],d)},` +
`${r(pts[i+4],d)},${r(pts[i+5],d)},` +
`${r(pts[i+6],d)},${r(pts[i+7],d)}`);
x = pts[i+6], y = pts[i+7];
}
return output.join('\n');
}
}
Insert cell
Insert cell
function bezpts_to_context(context, pts) {
let x = 1/0, y = 1/0, n = pts.length;
for (let i = 0; i < n; i += 8) {
// If the endpoints of two adjacent segments are not close
// together, "move to" the startpoint of the next segment.
if (Math.abs(pts[i] - x) + Math.abs(pts[i+1] - y) > 1e-12)
context.moveTo(pts[i], pts[i+1]);
context.bezierCurveTo(...pts.slice(i+2, i+8));
x = pts[i+6], y = pts[i+7];
}
}
Insert cell
{
const context = DOM.context2d(200, 200);
context.beginPath();
bezpts_to_context(context, circlebez);
context.fillStyle = "#f0f4ff";
context.fill();
context.stroke();
return context.canvas;
}
Insert cell
Insert cell
vals2coeffs7 = {
const c0 = Math.sqrt(3); // "twiddle" factor
const d0 = 1 / 6, d1 = 1 / 12; // output scaling factors
return function vals2coeffs7([x0, x1, x2, x3, x4, x5, x6]) {
const
z0 = x6 + x0, z1 = x6 - x0, z2 = x5 + x1, z3 = c0*(x5 - x1),
z4 = x4 + x2, z5 = x4 - x2, z6 = 2*x3,
w0 = z0 + z6, w1 = z0 - z6, w2 = z2 + z4, w3 = z2 - z4, w4 = z1 + z5;
return [
d1*(w0 + 2*w2), d0*(w4 + z3), d0*(w1 + w3), d0*(z1 - 2*z5),
d0*(w0 - w2), d0*(w4 - z3), d1*(w1 - 2*w3)];
}
}
Insert cell
Insert cell
import {math_css, $, $$} with {$css as $css} from "@jrus/misc"
Insert cell
$css = html`${math_css}`
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