1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-18 06:21:26 +02:00

figured out how to reuse sketches with data-attribute parameters

This commit is contained in:
Pomax
2020-08-26 21:56:58 -07:00
93 changed files with 5805 additions and 24390 deletions

View File

@@ -21,7 +21,7 @@ React is nice, Webpack is convenient, but there's just very little need to serve
- [ ] ja-JP
- [ ] zh-CN
- [x] Figure out why pages scroll on focus in Firefox (https://github.com/Pomax/BezierInfo-2/issues/262)
- [ ] Firefox for Android does not support static class fields (nightly does). Should I care, or will it not matter a month from now?
- [x] ~Firefox for Android does not support static class fields (nightly does). Should I care, or will it not matter a month from now?~ Mozilla released a new Firefox for Android that's finally up to date wrt modern JS, so this is no longer an issue.
- [x] now that github supports gh-pages from not just the root dir, move all the code into a `src` dir, and all the content into a `docs` dir. It's a stupid name, but GH doesn't support `public`. Hopefully "yet" but who knows how they work.
- [x] implement custom lazy loading that kicks in when images are about 2 screens away from being in screen. The standard browser `loading="lazy"` distance is entirely useless.
- [x] scope LaTeX images to each section (similar to the placeholder images) so that it's easier to redo just one section's latex code, rather than clearing and regenerating all ~250 latex blocks.
@@ -29,6 +29,7 @@ React is nice, Webpack is convenient, but there's just very little need to serve
- [x] Add a `setSlider(qs, handler)` API function so that sketches can hook into locally scoped HTML UI elements (like `<input type="range">`
- [ ] figure out how to force `graphics-element` elements to preallocate their bounding box, so that progressive page loading doesn't cause reflow.
- [ ] consider swammping out the `perform-code-surgery.js` regex approach to a dedicated DFA lexer, with scope tracking.
- [x] add data-attributes to sketches for same-sketch, different-parameters runs, in a way that works with the placeholder generation.
### Section conversion:

View File

@@ -0,0 +1,29 @@
let curve;
setup() {
let type = getParameter(`type`, `quadratic`);
curve = (type === `quadratic`) ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `steps`, type === `quadratic` ? 4 : 8);
}
draw() {
clear();
let alen = 0;
const len = curve.length();
const LUT = curve.getLUT(this.steps + 1);
setStroke("red");
curve.drawSkeleton(`lightblue`);
LUT.forEach((p1,i) => {
if (i===0) return;
let p0 = LUT[i-1];
line(p0.x, p0.y, p1.x, p1.y);
alen += dist(p0.x, p0.y, p1.x, p1.y);
});
curve.drawPoints();
setFill(`black`);
text(`Approximate length, ${this.steps} steps: ${alen.toFixed(2)} (true: ${len.toFixed(2)})`, 10, 15);
};

View File

@@ -4,7 +4,16 @@ Sometimes, we don't actually need the precision of a true arc length, and we can
If we combine the work done in the previous sections on curve flattening and arc length computation, we can implement these with minimal effort:
<Graphic title="Approximate quadratic curve arc length" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
<Graphic title="Approximate cubic curve arc length" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
<div class="figure">
Try clicking on the sketch and using your up and down arrow keys to lower the number of segments for both the quadratic and cubic curve. You may notice that the error in length is actually pretty significant, even if the percentage is fairly low: if the number of segments used yields an error of 0.1% or higher, the flattened curve already looks fairly obviously flattened. And of course, the longer the curve, the more significant the error will be.
<graphics-element title="Approximate quadratic curve arc length" src="./approximate.js" data-type="quadratic">
<input type="range" min="1" max="24" step="1" value="4" class="slide-control">
</graphics-element>
<graphics-element title="Approximate cubic curve arc length" src="./approximate.js" data-type="cubic">
<input type="range" min="1" max="32" step="1" value="8" class="slide-control">
</graphics-element>
</div>
You may notice that even though the error in length is actually pretty significant in absolute terms, even at a low number of segments we get a length that agrees with the true length when it comes to just the integer part of the arc length. Quite often, approximations can drastically speed things up!

View File

@@ -1,72 +0,0 @@
module.exports = {
// These are functions that can be called "From the page",
// rather than being internal to the sketch. This is useful
// for making on-page controls hook into the sketch code.
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
/**
* Set up the default quadratic curve.
*/
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 10;
},
/**
* Set up the default cubic curve.
*/
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 16;
},
/**
* Draw a curve and its polygon-approximation,
* showing the "true" length of the curve vs. the
* length based on tallying up the polygon sections.
*/
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var pts = curve.getLUT(api.steps);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
var len = curve.length();
var alen = 0;
for(var i=0,p1,dx,dy; i<pts.length-1; i++) {
p0 = pts[i];
p1 = pts[i+1];
dx = p1.x-p0.x;
dy = p1.y-p0.y;
alen += Math.sqrt(dx*dx+dy*dy);
}
alen = ((100*alen)|0)/100;
len = ((100*len)|0)/100;
api.text("Approximate length, "+api.steps+" steps: "+alen+" (true: "+len+")", {x:10, y: 15});
}
};

View File

