1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-01 04:22:28 +02:00

reordering

This commit is contained in:
Pomax
2020-08-12 22:27:57 -07:00
parent 6498341566
commit 60f24a5d1f
20 changed files with 661 additions and 380 deletions

View File

@@ -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>

View File

@@ -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)には、最適な次元削減で必要になる行列について(数学的ですが)良い解説があります。時間があれば、これを直接この記事の中で説明したいところです。

View File

@@ -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;

View 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -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>
&lt;Graphic title={"A " + this.getOrder() + " order Bézier curve"}
setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown}
onMouseMove={this.onMouseMove} /&gt;
</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>

View File

@@ -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 -->

View File

@@ -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>
&lt;Graphic title={this.state.order + "次のベジエ曲線"}
setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown}
/&gt;
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>

View File

@@ -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);
}
}

View File

@@ -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 };

View 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 };

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View 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;

View File

@@ -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 = (()=>{});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
&lt;Graphic title={"A " + this.getOrder() + " order Bézier curve"}
setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown}
onMouseMove={this.onMouseMove} /&gt;
</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>