mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-09-01 04:22:28 +02:00
reordering
This commit is contained in:
@@ -134,4 +134,4 @@ The steps taken here are:
|
||||
|
||||
And we're done: we now have an expression that lets us approximate an `n+1`<sup>th</sup> order curve with a lower `n`<sup>th</sup> order curve. It won't be an exact fit, but it's definitely a best approximation. So, let's implement these rules for raising and lowering curve order to a (semi) random curve, using the following graphic. Select the sketch, which has movable control points, and press your up and down arrow keys to raise or lower the curve order.
|
||||
|
||||
<Graphic title={"A " + this.getOrder() + " order Bézier curve"} setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} onMouseMove={this.onMouseMove} />
|
||||
<graphics-element title="A variable-order Bézier curve" src="./reorder.js"></graphics-element>
|
||||
|
@@ -1,25 +0,0 @@
|
||||
# 曲線の次数下げと次数上げ
|
||||
|
||||
ベジエ曲線のおもしろい性質のひとつに、「*n*次の曲線は常に、*n+1*次の曲線で完璧に表すことができる」というものがあります。このとき、制御点は新しいものになります。
|
||||
|
||||
たとえば3点で定義される曲線があるとき、これを正確に再現するような、4点で定義される曲線を作ることができます。始点と終点はそのままにして、「1/3 始点 + 2/3 制御点」と「2/3 制御点 + 1/3 終点」を新たな2つの制御点に選べば、元の曲線と正確に一致する曲線が得られます。異なっているのは、2次ではなく3次の曲線だという点だけです。
|
||||
|
||||
*n*次の曲線を*n+1*次の曲線へと次数上げするための一般の規則は、次のようになります(始点と終点の重みは、元の曲線のものと変わらないことがわかります)。
|
||||
|
||||
\[
|
||||
Bézier(k,t) = \sum_{i=0}^{k}
|
||||
\underset{二項係数部分の項}{\underbrace{\binom{k}{i}}}
|
||||
\cdot\
|
||||
\underset{多項式部分の項}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
|
||||
\ \cdot \
|
||||
\underset{新しい重み}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}}
|
||||
\qquad ただし\ k = n+1。また\ i = 0\ のとき\ w_{i-1}=0
|
||||
\]
|
||||
|
||||
しかし同時にこの規則から、*n*次の曲線を*n-1*次の曲線へと次数下げすることは、一般には**不可能**だという結論も得られます。なぜなら、制御点をきれいに「引き離す」ことができないからです。試してみたところで、得られる曲線は元と同じにはなりません。それどころか、まったくの別物に見えるかもしれません。
|
||||
|
||||
下の図では(半分)ランダムな曲線に対して、この規則を試してみることができます。図を選択して上下キーを押すと、次数上げや次数下げができます。
|
||||
|
||||
<Graphic title={this.state.order + "次のベジエ曲線"} setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
[SirVer's Castle](http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves)には、最適な次元削減で必要になる行列について(数学的ですが)良い解説があります。時間があれば、これを直接この記事の中で説明したいところです。
|
@@ -1,150 +0,0 @@
|
||||
var invert = require('../../../lib/matrix-invert.js');
|
||||
var multiply = require('../../../lib/matrix-multiply.js');
|
||||
var transpose = require('../../../lib/matrix-transpose.js');
|
||||
|
||||
var Reordering = {
|
||||
statics: {
|
||||
keyHandlingOptions: {
|
||||
values: {
|
||||
"38": function(api) {
|
||||
api.setCurve(api.curve.raise());
|
||||
api.redraw();
|
||||
},
|
||||
"40": function(api) {
|
||||
api.setCurve(Reordering.lower(api));
|
||||
api.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Based on http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
|
||||
lower: function(api) {
|
||||
var curve = api.curve,
|
||||
pts = curve.points,
|
||||
k = pts.length,
|
||||
M = [],
|
||||
n = k-1,
|
||||
i;
|
||||
|
||||
// build M, which will be (k) rows by (k-1) columns
|
||||
for(i=0; i<k; i++) {
|
||||
M[i] = (new Array(k - 1)).fill(0);
|
||||
if(i===0) { M[i][0] = 1; }
|
||||
else if(i===n) { M[i][i-1] = 1; }
|
||||
else {
|
||||
M[i][i-1] = i / k;
|
||||
M[i][i] = 1 - M[i][i-1];
|
||||
}
|
||||
}
|
||||
|
||||
// then, apply our matrix operations:
|
||||
var Mt = transpose(M);
|
||||
var Mc = multiply(Mt, M);
|
||||
var Mi = invert(Mc);
|
||||
|
||||
if (!Mi) {
|
||||
console.error('MtM has no inverse?');
|
||||
return curve;
|
||||
}
|
||||
|
||||
var V = multiply(Mi, Mt);
|
||||
|
||||
// And then we map our k-order list of coordinates
|
||||
// to an n-order list of coordinates, instead:
|
||||
var x = pts.map(p => [p.x]);
|
||||
var nx = multiply(V, x);
|
||||
|
||||
var y = pts.map(p => [p.y]);
|
||||
var ny = multiply(V, y);
|
||||
|
||||
var npts = nx.map((x,i) => {
|
||||
return {
|
||||
x: x[0],
|
||||
y: ny[i][0]
|
||||
};
|
||||
});
|
||||
|
||||
return new api.Bezier(npts);
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
order: 0
|
||||
};
|
||||
},
|
||||
|
||||
setup: function(api) {
|
||||
var points = [];
|
||||
var w = api.getPanelWidth(),
|
||||
h = api.getPanelHeight();
|
||||
for (var i=0; i<10; i++) {
|
||||
points.push({
|
||||
x: w/2 + (Math.random() * 20) + Math.cos(Math.PI*2 * i/10) * (w/2 - 40),
|
||||
y: h/2 + (Math.random() * 20) + Math.sin(Math.PI*2 * i/10) * (h/2 - 40)
|
||||
});
|
||||
}
|
||||
var curve = new api.Bezier(points);
|
||||
api.setCurve(curve);
|
||||
},
|
||||
|
||||
draw: function(api, curve) {
|
||||
api.reset();
|
||||
var pts = curve.points;
|
||||
|
||||
this.setState({
|
||||
order: pts.length
|
||||
});
|
||||
|
||||
var p0 = pts[0];
|
||||
|
||||
// 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:
|
||||
for(var t=0; t<=1; t+=0.01) {
|
||||
var q = JSON.parse(JSON.stringify(pts));
|
||||
while(q.length > 1) {
|
||||
for (var 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);
|
||||
}
|
||||
api.drawLine(p0, q[0]);
|
||||
p0 = q[0];
|
||||
}
|
||||
|
||||
p0 = pts[0];
|
||||
api.setColor("black");
|
||||
api.drawCircle(p0,3);
|
||||
pts.forEach(p => {
|
||||
if(p===p0) return;
|
||||
api.setColor("#DDD");
|
||||
api.drawLine(p0,p);
|
||||
api.setColor("black");
|
||||
api.drawCircle(p,3);
|
||||
p0 = p;
|
||||
});
|
||||
},
|
||||
|
||||
getOrder: function() {
|
||||
var order = this.state.order;
|
||||
if (order%10 === 1 && order !== 11) {
|
||||
order += "st";
|
||||
} else if (order%10 === 2 && order !== 12) {
|
||||
order += "nd";
|
||||
} else if (order%10 === 3 && order !== 13) {
|
||||
order += "rd";
|
||||
} else {
|
||||
order += "th";
|
||||
}
|
||||
return order;
|
||||
},
|
||||
|
||||
onMouseMove: function(evt, api) {
|
||||
api.redraw();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Reordering;
|
158
chapters/reordering/reorder.js
Normal file
158
chapters/reordering/reorder.js
Normal file
@@ -0,0 +1,158 @@
|
||||
setup() {
|
||||
const points = this.points = [],
|
||||
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);
|
||||
}
|
||||
|
||||
draw() {
|
||||
clear();
|
||||
this.drawCurve();
|
||||
}
|
||||
|
||||
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));
|
||||
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);
|
||||
}
|
||||
vertex(q[0].x, q[0].y);
|
||||
}
|
||||
end();
|
||||
|
||||
start();
|
||||
setStroke(`lightgrey`);
|
||||
pts.forEach(p => vertex(p.x, p.y));
|
||||
end();
|
||||
|
||||
setStroke(`black`);
|
||||
pts.forEach(p => circle(p.x, p.y, 3));
|
||||
}
|
||||
|
||||
raise() {
|
||||
const p = this.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];
|
||||
this.points = np;
|
||||
|
||||
resetMovable(this.points);
|
||||
}
|
||||
|
||||
lower() {
|
||||
// Based on http://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
|
||||
|
||||
const pts = this.points,
|
||||
k = pts.length,
|
||||
data = [],
|
||||
n = k-1;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
console.error('MtM has no inverse?');
|
||||
return curve;
|
||||
}
|
||||
|
||||
// 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(pts.map(p => [p.x]));
|
||||
const nx = V.multiply(x);
|
||||
const y = new Matrix(pts.map(p => [p.y]));
|
||||
const ny = V.multiply(y);
|
||||
|
||||
this.points = nx.data.map((x,i) => ({
|
||||
x: x[0],
|
||||
y: ny.data[i][0]
|
||||
}));
|
||||
|
||||
resetMovable(this.points);
|
||||
}
|
||||
|
||||
onKeyDown() {
|
||||
const key = this.keyboard.currentKey;
|
||||
if (key === `ArrowUp`) {
|
||||
this.raise();
|
||||
}
|
||||
if (key === `ArrowDown`) {
|
||||
this.lower();
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
onMouseMove() {
|
||||
if (this.cursor.down && !this.currentPoint) {
|
||||
if (this.cursor.y < this.height/2) {
|
||||
this.lowerTimer = clearInterval(this.lowerTimer);
|
||||
if (!this.raiseTimer) {
|
||||
this.raiseTimer = setInterval(() => {
|
||||
this.raise();
|
||||
redraw();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
if (this.cursor.y > this.height/2) {
|
||||
this.raiseTimer = clearInterval(this.raiseTimer);
|
||||
if (!this.lowerTimer) {
|
||||
this.lowerTimer = setInterval(() => {
|
||||
this.lower();
|
||||
redraw();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} else { redraw(); }
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
this.raiseTimer = clearInterval(this.raiseTimer);
|
||||
this.lowerTimer = clearInterval(this.lowerTimer);
|
||||
}
|
BIN
images/chapters/reordering/140b23b10b4159b03b2a555db7ddf826.png
Normal file
BIN
images/chapters/reordering/140b23b10b4159b03b2a555db7ddf826.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
images/chapters/reordering/4541eeb2113d81cbc0c0a56122570d48.png
Normal file
BIN
images/chapters/reordering/4541eeb2113d81cbc0c0a56122570d48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
29
index.html
29
index.html
@@ -49,12 +49,16 @@
|
||||
<!-- my own referral/page hit tracker, because Google knows enough -->
|
||||
<script src="./lib/site/referrer.js" type="module" async></script>
|
||||
|
||||
<!-- the part that makes interactive graphics work: an HTML5 <graphics-element> custom element -->
|
||||
<!--
|
||||
The part that makes interactive graphics work: an HTML5 <graphics-element> custom element.
|
||||
Note that we're not defering this: we just want it to kick in as soon as possible, and
|
||||
given how much HTML there is, that means this can, and thus should, kick in before the
|
||||
document is done even transferring.
|
||||
-->
|
||||
<script
|
||||
src="./lib/custom-element/graphics-element.js"
|
||||
type="module"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
<link rel="stylesheet" href="./lib/custom-element/graphics-element.css" />
|
||||
|
||||
@@ -2182,11 +2186,22 @@ function drawCurve(points[], t):
|
||||
control points, and press your up and down arrow keys to raise or
|
||||
lower the curve order.
|
||||
</p>
|
||||
<p>
|
||||
<Graphic title={"A " + this.getOrder() + " order Bézier curve"}
|
||||
setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown}
|
||||
onMouseMove={this.onMouseMove} />
|
||||
</p>
|
||||
<graphics-element
|
||||
title="A variable-order Bézier curve"
|
||||
width="275"
|
||||
height="275"
|
||||
src="./chapters/reordering/reorder.js"
|
||||
>
|
||||
<fallback-image>
|
||||
<img
|
||||
width="275px"
|
||||
height="275px"
|
||||
src="images\chapters\reordering\4541eeb2113d81cbc0c0a56122570d48.png"
|
||||
loading="lazy"
|
||||
/>
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element
|
||||
>
|
||||
</section>
|
||||
<section id="derivatives">
|
||||
<h1><a href="#derivatives">Derivatives</a></h1>
|
||||
|
@@ -36,8 +36,13 @@
|
||||
<!-- my own referral/page hit tracker, because Google knows enough -->
|
||||
<script src="./lib/site/referrer.js" type="module" async></script>
|
||||
|
||||
<!-- the part that makes interactive graphics work: an HTML5 <graphics-element> custom element -->
|
||||
<script src="./lib/custom-element/graphics-element.js" type="module" async defer></script>
|
||||
<!--
|
||||
The part that makes interactive graphics work: an HTML5 <graphics-element> custom element.
|
||||
Note that we're not defering this: we just want it to kick in as soon as possible, and
|
||||
given how much HTML there is, that means this can, and thus should, kick in before the
|
||||
document is done even transferring.
|
||||
-->
|
||||
<script src="./lib/custom-element/graphics-element.js" type="module" async></script>
|
||||
<link rel="stylesheet" href="./lib/custom-element/graphics-element.css" />
|
||||
|
||||
<!-- page styling -->
|
||||
|
252
ja-JP/index.html
252
ja-JP/index.html
@@ -51,12 +51,16 @@
|
||||
<!-- my own referral/page hit tracker, because Google knows enough -->
|
||||
<script src="./lib/site/referrer.js" type="module" async></script>
|
||||
|
||||
<!-- the part that makes interactive graphics work: an HTML5 <graphics-element> custom element -->
|
||||
<!--
|
||||
The part that makes interactive graphics work: an HTML5 <graphics-element> custom element.
|
||||
Note that we're not defering this: we just want it to kick in as soon as possible, and
|
||||
given how much HTML there is, that means this can, and thus should, kick in before the
|
||||
document is done even transferring.
|
||||
-->
|
||||
<script
|
||||
src="./lib/custom-element/graphics-element.js"
|
||||
type="module"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
<link rel="stylesheet" href="./lib/custom-element/graphics-element.css" />
|
||||
|
||||
@@ -103,7 +107,7 @@
|
||||
<li><a href="#flattening">簡略化した描画</a></li>
|
||||
<li><a href="#splitting">曲線の分割</a></li>
|
||||
<li><a href="#matrixsplit">行列による曲線の分割</a></li>
|
||||
<li><a href="#reordering">曲線の次数下げと次数上げ</a></li>
|
||||
<li><a href="#reordering">Lowering and elevating curve order</a></li>
|
||||
<li><a href="#derivatives">Derivatives</a></li>
|
||||
<li><a href="#pointvectors">Tangents and normals</a></li>
|
||||
<li><a href="#pointvectors3d">Working with 3D normals</a></li>
|
||||
@@ -1534,42 +1538,244 @@ function drawCurve(points[], t):
|
||||
</p>
|
||||
</section>
|
||||
<section id="reordering">
|
||||
<h1><a href="#reordering">曲線の次数下げと次数上げ</a></h1>
|
||||
<h1><a href="#reordering">Lowering and elevating curve order</a></h1>
|
||||
<p>
|
||||
ベジエ曲線のおもしろい性質のひとつに、「<em>n</em>次の曲線は常に、<em>n+1</em>次の曲線で完璧に表すことができる」というものがあります。このとき、制御点は新しいものになります。
|
||||
One interesting property of Bézier curves is that an
|
||||
<em>n<sup>th</sup></em> order curve can always be perfectly
|
||||
represented by an <em>(n+1)<sup>th</sup></em> order curve, by giving
|
||||
the higher-order curve specific control points.
|
||||
</p>
|
||||
<p>
|
||||
たとえば3点で定義される曲線があるとき、これを正確に再現するような、4点で定義される曲線を作ることができます。始点と終点はそのままにして、「1/3
|
||||
始点 + 2/3 制御点」と「2/3 制御点 + 1/3
|
||||
終点」を新たな2つの制御点に選べば、元の曲線と正確に一致する曲線が得られます。異なっているのは、2次ではなく3次の曲線だという点だけです。
|
||||
If we have a curve with three points, then we can create a curve
|
||||
with four points that exactly reproduces the original curve. First,
|
||||
we give it the same start and end points, and for its two control
|
||||
points we pick "1/3<sup>rd</sup> start + 2/3<sup>rd</sup> control"
|
||||
and "2/3<sup>rd</sup> control + 1/3<sup>rd</sup> end". Now we have
|
||||
exactly the same curve as before, except represented as a cubic
|
||||
curve rather than a quadratic curve.
|
||||
</p>
|
||||
<p>
|
||||
<em>n</em
|
||||
>次の曲線を<em>n+1</em>次の曲線へと次数上げするための一般の規則は、次のようになります(始点と終点の重みは、元の曲線のものと変わらないことがわかります)。
|
||||
The general rule for raising an <em>n<sup>th</sup></em> order curve
|
||||
to an <em>(n+1)<sup>th</sup></em> order curve is as follows
|
||||
(observing that the start and end weights are the same as the start
|
||||
and end weights for the old curve):
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/f4fe5505fa8dc61c82e6d5b0a2882047.svg"
|
||||
width="836px"
|
||||
src="images/latex/faf29599c9307f930ec28065c96fde2a.svg"
|
||||
width="768px"
|
||||
height="61px"
|
||||
/>
|
||||
<p>
|
||||
しかし同時にこの規則から、<em>n</em>次の曲線を<em>n-1</em>次の曲線へと次数下げすることは、一般には<strong>不可能</strong>だという結論も得られます。なぜなら、制御点をきれいに「引き離す」ことができないからです。試してみたところで、得られる曲線は元と同じにはなりません。それどころか、まったくの別物に見えるかもしれません。
|
||||
</p>
|
||||
<p>
|
||||
下の図では(半分)ランダムな曲線に対して、この規則を試してみることができます。図を選択して上下キーを押すと、次数上げや次数下げができます。
|
||||
</p>
|
||||
<p>
|
||||
<Graphic title={this.state.order + "次のベジエ曲線"}
|
||||
setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown}
|
||||
/>
|
||||
However, this rule also has as direct consequence that you
|
||||
<strong>cannot</strong> generally safely lower a curve from
|
||||
<em>n<sup>th</sup></em> order to <em>(n-1)<sup>th</sup></em> order,
|
||||
because the control points cannot be "pulled apart" cleanly. We can
|
||||
try to, but the resulting curve will not be identical to the
|
||||
original, and may in fact look completely different.
|
||||
</p>
|
||||
<p>
|
||||
However, there is a surprisingly good way to ensure that a lower
|
||||
order curve looks "as close as reasonably possible" to the original
|
||||
curve: we can optimise the "least-squares distance" between the
|
||||
original curve and the lower order curve, in a single operation
|
||||
(also explained over on
|
||||
<a
|
||||
href="http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves"
|
||||
>SirVer's Castle</a
|
||||
>には、最適な次元削減で必要になる行列について(数学的ですが)良い解説があります。時間があれば、これを直接この記事の中で説明したいところです。
|
||||
>Sirver's Castle</a
|
||||
>). However, to use it, we'll need to do some calculus work and then
|
||||
switch over to linear algebra. As mentioned in the section on matrix
|
||||
representations, some things can be done much more easily with
|
||||
matrices than with calculus functions, and this is one of those
|
||||
things. So... let's go!
|
||||
</p>
|
||||
<p>
|
||||
We start by taking the standard Bézier function, and condensing it a
|
||||
little:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/1244a85c1f9044b6f77cb709c682159c.svg"
|
||||
width="408px"
|
||||
height="41px"
|
||||
/>
|
||||
<p>
|
||||
Then, we apply one of those silly (actually, super useful) calculus
|
||||
tricks: since our <code>t</code> value is always between zero and
|
||||
one (inclusive), we know that <code>(1-t)</code> plus
|
||||
<code>t</code> always sums to 1. As such, we can express any value
|
||||
as a sum of <code>t</code> and <code>1-t</code>:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/b2fda1dcce5bb13317aa42ebf5e7ea6c.svg"
|
||||
width="379px"
|
||||
height="16px"
|
||||
/>
|
||||
<p>
|
||||
So, with that seemingly trivial observation, we rewrite that Bézier
|
||||
function by splitting it up into a sum of a <code>(1-t)</code> and
|
||||
<code>t</code> component:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/41e184228d85023abdadd6ce2acb54c7.svg"
|
||||
width="316px"
|
||||
height="67px"
|
||||
/>
|
||||
<p>
|
||||
So far so good. Now, to see why we did this, let's write out the
|
||||
<code>(1-t)</code> and <code>t</code> parts, and see what that gives
|
||||
us. I promise, it's about to make sense. We start with
|
||||
<code>(1-t)</code>:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/4debbed5922d2bd84fd322c616872d20.svg"
|
||||
width="387px"
|
||||
height="160px"
|
||||
/>
|
||||
<p>
|
||||
So by using this seemingly silly trick, we can suddenly express part
|
||||
of our n<sup>th</sup> order Bézier function in terms of an (n+1)<sup
|
||||
>th</sup
|
||||
>
|
||||
order Bézier function. And that sounds a lot like raising the curve
|
||||
order! Of course we need to be able to repeat that trick for the
|
||||
<code>t</code> part, but that's not a problem:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/483c89c8726f7fd0dca0b7de339b04bd.svg"
|
||||
width="471px"
|
||||
height="159px"
|
||||
/>
|
||||
<p>
|
||||
So, with both of those changed from an order
|
||||
<code>n</code> expression to an order <code>(n+1)</code> expression,
|
||||
we can put them back together again. Now, where the order
|
||||
<code>n</code> function had a summation from 0 to <code>n</code>,
|
||||
the order <code>n+1</code> function uses a summation from 0 to
|
||||
<code>n+1</code>, but this shouldn't be a problem as long as we can
|
||||
add some new terms that "contribute nothing". In the next section on
|
||||
derivatives, there is a discussion about why "higher terms than
|
||||
there is a binomial for" and "lower than zero terms" both
|
||||
"contribute nothing". So as long as we can add terms that have the
|
||||
same form as the terms we need, we can just include them in the
|
||||
summation, they'll sit there and do nothing, and the resulting
|
||||
function stays identical to the lower order curve.
|
||||
</p>
|
||||
<p>Let's do this:</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/dd8d8d98f66ce9f51b95cbf48225e97b.svg"
|
||||
width="465px"
|
||||
height="257px"
|
||||
/>
|
||||
<p>
|
||||
And this is where we switch over from calculus to linear algebra,
|
||||
and matrices: we can now express this relation between Bézier(n,t)
|
||||
and Bézier(n+1,t) as a very simple matrix multiplication:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/773fdc86b686647c823b4f499aca3a35.svg"
|
||||
width="71px"
|
||||
height="16px"
|
||||
/>
|
||||
<p>
|
||||
where the matrix <strong>M</strong> is an <code>n+1</code> by
|
||||
<code>n</code> matrix, and looks like:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/7a9120997e4a4855ecda435553a7bbdf.svg"
|
||||
width="336px"
|
||||
height="187px"
|
||||
/>
|
||||
<p>
|
||||
That might look unwieldy, but it's really just a mostly-zeroes
|
||||
matrix, with a very simply fraction on the diagonal, and an even
|
||||
simpler fraction to the left of it. Multiplying a list of
|
||||
coordinates with this matrix means we can plug the resulting
|
||||
transformed coordinates into the one-order-higher function and get
|
||||
an identical looking curve.
|
||||
</p>
|
||||
<p>Not too bad!</p>
|
||||
<p>
|
||||
Equally interesting, though, is that with this matrix operation
|
||||
established, we can now use an incredibly powerful and ridiculously
|
||||
simple way to find out a "best fit" way to reverse the operation,
|
||||
called
|
||||
<a href="http://mathworld.wolfram.com/NormalEquation.html"
|
||||
>the normal equation</a
|
||||
>. What it does is minimize the sum of the square differences
|
||||
between one set of values and another set of values. Specifically,
|
||||
if we can express that as some function <strong>A x = b</strong>, we
|
||||
can use it. And as it so happens, that's exactly what we're dealing
|
||||
with, so:
|
||||
</p>
|
||||
<img
|
||||
class="LaTeX SVG"
|
||||
src="images/latex/d52f60b331c1b8d6733eb5217adfbc4d.svg"
|
||||
width="272px"
|
||||
height="116px"
|
||||
/>
|
||||
<p>The steps taken here are:</p>
|
||||
<ol>
|
||||
<li>
|
||||
We have a function in a form that the normal equation can be used
|
||||
with, so
|
||||
</li>
|
||||
<li>apply the normal equation!</li>
|
||||
<li>
|
||||
Then, we want to end up with just B<sub>n</sub> on the left, so we
|
||||
start by left-multiply both sides such that we'll end up with lots
|
||||
of stuff on the left that simplified to "a factor 1", which in
|
||||
matrix maths is the
|
||||
<a href="https://en.wikipedia.org/wiki/Identity_matrix"
|
||||
>identity matrix</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
In fact, by left-multiplying with the inverse of what was already
|
||||
there, we've effectively "nullified" (but really, one-inified)
|
||||
that big, unwieldy block into the identity matrix
|
||||
<strong>I</strong>. So we substitute the mess with
|
||||
<strong>I</strong>, and then
|
||||
</li>
|
||||
<li>
|
||||
because multiplication with the identity matrix does nothing (like
|
||||
multiplying by 1 does nothing in regular algebra), we just drop
|
||||
it.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
And we're done: we now have an expression that lets us approximate
|
||||
an <code>n+1</code><sup>th</sup> order curve with a lower
|
||||
<code>n</code><sup>th</sup> order curve. It won't be an exact fit,
|
||||
but it's definitely a best approximation. So, let's implement these
|
||||
rules for raising and lowering curve order to a (semi) random curve,
|
||||
using the following graphic. Select the sketch, which has movable
|
||||
control points, and press your up and down arrow keys to raise or
|
||||
lower the curve order.
|
||||
</p>
|
||||
<graphics-element
|
||||
title="A variable-order Bézier curve"
|
||||
width="275"
|
||||
height="275"
|
||||
src="./chapters/reordering/reorder.js"
|
||||
>
|
||||
<fallback-image>
|
||||
<img
|
||||
width="275px"
|
||||
height="275px"
|
||||
src="images\chapters\reordering\4541eeb2113d81cbc0c0a56122570d48.png"
|
||||
loading="lazy"
|
||||
/>
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element
|
||||
>
|
||||
</section>
|
||||
<section id="derivatives">
|
||||
<h1><a href="#derivatives">Derivatives</a></h1>
|
||||
|
@@ -153,7 +153,10 @@ class BaseAPI {
|
||||
// We don't want to interfere with the browser, so we're only
|
||||
// going to allow unmodified keys, or shift-modified keys,
|
||||
// and tab has to always work. For obvious reasons.
|
||||
if (!evt.altKey && !evt.ctrlKey && !evt.metaKey && evt.key !== "Tab") {
|
||||
const tab = evt.key !== "Tab";
|
||||
const functionKey = evt.key.match(/F\d+/) === null;
|
||||
const specificCheck = tab && functionKey;
|
||||
if (!evt.altKey && !evt.ctrlKey && !evt.metaKey && specificCheck) {
|
||||
this.stopEvent(evt);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { enrich } from "../lib/enrich.js";
|
||||
import { Bezier } from "./types/bezier.js";
|
||||
import { Vector } from "./types/vector.js";
|
||||
import { Shape } from "./util/shape.js";
|
||||
import { Matrix } from "./util/matrix.js";
|
||||
import { BaseAPI } from "./base-api.js";
|
||||
|
||||
const MOUSE_PRECISION_ZONE = 5;
|
||||
@@ -103,6 +104,11 @@ class GraphicsAPI extends BaseAPI {
|
||||
this.currentPoint = undefined;
|
||||
}
|
||||
|
||||
resetMovable(points) {
|
||||
this.moveable.splice(0, this.moveable.length);
|
||||
if (points) this.setMovable(points);
|
||||
}
|
||||
|
||||
setMovable(points) {
|
||||
points.forEach((p) => this.moveable.push(p));
|
||||
}
|
||||
@@ -479,6 +485,10 @@ class GraphicsAPI extends BaseAPI {
|
||||
return Math.round(v);
|
||||
}
|
||||
|
||||
random(v = 1) {
|
||||
return Math.random() * v;
|
||||
}
|
||||
|
||||
abs(v) {
|
||||
return Math.abs(v);
|
||||
}
|
||||
@@ -517,4 +527,4 @@ class GraphicsAPI extends BaseAPI {
|
||||
}
|
||||
}
|
||||
|
||||
export { GraphicsAPI, Bezier, Vector };
|
||||
export { GraphicsAPI, Bezier, Vector, Matrix };
|
||||
|
142
lib/custom-element/api/util/matrix.js
Normal file
142
lib/custom-element/api/util/matrix.js
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copied from http://blog.acipo.com/matrix-inversion-in-javascript/
|
||||
|
||||
function invert(M) {
|
||||
// I use Guassian Elimination to calculate the inverse:
|
||||
// (1) 'augment' the matrix (left) by the identity (on the right)
|
||||
// (2) Turn the matrix on the left into the identity by elemetry row ops
|
||||
// (3) The matrix on the right is the inverse (was the identity matrix)
|
||||
// There are 3 elemtary row ops: (I combine b and c in my code)
|
||||
// (a) Swap 2 rows
|
||||
// (b) Multiply a row by a scalar
|
||||
// (c) Add 2 rows
|
||||
|
||||
//if the matrix isn't square: exit (error)
|
||||
if (M.length !== M[0].length) {
|
||||
console.log("not square");
|
||||
return;
|
||||
}
|
||||
|
||||
//create the identity matrix (I), and a copy (C) of the original
|
||||
var i = 0,
|
||||
ii = 0,
|
||||
j = 0,
|
||||
dim = M.length,
|
||||
e = 0,
|
||||
t = 0;
|
||||
var I = [],
|
||||
C = [];
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// Create the row
|
||||
I[I.length] = [];
|
||||
C[C.length] = [];
|
||||
for (j = 0; j < dim; j += 1) {
|
||||
//if we're on the diagonal, put a 1 (for identity)
|
||||
if (i == j) {
|
||||
I[i][j] = 1;
|
||||
} else {
|
||||
I[i][j] = 0;
|
||||
}
|
||||
|
||||
// Also, make the copy of the original
|
||||
C[i][j] = M[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform elementary row operations
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// get the element e on the diagonal
|
||||
e = C[i][i];
|
||||
|
||||
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
|
||||
if (e == 0) {
|
||||
//look through every row below the i'th row
|
||||
for (ii = i + 1; ii < dim; ii += 1) {
|
||||
//if the ii'th row has a non-0 in the i'th col
|
||||
if (C[ii][i] != 0) {
|
||||
//it would make the diagonal have a non-0 so swap it
|
||||
for (j = 0; j < dim; j++) {
|
||||
e = C[i][j]; //temp store i'th row
|
||||
C[i][j] = C[ii][j]; //replace i'th row by ii'th
|
||||
C[ii][j] = e; //repace ii'th by temp
|
||||
e = I[i][j]; //temp store i'th row
|
||||
I[i][j] = I[ii][j]; //replace i'th row by ii'th
|
||||
I[ii][j] = e; //repace ii'th by temp
|
||||
}
|
||||
//don't bother checking other rows since we've swapped
|
||||
break;
|
||||
}
|
||||
}
|
||||
//get the new diagonal
|
||||
e = C[i][i];
|
||||
//if it's still 0, not invertable (error)
|
||||
if (e == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale this row down by e (so we have a 1 on the diagonal)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[i][j] = C[i][j] / e; //apply to original matrix
|
||||
I[i][j] = I[i][j] / e; //apply to identity
|
||||
}
|
||||
|
||||
// Subtract this row (scaled appropriately for each row) from ALL of
|
||||
// the other rows so that there will be 0's in this column in the
|
||||
// rows above and below this one
|
||||
for (ii = 0; ii < dim; ii++) {
|
||||
// Only apply to other rows (we want a 1 on the diagonal)
|
||||
if (ii == i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to change this element to 0
|
||||
e = C[ii][i];
|
||||
|
||||
// Subtract (the row above(or below) scaled by e) from (the
|
||||
// current row) but start at the i'th column and assume all the
|
||||
// stuff left of diagonal is 0 (which it should be if we made this
|
||||
// algorithm correctly)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[ii][j] -= e * C[i][j]; //apply to original matrix
|
||||
I[ii][j] -= e * I[i][j]; //apply to identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we've done all operations, C should be the identity
|
||||
//matrix I should be the inverse:
|
||||
return I;
|
||||
}
|
||||
|
||||
function multiply(m1, m2) {
|
||||
var M = [];
|
||||
var m2t = transpose(m2);
|
||||
m1.forEach((row, r) => {
|
||||
M[r] = [];
|
||||
m2t.forEach((col, c) => {
|
||||
M[r][c] = row.map((v, i) => col[i] * v).reduce((a, v) => a + v, 0);
|
||||
});
|
||||
});
|
||||
return M;
|
||||
}
|
||||
|
||||
function transpose(M) {
|
||||
return M[0].map((col, i) => M.map((row) => row[i]));
|
||||
}
|
||||
|
||||
class Matrix {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
}
|
||||
multiply(other) {
|
||||
return new Matrix(multiply(this.data, other.data));
|
||||
}
|
||||
invert() {
|
||||
return new Matrix(invert(this.data));
|
||||
}
|
||||
transpose() {
|
||||
return new Matrix(transpose(this.data));
|
||||
}
|
||||
}
|
||||
|
||||
export { Matrix };
|
@@ -152,7 +152,7 @@ class GraphicsElement extends CustomElement {
|
||||
const height = this.getAttribute(`height`, 200);
|
||||
|
||||
this.code = `
|
||||
import { GraphicsAPI, Bezier, Vector } from "${MODULE_PATH}/api/graphics-api.js";
|
||||
import { GraphicsAPI, Bezier, Vector, Matrix } from "${MODULE_PATH}/api/graphics-api.js";
|
||||
|
||||
${globalCode}
|
||||
|
||||
|
103
package-lock.json
generated
103
package-lock.json
generated
@@ -162,12 +162,6 @@
|
||||
"integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
|
||||
"dev": true
|
||||
},
|
||||
"bezier-js": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-2.6.1.tgz",
|
||||
"integrity": "sha512-jelZM33eNzcZ9snJ/5HqJLw3IzXvA8RFcBjkdOB8SDYyOvW8Y2tTosojAiBTnD1MhbHoWUYNbxUXxBl61TxbRg==",
|
||||
"dev": true
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
||||
@@ -201,12 +195,6 @@
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
|
||||
"dev": true
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
@@ -269,16 +257,6 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"dev": true
|
||||
},
|
||||
"clean-html": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-html/-/clean-html-1.5.0.tgz",
|
||||
"integrity": "sha512-eDu0vN44ZBvoEU0oRIKwWPIccGWXtdnUNmKJuTukZ1de00Uoqavb5pfIMKiC7/r+knQ5RbvAjGuVZiN3JwJL4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"htmlparser2": "^3.8.2",
|
||||
"minimist": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"coa": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
|
||||
@@ -329,18 +307,6 @@
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
@@ -526,15 +492,6 @@
|
||||
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
|
||||
"dev": true
|
||||
},
|
||||
"domhandler": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
|
||||
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "1"
|
||||
}
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
|
||||
@@ -621,9 +578,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"file-type": {
|
||||
"version": "14.7.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-14.7.0.tgz",
|
||||
"integrity": "sha512-85lP/GKzazJlM2rMTp6J6OvanrTHNzUrb/VtrVPtJZ/ku5/kO3MUOJeDyb3YJIVsRyYWUt9vExp+gAM8WG1SJQ==",
|
||||
"version": "14.7.1",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-14.7.1.tgz",
|
||||
"integrity": "sha512-sXAMgFk67fQLcetXustxfKX+PZgHIUFn96Xld9uH8aXPdX3xOp0/jg9OdouVTvQrf7mrn+wAa4jN/y9fUOOiRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-web-to-node-stream": "^2.0.0",
|
||||
@@ -795,48 +752,6 @@
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
|
||||
"dev": true
|
||||
},
|
||||
"html": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html/-/html-1.0.0.tgz",
|
||||
"integrity": "sha1-pUT6nqVJK/s6LMqCEKEL57WvH2E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"concat-stream": "^1.4.7"
|
||||
}
|
||||
},
|
||||
"htmlparser2": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^1.3.1",
|
||||
"domhandler": "^2.3.0",
|
||||
"domutils": "^1.5.1",
|
||||
"entities": "^1.1.1",
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
|
||||
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
@@ -890,12 +805,6 @@
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"indent": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/indent/-/indent-0.0.2.tgz",
|
||||
"integrity": "sha1-jHnwgBkFWbaHA0uEx676l9WpEdk=",
|
||||
"dev": true
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
@@ -2110,12 +2019,6 @@
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||
"dev": true
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
|
@@ -33,18 +33,13 @@
|
||||
"ebook"
|
||||
],
|
||||
"devDependencies": {
|
||||
"bezier-js": "^2.6.1",
|
||||
"canvas": "^2.6.1",
|
||||
"clean-html": "^1.5.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob": "^7.1.6",
|
||||
"html": "^1.0.0",
|
||||
"http-server": "^0.12.3",
|
||||
"indent": "0.0.2",
|
||||
"marked": "^1.1.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nunjucks": "^3.2.2",
|
||||
"open": "^7.1.0",
|
||||
"open-cli": "^6.0.1",
|
||||
"prettier": "^2.0.5",
|
||||
"svgo": "^1.3.2"
|
||||
|
58
tools/build/markdown/generate-fallback-image.js
Normal file
58
tools/build/markdown/generate-fallback-image.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { createHash } from "crypto";
|
||||
import { generateGraphicsModule } from "./generate-graphics-module.js";
|
||||
|
||||
const moduleURL = new URL(import.meta.url);
|
||||
const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``));
|
||||
const __root = path.join(__dirname, `..`, `..`, `..`);
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
async function generateFallbackImage(src, width, height) {
|
||||
// Get the sketch code
|
||||
const sourcePath = path.join(__root, src);
|
||||
let code;
|
||||
try {
|
||||
code = fs.readFileSync(sourcePath).toString(`utf8`);
|
||||
} catch (e) {
|
||||
console.log(`could not read file "${sourcePath}".`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Do we need to even generate a file here?
|
||||
const hash = createHash(`md5`).update(code).digest(`hex`);
|
||||
const destPath = path.dirname(path.join(__root, `images`, src));
|
||||
const filename = path.join(destPath, `${hash}.png`);
|
||||
if (fs.existsSync(filename)) return hash;
|
||||
|
||||
// If we get here, we need to actually run the magic: convert
|
||||
// this to a valid JS module code and write this to a temporary
|
||||
// file so we can import it.
|
||||
const nodeCode = generateGraphicsModule(code, width, height);
|
||||
const fileName = `./nodecode.${Date.now()}.${Math.random()}.js`;
|
||||
const tempFile = path.join(__dirname, fileName);
|
||||
fs.writeFileSync(tempFile, nodeCode, `utf8`);
|
||||
|
||||
// Then we import our entirely valid JS module, which will run
|
||||
// the sketch code and export a canvas instance that we can
|
||||
// turn into an actual image file.
|
||||
const { canvas } = await import(fileName);
|
||||
|
||||
// fs.unlinkSync(tempFile);
|
||||
|
||||
// The canvas runs setup() + draw() as part of the module load, so
|
||||
// all we have to do now is get the image data and writ it to file.
|
||||
const dataURI = canvas.toDataURL();
|
||||
const start = dataURI.indexOf(`base64,`) + 7;
|
||||
const imageData = Buffer.from(dataURI.substring(start), `base64`);
|
||||
|
||||
fs.ensureDirSync(path.dirname(filename));
|
||||
fs.writeFileSync(filename, imageData);
|
||||
console.log(`Generated fallback image for ${src}`);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export default generateFallbackImage;
|
@@ -13,7 +13,7 @@ function generateGraphicsModule(code, width, height) {
|
||||
return prettier.format(
|
||||
`
|
||||
import CanvasBuilder from 'canvas';
|
||||
import { GraphicsAPI, Bezier, Vector } from "../../../lib/custom-element/api/graphics-api.js";
|
||||
import { GraphicsAPI, Bezier, Vector, Matrix } from "../../../lib/custom-element/api/graphics-api.js";
|
||||
|
||||
const noop = (()=>{});
|
||||
|
||||
|
@@ -1,11 +1,5 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { createHash } from "crypto";
|
||||
import { generateGraphicsModule } from "./generate-graphics-module.js";
|
||||
|
||||
const moduleURL = new URL(import.meta.url);
|
||||
const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``));
|
||||
const __root = path.join(__dirname, `..`, `..`, `..`);
|
||||
import generateFallbackImage from "./generate-fallback-image.js";
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
@@ -78,52 +72,4 @@ async function preprocessGraphicsElement(chapter, localeStrings, markdown) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* ...docs go here...
|
||||
*/
|
||||
async function generateFallbackImage(src, width, height) {
|
||||
// Get the sketch code
|
||||
const sourcePath = path.join(__root, src);
|
||||
let code;
|
||||
try {
|
||||
code = fs.readFileSync(sourcePath).toString(`utf8`);
|
||||
} catch (e) {
|
||||
console.log(`could not read file "${sourcePath}".`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Do we need to even generate a file here?
|
||||
const hash = createHash(`md5`).update(code).digest(`hex`);
|
||||
const destPath = path.dirname(path.join(__root, `images`, src));
|
||||
const filename = path.join(destPath, `${hash}.png`);
|
||||
if (fs.existsSync(filename)) return hash;
|
||||
|
||||
// If we get here, we need to actually run the magic: convert
|
||||
// this to a valid JS module code and write this to a temporary
|
||||
// file so we can import it.
|
||||
const nodeCode = generateGraphicsModule(code, width, height);
|
||||
const fileName = `./nodecode.${Date.now()}.${Math.random()}.js`;
|
||||
const tempFile = path.join(__dirname, fileName);
|
||||
fs.writeFileSync(tempFile, nodeCode, `utf8`);
|
||||
|
||||
// Then we import our entirely valid JS module, which will run
|
||||
// the sketch code and export a canvas instance that we can
|
||||
// turn into an actual image file.
|
||||
const { canvas } = await import(fileName);
|
||||
|
||||
fs.unlinkSync(tempFile);
|
||||
|
||||
// The canvas runs setup() + draw() as part of the module load, so
|
||||
// all we have to do now is get the image data and writ it to file.
|
||||
const dataURI = canvas.toDataURL();
|
||||
const start = dataURI.indexOf(`base64,`) + 7;
|
||||
const imageData = Buffer.from(dataURI.substring(start), `base64`);
|
||||
|
||||
fs.ensureDirSync(path.dirname(filename));
|
||||
fs.writeFileSync(filename, imageData);
|
||||
console.log(`Generated fallback image for ${src}`);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export default preprocessGraphicsElement;
|
||||
|
@@ -54,7 +54,7 @@ class LocaleStrings {
|
||||
Object.keys(localeStringData).forEach((id) => {
|
||||
const map = localeStringData[id];
|
||||
if (typeof map !== "object") return;
|
||||
const value = map[locale] ? map[locale] : map[defaultLocale];
|
||||
const value = map[locale] ? map[locale] : map[defaultLocale];
|
||||
if (!value) throw new Error(`unknown locale string id "${id}".`);
|
||||
strings[id] = value;
|
||||
|
||||
|
@@ -51,12 +51,16 @@
|
||||
<!-- my own referral/page hit tracker, because Google knows enough -->
|
||||
<script src="./lib/site/referrer.js" type="module" async></script>
|
||||
|
||||
<!-- the part that makes interactive graphics work: an HTML5 <graphics-element> custom element -->
|
||||
<!--
|
||||
The part that makes interactive graphics work: an HTML5 <graphics-element> custom element.
|
||||
Note that we're not defering this: we just want it to kick in as soon as possible, and
|
||||
given how much HTML there is, that means this can, and thus should, kick in before the
|
||||
document is done even transferring.
|
||||
-->
|
||||
<script
|
||||
src="./lib/custom-element/graphics-element.js"
|
||||
type="module"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
<link rel="stylesheet" href="./lib/custom-element/graphics-element.css" />
|
||||
|
||||
@@ -1774,11 +1778,22 @@ function drawCurve(points[], t):
|
||||
control points, and press your up and down arrow keys to raise or
|
||||
lower the curve order.
|
||||
</p>
|
||||
<p>
|
||||
<Graphic title={"A " + this.getOrder() + " order Bézier curve"}
|
||||
setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown}
|
||||
onMouseMove={this.onMouseMove} />
|
||||
</p>
|
||||
<graphics-element
|
||||
title="A variable-order Bézier curve"
|
||||
width="275"
|
||||
height="275"
|
||||
src="./chapters/reordering/reorder.js"
|
||||
>
|
||||
<fallback-image>
|
||||
<img
|
||||
width="275px"
|
||||
height="275px"
|
||||
src="images\chapters\reordering\4541eeb2113d81cbc0c0a56122570d48.png"
|
||||
loading="lazy"
|
||||
/>
|
||||
Scripts are disabled. Showing fallback image.
|
||||
</fallback-image></graphics-element
|
||||
>
|
||||
</section>
|
||||
<section id="derivatives">
|
||||
<h1><a href="#derivatives">Derivatives</a></h1>
|
||||
|
Reference in New Issue
Block a user