@@ -1,26 +1,33 @@
let curve;
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
let type = getParameter(`type`, `quadratic`);
if (type === `quadratic`) {
curve = Bezier.defaultQuadratic(this);
} else {
curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
}
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
noFill();
let minx = Number.MAX_SAFE_INTEGER,
miny = minx,
maxx = Number.MIN_SAFE_INTEGER,
maxy = maxx;
maxy = maxx,
extrema = curve.extrema();
noFill();
setStroke(`red`);
let extrema = curve.extrema();
[0, ...extrema.x, ...extrema.y, 1].forEach(t => {
let p = curve.get(t);
if (p.x < minx) minx = p.x;

View File

@@ -10,7 +10,9 @@ If we have the extremities, and the start/end points, a simple for-loop that tes
Applying this approach to our previous root finding, we get the following [axis-aligned bounding boxes](https://en.wikipedia.org/wiki/Bounding_volume#Common_types) (with all curve extremity points shown on the curve):
<graphics-element title="Quadratic Bézier bounding box" src="./quadratic.js"></graphics-element>
<graphics-element title="Cubic Bézier bounding box" src="./cubic.js"></graphics-element>
<div class="figure">
<graphics-element title="Quadratic Bézier bounding box" src="./bbox.js" data-type="quadratic"></graphics-element>
<graphics-element title="Cubic Bézier bounding box" src="./bbox.js" data-type="cubic"></graphics-element>
</div>
We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first need to look at how aligning works.

View File

@@ -1,41 +0,0 @@
setup() {
const curve = this.curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
noFill();
let minx = Number.MAX_SAFE_INTEGER,
miny = minx,
maxx = Number.MIN_SAFE_INTEGER,
maxy = maxx;
setStroke(`red`);
let extrema = curve.extrema();
[0, ...extrema.x, ...extrema.y, 1].forEach(t => {
let p = curve.get(t);
if (p.x < minx) minx = p.x;
if (p.x > maxx) maxx = p.x;
if (p.y < miny) miny = p.y;
if (p.y > maxy) maxy = p.y;
if (t > 0 && t< 1) circle(p.x, p.y, 3);
});
setStroke(`#0F0`);
rect(minx, miny, maxx - minx, maxy - miny);
}
onMouseMove() {
redraw();
}

View File

@@ -1,6 +1,13 @@
let curve;
setup() {
const curve = this.curve = Bezier.defaultQuadratic(this);
let type = getParameter(`type`, `quadratic`);
if (type === `quadratic`) {
curve = Bezier.defaultQuadratic(this);
} else {
curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
}
setMovable(curve.points);
}
@@ -8,7 +15,6 @@ draw() {
resetTransform();
clear();
const dim = this.height;
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
@@ -21,8 +27,9 @@ draw() {
translate(40,20);
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
let pcount = curve.points.length;
new Bezier(this, curve.points.map((p,i) => ({
x: (i/2) * dim,
x: (i/(pcount-1)) * dim,
y: p.x
}))).drawCurve();
@@ -36,11 +43,7 @@ draw() {
drawAxes(`t`, 0,1, `Y`, 0, dim, dim, dim);
new Bezier(this, curve.points.map((p,i) => ({
x: (i/2) * dim,
x: (i/(pcount-1)) * dim,
y: p.y
}))).drawCurve();
}
onMouseMove() {
redraw();
}

View File

@@ -8,5 +8,6 @@ Let's look at how a parametric Bézier curve "splits up" into two normal functio
If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.
<graphics-element title="Quadratic Bézier curve components" width="825" src="./quadratic.js"></graphics-element>
<graphics-element title="Cubic Bézier curve components" width="825" src="./cubic.js"></graphics-element>
<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>

View File

@@ -1,46 +0,0 @@
setup() {
const curve = this.curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
setMovable(curve.points);
}
draw() {
resetTransform();
clear();
const dim = this.height;
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
translate(dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
new Bezier(this, curve.points.map((p,i) => ({
x: (i/3) * dim,
y: p.x
}))).drawCurve();
resetTransform();
translate(2*dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0,1, `Y`, 0, dim, dim, dim);
new Bezier(this, curve.points.map((p,i) => ({
x: (i/3) * dim,
y: p.y
}))).drawCurve();
}
onMouseMove() {
redraw();
}

View File

@@ -5,15 +5,15 @@ Bézier curves are, like all "splines", interpolation functions. This means that
The following graphs show the interpolation functions for quadratic and cubic curves, with "S" being the strength of a point's contribution to the total sum of the Bézier function. Click-and-drag to see the interpolation percentages for each curve-defining point at a specific <i>t</i> value.
<div class="figure">
<graphics-element title="Quadratic interpolations" src="./lerp-quadratic.js">
<graphics-element title="Quadratic interpolations" src="./lerp.js" data-degree="3">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
<graphics-element title="Cubic interpolations" src="./lerp-cubic.js">
<graphics-element title="Cubic interpolations" src="./lerp.js" data-degree="4">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
<graphics-element title="15th degree interpolations" src="./lerp-fifteenth.js">
<graphics-element title="15th degree interpolations" src="./lerp.js" data-degree="15">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
</div>

View File

@@ -5,15 +5,15 @@
下のグラフは、2次ベジエ曲線や3次ベジエ曲線の補間関数を表しています。ここでSは、ベジエ関数全体に対しての、その点の寄与の大きさを示します。ある<i>t</i>において、ベジエ曲線を定義する各点の補間率がどのようになっているのか、クリックドラッグをして確かめてみてください。
<div class="figure">
<graphics-element title="2次の補間" src="./lerp-quadratic.js">
<graphics-element title="2次の補間" src="./lerp.js" data-degree="3">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
<graphics-element title="3次の補間" src="./lerp-cubic.js">
<graphics-element title="3次の補間" src="./lerp.js" data-degree="4">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
<graphics-element title="15次の補間" src="./lerp-fifteenth.js">
<graphics-element title="15次の補間" src="./lerp.js" data-degree="15">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
</div>

View File

@@ -5,15 +5,15 @@
下面的图形显示了二次曲线和三次曲线的差值方程“S”代表了点对贝塞尔方程总和的贡献。点击拖动点来看看在特定的<i>t</i>值时,每个曲线定义的点的插值百分比。
<div class="figure">
<graphics-element title="二次插值" src="./lerp-quadratic.js">
<graphics-element title="二次插值" src="./lerp.js" data-degree="3">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
<graphics-element title="三次插值" src="./lerp-cubic.js">
<graphics-element title="三次插值" src="./lerp.js" data-degree="4">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
<graphics-element title="15次插值" src="./lerp-fifteenth.js">
<graphics-element title="15次插值" src="./lerp.js" data-degree="15">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
</div>

View File

@@ -1,56 +0,0 @@
setup() {
const w = this.width,
h = this.height;
this.f = [
t => ({ x: t * w, y: h * (1-t) ** 3 }),
t => ({ x: t * w, y: h * 3 * (1-t) ** 2 * t }),
t => ({ x: t * w, y: h * 3 * (1-t) * t ** 2 }),
t => ({ x: t * w, y: h * t ** 3})
];
this.s = this.f.map(f => plot(f) );
setSlider(`.slide-control`, `position`, 0);
}
draw() {
resetTransform();
clear();
setFill(`black`);
setStroke(`black`);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0, 1, `S`, `0%`, `100%`);
noFill();
this.s.forEach(s => {
setStroke( randomColor() );
drawShape(s);
})
this.drawHighlight();
}
drawHighlight() {
let c = screenToWorld({
x: map(this.position, 0, 1, -10, this.width + 10),
y: this.height/2
});
if (c.x < 0) return;
if (c.x > this.width) return;
noStroke();
setFill(`rgba(255,0,0,0.3)`);
rect(c.x - 2, 0, 5, this.height);
const p = this.f.map(f => f(c.x / this.width));
setFill(`black`);
p.forEach(p => {
circle(p.x, p.y, 3);
text(`${ round(100 * p.y/this.height) }%`, p.x + 10, p.y);
});
}

View File

@@ -1,56 +0,0 @@
setup() {
const w = this.width,
h = this.height;
this.f = [
t => ({ x: t * w, y: h * (1-t) ** 2 }),
t => ({ x: t * w, y: h * 2 * (1-t) * t }),
t => ({ x: t * w, y: h * t ** 2 })
];
this.s = this.f.map(f => plot(f) );
setSlider(`.slide-control`, `position`, 0);
}
draw() {
resetTransform();
clear();
setFill(`black`);
setStroke(`black`);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0, 1, `S`, `0%`, `100%`);
noFill();
this.s.forEach(s => {
setStroke( randomColor() );
drawShape(s);
})
this.drawHighlight();
}
drawHighlight() {
let c = screenToWorld({
x: map(this.position, 0, 1, -10, this.width + 10),
y: this.height/2
});
if (c.x < 0) return;
if (c.x > this.width) return;
noStroke();
setFill(`rgba(255,0,0,0.3)`);
rect(c.x - 2, 0, 5, this.height);
const p = this.f.map(f => f(c.x / this.width));
setFill(`black`);
p.forEach(p => {
circle(p.x, p.y, 3);
text(`${ round(100 * p.y/this.height) }%`, p.x + 10, p.y);
});
}

View File

@@ -1,7 +1,33 @@
setup() {
this.degree = 15;
const w = this.width,
h = this.height;
const degree = this.getParameter(`degree`, 3);
if (degree === 3) {
this.f = [
t => ({ x: t * w, y: h * (1-t) ** 2 }),
t => ({ x: t * w, y: h * 2 * (1-t) * t }),
t => ({ x: t * w, y: h * t ** 2 })
];
} else if (degree === 4) {
this.f = [
t => ({ x: t * w, y: h * (1-t) ** 3 }),
t => ({ x: t * w, y: h * 3 * (1-t) ** 2 * t }),
t => ({ x: t * w, y: h * 3 * (1-t) * t ** 2 }),
t => ({ x: t * w, y: h * t ** 3})
];
} else {
this.triangle = [[1], [1,1]];
this.generate();
this.f = [...new Array(degree + 1)].map((_,i) => {
return t => ({
x: t * w,
y: h * this.binomial(degree,i) * (1-t) ** (degree-i) * t ** (i)
});
});
}
this.s = this.f.map(f => plot(f, 0, 1, degree*4) );
setSlider(`.slide-control`, `position`, 0)
}
@@ -17,21 +43,6 @@ binomial(n,k) {
return this.triangle[n][k];
}
generate() {
const w = this.width,
h = this.height,
d = this.degree;
this.f = [...new Array(d+1)].map((_,i) => {
return t => ({
x: t * w,
y: h * this.binomial(d,i) * (1-t) ** (d-i) * t ** (i)
});
});
this.s = this.f.map(f => plot(f, 0, 1, d*4) );
}
draw() {
resetTransform();
clear();

View File

@@ -6,7 +6,7 @@ Problem solved!
But, if we think about this a little more, this cannot possible work, because of something that you may have noticed in the section on [reordering curves](#reordering): what a curve looks like, and the function that draws that curve, are not in some kind of universal, fixed, one-to-one relation. If we have some quadratic curve, then simply by raising the curve order we can get corresponding cubic, quartic, and higher and higher mathematical expressions that all draw the _exact same curve_ but with wildly different derivatives. So: if we want to make a transition from one curve to the next look good, and we want to use the derivative, then we suddenly need to answer the question: "Which derivative?".
How would you even decide? What makes the cubic derivatives better or less suited than, say, quintic derivatives? Wouldn't it be nicer if we could use something that was inherent to the curve, without being tied to the functions that yield that curve? And (of course) as it turns out, there is a way to define curvature in such a way that it only relies on what the curve actually looks like, and given where this section is in the larger body of this Primer, it should hopefully not be surprising that thee thing we can use to define curvature is the thing we talked about in the previous section: arc length.
How would you even decide? What makes the cubic derivatives better or less suited than, say, quintic derivatives? Wouldn't it be nicer if we could use something that was inherent to the curve, without being tied to the functions that yield that curve? And (of course) as it turns out, there is a way to define curvature in such a way that it only relies on what the curve actually looks like, and given where this section is in the larger body of this Primer, it should hopefully not be surprising that the thing we can use to define curvature is the thing we talked about in the previous section: arc length.
Intuitively, this should make sense, even if we have no idea what the maths would look like: if we travel some fixed distance along some curve, then the point at that distance is simply the point at that distance. It doesn't matter what function we used to draw the curve: once we know what the curve looks like, the function(s) used to draw it become irrelevant: a point a third along the full distance of the curve is simply the point a third along the distance of the curve.

View File

@@ -22,7 +22,9 @@ In the case of Bézier curves, extending the interval simply makes our curve "ke
The following two graphics show you Bézier curves rendered "the usual way", as well as the curves they "lie on" if we were to extend the `t` values much further. As you can see, there's a lot more "shape" hidden in the rest of the curve, and we can model those parts by moving the curve points around.
<graphics-element title="Quadratic infinite interval Bézier curve" src="./quadratic.js"></graphics-element>
<graphics-element title="Cubic infinite interval Bézier curve" src="./cubic.js"></graphics-element>
<div class="figure">
<graphics-element title="Quadratic infinite interval Bézier curve" src="./extended.js" data-type="quadratic"></graphics-element>
<graphics-element title="Cubic infinite interval Bézier curve" src="./extended.js" data-type="cubic"></graphics-element>
</div>
In fact, there are curves used in graphics design and computer modelling that do the opposite of Bézier curves; rather than fixing the interval, and giving you freedom to choose the coordinates, they fix the coordinates, but give you freedom over the interval. A great example of this is the ["Spiro" curve](http://levien.com/phd/phd.html), which is a curve based on part of a [Cornu Spiral, also known as Euler's Spiral](https://en.wikipedia.org/wiki/Euler_spiral). It's a very aesthetically pleasing curve and you'll find it in quite a few graphics packages like [FontForge](https://fontforge.github.io) and [Inkscape](https://inkscape.org). It has even been used in font design, for example for the Inconsolata typeface.

View File

@@ -22,7 +22,9 @@
下の2つの図は「いつもの方法」で描いたベジエ曲線ですが、これと一緒に、`t`の値をずっと先まで広げた場合の「延びた」ベジエ曲線も表示しています。見てわかるように、曲線の残りの部分には多くの「かたち」が隠れています。そして曲線の点を動かせば、その部分の形状も変わります。
<graphics-element title="無限区間の2次ベジエ曲線" src="./quadratic.js"></graphics-element>
<graphics-element title="無限区間の3次ベジエ曲線" src="./cubic.js"></graphics-element>
<div class="figure">
<graphics-element title="無限区間の2次ベジエ曲線" src="./extended.js" data-type="quadratic"></graphics-element>
<graphics-element title="無限区間の3次ベジエ曲線" src="./extended.js" data-type="cubic"></graphics-element>
</div>
実際に、グラフィックデザインやコンピュータモデリングで使われている曲線の中には、座標が固定されていて、区間は自由に動かせるような曲線があります。これは、区間が固定されていて、座標を自由に動かすことのできるベジエ曲線とは反対になっています。すばらしい例が[「Spiro」曲線](http://levien.com/phd/phd.html)で、これは[オイラー螺旋とも呼ばれるクロソイド曲線](https://ja.wikipedia.org/wiki/クロソイド曲線)の一部分に基づいた曲線です。非常に美しく心地よい曲線で、[FontForge](https://fontforge.github.io)や[Inkscape](https://inkscape.org/ja/)など多くのグラフィックアプリに実装されており、フォントデザインにも利用されていますInconsolataフォントなど

View File

@@ -22,7 +22,9 @@
下面两个图形给你展示了以“普通方式”来渲染的贝塞尔曲线,以及如果我们扩大`t`值时它们所“位于”的曲线。如你所见,曲线的剩余部分隐藏了很多“形状”,我们可以通过移动曲线的点来建模这部分。
<graphics-element title="二次无限区间贝塞尔曲线" src="./quadratic.js"></graphics-element>
<graphics-element title="次无限区间贝塞尔曲线" src="./cubic.js"></graphics-element>
<div class="figure">
<graphics-element title="次无限区间贝塞尔曲线" src="./extended.js" data-type="quadratic"></graphics-element>
<graphics-element title="三次无限区间贝塞尔曲线" src="./extended.js" data-type="cubic"></graphics-element>
</div>
实际上,图形设计和计算机建模中还用了一些和贝塞尔曲线相反的曲线,这些曲线没有固定区间和自由的坐标,相反,它们固定座标但给你自由的区间。["Spiro"曲线](http://levien.com/phd/phd.html)就是一个很好的例子,它的构造是基于[羊角螺线,也就是欧拉螺线](https://zh.wikipedia.org/wiki/%E7%BE%8A%E8%A7%92%E8%9E%BA%E7%BA%BF)的一部分。这是在美学上很令人满意的曲线,你可以在一些图形包中看到它,比如[FontForge](https://fontforge.github.io)和[Inkscape](https://inkscape.org)它也被用在一些字体设计中比如Inconsolata字体

View File

@@ -1,11 +1,12 @@
let curve;
setup() {
this.curve = Bezier.defaultCubic(this);
setMovable(this.curve.points);
const type = this.getParameter(`type`, `quadratic`);
curve = (type === `quadratic`) ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this);
setMovable(curve.points);
}
draw() {
const curve = this.curve;
clear();
curve.drawCurve();
curve.drawSkeleton();

View File

@@ -1,32 +0,0 @@
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
}
draw() {
const curve = this.curve;
clear();
curve.drawCurve();
curve.drawSkeleton();
let step=0.05, min=-10, max=10;
let pt = curve.get(min - step), pn;
setStroke(`skyblue`);
for (let t=min; t<=step; t+=step) {
pn = curve.get(t);
line(pt.x, pt.y, pn.x, pn.y);
pt = pn;
}
pt = curve.get(1);
for (let t=1+step; t<=max; t+=step) {
pn = curve.get(t);
line(pt.x, pt.y, pn.x, pn.y);
pt = pn;
}
curve.drawPoints();
}

View File

@@ -214,8 +214,8 @@ As it turns out, Newton-Raphson is so blindingly fast that we could get away wit
So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics. For the quadratic curve, that means just the first derivative, in red:
<graphics-element title="Quadratic Bézier curve extremities" width="825" src="./quadratic.js"></graphics-element>
<graphics-element title="Quadratic Bézier curve extremities" width="825" src="./extremities.js" data-type="quadratic"></graphics-element>
And for cubic curves, that means first and second derivatives, in red and purple respectively:
<graphics-element title="Cubic Bézier curve extremities" width="825" src="./cubic.js"></graphics-element>
<graphics-element title="Cubic Bézier curve extremities" width="825" src="./extremities.js" data-type="cubic"></graphics-element>

View File

@@ -1,122 +0,0 @@
setup() {
const curve = this.curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
setMovable(curve.points);
}
draw() {
resetTransform();
clear();
const dim = this.height;
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
translate(dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
this.plotDimension(dim, new Bezier(this, curve.points.map((p,i) => ({
x: (i/3) * dim,
y: p.x
}))));
resetTransform();
translate(2*dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
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/3) * dim,
y: p.y
}))));
}
plotDimension(dim, dimension) {
cacheStyle();
dimension.drawCurve();
setFill(`red`);
setStroke(`red`);
// There are four possible extrema: t=0, t=1, and
// up to two t values that solves B'(t)=0, provided
// that they lie between 0 and 1. But of those four,
// only two will be real extrema (one minimum value,
// and one maximum value)
// First we compute the "simple" cases:
let t1 = 0; let y1 = dimension.get(t1).y;
let t2 = 1; let y2 = dimension.get(t2).y;
// We assume y1 < y2, but is that actually true?
let reverse = (y2 < y1);
// Are there a solution for B'(t) = 0?
let roots = this.getRoots(...dimension.dpoints[0].map(p => p.y));
roots.forEach(t =>{
// Is that solution a value in [0,1]?
if (t > 0 && t < 1) {
// It is, so we have either a new minimum value
// or new maximum value:
let dp = dimension.get(t);
if (reverse) {
if (dp.y < y2) { t2 = t; y2 = dp.y; }
if (dp.y > y1) { t1 = t; y1 = dp.y; }
} else {
if (dp.y < y1) { t1 = t; y1 = dp.y; }
if (dp.y > y2) { t2 = t; y2 = dp.y; }
}
}
});
// Done, show our derivative-based extrema:
circle(t1 * dim, y1, 3);
text(`t = ${t1.toFixed(2)}`, map(t1, 0,1, 15,dim-15), y1 + 25);
circle(t2 * dim, y2, 3);
text(`t = ${t2.toFixed(2)}`, map(t2, 0,1, 15,dim-15), y2 + 25);
// And then show the second derivate inflection, if there is one
setFill(`purple`);
setStroke(`purple`);
this.getRoots(...dimension.dpoints[1].map(p => p.y)).forEach(t =>{
if (t > 0 && t < 1) {
let d = dimension.get(t);
circle(t * dim, d.y, 3);
text(`t = ${t.toFixed(2)}`, map(t, 0,1, 15,dim-15), d.y + 25);
}
});
restoreStyle();
}
getRoots(v1, v2, v3) {
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;
if (a === 0) return [];
if (d < 0) return [];
const f = -b / (2*a);
if (d === 0) return [f]
const l = sqrt(d) / (2*a);
return [f-l, f+l];
}
onMouseMove() {
redraw();
}

View File

@@ -0,0 +1,162 @@
let curve;
setup() {
const type = this.getParameter(`type`, `quadratic`);
if (type === `quadratic`) {
curve = Bezier.defaultQuadratic(this);
} else {
curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
}
setMovable(curve.points);
}
draw() {
resetTransform();
clear();
const dim = this.height;
const degree = curve.points.length - 1;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
translate(dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
this.plotDimension(dim, new Bezier(this, curve.points.map((p,i) => ({
x: (i/degree) * dim,
y: p.x
}))));
resetTransform();
translate(2*dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
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
}))))
}
plotDimension(dim, dimension) {
cacheStyle();
dimension.drawCurve();
setFill(`red`);
setStroke(`red`);
// There are three possible extrema: t=0, t=1, and
// the t value that solves B'(t)=0, provided that
// value is between 0 and 1. But of those three,
// only two will be real extrema (one minimum value,
// and one maximum value)
// First we compute the "simple" cases:
let t1 = 0; let y1 = dimension.get(t1).y;
let t2 = 1; let y2 = dimension.get(t2).y;
// We assume y1 < y2, but is that actually true?
let reverse = (y2 < y1);
if (curve.points.length === 3) {
this.plotQuadraticDimension(t1, y1, t2, y2, dim, dimension, reverse);
} else {
this.plotCubicDimension(t1, y1, t2, y2, dim, dimension, reverse);
}
}
plotQuadraticDimension(t1, y1, t2, y2, dim, dimension, reverse) {
// Is there a solution for B'(t) = 0?
let dpoints = dimension.dpoints[0];
let t3 = -dpoints[0].y / (dpoints[1].y - dpoints[0].y);
// Is that solution a value in [0,1]?
if (t3 > 0 && t3 < 1) {
// It is, so we have either a new minimum value
// or new maximum value:
let dp = dimension.get(t3);
if (reverse) {
if (dp.y < y2) { t2 = t3; y2 = dp.y; }
if (dp.y > y1) { t1 = t3; y1 = dp.y; }
} else {
if (dp.y < y1) { t1 = t3; y1 = dp.y; }
if (dp.y > y2) { t2 = t3; y2 = dp.y; }
}
}
// Done, show our extrema:
circle(t1 * dim, y1, 3);
text(`t = ${t1.toFixed(2)}`, map(t1, 0,1, 15,dim-15), y1 + 25);
circle(t2 * dim, y2, 3);
text(`t = ${t2.toFixed(2)}`, map(t2, 0,1, 15,dim-15), y2 + 25);
restoreStyle();
}
plotCubicDimension(t1, y1, t2, y2, dim, dimension, reverse) {
// Are there a solution for B'(t) = 0?
let roots = this.getRoots(...dimension.dpoints[0].map(p => p.y));
roots.forEach(t => {
// Is that solution a value in [0,1]?
if (t > 0 && t < 1) {
// It is, so we have either a new minimum value
// or new maximum value:
let dp = dimension.get(t);
if (reverse) {
if (dp.y < y2) { t2 = t; y2 = dp.y; }
if (dp.y > y1) { t1 = t; y1 = dp.y; }
} else {
if (dp.y < y1) { t1 = t; y1 = dp.y; }
if (dp.y > y2) { t2 = t; y2 = dp.y; }
}
}
});
// Done, show our derivative-based extrema:
circle(t1 * dim, y1, 3);
text(`t = ${t1.toFixed(2)}`, map(t1, 0,1, 15,dim-15), y1 + 25);
circle(t2 * dim, y2, 3);
text(`t = ${t2.toFixed(2)}`, map(t2, 0,1, 15,dim-15), y2 + 25);
// And then show the second derivate inflection, if there is one
setFill(`purple`);
setStroke(`purple`);
this.getRoots(...dimension.dpoints[1].map(p => p.y)).forEach(t =>{
if (t > 0 && t < 1) {
let d = dimension.get(t);
circle(t * dim, d.y, 3);
text(`t = ${t.toFixed(2)}`, map(t, 0,1, 15,dim-15), d.y + 25);
}
});
restoreStyle();
}
getRoots(v1, v2, v3) {
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;
if (a === 0) return [];
if (d < 0) return [];
const f = -b / (2*a);
if (d === 0) return [f]
const l = sqrt(d) / (2*a);
return [f-l, f+l];
}

View File

@@ -1,87 +0,0 @@
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
}
draw() {
resetTransform();
clear();
const dim = this.height;
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
translate(dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
translate(40,20);
drawAxes(`t`, 0, 1, `X`, 0, dim, dim, dim);
this.plotDimension(dim, new Bezier(this, curve.points.map((p,i) => ({
x: (i/2) * dim,
y: p.x
}))));
resetTransform();
translate(2*dim, 0);
setStroke(`black`);
line(0,0,0,dim);
scale(0.8, 0.9);
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/2) * dim,
y: p.y
}))))
}
plotDimension(dim, dimension) {
cacheStyle();
dimension.drawCurve();
setFill(`red`);
setStroke(`red)`);
// There are three possible extrema: t=0, t=1, and
// the t value that solves B'(t)=0, provided that
// value is between 0 and 1. But of those three,
// only two will be real extrema (one minimum value,
// and one maximum value)
// First we compute the "simple" cases:
let t1 = 0; let y1 = dimension.get(t1).y;
let t2 = 1; let y2 = dimension.get(t2).y;
// We assume y1 < y2, but is that actually true?
let reverse = (y2 < y1);
// Is there a solution for B'(t) = 0?
let dpoints = dimension.dpoints[0];
let t3 = -dpoints[0].y / (dpoints[1].y - dpoints[0].y);
// Is that solution a value in [0,1]?
if (t3 > 0 && t3 < 1) {
// It is, so we have either a new minimum value
// or new maximum value:
let dp = dimension.get(t3);
if (reverse) {
if (dp.y < y2) { t2 = t3; y2 = dp.y; }
if (dp.y > y1) { t1 = t3; y1 = dp.y; }
} else {
if (dp.y < y1) { t1 = t3; y1 = dp.y; }
if (dp.y > y2) { t2 = t3; y2 = dp.y; }
}
}
// Done, show our extrema:
circle(t1 * dim, y1, 3);
text(`t = ${t1.toFixed(2)}`, map(t1, 0,1, 15,dim-15), y1 + 25);
circle(t2 * dim, y2, 3);
text(`t = ${t2.toFixed(2)}`, map(t2, 0,1, 15,dim-15), y2 + 25);
restoreStyle();
}

View File

@@ -5,11 +5,11 @@ We can also simplify the drawing process by "sampling" the curve at certain poin
We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.
<div class="figure">
<graphics-element title="Flattening a quadratic curve" src="./quadratic.js">
<graphics-element title="Flattening a quadratic curve" src="./flatten.js" data-type="quadratic">
<input type="range" min="1" max="16" step="1" value="4" class="slide-control">
</graphics-element>
<graphics-element title="Flattening a cubic curve" src="./cubic.js">
<graphics-element title="Flattening a cubic curve" src="./flatten.js" data-type="cubic">
<input type="range" min="1" max="24" step="1" value="8" class="slide-control">
</graphics-element>
</div>

View File

@@ -5,11 +5,11 @@
例えば「X個の線分がほしい」場合には、分割数がそうなるようにサンプリング間隔を選び、曲線をサンプリングします。この方法の利点は速さです。曲線の座標を100個だの1000個だの計算するのではなく、ずっと少ない回数のサンプリングでも、十分きれいに見えるような曲線を作ることができるのです。欠点はもちろん、「本物の曲線」に比べて精度が損なわれてしまうことです。したがって、交点の検出や曲線の位置揃えを正しく行いたい場合には、平坦化した曲線は普通利用できません。
<div class="figure">
<graphics-element title="2次ベジエ曲線の平坦化" src="./quadratic.js">
<graphics-element title="2次ベジエ曲線の平坦化" src="./flatten.js" data-type="quadratic">
<input type="range" min="1" max="16" step="1" value="4" class="slide-control">
</graphics-element>
<graphics-element title="3次ベジエ曲線の平坦化" src="./cubic.js">
<graphics-element title="3次ベジエ曲線の平坦化" src="./flatten.js" data-type="cubic">
<input type="range" min="1" max="24" step="1" value="8" class="slide-control">
</graphics-element>
</div>

View File

@@ -5,11 +5,11 @@
我们可以先确定“想要X个分段”然后在间隔的地方采样曲线得到一定数量的分段。这种方法的优点是速度很快比起遍历100甚至1000个曲线坐标我们可以采样比较少的点仍然得到看起来足够好的曲线。这么做的缺点是我们失去了“真正的曲线”的精度因此不能用此方法来做真实的相交检测或曲率对齐。
<div class="figure">
<graphics-element title="拉平一条二次曲线" src="./quadratic.js">
<graphics-element title="拉平一条二次曲线" src="./flatten.js" data-type="quadratic">
<input type="range" min="1" max="16" step="1" value="4" class="slide-control">
</graphics-element>
<graphics-element title="拉平一条三次曲线" src="./cubic.js">
<graphics-element title="拉平一条三次曲线" src="./flatten.js" data-type="cubic">
<input type="range" min="1" max="24" step="1" value="8" class="slide-control">
</graphics-element>
</div>

View File

@@ -1,28 +0,0 @@
setup() {
this.curve = Bezier.defaultCubic(this);
setMovable(this.curve.points);
setSlider(`.slide-control`, `steps`, 8);
}
draw() {
clear();
this.curve.drawSkeleton();
noFill();
start();
for(let i=0, e=this.steps; i<=e; i++) {
let p = this.curve.get(i/e);
vertex(p.x, p.y);
}
end();
this.curve.drawPoints();
setFill(`black`);
text(`Flattened to ${this.steps} segments`, 10, 15);
}
onMouseMove() {
redraw();
}

