From 60f24a5d1f5f6d8763f8786e82fa351cf83a832f Mon Sep 17 00:00:00 2001 From: Pomax Date: Wed, 12 Aug 2020 22:27:57 -0700 Subject: [PATCH] reordering --- chapters/reordering/content.en-GB.md | 2 +- chapters/reordering/content.ja-JP.md | 25 -- chapters/reordering/handler.js | 150 ----------- chapters/reordering/reorder.js | 158 +++++++++++ .../140b23b10b4159b03b2a555db7ddf826.png | Bin 0 -> 5360 bytes .../4541eeb2113d81cbc0c0a56122570d48.png | Bin 0 -> 10584 bytes index.html | 29 +- index.template.html | 9 +- ja-JP/index.html | 252 ++++++++++++++++-- lib/custom-element/api/base-api.js | 5 +- lib/custom-element/api/graphics-api.js | 12 +- lib/custom-element/api/util/matrix.js | 142 ++++++++++ lib/custom-element/graphics-element.js | 2 +- package-lock.json | 103 +------ package.json | 5 - .../build/markdown/generate-fallback-image.js | 58 ++++ .../markdown/generate-graphics-module.js | 2 +- .../markdown/preprocess-graphics-element.js | 56 +--- tools/locale-strings.js | 2 +- zh-CN/index.html | 29 +- 20 files changed, 661 insertions(+), 380 deletions(-) delete mode 100644 chapters/reordering/content.ja-JP.md delete mode 100644 chapters/reordering/handler.js create mode 100644 chapters/reordering/reorder.js create mode 100644 images/chapters/reordering/140b23b10b4159b03b2a555db7ddf826.png create mode 100644 images/chapters/reordering/4541eeb2113d81cbc0c0a56122570d48.png create mode 100644 lib/custom-element/api/util/matrix.js create mode 100644 tools/build/markdown/generate-fallback-image.js diff --git a/chapters/reordering/content.en-GB.md b/chapters/reordering/content.en-GB.md index b936c776..4b038637 100644 --- a/chapters/reordering/content.en-GB.md +++ b/chapters/reordering/content.en-GB.md @@ -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`th order curve with a lower `n`th 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. - + diff --git a/chapters/reordering/content.ja-JP.md b/chapters/reordering/content.ja-JP.md deleted file mode 100644 index b4bd8d8a..00000000 --- a/chapters/reordering/content.ja-JP.md +++ /dev/null @@ -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*次の曲線へと次数下げすることは、一般には**不可能**だという結論も得られます。なぜなら、制御点をきれいに「引き離す」ことができないからです。試してみたところで、得られる曲線は元と同じにはなりません。それどころか、まったくの別物に見えるかもしれません。 - -下の図では(半分)ランダムな曲線に対して、この規則を試してみることができます。図を選択して上下キーを押すと、次数上げや次数下げができます。 - - - -[SirVer's Castle](http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves)には、最適な次元削減で必要になる行列について(数学的ですが)良い解説があります。時間があれば、これを直接この記事の中で説明したいところです。 diff --git a/chapters/reordering/handler.js b/chapters/reordering/handler.js deleted file mode 100644 index 0160cfd2..00000000 --- a/chapters/reordering/handler.js +++ /dev/null @@ -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 [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 { - 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; diff --git a/chapters/reordering/reorder.js b/chapters/reordering/reorder.js new file mode 100644 index 00000000..2e08e2bb --- /dev/null +++ b/chapters/reordering/reorder.js @@ -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 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 [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); +} diff --git a/images/chapters/reordering/140b23b10b4159b03b2a555db7ddf826.png b/images/chapters/reordering/140b23b10b4159b03b2a555db7ddf826.png new file mode 100644 index 0000000000000000000000000000000000000000..36384d8a16c088d94d66cbaa59d8af4001f6e574 GIT binary patch literal 5360 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 XZxCGgdUp=I!i4B(7@(@uZJ+!PE{^WU literal 0 HcmV?d00001 diff --git a/images/chapters/reordering/4541eeb2113d81cbc0c0a56122570d48.png b/images/chapters/reordering/4541eeb2113d81cbc0c0a56122570d48.png new file mode 100644 index 0000000000000000000000000000000000000000..9b71de14112b56531e38b27d74a5faa3f2261e9d GIT binary patch literal 10584 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 GhyEXs(*hm< literal 0 HcmV?d00001 diff --git a/index.html b/index.html index a8dfb64f..d4b18fdc 100644 --- a/index.html +++ b/index.html @@ -49,12 +49,16 @@ - + @@ -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.

-

- <Graphic title={"A " + this.getOrder() + " order Bézier curve"} - setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} - onMouseMove={this.onMouseMove} /> -

+ + + + Scripts are disabled. Showing fallback image. +

Derivatives

diff --git a/index.template.html b/index.template.html index d9f1ebdb..58342695 100644 --- a/index.template.html +++ b/index.template.html @@ -36,8 +36,13 @@ - - + + diff --git a/ja-JP/index.html b/ja-JP/index.html index de2673ef..8383f0d8 100644 --- a/ja-JP/index.html +++ b/ja-JP/index.html @@ -51,12 +51,16 @@ - + @@ -103,7 +107,7 @@
  • 簡略化した描画
  • 曲線の分割
  • 行列による曲線の分割
  • -
  • 曲線の次数下げと次数上げ
  • +
  • Lowering and elevating curve order
  • Derivatives
  • Tangents and normals
  • Working with 3D normals
  • @@ -1534,42 +1538,244 @@ function drawCurve(points[], t):

    -

    曲線の次数下げと次数上げ

    +

    Lowering and elevating curve order

    - ベジエ曲線のおもしろい性質のひとつに、「n次の曲線は常に、n+1次の曲線で完璧に表すことができる」というものがあります。このとき、制御点は新しいものになります。 + One interesting property of Bézier curves is that an + nth order curve can always be perfectly + represented by an (n+1)th order curve, by giving + the higher-order curve specific control points.

    - たとえば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/3rd start + 2/3rd control" + and "2/3rd control + 1/3rd end". Now we have + exactly the same curve as before, except represented as a cubic + curve rather than a quadratic curve.

    - n次の曲線をn+1次の曲線へと次数上げするための一般の規則は、次のようになります(始点と終点の重みは、元の曲線のものと変わらないことがわかります)。 + The general rule for raising an nth order curve + to an (n+1)th 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):

    - しかし同時にこの規則から、n次の曲線をn-1次の曲線へと次数下げすることは、一般には不可能だという結論も得られます。なぜなら、制御点をきれいに「引き離す」ことができないからです。試してみたところで、得られる曲線は元と同じにはなりません。それどころか、まったくの別物に見えるかもしれません。 -

    -

    - 下の図では(半分)ランダムな曲線に対して、この規則を試してみることができます。図を選択して上下キーを押すと、次数上げや次数下げができます。 -

    -

    - <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 + cannot generally safely lower a curve from + nth order to (n-1)th 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.

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

    +

    + We start by taking the standard Bézier function, and condensing it a + little: +

    + +

    + Then, we apply one of those silly (actually, super useful) calculus + tricks: since our t value is always between zero and + one (inclusive), we know that (1-t) plus + t always sums to 1. As such, we can express any value + as a sum of t and 1-t: +

    + +

    + So, with that seemingly trivial observation, we rewrite that Bézier + function by splitting it up into a sum of a (1-t) and + t component: +

    + +

    + So far so good. Now, to see why we did this, let's write out the + (1-t) and t parts, and see what that gives + us. I promise, it's about to make sense. We start with + (1-t): +

    + +

    + So by using this seemingly silly trick, we can suddenly express part + of our nth order Bézier function in terms of an (n+1)th + 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 + t part, but that's not a problem: +

    + +

    + So, with both of those changed from an order + n expression to an order (n+1) expression, + we can put them back together again. Now, where the order + n function had a summation from 0 to n, + the order n+1 function uses a summation from 0 to + n+1, 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. +

    +

    Let's do this:

    + +

    + 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: +

    + +

    + where the matrix M is an n+1 by + n matrix, and looks like: +

    + +

    + 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. +

    +

    Not too bad!

    +

    + 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 + the normal equation. 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 A x = b, we + can use it. And as it so happens, that's exactly what we're dealing + with, so: +

    + +

    The steps taken here are:

    +
      +
    1. + We have a function in a form that the normal equation can be used + with, so +
    2. +
    3. apply the normal equation!
    4. +
    5. + Then, we want to end up with just Bn 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 + identity matrix. +
    6. +
    7. + 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 + I. So we substitute the mess with + I, and then +
    8. +
    9. + because multiplication with the identity matrix does nothing (like + multiplying by 1 does nothing in regular algebra), we just drop + it. +
    10. +
    +

    + And we're done: we now have an expression that lets us approximate + an n+1th order curve with a lower + nth 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. +

    + + + + Scripts are disabled. Showing fallback image. +

    Derivatives

    diff --git a/lib/custom-element/api/base-api.js b/lib/custom-element/api/base-api.js index 9414c649..e080a396 100644 --- a/lib/custom-element/api/base-api.js +++ b/lib/custom-element/api/base-api.js @@ -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); } } diff --git a/lib/custom-element/api/graphics-api.js b/lib/custom-element/api/graphics-api.js index d065782c..49caf566 100644 --- a/lib/custom-element/api/graphics-api.js +++ b/lib/custom-element/api/graphics-api.js @@ -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 }; diff --git a/lib/custom-element/api/util/matrix.js b/lib/custom-element/api/util/matrix.js new file mode 100644 index 00000000..9fc40c88 --- /dev/null +++ b/lib/custom-element/api/util/matrix.js @@ -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 }; diff --git a/lib/custom-element/graphics-element.js b/lib/custom-element/graphics-element.js index 44d5c63c..c96760fd 100644 --- a/lib/custom-element/graphics-element.js +++ b/lib/custom-element/graphics-element.js @@ -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} diff --git a/package-lock.json b/package-lock.json index b3a6e791..7374e8fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4d738cae..5f26bfa5 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/tools/build/markdown/generate-fallback-image.js b/tools/build/markdown/generate-fallback-image.js new file mode 100644 index 00000000..23c0c25d --- /dev/null +++ b/tools/build/markdown/generate-fallback-image.js @@ -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; diff --git a/tools/build/markdown/generate-graphics-module.js b/tools/build/markdown/generate-graphics-module.js index 078a5d06..c3b57f92 100644 --- a/tools/build/markdown/generate-graphics-module.js +++ b/tools/build/markdown/generate-graphics-module.js @@ -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 = (()=>{}); diff --git a/tools/build/markdown/preprocess-graphics-element.js b/tools/build/markdown/preprocess-graphics-element.js index e28fa1af..c560bd03 100644 --- a/tools/build/markdown/preprocess-graphics-element.js +++ b/tools/build/markdown/preprocess-graphics-element.js @@ -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; diff --git a/tools/locale-strings.js b/tools/locale-strings.js index c9e926b8..6548b3e1 100644 --- a/tools/locale-strings.js +++ b/tools/locale-strings.js @@ -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; diff --git a/zh-CN/index.html b/zh-CN/index.html index b9d19416..580ccc95 100644 --- a/zh-CN/index.html +++ b/zh-CN/index.html @@ -51,12 +51,16 @@ - + @@ -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.

    -

    - <Graphic title={"A " + this.getOrder() + " order Bézier curve"} - setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} - onMouseMove={this.onMouseMove} /> -

    + + + + Scripts are disabled. Showing fallback image. +

    Derivatives