Published
Edited
Jan 28, 2022
1 fork
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createBezierCurveTo_subdivided(subdivisions) {
return function bezierCurveTo_subdivided(
ctx,
x1,
y1,
cp1x,
cp1y,
cp2x,
cp2y,
x2,
y2
) {
const colors = colorRotator();
for (let i = 1; i <= subdivisions; i++) {
const t = i / subdivisions;
const tx = b3(t, x1, cp1x, cp2x, x2);
const ty = b3(t, y1, cp1y, cp2y, y2);

ctx.strokeStyle = colors.next().value;
ctx.lineTo(tx, ty);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tx, ty); // Wonky but easier than keeping prev coordinates...
}
};
}
Insert cell
Insert cell
Insert cell
function createBezierCurveTo_stepsize(idealStepSize) {
return function bezierCurveTo_stepsize(
ctx,
x1,
y1,
cp1x,
cp1y,
cp2x,
cp2y,
x2,
y2
) {
const length = arclength_approx(20, x1, y1, cp1x, cp1y, cp2x, cp2y, x2, y2);
const subdivisions = Math.ceil(length / idealStepSize);
const colors = colorRotator();

for (let i = 1; i <= subdivisions; i++) {
const t = i / subdivisions;
const tx = b3(t, x1, cp1x, cp2x, x2);
const ty = b3(t, y1, cp1y, cp2y, y2);

ctx.strokeStyle = colors.next().value;
ctx.lineTo(tx, ty);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tx, ty); // Wonky but easier than keeping prev coordinates...
}
};
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createBezierCurveTo_circular(tolerance, debuggin = false) {
// Based on https://pomax.github.io/bezierinfo/#arcapproximation
return function bezierCurveTo_circular(
ctx,
x1,
y1,
cp1x,
cp1y,
cp2x,
cp2y,
x2,
y2
) {
function pointForT(t) {
return bezierCurve(t, [x1, y1], [cp1x, cp1y], [cp2x, cp2y], [x2, y2]);
}

const colors = colorRotator();

let t = 0;
while (t < 1) {
let e = 1;

// Yikes! Does this algorithm always stop? I think so...
while (true) {
const n = (t + e) / 2;

const p1 = pointForT(t);
const p2 = pointForT(n);
const p3 = pointForT(e);

const { center, radius } = circleFromThreePoints(p1, p2, p3);

// Choose two points between curve segments t to n and n to e
const check1 = pointForT((t + n) / 2);
const check2 = pointForT((n + e) / 2);

const error1 = computeCircularError(check1, center, radius);
const error2 = computeCircularError(check2, center, radius);
const maxError = Math.max(error1, error2);

if (maxError < tolerance) {
const startAngle = angle(center, p1);
const middleAngle = angle(center, p2);
const endAngle = angle(center, p3);

ctx.beginPath();
ctx.strokeStyle = colors.next().value;
ctx.arc(
...center,
radius,
startAngle,
endAngle,
// I don't understand this following logic for CW/CCW, but it works...
startAngle > middleAngle || middleAngle > endAngle
);
ctx.stroke();

if (debuggin) {
ctx.beginPath();
ctx.globalAlpha = 0.25;
ctx.lineWidth = 1;
ctx.arc(...center, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.lineWidth = 2;
ctx.globalAlpha = 1;
}

t = e;
break;
} else {
// Not quite binary search...
e = n;
}
}
}
};
}
Insert cell
// Compare the distance from a point on the curve to the center of the circle to the radius of the circle.
// We want this number to be 0.
function computeCircularError(pointOnCurve, center, radius) {
const actual = distance(pointOnCurve, center);
const expected = radius;
return Math.abs(actual - expected);
}
Insert cell
Insert cell
Insert cell
Insert cell
function createBezierCurveTo_circularSubdivided(
subdivisions,
debuggin = false
) {
// Based on https://pomax.github.io/bezierinfo/#arcapproximation
return function bezierCurveTo_circular(
ctx,
x1,
y1,
cp1x,
cp1y,
cp2x,
cp2y,
x2,
y2
) {
function pointForT(t) {
return bezierCurve(t, [x1, y1], [cp1x, cp1y], [cp2x, cp2y], [x2, y2]);
}

const colors = colorRotator();

for (let i = 1; i <= subdivisions; i++) {
const t = (i - 1) / subdivisions;
const e = i / subdivisions;
const n = (t + e) / 2;

const p1 = pointForT(t);
const p2 = pointForT(n);
const p3 = pointForT(e);

const { center, radius } = circleFromThreePoints(p1, p2, p3);

// Choose two points between curve segments t to n and n to e
const check1 = pointForT((t + n) / 2);
const check2 = pointForT((n + e) / 2);

const error1 = computeCircularError(check1, center, radius);
const error2 = computeCircularError(check2, center, radius);
const maxError = Math.max(error1, error2);

const startAngle = angle(center, p1);
const middleAngle = angle(center, p2);
const endAngle = angle(center, p3);

ctx.beginPath();
ctx.strokeStyle = colors.next().value;
ctx.arc(
...center,
radius,
startAngle,
endAngle,
// I don't understand this following logic for CW/CCW, but it works...
startAngle > middleAngle || middleAngle > endAngle
);
ctx.stroke();

if (debuggin) {
ctx.beginPath();
ctx.globalAlpha = 0.25;
ctx.lineWidth = 1;
ctx.arc(...center, radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.lineWidth = 2;
ctx.globalAlpha = 1;
}
}
};
}
Insert cell
Insert cell
function angle([x1, y1], [x2, y2]) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.atan2(dy, dx);
}
Insert cell
function makeFigure(width, height, caption, fn) {
const offset = 30;

return html`
<figure>
${compareBezier(
width,
height,
[offset, offset],
[offset, height * 2 - offset],
[width + offset, offset],
[width - offset, height - offset],
fn
)}
<figcaption>${caption}</figcaption>
</figure>
`;
}
Insert cell
function* colorRotator() {
const colors = [
'#FF4C6D',
'#00D3FF',
'#FF964E',
'#00F1BA',
'#DF85FF',
'#FFCC2C'
];

let index = 0;

while (true) {
const color = colors[index];
index = (index + 1) % colors.length;
yield color;
}
}
Insert cell
// Draw a reference curve and a comparison curve using the same control points
function compareBezier(width, height, start, cp1, cp2, end, pathFn) {
const ctx = DOM.context2d(width, height, window.devicePixelRatio);

// Reference curve using browser API
ctx.beginPath();
ctx.lineWidth = 8;
ctx.strokeStyle = '#eee';
ctx.moveTo(...start);
ctx.bezierCurveTo(...cp1, ...cp2, ...end);
ctx.stroke();

// Compare curve using provided pathFn
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(...start);
pathFn(ctx, ...start, ...cp1, ...cp2, ...end);

return ctx.canvas;
}
Insert cell
Insert cell
function distance_4(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
Insert cell
function distance(p1, p2) {
return distance_4(...p1, ...p2);
}
Insert cell
function midpoint([x1, y1], [x2, y2]) {
return [(x1 + x2) / 2, (y1 + y2) / 2];
}
Insert cell
// Finds a coordinate describing a line perpendicular to the line
// from p1 to p2 that passes through p2
function perpendicularToLine([x1, y1], [x2, y2]) {
return [
x2 - (25 * (y1 - y2)) / distance([x1, y1], [x2, y2]),
y2 + (25 * (x1 - x2)) / distance([x1, y1], [x2, y2])
];
}
Insert cell
function circleFromThreePoints(p1, p2, p3) {
const mp1 = midpoint(p1, p2);
const mp2 = midpoint(p1, p3);

const perp1 = perpendicularToLine(p1, mp1);
const perp2 = perpendicularToLine(p1, mp2);

const center = lineLineIntersection([perp1, mp1], [perp2, mp2]);

const radius = distance(center, p1);
return { center, radius };
}
Insert cell
// From https://pomax.github.io/bezierinfo/#intersections
function lineLineIntersection_8(x1, y1, x2, y2, x3, y3, x4, y4) {
const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
const ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);

// If d === 0, the lines are paralllel and there is no intersection.
// This will return the coordinate [Infinity, Infinity] which is suitable for use here.
return [nx / d, ny / d];
}
Insert cell
// From https://pomax.github.io/bezierinfo/#intersections
function lineLineIntersection_4(p1, p2, p3, p4) {
const [x1, y1] = p1;
const [x2, y2] = p2;
const [x3, y3] = p3;
const [x4, y4] = p4;
return lineLineIntersection_8(x1, y1, x2, y2, x3, y3, x4, y4);
}
Insert cell
// From https://pomax.github.io/bezierinfo/#intersections
function lineLineIntersection(line1, line2) {
return lineLineIntersection_4(...line1, ...line2);
}
Insert cell
Insert cell
function b3p0(t, p) {
var k = 1 - t;
return k * k * k * p;
}
Insert cell
function b3p1(t, p) {
var k = 1 - t;
return 3 * k * k * t * p;
}
Insert cell
function b3p2(t, p) {
var k = 1 - t;
return 3 * k * t * t * p;
}
Insert cell
function b3p3(t, p) {
return t * t * t * p;
}
Insert cell
// Compute value on a single dimension for bezier curve with given control points
function b3(t, p0, p1, p2, p3) {
return b3p0(t, p0) + b3p1(t, p1) + b3p2(t, p2) + b3p3(t, p3);
}
Insert cell
function bezierCurve(t, [p0x, p0y], [p1x, p1y], [p2x, p2y], [p3x, p3y]) {
return [b3(t, p0x, p1x, p2x, p3x), b3(t, p0y, p1y, p2y, p3y)];
}
Insert cell
// Approximate the length of a curve via linear interpolation. Could be very off!
function arclength_approx(
subdivisions,
x1,
y1,
cp1x,
cp1y,
cp2x,
cp2y,
x2,
y2
) {
let px = x1;
let py = y1;
let length = 0;
for (let i = 1; i <= subdivisions; i++) {
const t = i / subdivisions;
const tx = b3(t, x1, cp1x, cp2x, x2);
const ty = b3(t, y1, cp1y, cp2y, y2);
length += distance_4(px, py, tx, ty);
px = tx;
py = ty;
}
return length;
}
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