View File

@@ -0,0 +1,27 @@
let curve;
setup() {
const type = getParameter(`type`, `quadratic`);
curve = (type === `quadratic`) ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `steps`, (type === `quadratic`) ? 4 : 8);
}
draw() {
clear();
curve.drawSkeleton();
noFill();
start();
for(let i=0, e=this.steps; i<=e; i++) {
let p = curve.get(i/e);
vertex(p.x, p.y);
}
end();
curve.drawPoints();
setFill(`black`);
text(`Flattened to ${this.steps} segments`, 10, 15);
}

View File

@@ -1,28 +0,0 @@
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
setSlider(`.slide-control`, `steps`, 4);
}
draw() {
clear();
this.curve.drawSkeleton();
noFill();
start();
for(let i=0, e=this.steps; i<=e; i++) {
let p = this.curve.get(i/e);
vertex(p.x, p.y);
}
end();
this.curve.drawPoints();
setFill(`black`);
text(`Flattened to ${this.steps} segments`, 10, 15);
}
onMouseMove() {
redraw();
}

View File

@@ -1,12 +1,13 @@
let curve;
setup() {
const curve = this.curve = new Bezier(this, 70,250, 120,15, 20,95, 225,80);
curve = new Bezier(this, 70,250, 120,15, 20,95, 225,80);
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();

View File

@@ -1,16 +1,13 @@
let curve;
setup() {
this.curve = Bezier.defaultCubic(this);
setMovable(this.curve.points);
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
}
onMouseMove() {
redraw();
}

View File

@@ -1,16 +1,13 @@
let curve;
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
curve = Bezier.defaultQuadratic(this);
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
}
onMouseMove() {
redraw();
}

View File

@@ -1,12 +1,14 @@
let curve;
setup() {
this.curve = Bezier.defaultCubic(this);
setMovable(this.curve.points);
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
const pts = curve.points;
const f = 15;

View File

@@ -1,12 +1,14 @@
let curve;
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
curve = Bezier.defaultQuadratic(this);
setMovable(curve.points);
}
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
const pts = curve.points;
const f = 15;

View File

@@ -1,6 +1,7 @@
let points = [];
setup() {
const points = this.points = [],
w = this.width,
const w = this.width,
h = this.height;
for (let i=0; i<10; i++) {
points.push({
@@ -28,12 +29,10 @@ 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:
const pts = this.points;
start();
noFill();
for(let t=0; t<=1; t+=0.01) {
let q = JSON.parse(JSON.stringify(pts));
let q = JSON.parse(JSON.stringify(points));
while(q.length > 1) {
for (let i=0; i<q.length-1; i++) {
q[i] = {
@@ -49,15 +48,15 @@ drawCurve() {
start();
setStroke(`lightgrey`);
pts.forEach(p => vertex(p.x, p.y));
points.forEach(p => vertex(p.x, p.y));
end();
setStroke(`black`);
pts.forEach(p => circle(p.x, p.y, 3));
points.forEach(p => circle(p.x, p.y, 3));
}
raise() {
const p = this.points,
const p = points,
np = [p[0]],
k = p.length;
for (let i = 1, pi, pim; i < k; i++) {
@@ -69,9 +68,9 @@ raise() {
};
}
np[k] = p[k - 1];
this.points = np;
points = np;
resetMovable(this.points);
resetMovable(points);
redraw();
}
@@ -83,8 +82,8 @@ lower() {
// first or last point... it starts to travel
// A LOT more than it looks like it should... O_o
const pts = this.points,
k = pts.length,
const p = points,
k = p.length,
data = [],
n = k-1;
@@ -108,8 +107,7 @@ lower() {
const Mi = Mc.invert();
if (!Mi) {
console.error('MtM has no inverse?');
return curve;
return console.error('MtM has no inverse?');
}
// And then we map our k-order list of coordinates
@@ -120,15 +118,11 @@ lower() {
const y = new Matrix(pts.map(p => [p.y]));
const ny = V.multiply(y);
this.points = nx.data.map((x,i) => ({
points = nx.data.map((x,i) => ({
x: x[0],
y: ny.data[i][0]
}));
resetMovable(this.points);
redraw();
}
onMouseMove() {
resetMovable(points);
redraw();
}

View File

@@ -1,6 +1,8 @@
let curve;
setup() {
this.curve = Bezier.defaultCubic(this);
setMovable(this.curve.points);
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `position`, 0.5);
}
@@ -8,8 +10,8 @@ draw() {
resetTransform();
clear();
let p = this.curve.get(this.position);
const struts = this.struts = this.curve.getStrutPoints(this.position);
let p = curve.get(this.position);
const struts = this.struts = curve.getStrutPoints(this.position);
const c1 = new Bezier(this, [struts[0], struts[4], struts[7], struts[9]]);
const c2 = new Bezier(this, [struts[9], struts[8], struts[6], struts[3]]);
@@ -29,24 +31,24 @@ draw() {
}
drawBasics(p) {
this.curve.drawCurve(`lightgrey`);
this.curve.drawSkeleton(`lightgrey`);
this.curve.drawPoints(false);
curve.drawCurve(`lightgrey`);
curve.drawSkeleton(`lightgrey`);
curve.drawPoints(false);
noFill();
setStroke(`red`);
circle(p.x, p.y, 3);
setStroke(`lightblue`);
this.curve.drawStruts(this.struts);
curve.drawStruts(this.struts);
setFill(`black`)
text(`The full curve, with struts`, 10, 15);
}
drawSegment(c, p, halfLabel) {
setStroke(`lightblue`);
this.curve.drawCurve(`lightblue`);
this.curve.drawSkeleton(`lightblue`);
this.curve.drawStruts(this.struts);
curve.drawCurve(`lightblue`);
curve.drawSkeleton(`lightblue`);
curve.drawStruts(this.struts);
c.drawCurve();
c.drawSkeleton(`black`);
noFill();

View File

@@ -4,7 +4,9 @@ With our knowledge of bounding boxes, and curve alignment, We can now form the "
We now have nice tight bounding boxes for our curves:
<graphics-element title="Aligning a quadratic curve" src="./quadratic.js"></graphics-element>
<graphics-element title="Aligning a cubic curve" src="./cubic.js"></graphics-element>
<div class="figure">
<graphics-element title="Aligning a quadratic curve" src="./tightbounds.js" data-type="quadratic"></graphics-element>
<graphics-element title="Aligning a cubic curve" src="./tightbounds.js" data-type="cubic"></graphics-element>
</div>
These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding precision is often not worth it.

View File

@@ -1,74 +0,0 @@
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
}
draw() {
const curve = this.curve;
clear();
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
let translated = this.translatePoints(curve.points);
let rotated = this.rotatePoints(translated);
let rtcurve = new Bezier(this, rotated);
let extrema = rtcurve.extrema();
let minx = Number.MAX_SAFE_INTEGER,
miny = minx,
maxx = Number.MIN_SAFE_INTEGER,
maxy = maxx;
setStroke(`red`);
[0, ...extrema.x, ...extrema.y, 1].forEach(t => {
let p = curve.get(t);
let rtp = rtcurve.get(t);
if (rtp.x < minx) minx = rtp.x;
if (rtp.x > maxx) maxx = rtp.x;
if (rtp.y < miny) miny = rtp.y;
if (rtp.y > maxy) maxy = rtp.y;
if (t > 0 && t< 1) circle(p.x, p.y, 3);
});
noFill();
setStroke(`#0F0`);
let tx = curve.points[0].x;
let ty = curve.points[0].y;
let a = rotated[0].a;
start();
vertex(tx + minx * cos(a) - miny * sin(a), ty + minx * sin(a) + miny * cos(a));
vertex(tx + maxx * cos(a) - miny * sin(a), ty + maxx * sin(a) + miny * cos(a));
vertex(tx + maxx * cos(a) - maxy * sin(a), ty + maxx * sin(a) + maxy * cos(a));
vertex(tx + minx * cos(a) - maxy * sin(a), ty + minx * sin(a) + maxy * cos(a));
end(true);
}
translatePoints(points) {
// translate to (0,0)
let m = points[0];
return points.map(v => {
return {
x: v.x - m.x,
y: v.y - m.y
}
});
}
rotatePoints(points) {
// rotate so that last point is (...,0)
let dx = points[2].x;
let dy = points[2].y;
let a = atan2(dy, dx);
return points.map(v => {
return {
a: a,
x: v.x * cos(-a) - v.y * sin(-a),
y: v.x * sin(-a) + v.y * cos(-a)
};
});
}

View File

@@ -1,12 +1,12 @@
let curve;
setup() {
const curve = this.curve = Bezier.defaultCubic(this);
curve.points[2].x = 210;
const type = this.type = getParameter(`type`, `quadratic`);
curve = (type === `quadratic`) ? Bezier.defaultQuadratic(this) : Bezier.defaultCubic(this);
setMovable(curve.points);
}
draw() {
const curve = this.curve;
clear();
curve.drawSkeleton();
curve.drawCurve();
@@ -62,8 +62,9 @@ translatePoints(points) {
rotatePoints(points) {
// rotate so that last point is (...,0)
let dx = points[3].x;
let dy = points[3].y;
let last = this.type === `quadratic` ? 2 : 3;
let dx = points[last].x;
let dy = points[last].y;
let a = atan2(dy, dx);
return points.map(v => {
return {

View File

@@ -1,18 +1,20 @@
let curve;
setup() {
this.curve = Bezier.defaultCubic(this);
setMovable(this.curve.points);
curve = Bezier.defaultCubic(this);
setMovable(curve.points);
const inputs = findAll(`input[type=range]`);
if (inputs) {
const ratios = inputs.map(i => parseFloat(i.value));
this.curve.setRatios(ratios);
curve.setRatios(ratios);
inputs.forEach((input,pos) => {
const span = input.nextSibling;
input.listen(`input`, evt => {
const value = parseFloat(evt.target.value);
span.textContent = ratios[pos] = value;
this.curve.update();
curve.update();
this.redraw();
});
})
@@ -21,12 +23,7 @@ setup() {
draw() {
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
curve.drawPoints();
}
onMouseMove() {
redraw();
}

View File

@@ -1,6 +1,8 @@
let curve;
setup() {
this.curve = Bezier.defaultQuadratic(this);
setMovable(this.curve.points);
curve = Bezier.defaultQuadratic(this);
setMovable(curve.points);
setSlider(`.slide-control`, `step`, 25)
}
@@ -16,28 +18,28 @@ draw() {
drawBasics() {
setStroke(`black`);
setFill(`black`);
this.curve.drawSkeleton();
curve.drawSkeleton();
text(`First linear interpolation, spaced ${this.step}% (${Math.floor(99/this.step)} steps)`, {x:5, y:15});
translate(this.height, 0);
line(0, 0, 0, this.height);
this.curve.drawSkeleton();
curve.drawSkeleton();
text(`Second interpolation, between each generated pair`, {x:5, y:15});
translate(this.height, 0);
line(0, 0, 0, this.height);
this.curve.drawSkeleton();
curve.drawSkeleton();
text(`Curve points generated this way`, {x:5, y:15});
}
drawPointCurve() {
setStroke(`lightgrey`);
for(let i=1, e=50, p; i<=e; i++) {
p = this.curve.get(i/e);
p = curve.get(i/e);
circle(p.x, p.y, 1);
}
}
@@ -46,7 +48,7 @@ drawInterpolations() {
for(let i=this.step; i<100; i+=this.step) {
resetTransform();
this.setIterationColor(i);
let [np2, np3] = this.drawFirstInterpolation(this.curve.points, i);
let [np2, np3] = this.drawFirstInterpolation(curve.points, i);
let np4 = this.drawSecondInterpolation(np2, np3, i);
this.drawOnCurve(np4, i);
}
@@ -94,9 +96,9 @@ drawOnCurve(np4, i) {
drawCurveCoordinates() {
this.resetTransform();
this.curve.drawPoints();
curve.drawPoints();
translate(this.height, 0);
this.curve.drawPoints();
curve.drawPoints();
translate(this.height, 0);
this.curve.drawPoints();
curve.drawPoints();
}

View File

@@ -1,13 +1,14 @@
let curve;
setup() {
this.curve = new Bezier(this, 20, 250, 30, 20, 200, 250, 220, 20);
setMovable(this.curve.points);
curve = new Bezier(this, 20, 250, 30, 20, 200, 250, 220, 20);
setMovable(curve.points);
setSlider(`.slide-control`, `position`, 0.5);
}
draw() {
resetTransform();
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
@@ -53,9 +54,3 @@ draw() {
line(0, h-x, w, h-x);
}
}
onMouseMove() {
if (this.cursor.down) {
redraw();
}
}

View File

@@ -1,13 +1,14 @@
let curve;
setup() {
this.curve = new Bezier(this, 20, 250, 30, 20, 200, 250, 220, 20);
setMovable(this.curve.points);
curve = new Bezier(this, 20, 250, 30, 20, 200, 250, 220, 20);
setMovable(curve.points);
setSlider(`.slide-control`, `position`, 0.5);
}
draw() {
resetTransform();
clear();
const curve = this.curve;
curve.drawSkeleton();
curve.drawCurve();
@@ -19,14 +20,14 @@ draw() {
// prepare our values for root finding:
let x = round(bbox.x.min + (bbox.x.max - bbox.x.min) * this.position);
let xcoords = this.curve.points.map((p,i) => ({x:i/3, y: p.x - x}));
let xcoords = curve.points.map((p,i) => ({x:i/3, y: p.x - x}));
// find our root:
let roots = Bezier.getUtils().roots(xcoords);
let t = this.position===0 ? 0 : this.position===1 ? 1 : roots[0];
// find our answer:
let y = round(this.curve.get(t).y);
let y = round(curve.get(t).y);
setStroke("red");
line(x, y, x, h);
@@ -37,9 +38,3 @@ draw() {
text(`x=${x}`, x + 5, h - (h-y)/2);
text(`t=${((t*100)|0)/100}`, x + 15, y);
}
onMouseMove() {
if (this.cursor.down) {
redraw();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -100,7 +100,6 @@ class BaseAPI {
// of its own mouseMove handling.
if (this.movable.length && this.currentPoint && !this.redrawing) {
this.redraw();
this.redrawing = false;
}
})
);
@@ -256,6 +255,7 @@ class BaseAPI {
redraw() {
this.redrawing = true;
this.draw();
this.redrawing = false;
}
}

View File

@@ -157,7 +157,7 @@ class GraphicsAPI extends BaseAPI {
* @param {String} local query selector for the type=range element.
* @param {String} propname the name of the property to control.
* @param {float} initial the initial value for this property.
* @param {boolean} redraw whether or not to redraw after update the value from the slider.
* @param {boolean} redraw whether or not to redraw after updating the value from the slider.
*/
setSlider(qs, propname, initial, redraw = true) {
if (typeof this[propname] !== `undefined`) {
@@ -177,7 +177,9 @@ class GraphicsAPI extends BaseAPI {
slider.listen(`input`, (evt) => {
this[propname] = parseFloat(evt.target.value);
if (redraw) this.redraw();
if (redraw && !this.redrawing) {
this.redraw();
}
});
return slider;
@@ -711,6 +713,12 @@ class GraphicsAPI extends BaseAPI {
constrain(v, s, e) {
return v < s ? s : v > e ? e : v;
}
dist(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return this.sqrt(dx * dx + dy * dy);
}
}
export { GraphicsAPI, Bezier, Vector, Matrix, Shape };

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,8 @@
"url": "https://github.com/Pomax/BezierInfo-2/issues"
},
"scripts": {
"start": "run-s clean time lint:* build pretty time && rm -f .timing",
"test": "run-s start -- --pretty&& run-p watch server browser",
"start": "run-s clean time lint:* build pretty time clean",
"test": "run-s start && run-p watch server browser",
"------": "--- note that due to github's naming policy, the public dir is called 'docs' rather than 'public' ---",
"browser": "open-cli http://localhost:8000",
"build": "node ./src/build.js",

View File

@@ -64,11 +64,11 @@ async function createIndexPages(locale, localeStrings, chapters) {
let index = nunjucks.render(`index.template.html`, context);
if (typeof process !== "undefined") {
if (process.argv.indexOf(`--pretty`) !== 0) {
index = prettier.format(index, { parser: `html` });
}
}
// if (typeof process !== "undefined") {
// if (process.argv.indexOf(`--pretty`) !== 0) {
// index = prettier.format(index, { parser: `html` });
// }
// }
// Prettification happens as an npm script action

View File

@@ -34,8 +34,7 @@ function generateGraphicsModule(chapter, code, width, height, dataset) {
const globalCode = split.quasiGlobal;
const classCode = performCodeSurgery(split.classCode);
return prettier.format(
`
let moduleCode = `
import CanvasBuilder from 'canvas';
import { GraphicsAPI, Bezier, Vector, Matrix, Shape } from "${GRAPHICS_API_LOCATION}";
@@ -64,12 +63,11 @@ function generateGraphicsModule(chapter, code, width, height, dataset) {
const canvas = example.canvas;
export { canvas };
`,
{
// I'm not transpiling, I'm assuming Prettier just uses Babel as AST parser/rewriter.
parser: `babel`,
}
);
`;
// return prettier.format(moduleCode, { parser: `babel` });
return moduleCode;
}
export { generateGraphicsModule };