full regeneration
@@ -15,6 +15,7 @@ draw() {
|
||||
translate(this.width/2, 0);
|
||||
line(0,0,0,this.height);
|
||||
|
||||
// draw the realigned curve:
|
||||
this.drawRTCurve(
|
||||
this.rotatePoints(
|
||||
this.translatePoints(
|
||||
@@ -25,7 +26,7 @@ draw() {
|
||||
}
|
||||
|
||||
translatePoints(points) {
|
||||
// translate to (0,0)
|
||||
// translate so that the curve starts at (0,0)
|
||||
let m = points[0];
|
||||
return points.map(v => {
|
||||
return {
|
||||
@@ -36,7 +37,7 @@ translatePoints(points) {
|
||||
}
|
||||
|
||||
rotatePoints(points) {
|
||||
// rotate so that last point is (...,0)
|
||||
// rotate the curve so that the point is (...,0)
|
||||
let degree = curve.points.length - 1;
|
||||
let dx = points[degree].x;
|
||||
let dy = points[degree].y;
|
||||
@@ -50,13 +51,18 @@ rotatePoints(points) {
|
||||
}
|
||||
|
||||
drawRTCurve(points) {
|
||||
let degree = curve.points.length - 1;
|
||||
let ncurve = new Bezier(this, points);
|
||||
// draw axes
|
||||
translate(60, this.height/2);
|
||||
setStroke(`grey`);
|
||||
line(0,-this.height,0,this.height);
|
||||
line(-60,0,this.width,0);
|
||||
line(0, -this.height, 0, this.height);
|
||||
line(-60, 0, this.width, 0);
|
||||
|
||||
// draw transformed curve
|
||||
let degree = curve.points.length - 1;
|
||||
let ncurve = new Bezier(this, points);
|
||||
ncurve.drawCurve();
|
||||
|
||||
// and label the last point in the transformed curve
|
||||
setFill(`black`);
|
||||
text(`(0,0)`, 5,15);
|
||||
text(`(${points[degree].x|0},0)`, points[degree].x, -5, CENTER);
|
||||
|
@@ -40,7 +40,8 @@ If we drop all the zero-terms, this gives us:
|
||||
|
||||
We can see that our original curve definition has been simplified considerably. The following graphics illustrate the result of aligning our example curves to the x-axis, with the cubic case using the coordinates that were just used in the example formulae:
|
||||
|
||||
<div class="figure">
|
||||
<graphics-element title="Aligning a quadratic curve" width="550" src="./aligning.js" data-type="quadratic"></graphics-element>
|
||||
|
||||
|
||||
|
||||
<graphics-element title="Aligning a cubic curve" width="550" src="./aligning.js" data-type="cubic"></graphics-element>
|
||||
</div>
|
||||
|
@@ -1,57 +1,4 @@
|
||||
const Tvalues24 = [
|
||||
-0.0640568928626056260850430826247450385909,
|
||||
0.0640568928626056260850430826247450385909,
|
||||
-0.1911188674736163091586398207570696318404,
|
||||
0.1911188674736163091586398207570696318404,
|
||||
-0.3150426796961633743867932913198102407864,
|
||||
0.3150426796961633743867932913198102407864,
|
||||
-0.4337935076260451384870842319133497124524,
|
||||
0.4337935076260451384870842319133497124524,
|
||||
-0.5454214713888395356583756172183723700107,
|
||||
0.5454214713888395356583756172183723700107,
|
||||
-0.6480936519369755692524957869107476266696,
|
||||
0.6480936519369755692524957869107476266696,
|
||||
-0.7401241915785543642438281030999784255232,
|
||||
0.7401241915785543642438281030999784255232,
|
||||
-0.8200019859739029219539498726697452080761,
|
||||
0.8200019859739029219539498726697452080761,
|
||||
-0.8864155270044010342131543419821967550873,
|
||||
0.8864155270044010342131543419821967550873,
|
||||
-0.9382745520027327585236490017087214496548,
|
||||
0.9382745520027327585236490017087214496548,
|
||||
-0.9747285559713094981983919930081690617411,
|
||||
0.9747285559713094981983919930081690617411,
|
||||
-0.9951872199970213601799974097007368118745,
|
||||
0.9951872199970213601799974097007368118745,
|
||||
];
|
||||
|
||||
const Cvalues24 = [
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
];
|
||||
|
||||
import { C, T } from "./ct-values.js";
|
||||
|
||||
setup() {
|
||||
this.curve = Bezier.defaultCubic(this);
|
||||
@@ -71,14 +18,11 @@ draw() {
|
||||
}
|
||||
|
||||
computeLength(curve) {
|
||||
const z = 0.5,
|
||||
len = Tvalues24.length;
|
||||
|
||||
const z = 0.5, len = T.length;
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0, t; i < len; i++) {
|
||||
t = z * Tvalues24[i] + z;
|
||||
sum += Cvalues24[i] * this.arcfn(t, curve.derivative(t));
|
||||
t = z * T[i] + z;
|
||||
sum += C[i] * this.arcfn(t, curve.derivative(t));
|
||||
}
|
||||
return z * sum;
|
||||
}
|
||||
|
55
docs/chapters/arclength/ct-values.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const T = [
|
||||
-0.0640568928626056260850430826247450385909,
|
||||
0.0640568928626056260850430826247450385909,
|
||||
-0.1911188674736163091586398207570696318404,
|
||||
0.1911188674736163091586398207570696318404,
|
||||
-0.3150426796961633743867932913198102407864,
|
||||
0.3150426796961633743867932913198102407864,
|
||||
-0.4337935076260451384870842319133497124524,
|
||||
0.4337935076260451384870842319133497124524,
|
||||
-0.5454214713888395356583756172183723700107,
|
||||
0.5454214713888395356583756172183723700107,
|
||||
-0.6480936519369755692524957869107476266696,
|
||||
0.6480936519369755692524957869107476266696,
|
||||
-0.7401241915785543642438281030999784255232,
|
||||
0.7401241915785543642438281030999784255232,
|
||||
-0.8200019859739029219539498726697452080761,
|
||||
0.8200019859739029219539498726697452080761,
|
||||
-0.8864155270044010342131543419821967550873,
|
||||
0.8864155270044010342131543419821967550873,
|
||||
-0.9382745520027327585236490017087214496548,
|
||||
0.9382745520027327585236490017087214496548,
|
||||
-0.9747285559713094981983919930081690617411,
|
||||
0.9747285559713094981983919930081690617411,
|
||||
-0.9951872199970213601799974097007368118745,
|
||||
0.9951872199970213601799974097007368118745,
|
||||
];
|
||||
|
||||
const C = [
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1279381953467521569740561652246953718517,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.1258374563468282961213753825111836887264,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.121670472927803391204463153476262425607,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1155056680537256013533444839067835598622,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.1074442701159656347825773424466062227946,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.0976186521041138882698806644642471544279,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.086190161531953275917185202983742667185,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0733464814110803057340336152531165181193,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0592985849154367807463677585001085845412,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0442774388174198061686027482113382288593,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0285313886289336631813078159518782864491,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
0.0123412297999871995468056670700372915759,
|
||||
];
|
||||
|
||||
export { C, T }
|
@@ -16,6 +16,9 @@ draw() {
|
||||
|
||||
setStroke("red");
|
||||
curve.drawSkeleton(`lightblue`);
|
||||
|
||||
// instead of running an arclength summation, we
|
||||
// just... sum the lengths of our line segments.
|
||||
LUT.forEach((p1,i) => {
|
||||
if (i===0) return;
|
||||
let p0 = LUT[i-1];
|
||||
|
@@ -18,6 +18,9 @@ draw() {
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
// Start with complete nonsense min/max values, where
|
||||
// min is huge and max is tiny, so we can bring them
|
||||
// down and up, respectively.
|
||||
|
||||
let minx = Number.MAX_SAFE_INTEGER,
|
||||
miny = minx,
|
||||
@@ -28,6 +31,7 @@ draw() {
|
||||
noFill();
|
||||
setStroke(`red`);
|
||||
|
||||
// For each extremum, see if that changes the bbox values.
|
||||
[0, ...extrema.x, ...extrema.y, 1].forEach(t => {
|
||||
let p = curve.get(t);
|
||||
if (p.x < minx) minx = p.x;
|
||||
@@ -37,6 +41,7 @@ draw() {
|
||||
if (t > 0 && t< 1) circle(p.x, p.y, 3);
|
||||
});
|
||||
|
||||
// And we're done.
|
||||
setStroke(`#0F0`);
|
||||
rect(minx, miny, maxx - minx, maxy - miny);
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
setPanelCount(3);
|
||||
|
||||
let type = this.parameters.type ?? `quadratic`;
|
||||
if (type === `quadratic`) {
|
||||
curve = Bezier.defaultQuadratic(this);
|
||||
@@ -14,11 +16,21 @@ setup() {
|
||||
draw() {
|
||||
clear();
|
||||
const dim = this.height;
|
||||
let pcount = curve.points.length;
|
||||
curve.drawSkeleton();
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
translate(dim, 0);
|
||||
nextPanel();
|
||||
this.drawComponentX(dim, pcount);
|
||||
|
||||
resetTransform();
|
||||
nextPanel();
|
||||
nextPanel();
|
||||
this.drawComponentY(dim, pcount);
|
||||
}
|
||||
|
||||
drawComponentX(dim, pcount) {
|
||||
setStroke(`black`);
|
||||
line(0,0,0,dim);
|
||||
|
||||
@@ -26,14 +38,15 @@ draw() {
|
||||
translate(40,20);
|
||||
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
|
||||
|
||||
let pcount = curve.points.length;
|
||||
// remap our curve so that y becomes our x
|
||||
// coordinates, and x becomes the t intervals.
|
||||
new Bezier(this, curve.points.map((p,i) => ({
|
||||
x: (i/(pcount-1)) * dim,
|
||||
y: p.x
|
||||
}))).drawCurve();
|
||||
}
|
||||
|
||||
resetTransform();
|
||||
translate(2*dim, 0);
|
||||
drawComponentY(dim, pcount) {
|
||||
setStroke(`black`);
|
||||
line(0,0,0,dim);
|
||||
|
||||
@@ -41,6 +54,8 @@ draw() {
|
||||
translate(40,20);
|
||||
drawAxes(`t`, 0,1, `Y`, 0, dim, dim, dim);
|
||||
|
||||
// remap our curve, leaving y as is, but
|
||||
// making x be the t intervals.
|
||||
new Bezier(this, curve.points.map((p,i) => ({
|
||||
x: (i/(pcount-1)) * dim,
|
||||
y: p.y
|
||||
|
@@ -10,4 +10,6 @@ If you move points in a curve sideways, you should only see the middle graph cha
|
||||
|
||||
<graphics-element title="Quadratic Bézier curve components" width="825" src="./components.js" data-type="quadratic"></graphics-element>
|
||||
|
||||
|
||||
|
||||
<graphics-element title="Cubic Bézier curve components" width="825" src="./components.js" data-type="cubic"></graphics-element>
|
||||
|
@@ -19,6 +19,8 @@ draw() {
|
||||
curve.drawPoints();
|
||||
});
|
||||
|
||||
// For the "both directions" version, we also want
|
||||
// to show the circle that fits our curve.
|
||||
if (this.parameters.omni) {
|
||||
let t = this.position;
|
||||
let curve = q;
|
||||
@@ -30,14 +32,16 @@ draw() {
|
||||
drawCurvature(curve) {
|
||||
let s, t, p, n, k, ox, oy;
|
||||
for(s=0; s<256; s++) {
|
||||
setStroke(`rgba(255,127,${s},0.6)`);
|
||||
// compute the curvature at `t`:
|
||||
t = s/255;
|
||||
p = curve.get(t);
|
||||
n = curve.normal(t);
|
||||
k = this.computeCurvature(curve, t) * 10000;
|
||||
|
||||
// and then draw it.
|
||||
ox = k * n.x;
|
||||
oy = k * n.y;
|
||||
setStroke(`rgba(255,127,${s},0.6)`);
|
||||
line(p.x, p.y, p.x + ox, p.y + oy);
|
||||
|
||||
// And if requested, also draw it along the anti-normal.
|
||||
@@ -55,6 +59,7 @@ computeCurvature(curve, t) {
|
||||
qdsum = d.x * d.x + d.y * d.y,
|
||||
dnm = qdsum ** 3/2;
|
||||
|
||||
// shortcut
|
||||
if (num === 0 || dnm === 0) return 0;
|
||||
|
||||
return num / dnm;
|
||||
@@ -70,7 +75,6 @@ drawIncidentCircle(curve, t) {
|
||||
|
||||
setFill(`rgba(200,200,255,0.4)`);
|
||||
setStroke(`red`);
|
||||
|
||||
line(p.x, p.y, rx, ry);
|
||||
circle(p.x, p.y, 3);
|
||||
circle(rx, ry, 3);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
setPanelCount(3);
|
||||
const type = this.parameters.type ?? `quadratic`;
|
||||
if (type === `quadratic`) {
|
||||
curve = Bezier.defaultQuadratic(this);
|
||||
@@ -19,7 +20,19 @@ draw() {
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
translate(dim, 0);
|
||||
nextPanel();
|
||||
|
||||
this.drawComponentX(dim, degree);
|
||||
|
||||
resetTransform();
|
||||
nextPanel();
|
||||
nextPanel();
|
||||
|
||||
this.drawComponentY(dim, degree);
|
||||
}
|
||||
|
||||
|
||||
drawComponentX(dim, degree) {
|
||||
setStroke(`black`);
|
||||
line(0,0,0,dim);
|
||||
|
||||
@@ -27,13 +40,16 @@ draw() {
|
||||
translate(40,20);
|
||||
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
|
||||
|
||||
this.plotDimension(dim, new Bezier(this, curve.points.map((p,i) => ({
|
||||
const B = new Bezier(this, curve.points.map((p,i) => ({
|
||||
x: (i/degree) * dim,
|
||||
y: p.x
|
||||
}))));
|
||||
})));
|
||||
|
||||
resetTransform();
|
||||
translate(2*dim, 0);
|
||||
// this is where things differ from the previous section
|
||||
this.plotDimension(dim, B);
|
||||
}
|
||||
|
||||
drawComponentY(dim, degree) {
|
||||
setStroke(`black`);
|
||||
line(0,0,0,dim);
|
||||
|
||||
@@ -41,10 +57,13 @@ draw() {
|
||||
translate(40,20);
|
||||
drawAxes(`t`, 0,1, `Y`, 0, dim, dim, dim);
|
||||
|
||||
this.plotDimension(dim, new Bezier(this, curve.points.map((p,i) => ({
|
||||
x: (i/degree) * dim,
|
||||
y: p.y
|
||||
}))))
|
||||
const B = new Bezier(this, curve.points.map((p,i) => ({
|
||||
x: (i/degree) * dim,
|
||||
y: p.y
|
||||
})));
|
||||
|
||||
// this is where things differ from the previous section
|
||||
this.plotDimension(dim, B)
|
||||
}
|
||||
|
||||
plotDimension(dim, dimension) {
|
||||
@@ -144,18 +163,27 @@ plotCubicDimension(t1, y1, t2, y2, dim, dimension, reverse) {
|
||||
}
|
||||
|
||||
getRoots(v1, v2, v3) {
|
||||
if (v3 === undefined) {
|
||||
return [-v1 / (v2 - v1)];
|
||||
}
|
||||
// is this actually a line?
|
||||
if (v3 === undefined) return [-v1 / (v2 - v1)];
|
||||
|
||||
const a = v1 - 2*v2 + v3,
|
||||
b = 2 * (v2 - v1),
|
||||
c = v1,
|
||||
d = b*b - 4*a*c;
|
||||
// quadratic root finding is not super complex.
|
||||
const a = v1 - 2*v2 + v3;
|
||||
|
||||
// no root:
|
||||
if (a === 0) return [];
|
||||
|
||||
const b = 2 * (v2 - v1),
|
||||
c = v1,
|
||||
d = b*b - 4*a*c;
|
||||
|
||||
// no root:
|
||||
if (d < 0) return [];
|
||||
|
||||
// one root:
|
||||
const f = -b / (2*a);
|
||||
if (d === 0) return [f]
|
||||
|
||||
// two roots:
|
||||
const l = sqrt(d) / (2*a);
|
||||
return [f-l, f+l];
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ draw() {
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
// align our curve and let's do some root finding
|
||||
const p = curve.align().points,
|
||||
|
||||
a = p[2].x * p[1].y,
|
||||
@@ -25,8 +26,11 @@ draw() {
|
||||
|
||||
roots = [];
|
||||
|
||||
if (this.almost(x, 0) ) {
|
||||
if (!this.almost(y, 0) ) {
|
||||
// because of floating point maths, we can't check whether
|
||||
// x or y "are" zero, because they could be some value that is
|
||||
// just off from zero due to floating point compound errors.
|
||||
if (approx(x, 0)) {
|
||||
if (!approx(y, 0)) {
|
||||
roots.push(-z / y);
|
||||
}
|
||||
}
|
||||
@@ -36,15 +40,14 @@ draw() {
|
||||
sq = sqrt(det),
|
||||
d2 = 2 * x;
|
||||
|
||||
if (!this.almost(d2, 0) ) {
|
||||
if (!approx(d2, 0) ) {
|
||||
roots.push(-(y+sq) / d2);
|
||||
roots.push((sq-y) / d2);
|
||||
}
|
||||
}
|
||||
|
||||
setStroke(`red`);
|
||||
setFill(`red`);
|
||||
|
||||
// Aaaan let's draw them
|
||||
setColor(`red`);
|
||||
roots.forEach(t => {
|
||||
if (0 <= t && t <= 1) {
|
||||
let p = curve.get(t);
|
||||
@@ -53,7 +56,3 @@ draw() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
almost(v1, v2, epsilon=0.00001) {
|
||||
return abs(v1 - v2) < epsilon;
|
||||
}
|
||||
|
@@ -3,36 +3,37 @@
|
||||
If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested in are the tangent vector and normal vector for curve points. These are actually really easy to find. For moving and orienting along a curve, we use the tangent, which indicates the direction of travel at specific points, and is literally just the first derivative of our curve:
|
||||
|
||||
\[
|
||||
\left \{ \begin{matrix}
|
||||
\begin{matrix}
|
||||
tangent_x(t) = B'_x(t) \\
|
||||
\\
|
||||
tangent_y(t) = B'_y(t)
|
||||
\end{matrix} \right.
|
||||
\end{matrix}
|
||||
\]
|
||||
|
||||
This gives us the directional vector we want. We can normalize it to give us uniform directional vectors (having a length of 1.0) at each point, and then do whatever it is we want to do based on those directions:
|
||||
|
||||
\[
|
||||
d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2}
|
||||
\]
|
||||
|
||||
\[
|
||||
\left \{ \begin{matrix}
|
||||
\hat{x}(t) = || tangent_x(t) ||
|
||||
=\frac{tangent_x(t)}{ || tangent(t) || }
|
||||
\begin{matrix}
|
||||
d = \left \| tangent(t) \right \| = \sqrt{B'_x(t)^2 + B'_y(t)^2} \\
|
||||
\\
|
||||
\hat{x}(t) = \left \| tangent_x(t) \right \|
|
||||
=\frac{tangent_x(t)}{ \left \| tangent(t) \right \| }
|
||||
= \frac{B'_x(t)}{d} \\
|
||||
\hat{y}(t) = || tangent_y(t) ||
|
||||
= \frac{tangent_y(t)}{ || tangent(t) || }
|
||||
\\
|
||||
\hat{y}(t) = \left \| tangent_y(t) \right \|
|
||||
= \frac{tangent_y(t)}{ \left \| tangent(t) \right \| }
|
||||
= \frac{B'_y(t)}{d}
|
||||
\end{matrix} \right.
|
||||
\end{matrix}
|
||||
\]
|
||||
|
||||
The tangent is very useful for moving along a line, but what if we want to move away from the curve instead, perpendicular to the curve at some point <i>t</i>? In that case we want the *normal* vector. This vector runs at a right angle to the direction of the curve, and is typically of length 1.0, so all we have to do is rotate the normalized directional vector and we're done:
|
||||
|
||||
\[
|
||||
\left \{ \begin{array}{l}
|
||||
\begin{array}{l}
|
||||
normal_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\
|
||||
\\
|
||||
normal_y(t) = \underset{quarter\ circle\ rotation} {\underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }} = \hat{x}(t)
|
||||
\end{array} \right.
|
||||
\end{array}
|
||||
\]
|
||||
|
||||
<div class="note">
|
||||
|
@@ -6,6 +6,7 @@ setup() {
|
||||
curve = Bezier.defaultQuadratic(this);
|
||||
} else {
|
||||
curve = Bezier.defaultCubic(this);
|
||||
// to show this off for Cubic curves we need to change some of the points
|
||||
curve.points[0].x = 30;
|
||||
curve.points[0].y = 230;
|
||||
curve.points[1].x = 75;
|
||||
@@ -25,24 +26,30 @@ draw() {
|
||||
let t = i/10.0;
|
||||
let p = curve.get(t);
|
||||
let d = this.type === `quadratic` ? this.getQuadraticDerivative(t, pts) : this.getCubicDerivative(t, pts);
|
||||
|
||||
let m = sqrt(d.x*d.x + d.y*d.y);
|
||||
d = { x: d.x/m, y: d.y/m };
|
||||
let n = this.getNormal(t, d);
|
||||
|
||||
setStroke(`blue`);
|
||||
line(p.x, p.y, p.x + d.x*f, p.y + d.y*f);
|
||||
|
||||
setStroke(`red`);
|
||||
line(p.x, p.y, p.x + n.x*f, p.y + n.y*f);
|
||||
|
||||
setStroke(`black`);
|
||||
circle(p.x, p.y, 3);
|
||||
this.drawVectors(f, t, p, d);
|
||||
}
|
||||
|
||||
curve.drawPoints();
|
||||
}
|
||||
|
||||
drawVectors(f, t, p, d) {
|
||||
let m = sqrt(d.x*d.x + d.y*d.y);
|
||||
d = { x: d.x/m, y: d.y/m };
|
||||
let n = this.getNormal(t, d);
|
||||
|
||||
// draw the tangent vector
|
||||
setStroke(`blue`);
|
||||
line(p.x, p.y, p.x + d.x*f, p.y + d.y*f);
|
||||
|
||||
// draw the normal vector
|
||||
setStroke(`red`);
|
||||
line(p.x, p.y, p.x + n.x*f, p.y + n.y*f);
|
||||
|
||||
// and the point these are for
|
||||
setStroke(`black`);
|
||||
circle(p.x, p.y, 3);
|
||||
}
|
||||
|
||||
getQuadraticDerivative(t, points) {
|
||||
let mt = (1 - t), d = [
|
||||
{
|
||||
|
@@ -4,6 +4,7 @@ import { project, projectXY, projectXZ, projectYZ } from "./projection.js";
|
||||
let d, cube;
|
||||
|
||||
setup() {
|
||||
// step 1: let's define a cube to show our curve "in"
|
||||
d = this.width/2 + 25;
|
||||
cube = [
|
||||
{x:0, y:0, z:0},
|
||||
@@ -15,16 +16,25 @@ setup() {
|
||||
{x:d, y:d, z:d},
|
||||
{x:0, y:d, z:d}
|
||||
].map(p => project(p));
|
||||
|
||||
// step 2: let's also define our 3D curve
|
||||
const points = this.points = [
|
||||
{x:120, y: 0, z: 0},
|
||||
{x:120, y:220, z: 0},
|
||||
{x: 30, y: 0, z: 30},
|
||||
{x: 0, y: 0, z:200}
|
||||
];
|
||||
|
||||
// step 3: to draw this curve to the screen, we need to project the
|
||||
// coordinates from 3D to 2D, for which we use what is called
|
||||
// a "cabinet projection".
|
||||
this.curve = new Bezier(this, points.map(p => project(p)));
|
||||
|
||||
// We also construct handy projections on just the X/Y, X/Z, and Y/Z planes.
|
||||
this.cxy = new Bezier(this, points.map(p => projectXY(p)));
|
||||
this.cxz = new Bezier(this, points.map(p => projectXZ(p)));
|
||||
this.cyz = new Bezier(this, points.map(p => projectYZ(p)));
|
||||
|
||||
setSlider(`.slide-control`, `position`, 0);
|
||||
}
|
||||
|
||||
@@ -33,21 +43,32 @@ draw() {
|
||||
translate(this.width/2 - 60, this.height/2 + 75);
|
||||
const curve = this.curve;
|
||||
|
||||
// Draw all our planar curve projections first
|
||||
this.drawCurveProjections();
|
||||
|
||||
// And the "back" side of our cube
|
||||
this.drawCubeBack();
|
||||
|
||||
// Then, we draw the real curve
|
||||
curve.drawCurve(`grey`);
|
||||
setStroke(`grey`)
|
||||
line(curve.points[0].x, curve.points[0].y, curve.points[1].x, curve.points[1].y);
|
||||
line(curve.points[2].x, curve.points[2].y, curve.points[3].x, curve.points[3].y);
|
||||
curve.points.forEach(p => circle(p.x, p.y, 2));
|
||||
|
||||
// And the current point on that curve
|
||||
this.drawPoint(this.position);
|
||||
|
||||
// and then we can add the "front" of the cube.
|
||||
this.drawCubeFront();
|
||||
}
|
||||
|
||||
drawCurveProjections() {
|
||||
this.cxy.drawCurve(`#EEF`);
|
||||
this.cxz.drawCurve(`#EEF`);
|
||||
this.cyz.drawCurve(`#EEF`);
|
||||
}
|
||||
|
||||
drawCubeBack() {
|
||||
const c = cube;
|
||||
|
||||
@@ -64,12 +85,6 @@ drawCubeBack() {
|
||||
line(c[0].x, c[0].y, c[4].x, c[4].y);
|
||||
}
|
||||
|
||||
drawCurveProjections() {
|
||||
this.cxy.drawCurve(`#EEF`);
|
||||
this.cxz.drawCurve(`#EEF`);
|
||||
this.cyz.drawCurve(`#EEF`);
|
||||
}
|
||||
|
||||
drawPoint(t) {
|
||||
const {o, r, n, dt} = this.getFrenetVectors(t, this.points);
|
||||
|
||||
@@ -78,8 +93,13 @@ drawPoint(t) {
|
||||
const p = project(o);
|
||||
circle(p.x, p.y, 3);
|
||||
|
||||
// Draw our axis of rotation,
|
||||
this.drawVector(p, vec.normalize(r), 40, `blue`, `r`);
|
||||
|
||||
// our normal,
|
||||
this.drawVector(p, vec.normalize(n), 40, `red`, `n`);
|
||||
|
||||
// and our derivative.
|
||||
this.drawVector(p, vec.normalize(dt), 40, `green`, `t′`);
|
||||
|
||||
setFill(`black`)
|
||||
@@ -88,8 +108,6 @@ drawPoint(t) {
|
||||
|
||||
drawCubeFront() {
|
||||
const c = cube;
|
||||
|
||||
// rest of the cube
|
||||
setStroke("lightgrey");
|
||||
line(c[1].x, c[1].y, c[2].x, c[2].y);
|
||||
line(c[2].x, c[2].y, c[3].x, c[3].y);
|
||||
@@ -103,13 +121,20 @@ drawCubeFront() {
|
||||
}
|
||||
|
||||
getFrenetVectors(t, originalPoints) {
|
||||
// The frenet vectors are based on the (unprojected) curve,
|
||||
// and its derivative curve.
|
||||
const curve = new Bezier(this, originalPoints);
|
||||
const d1curve = new Bezier(this, curve.dpoints[0]);
|
||||
const o = curve.get(t);
|
||||
const dt = d1curve.get(t);
|
||||
const ddt = d1curve.derivative(t);
|
||||
const o = curve.get(t);
|
||||
const b = vec.plus(dt, ddt);
|
||||
const r = vec.cross(b, dt);
|
||||
// project the derivative into the future
|
||||
const f = vec.plus(dt, ddt);
|
||||
// and then find the axis of rotation wrt the plane
|
||||
// spanned by the currented and projected derivative
|
||||
const r = vec.cross(f, dt);
|
||||
// after which the normal is found by rotating the
|
||||
// tangent in that plane.
|
||||
const n = vec.normalize(vec.cross(r, dt));
|
||||
return { o, dt, r, n };
|
||||
}
|
||||
|
@@ -1,23 +1,25 @@
|
||||
/**
|
||||
* A cabinet projection utility
|
||||
* A cabinet projection utility library
|
||||
*/
|
||||
|
||||
// Universal projector function
|
||||
// Universal projector function:
|
||||
|
||||
function project(point3d, offset = { x: 0, y: 0 }, phi = -Math.PI / 6) {
|
||||
// what they rarely tell you: if you want Z to "go up",
|
||||
// X to "come out of the screen", and Y to be the "left/right",
|
||||
// we need to switch some coordinates around:
|
||||
const x = point3d.y,
|
||||
y = -point3d.z,
|
||||
z = -point3d.x;
|
||||
const a = point3d.y,
|
||||
b = -point3d.z,
|
||||
c = -point3d.x / 2;
|
||||
|
||||
return {
|
||||
x: offset.x + x + (z / 2) * Math.cos(phi),
|
||||
y: offset.y + y + (z / 2) * Math.sin(phi),
|
||||
x: offset.x + a + c * Math.cos(phi),
|
||||
y: offset.y + b + c * Math.sin(phi),
|
||||
};
|
||||
}
|
||||
|
||||
// and some rebuilt planar projectors
|
||||
// and some planar projectors:
|
||||
|
||||
function projectXY(p, offset, phi) {
|
||||
return project({ x: p.x, y: p.y, z: 0 }, offset, phi);
|
||||
}
|
||||
@@ -30,6 +32,4 @@ function projectYZ(p, offset, phi) {
|
||||
return project({ x: 0, y: p.y, z: p.z }, offset, phi);
|
||||
}
|
||||
|
||||
|
||||
export { project, projectXY, projectXZ, projectYZ }
|
||||
|
||||
export { project, projectXY, projectXZ, projectYZ };
|
||||
|
@@ -4,6 +4,7 @@ import { project, projectXY, projectXZ, projectYZ } from "./projection.js";
|
||||
let d, cube;
|
||||
|
||||
setup() {
|
||||
// We have the same setup as for the previous graphic
|
||||
d = this.width/2 + 25;
|
||||
cube = [
|
||||
{x:0, y:0, z:0},
|
||||
@@ -71,6 +72,7 @@ drawCurveProjections() {
|
||||
}
|
||||
|
||||
drawPoint(t) {
|
||||
// The only thing different compared to the previous graphic is this call:
|
||||
const {o, r, n, dt } = this.getRMF(t, this.points);
|
||||
|
||||
setStroke(`red`);
|
||||
@@ -102,75 +104,6 @@ drawCubeFront() {
|
||||
line(c[7].x, c[7].y, c[4].x, c[4].y);
|
||||
}
|
||||
|
||||
getRMF(t, originalPoints) {
|
||||
const curve = new Bezier(this, originalPoints);
|
||||
const d1curve = new Bezier(this, curve.dpoints[0]);
|
||||
|
||||
if (!this.rmf_LUT) {
|
||||
this.rmf_LUT = this.generateRMF(originalPoints, curve, d1curve);
|
||||
}
|
||||
|
||||
// find the frame for "t".
|
||||
const last = this.rmf_LUT.length - 1;
|
||||
const f = t * last;
|
||||
const i = Math.floor(f);
|
||||
|
||||
// intenger index, or last index: we're done.
|
||||
if (f === i) return this.rmf_LUT[i];
|
||||
|
||||
// no integer index: interpolate based on the adjacent frames.
|
||||
const j = i + 1, ti = i/last, tj = j/last, ratio = (t - ti) / (tj - ti);
|
||||
return this.lerpFrames(ratio, this.rmf_LUT[i], this.rmf_LUT[j]);
|
||||
}
|
||||
|
||||
generateRMF(originalPoints, curve, d1curve) {
|
||||
const frames = []
|
||||
frames.push(this.getFrenetVectors(0, originalPoints));
|
||||
|
||||
for(let i=0, steps=24; i<steps; i++) {
|
||||
const x0 = frames[i],
|
||||
// get the next frame
|
||||
t = (i+1)/steps,
|
||||
x1 = {
|
||||
o: curve.get(t),
|
||||
dt: d1curve.get(t)
|
||||
},
|
||||
// then mirror the rotational axis and tangent
|
||||
v1 = vec.minus(x1.o, x0.o),
|
||||
c1 = vec.dot(v1, v1),
|
||||
riL = vec.minus(x0.r, vec.scale(v1, 2/c1 * vec.dot(v1, x0.r))),
|
||||
dtiL = vec.minus(x0.dt, vec.scale(v1, 2/c1 * vec.dot(v1, x0.dt))),
|
||||
// and use those to compute a more stable rotational axis
|
||||
v2 = vec.minus(x1.dt, dtiL),
|
||||
c2 = vec.dot(v2, v2);
|
||||
x1.r = vec.minus(riL, vec.scale(v2, 2/c2 * vec.dot(v2, riL)));
|
||||
// and with that stable axis, a new normal.
|
||||
x1.n = vec.cross(x1.r, x1.dt);
|
||||
frames.push(x1);
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
getFrenetVectors(t, originalPoints) {
|
||||
const curve = new Bezier(this, originalPoints);
|
||||
const d1curve = new Bezier(this, curve.dpoints[0]);
|
||||
const dt = d1curve.get(t);
|
||||
const ddt = d1curve.derivative(t);
|
||||
const o = curve.get(t);
|
||||
const b = vec.normalize(vec.plus(dt, ddt));
|
||||
const r = vec.normalize(vec.cross(b, dt));
|
||||
const n = vec.normalize(vec.cross(r, dt));
|
||||
return { o, dt, r, n };
|
||||
}
|
||||
|
||||
|
||||
lerpFrames(t, f1, f2) {
|
||||
var frame = {};
|
||||
[`o`, `dt`, `r`, `n`].forEach(type => frame[type] = vec.lerp(t, f1[type], f2[type]));
|
||||
return frame;
|
||||
}
|
||||
|
||||
drawVector(from, vec, length, color, label) {
|
||||
setStroke(color);
|
||||
setFill(`black`);
|
||||
@@ -189,3 +122,78 @@ drawVector(from, vec, length, color, label) {
|
||||
});
|
||||
text(label, from.x + txt.x, from.y + txt.y);
|
||||
}
|
||||
|
||||
// This is where things are... rather different
|
||||
|
||||
getRMF(t, originalPoints) {
|
||||
// If we don't have a rotation-minimizing lookup table, build it.
|
||||
if (!this.rmf_LUT) {
|
||||
const curve = new Bezier(this, originalPoints);
|
||||
const d1curve = new Bezier(this, curve.dpoints[0]);
|
||||
this.rmf_LUT = this.generateRMF(originalPoints, curve, d1curve);
|
||||
}
|
||||
|
||||
// find the frame for "t":
|
||||
const last = this.rmf_LUT.length - 1;
|
||||
const f = t * last;
|
||||
const i = Math.floor(f);
|
||||
|
||||
// If we're looking at an integer index, we're done.
|
||||
if (f === i) return this.rmf_LUT[i];
|
||||
|
||||
// If we're not, we need to interpolate the adjacent frames
|
||||
const j = i + 1, ti = i/last, tj = j/last, ratio = (t - ti) / (tj - ti);
|
||||
return this.lerpFrames(ratio, this.rmf_LUT[i], this.rmf_LUT[j]);
|
||||
}
|
||||
|
||||
generateRMF(originalPoints, curve, d1curve) {
|
||||
// Start with the frenet frame just before t=0 and shift it to t=0
|
||||
const first = this.getFrenetVectors(-0.001, originalPoints);
|
||||
first.o = curve.get(0);
|
||||
|
||||
// Then we construct each next rotation-minimizing fame by reflecting
|
||||
// the previous frame and correcting the resulting vectors.
|
||||
const frames = [first];
|
||||
for(let i=0, steps=24; i<steps; i++) {
|
||||
const x0 = frames[i],
|
||||
// get the next frame
|
||||
t = (i+1)/steps,
|
||||
x1 = {
|
||||
o: curve.get(t),
|
||||
dt: d1curve.get(t)
|
||||
},
|
||||
// then mirror the rotational axis and tangent
|
||||
v1 = vec.minus(x1.o, x0.o),
|
||||
c1 = vec.dot(v1, v1),
|
||||
riL = vec.minus(x0.r, vec.scale(v1, 2/c1 * vec.dot(v1, x0.r))),
|
||||
dtiL = vec.minus(x0.dt, vec.scale(v1, 2/c1 * vec.dot(v1, x0.dt))),
|
||||
// then use those to compute a more stable rotational axis
|
||||
v2 = vec.minus(x1.dt, dtiL),
|
||||
c2 = vec.dot(v2, v2);
|
||||
// Fix the axis of rotation vector...
|
||||
x1.r = vec.minus(riL, vec.scale(v2, 2/c2 * vec.dot(v2, riL)));
|
||||
// ... and then compute the normal as usual
|
||||
x1.n = vec.cross(x1.r, x1.dt);
|
||||
frames.push(x1);
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
getFrenetVectors(t, originalPoints) {
|
||||
const curve = new Bezier(this, originalPoints),
|
||||
d1curve = new Bezier(this, curve.dpoints[0]),
|
||||
dt = d1curve.get(t),
|
||||
ddt = d1curve.derivative(t),
|
||||
o = curve.get(t),
|
||||
b = vec.normalize(vec.plus(dt, ddt)),
|
||||
r = vec.normalize(vec.cross(b, dt)),
|
||||
n = vec.normalize(vec.cross(r, dt));
|
||||
return { o, dt, r, n };
|
||||
}
|
||||
|
||||
lerpFrames(t, f1, f2) {
|
||||
var frame = {};
|
||||
[`o`, `dt`, `r`, `n`].forEach(type => frame[type] = vec.lerp(t, f1[type], f2[type]));
|
||||
return frame;
|
||||
}
|
||||
|
@@ -1,71 +1,70 @@
|
||||
function normalize(v) {
|
||||
let z = v.z || 0;
|
||||
var d = Math.sqrt(v.x*v.x + v.y*v.y + z*z);
|
||||
let r = { x:v.x/d, y:v.y/d };
|
||||
if (v.z !== undefined) r.z = z/d;
|
||||
return r;
|
||||
let z = v.z || 0;
|
||||
var d = Math.sqrt(v.x * v.x + v.y * v.y + z * z);
|
||||
let r = { x: v.x / d, y: v.y / d };
|
||||
if (v.z !== undefined) r.z = z / d;
|
||||
return r;
|
||||
}
|
||||
|
||||
function dot(v1, v2) {
|
||||
let z1 = v1.z || 0;
|
||||
let z2 = v2.z || 0;
|
||||
return v1.x * v2.x + v1.y * v2.y + z1 * z2;
|
||||
let z1 = v1.z || 0;
|
||||
let z2 = v2.z || 0;
|
||||
return v1.x * v2.x + v1.y * v2.y + z1 * z2;
|
||||
}
|
||||
|
||||
function scale(v, s) {
|
||||
let r = {
|
||||
x: s * v.x,
|
||||
y: s * v.y
|
||||
}
|
||||
if (v.z !== undefined) {
|
||||
r.z = s * v.z
|
||||
}
|
||||
return r;
|
||||
let r = {
|
||||
x: s * v.x,
|
||||
y: s * v.y,
|
||||
};
|
||||
if (v.z !== undefined) {
|
||||
r.z = s * v.z;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function plus(v1, v2) {
|
||||
let r = {
|
||||
x: v1.x + v2.x,
|
||||
y: v1.y + v2.y
|
||||
};
|
||||
if (v1.z !== undefined || v2.z !== undefined) {
|
||||
r.z = (v1.z||0) + (v2.z||0);
|
||||
};
|
||||
return r;
|
||||
let r = {
|
||||
x: v1.x + v2.x,
|
||||
y: v1.y + v2.y,
|
||||
};
|
||||
if (v1.z !== undefined || v2.z !== undefined) {
|
||||
r.z = (v1.z || 0) + (v2.z || 0);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function minus(v1, v2) {
|
||||
let r = {
|
||||
x: v1.x - v2.x,
|
||||
y: v1.y - v2.y
|
||||
};
|
||||
if (v1.z !== undefined || v2.z !== undefined) {
|
||||
r.z = (v1.z||0) - (v2.z||0);
|
||||
};
|
||||
return r;
|
||||
let r = {
|
||||
x: v1.x - v2.x,
|
||||
y: v1.y - v2.y,
|
||||
};
|
||||
if (v1.z !== undefined || v2.z !== undefined) {
|
||||
r.z = (v1.z || 0) - (v2.z || 0);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function cross(v1, v2) {
|
||||
if (v1.z === undefined || v2.z === undefined) {
|
||||
throw new Error(`Cross product is not defined for 2D vectors.`);
|
||||
}
|
||||
return {
|
||||
x: v1.y * v2.z - v1.z * v2.y,
|
||||
y: v1.z * v2.x - v1.x * v2.z,
|
||||
z: v1.x * v2.y - v1.y * v2.x
|
||||
};
|
||||
if (v1.z === undefined || v2.z === undefined) {
|
||||
throw new Error(`Cross product is not defined for 2D vectors.`);
|
||||
}
|
||||
return {
|
||||
x: v1.y * v2.z - v1.z * v2.y,
|
||||
y: v1.z * v2.x - v1.x * v2.z,
|
||||
z: v1.x * v2.y - v1.y * v2.x,
|
||||
};
|
||||
}
|
||||
|
||||
function lerp(t, v1, v2) {
|
||||
let r = {
|
||||
x: (1-t)*v1.x + t*v2.x,
|
||||
y: (1-t)*v1.y + t*v2.y
|
||||
};
|
||||
if (v1.z !== undefined || v2.z !== undefined) {
|
||||
r.z = (1-t)*(v1.z||0) + t*(v2.z||0);
|
||||
};
|
||||
return r;
|
||||
let r = {
|
||||
x: (1 - t) * v1.x + t * v2.x,
|
||||
y: (1 - t) * v1.y + t * v2.y,
|
||||
};
|
||||
if (v1.z !== undefined || v2.z !== undefined) {
|
||||
r.z = (1 - t) * (v1.z || 0) + t * (v2.z || 0);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
export default { normalize, dot, scale, plus, minus, cross, lerp }
|
||||
export default { normalize, dot, scale, plus, minus, cross, lerp };
|
||||
|
@@ -1,17 +1,19 @@
|
||||
let points = [];
|
||||
|
||||
setup() {
|
||||
const w = this.width,
|
||||
h = this.height;
|
||||
for (let i=0; i<10; i++) {
|
||||
points.push({
|
||||
x: w/2 + random(20) + cos(PI*2 * i/10) * (w/2 - 40),
|
||||
y: h/2 + random(20) + sin(PI*2 * i/10) * (h/2 - 40)
|
||||
});
|
||||
}
|
||||
setMovable(points);
|
||||
this.bindButtons();
|
||||
const w = this.width,
|
||||
h = this.height;
|
||||
|
||||
// let's create a wiggle-circle by randomizing points on a circle
|
||||
for (let i=0; i<10; i++) {
|
||||
points.push({
|
||||
x: w/2 + random(20) + cos(PI*2 * i/10) * (w/2 - 40),
|
||||
y: h/2 + random(20) + sin(PI*2 * i/10) * (h/2 - 40)
|
||||
});
|
||||
}
|
||||
setMovable(points);
|
||||
this.bindButtons();
|
||||
}
|
||||
|
||||
bindButtons() {
|
||||
let rbutton = find(`.raise`);
|
||||
@@ -27,103 +29,105 @@ draw() {
|
||||
}
|
||||
|
||||
drawCurve() {
|
||||
// we can't "just draw" this curve, since it'll be an arbitrary order,
|
||||
// And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm:
|
||||
start();
|
||||
noFill();
|
||||
setStroke(`black`);
|
||||
for(let t=0; t<=1; t+=0.01) {
|
||||
let q = JSON.parse(JSON.stringify(points));
|
||||
while(q.length > 1) {
|
||||
for (let i=0; i<q.length-1; i++) {
|
||||
q[i] = {
|
||||
x: q[i].x + (q[i+1].x - q[i].x) * t,
|
||||
y: q[i].y + (q[i+1].y - q[i].y) * t
|
||||
};
|
||||
}
|
||||
q.splice(q.length-1, 1);
|
||||
// we can't "just draw" this curve, since it'll be an arbitrary order,
|
||||
// And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm:
|
||||
start();
|
||||
noFill();
|
||||
setStroke(`black`);
|
||||
for(let t=0; t<=1; t+=0.01) {
|
||||
let q = JSON.parse(JSON.stringify(points));
|
||||
while(q.length > 1) {
|
||||
for (let i=0; i<q.length-1; i++) {
|
||||
q[i] = {
|
||||
x: q[i].x + (q[i+1].x - q[i].x) * t,
|
||||
y: q[i].y + (q[i+1].y - q[i].y) * t
|
||||
};
|
||||
}
|
||||
vertex(q[0].x, q[0].y);
|
||||
q.splice(q.length-1, 1);
|
||||
}
|
||||
end();
|
||||
vertex(q[0].x, q[0].y);
|
||||
}
|
||||
end();
|
||||
|
||||
start();
|
||||
setStroke(`lightgrey`);
|
||||
points.forEach(p => vertex(p.x, p.y));
|
||||
end();
|
||||
start();
|
||||
setStroke(`lightgrey`);
|
||||
points.forEach(p => vertex(p.x, p.y));
|
||||
end();
|
||||
|
||||
setStroke(`black`);
|
||||
points.forEach(p => circle(p.x, p.y, 3));
|
||||
setStroke(`black`);
|
||||
points.forEach(p => circle(p.x, p.y, 3));
|
||||
}
|
||||
|
||||
raise() {
|
||||
const p = points,
|
||||
const p = points,
|
||||
np = [p[0]],
|
||||
k = p.length;
|
||||
for (let i = 1, pi, pim; i < k; i++) {
|
||||
pi = p[i];
|
||||
pim = p[i - 1];
|
||||
np[i] = {
|
||||
x: ((k - i) / k) * pi.x + (i / k) * pim.x,
|
||||
y: ((k - i) / k) * pi.y + (i / k) * pim.y,
|
||||
};
|
||||
}
|
||||
np[k] = p[k - 1];
|
||||
points = np;
|
||||
|
||||
resetMovable(points);
|
||||
redraw();
|
||||
// raising the order of a curve is lossless:
|
||||
for (let i = 1, pi, pim; i < k; i++) {
|
||||
pi = p[i];
|
||||
pim = p[i - 1];
|
||||
np[i] = {
|
||||
x: ((k - i) / k) * pi.x + (i / k) * pim.x,
|
||||
y: ((k - i) / k) * pi.y + (i / k) * pim.y,
|
||||
};
|
||||
}
|
||||
np[k] = p[k - 1];
|
||||
points = np;
|
||||
|
||||
resetMovable(points);
|
||||
redraw();
|
||||
}
|
||||
|
||||
lower() {
|
||||
// Based on https://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
|
||||
// Based on https://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
|
||||
|
||||
// TODO: FIXME: this is the same code as in the old codebase,
|
||||
// and it does something odd to the either the
|
||||
// first or last point... it starts to travel
|
||||
// A LOT more than it looks like it should... O_o
|
||||
// TODO: FIXME: this is the same code as in the old codebase,
|
||||
// and it does something odd to the either the
|
||||
// first or last point... it starts to travel
|
||||
// A LOT more than it looks like it should... O_o
|
||||
|
||||
const p = points,
|
||||
k = p.length,
|
||||
data = [],
|
||||
n = k-1;
|
||||
const p = points,
|
||||
k = p.length,
|
||||
data = [],
|
||||
n = k-1;
|
||||
|
||||
if (k <= 3) return;
|
||||
if (k <= 3) return;
|
||||
|
||||
// build M, which will be (k) rows by (k-1) columns
|
||||
for(let i=0; i<k; i++) {
|
||||
data[i] = (new Array(k - 1)).fill(0);
|
||||
if(i===0) { data[i][0] = 1; }
|
||||
else if(i===n) { data[i][i-1] = 1; }
|
||||
else {
|
||||
data[i][i-1] = i / k;
|
||||
data[i][i] = 1 - data[i][i-1];
|
||||
}
|
||||
// build M, which will be (k) rows by (k-1) columns
|
||||
for(let i=0; i<k; i++) {
|
||||
data[i] = (new Array(k - 1)).fill(0);
|
||||
if(i===0) { data[i][0] = 1; }
|
||||
else if(i===n) { data[i][i-1] = 1; }
|
||||
else {
|
||||
data[i][i-1] = i / k;
|
||||
data[i][i] = 1 - data[i][i-1];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply our matrix operations:
|
||||
const M = new Matrix(data);
|
||||
const Mt = M.transpose(M);
|
||||
const Mc = Mt.multiply(M);
|
||||
const Mi = Mc.invert();
|
||||
// Apply our matrix operations:
|
||||
const M = new Matrix(data);
|
||||
const Mt = M.transpose(M);
|
||||
const Mc = Mt.multiply(M);
|
||||
const Mi = Mc.invert();
|
||||
|
||||
if (!Mi) {
|
||||
return console.error('MtM has no inverse?');
|
||||
}
|
||||
if (!Mi) {
|
||||
return console.error('MtM has no inverse?');
|
||||
}
|
||||
|
||||
// And then we map our k-order list of coordinates
|
||||
// to an n-order list of coordinates, instead:
|
||||
const V = Mi.multiply(Mt);
|
||||
const x = new Matrix(points.map(p => [p.x]));
|
||||
const nx = V.multiply(x);
|
||||
const y = new Matrix(points.map(p => [p.y]));
|
||||
const ny = V.multiply(y);
|
||||
// And then we map our k-order list of coordinates
|
||||
// to an n-order list of coordinates, instead:
|
||||
const V = Mi.multiply(Mt);
|
||||
const x = new Matrix(points.map(p => [p.x]));
|
||||
const nx = V.multiply(x);
|
||||
const y = new Matrix(points.map(p => [p.y]));
|
||||
const ny = V.multiply(y);
|
||||
|
||||
points = nx.data.map((x,i) => ({
|
||||
x: x[0],
|
||||
y: ny.data[i][0]
|
||||
}));
|
||||
points = nx.data.map((x,i) => ({
|
||||
x: x[0],
|
||||
y: ny.data[i][0]
|
||||
}));
|
||||
|
||||
resetMovable(points);
|
||||
redraw();
|
||||
resetMovable(points);
|
||||
redraw();
|
||||
}
|
||||
|
@@ -12,11 +12,13 @@ draw() {
|
||||
curve.drawCurve();
|
||||
curve.drawPoints();
|
||||
|
||||
// Similar to aligning, we transform the curve first
|
||||
let translated = this.translatePoints(curve.points);
|
||||
let rotated = this.rotatePoints(translated);
|
||||
let rtcurve = new Bezier(this, rotated);
|
||||
let extrema = rtcurve.extrema();
|
||||
|
||||
// and the we run the regular bounding box code
|
||||
let minx = Number.MAX_SAFE_INTEGER,
|
||||
miny = minx,
|
||||
maxx = Number.MIN_SAFE_INTEGER,
|
||||
@@ -37,6 +39,8 @@ draw() {
|
||||
noFill();
|
||||
setStroke(`#0F0`);
|
||||
|
||||
// But, crucially, we now need to reverse-transform the bbox corners:
|
||||
|
||||
let tx = curve.points[0].x;
|
||||
let ty = curve.points[0].y;
|
||||
let a = rotated[0].a;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
let curve;
|
||||
|
||||
setup() {
|
||||
setPanelCount(2);
|
||||
curve = new Bezier(this, 20, 250, 30, 20, 200, 250, 220, 20);
|
||||
setMovable(curve.points);
|
||||
setSlider(`.slide-control`, `position`, 0.5);
|
||||
@@ -15,29 +16,27 @@ draw() {
|
||||
|
||||
let w = this.height;
|
||||
let h = this.height;
|
||||
|
||||
let bbox = curve.bbox();
|
||||
let x = bbox.x.min + (bbox.x.max - bbox.x.min) * this.position;
|
||||
|
||||
// construct an `x` value based on our slider position
|
||||
let x = bbox.x.min + (bbox.x.max - bbox.x.min) * this.position;
|
||||
if (bbox.x.min < x && x < bbox.x.max) {
|
||||
setStroke("red");
|
||||
line(x,0,x,h);
|
||||
text(`x=${x | 0}`, x + 5, h - 30);
|
||||
}
|
||||
|
||||
translate(w, 0);
|
||||
|
||||
// In the next panel, let's draw x = t(x)
|
||||
nextPanel()
|
||||
setStroke("black");
|
||||
line(0,0,0,h);
|
||||
|
||||
// draw x = t(x)
|
||||
line(0, h-20, w, h-20);
|
||||
text('0', 10, h-10);
|
||||
text('⅓', 10 + (w-10)/3, h-10);
|
||||
text('⅔', 10 + 2*(w-10)/3, h-10);
|
||||
text('1', w-10, h-10);
|
||||
let p, s = { x: 0, y: h - curve.get(0).x };
|
||||
|
||||
for (let step = 0.02, t = step; t < 1 + step; t += step) {
|
||||
p = {x: t * w, y: h - curve.get(t).x };
|
||||
line(s.x, s.y, p.x, p.y);
|
||||
@@ -48,6 +47,7 @@ draw() {
|
||||
text("↑\nx", 10, h/2);
|
||||
text("t →", w/2, h-10);
|
||||
|
||||
// and its "intercept" line
|
||||
if (bbox.x.min < x && x < bbox.x.max) {
|
||||
setStroke("red");
|
||||
line(0, h-x, w, h-x);
|
||||
|
@@ -28,6 +28,7 @@ draw() {
|
||||
// find our answer:
|
||||
let y = round(curve.get(t).y);
|
||||
|
||||
// and draw everything
|
||||
setStroke("red");
|
||||
line(x, y, x, h);
|
||||
line(x, y, 0, y);
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |