From c97b1b635e794e209b69a1272e2eb1caf3633091 Mon Sep 17 00:00:00 2001
From: Pomax
Date: Sun, 9 Aug 2020 17:16:50 -0700
Subject: [PATCH] control (#261)
---
chapters/control/content.en-GB.md | 8 +-
chapters/control/handler.js | 159 --------------
chapters/control/index.js | 3 -
chapters/control/lerp-cubic.js | 62 ++++++
chapters/control/lerp-fifteenth.js | 81 +++++++
chapters/control/lerp-quadratic.js | 61 ++++++
chapters/explanation/content.en-GB.md | 2 +-
chapters/explanation/content.ja-JP.md | 2 +-
chapters/explanation/content.zh-CN.md | 2 +-
chapters/introduction/content.en-GB.md | 4 +-
chapters/introduction/content.ja-JP.md | 4 +-
chapters/introduction/content.zh-CN.md | 4 +-
chapters/toc.js | 9 +-
index.html | 76 +++++--
lib/custom-element/api/base-api.js | 26 ++-
lib/custom-element/api/graphics-api.js | 199 ++++++++++++++++--
lib/custom-element/api/types/bezier.js | 4 +-
lib/custom-element/api/types/vector.js | 147 +++++++------
lib/custom-element/api/util/shape.js | 28 +--
lib/custom-element/custom-element.js | 10 +-
lib/custom-element/graphics-element.js | 15 +-
.../lib/perform-code-surgery.js | 1 -
lib/custom-element/lib/split-code-sections.js | 58 ++---
lib/site/referrer.js | 26 ++-
lib/site/social-updater.js | 57 +++--
package-lock.json | 4 -
package.json | 17 +-
tools/build.js | 7 +-
tools/build/create-index-page.js | 8 +-
tools/build/get-all-chapter-files.js | 5 +-
.../generate-graphics-module.js} | 13 +-
.../{ => graphics}/generate-placeholders.js | 25 ++-
.../build/{ => markdown}/convert-markdown.js | 31 +--
tools/build/markdown/inject-fallback.js | 39 ----
.../markdown/preprocess-graphics-element.js | 67 ++++++
tools/build/process-locale.js | 13 +-
tools/images/chapters/control/lerp-cubic.png | Bin 0 -> 16566 bytes
.../chapters/control/lerp-fifteenth.png | Bin 0 -> 29174 bytes
.../chapters/control/lerp-quadratic.png | Bin 0 -> 13980 bytes
tools/images/chapters/explanation/circle.png | Bin 0 -> 6586 bytes
tools/images/chapters/introduction/cubic.png | Bin 0 -> 10072 bytes
.../chapters/introduction/quadratic.png | Bin 0 -> 8807 bytes
.../images/chapters/whatis/interpolation.png | Bin 0 -> 30131 bytes
43 files changed, 812 insertions(+), 465 deletions(-)
delete mode 100644 chapters/control/handler.js
delete mode 100644 chapters/control/index.js
create mode 100644 chapters/control/lerp-cubic.js
create mode 100644 chapters/control/lerp-fifteenth.js
create mode 100644 chapters/control/lerp-quadratic.js
rename tools/build/{rewrite-graphics-element.js => graphics/generate-graphics-module.js} (70%)
rename tools/build/{ => graphics}/generate-placeholders.js (77%)
rename tools/build/{ => markdown}/convert-markdown.js (52%)
delete mode 100644 tools/build/markdown/inject-fallback.js
create mode 100644 tools/build/markdown/preprocess-graphics-element.js
create mode 100644 tools/images/chapters/control/lerp-cubic.png
create mode 100644 tools/images/chapters/control/lerp-fifteenth.png
create mode 100644 tools/images/chapters/control/lerp-quadratic.png
create mode 100644 tools/images/chapters/explanation/circle.png
create mode 100644 tools/images/chapters/introduction/cubic.png
create mode 100644 tools/images/chapters/introduction/quadratic.png
create mode 100644 tools/images/chapters/whatis/interpolation.png
diff --git a/chapters/control/content.en-GB.md b/chapters/control/content.en-GB.md
index 580468b5..1808c27d 100644
--- a/chapters/control/content.en-GB.md
+++ b/chapters/control/content.en-GB.md
@@ -5,9 +5,9 @@ Bézier curves are, like all "splines", interpolation functions. This means that
The following graphs show the interpolation functions for quadratic and cubic curves, with "S" being the strength of a point's contribution to the total sum of the Bézier function. Click or click-drag to see the interpolation percentages for each curve-defining point at a specific t value.
-
-
-
+
+
+
Also shown is the interpolation function for a 15th order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.
@@ -34,7 +34,7 @@ That looks complicated, but as it so happens, the "weights" are actually just th
Which gives us the curve we saw at the top of the article:
-
+
What else can we do with Bézier curves? Quite a lot, actually. The rest of this article covers a multitude of possible operations and algorithms that we can apply, and the tasks they achieve.
diff --git a/chapters/control/handler.js b/chapters/control/handler.js
deleted file mode 100644
index d80a55fd..00000000
--- a/chapters/control/handler.js
+++ /dev/null
@@ -1,159 +0,0 @@
-module.exports = {
- drawCubic: function(api) {
- var curve = api.getDefaultCubic();
- api.setCurve(curve);
- },
-
- drawCurve: function(api, curve) {
- api.reset();
- api.drawSkeleton(curve);
- api.drawCurve(curve);
- },
-
- drawFunction: function(api, label, where, generator) {
- api.setRandomColor();
- api.drawFunction(generator);
- api.setFill(api.getColor());
- if (label) api.text(label, where);
- },
-
- drawLerpBox: function(api, dim, pad, p) {
- api.noColor();
- api.setFill("rgba(0,0,100,0.2)");
- var p1 = {x: p.x-5, y:pad},
- p2 = {x:p.x + 5, y:dim};
- api.drawRect(p1, p2);
- api.setColor("black");
- },
-
- drawLerpPoint: function(api, tf, pad, fwh, p) {
- p.y = pad + tf*fwh;
- api.drawCircle(p, 3);
- api.setFill("black");
- api.text(((tf*10000)|0)/100 + "%", {x:p.x+10, y:p.y+4});
- api.noFill();
- },
-
- drawQuadraticLerp: function(api) {
- api.reset();
-
- var dim = api.getPanelWidth(),
- pad = 20,
- fwh = dim - pad*2;
-
- api.drawAxes(pad, "t",0,1, "S","0%","100%");
-
- var p = api.hover;
- if (p && p.x >= pad && p.x <= dim-pad) {
- this.drawLerpBox(api, dim, pad, p);
- var t = (p.x-pad)/fwh;
- this.drawLerpPoint(api, (1-t)*(1-t), pad, fwh, p);
- this.drawLerpPoint(api, 2*(1-t)*(t), pad, fwh, p);
- this.drawLerpPoint(api, (t)*(t), pad, fwh, p);
- }
-
- this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * (1-t) * (1-t)
- };
- });
- this.drawFunction(api, "second term", {x: dim/2 - 1.5*pad, y: dim/2 + pad}, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * 2 * (1-t) * (t)
- };
- });
- this.drawFunction(api, "third term", {x: fwh - pad*2.5, y: fwh}, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * (t) * (t)
- };
- });
- },
-
- drawCubicLerp: function(api) {
- api.reset();
-
- var dim = api.getPanelWidth(),
- pad = 20,
- fwh = dim - pad*2;
-
- api.drawAxes(pad, "t",0,1, "S","0%","100%");
-
- var p = api.hover;
- if (p && p.x >= pad && p.x <= dim-pad) {
- this.drawLerpBox(api, dim, pad, p);
- var t = (p.x-pad)/fwh;
- this.drawLerpPoint(api, (1-t)*(1-t)*(1-t), pad, fwh, p);
- this.drawLerpPoint(api, 3*(1-t)*(1-t)*(t), pad, fwh, p);
- this.drawLerpPoint(api, 3*(1-t)*(t)*(t), pad, fwh, p);
- this.drawLerpPoint(api, (t)*(t)*(t), pad, fwh, p);
- }
-
- this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * (1-t) * (1-t) * (1-t)
- };
- });
- this.drawFunction(api, "second term", {x: dim/2 - 4*pad, y: dim/2 }, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * 3 * (1-t) * (1-t) * (t)
- };
- });
- this.drawFunction(api, "third term", {x: dim/2 + 2*pad, y: dim/2}, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * 3 * (1-t) * (t) * (t)
- };
- });
- this.drawFunction(api, "fourth term", {x: fwh - pad*2.5, y: fwh}, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * (t) * (t) * (t)
- };
- });
- },
-
- draw15thLerp: function(api) {
- api.reset();
-
- var dim = api.getPanelWidth(),
- pad = 20,
- fwh = dim - pad*2;
-
- api.drawAxes(pad, "t",0,1, "S","0%","100%");
-
- var factors = [1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1];
-
- var p = api.hover, n;
- if (p && p.x >= pad && p.x <= dim-pad) {
- this.drawLerpBox(api, dim, pad, p);
- for(n=0; n<=15; n++) {
- var t = (p.x-pad)/fwh,
- tf = factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n);
- this.drawLerpPoint(api, tf, pad, fwh, p);
- }
- }
-
- for(n=0; n<=15; n++) {
- var label = false, position = false;
- if (n===0) {
- label = "first term";
- position = {x: pad + 5, y: fwh};
- }
- if (n===15) {
- label = "last term";
- position = {x: dim - 3.5*pad, y: fwh};
- }
- this.drawFunction(api, label, position, function(t) {
- return {
- x: pad + t * fwh,
- y: pad + fwh * factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n)
- };
- });
- }
- }
-};
diff --git a/chapters/control/index.js b/chapters/control/index.js
deleted file mode 100644
index 5f464b2b..00000000
--- a/chapters/control/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-var handler = require("./handler.js");
-var generateBase = require("../../generate-base");
-module.exports = generateBase("control", handler);
\ No newline at end of file
diff --git a/chapters/control/lerp-cubic.js b/chapters/control/lerp-cubic.js
new file mode 100644
index 00000000..8585f55e
--- /dev/null
+++ b/chapters/control/lerp-cubic.js
@@ -0,0 +1,62 @@
+setup() {
+ const w = this.width,
+ h = this.height;
+
+ this.f = [
+ t => ({ x: t * w, y: h * (1-t) ** 3 }),
+ t => ({ x: t * w, y: h * 3 * (1-t) ** 2 * t }),
+ t => ({ x: t * w, y: h * 3 * (1-t) * t ** 2 }),
+ t => ({ x: t * w, y: h * t ** 3})
+ ];
+
+ this.s = this.f.map(f => plot(f) );
+}
+
+draw() {
+ resetTransform();
+ clear();
+ setFill(`black`);
+ setStroke(`black`);
+
+ scale(0.8, 0.9);
+ translate(40,20);
+ drawAxes(`t`, 0, 10, `S`, `0%`, `100%`, 40, 20);
+
+ noFill();
+
+ this.s.forEach(s => {
+ setStroke( randomColor() );
+ drawShape(s);
+ })
+
+ this.drawHighlight();
+}
+
+drawHighlight() {
+ if (this.cursor.down) {
+
+ let c = screenToWorld(this.cursor);
+ if (c.x < 0) return;
+ if (c.x > this.width) return;
+
+ noStroke();
+ setFill(`rgba(255,0,0,0.3)`);
+ rect(c.x - 2, 0, 5, this.height);
+
+ const p = this.f.map(f => f(c.x / this.width));
+
+ setFill(`black`);
+ p.forEach(p => {
+ circle(p.x, p.y, 3);
+ text(`${ round(100 * p.y/this.height) }%`, p.x + 10, p.y);
+ });
+ }
+}
+
+onMouseMove() {
+ redraw();
+}
+
+onMouseUp() {
+ redraw();
+}
diff --git a/chapters/control/lerp-fifteenth.js b/chapters/control/lerp-fifteenth.js
new file mode 100644
index 00000000..fb41bbf1
--- /dev/null
+++ b/chapters/control/lerp-fifteenth.js
@@ -0,0 +1,81 @@
+setup() {
+ this.degree = 15;
+ this.triangle = [[1], [1,1]];
+ this.generate();
+}
+
+binomial(n,k) {
+ if (!this.triangle[n]) {
+ while(!this.triangle[n]) {
+ let last = this.triangle.slice(-1)[0];
+ let next = last.map((v,i) => v + last[i+1]);
+ next.pop();
+ this.triangle.push([1, ...next, 1]);
+ }
+ }
+ return this.triangle[n][k];
+}
+
+generate() {
+ const w = this.width,
+ h = this.height,
+ d = this.degree;
+
+ this.f = [...new Array(d+1)].map((_,i) => {
+ return t => ({
+ x: t * w,
+ y: h * this.binomial(d,i) * (1-t) ** (d-i) * t ** (i)
+ });
+ });
+
+ this.s = this.f.map(f => plot(f, 0, 1, d*4) );
+}
+
+draw() {
+ resetTransform();
+ clear();
+ setFill(`black`);
+ setStroke(`black`);
+
+ scale(0.8, 0.9);
+ translate(40,20);
+ drawAxes(`t`, 0, 10, `S`, `0%`, `100%`, 40, 20);
+
+ noFill();
+
+ this.s.forEach(s => {
+ setStroke( randomColor() );
+ drawShape(s);
+ })
+
+ this.drawHighlight();
+}
+
+drawHighlight() {
+ if (this.cursor.down) {
+
+ let c = screenToWorld(this.cursor);
+ if (c.x < 0) return;
+ if (c.x > this.width) return;
+
+ noStroke();
+ setFill(`rgba(255,0,0,0.3)`);
+ rect(c.x - 2, 0, 5, this.height);
+
+ const p = this.f.map(f => f(c.x / this.width));
+
+ setFill(`black`);
+ p.forEach(p => {
+ circle(p.x, p.y, 3);
+ text(`${ round(100 * p.y/this.height) }%`, p.x + 10, p.y);
+ });
+ }
+}
+
+onMouseMove() {
+ redraw();
+}
+
+onMouseUp() {
+ redraw();
+}
diff --git a/chapters/control/lerp-quadratic.js b/chapters/control/lerp-quadratic.js
new file mode 100644
index 00000000..e62887da
--- /dev/null
+++ b/chapters/control/lerp-quadratic.js
@@ -0,0 +1,61 @@
+setup() {
+ const w = this.width,
+ h = this.height;
+
+ this.f = [
+ t => ({ x: t * w, y: h * (1-t) ** 2 }),
+ t => ({ x: t * w, y: h * 2 * (1-t) * t }),
+ t => ({ x: t * w, y: h * t ** 2 })
+ ];
+
+ this.s = this.f.map(f => plot(f) );
+}
+
+draw() {
+ resetTransform();
+ clear();
+ setFill(`black`);
+ setStroke(`black`);
+
+ scale(0.8, 0.9);
+ translate(40,20);
+ drawAxes(`t`, 0, 10, `S`, `0%`, `100%`, 40, 20);
+
+ noFill();
+
+ this.s.forEach(s => {
+ setStroke( randomColor() );
+ drawShape(s);
+ })
+
+ this.drawHighlight();
+}
+
+drawHighlight() {
+ if (this.cursor.down) {
+
+ let c = screenToWorld(this.cursor);
+ if (c.x < 0) return;
+ if (c.x > this.width) return;
+
+ noStroke();
+ setFill(`rgba(255,0,0,0.3)`);
+ rect(c.x - 2, 0, 5, this.height);
+
+ const p = this.f.map(f => f(c.x / this.width));
+
+ setFill(`black`);
+ p.forEach(p => {
+ circle(p.x, p.y, 3);
+ text(`${ round(100 * p.y/this.height) }%`, p.x + 10, p.y);
+ });
+ }
+}
+
+onMouseMove() {
+ redraw();
+}
+
+onMouseUp() {
+ redraw();
+}
diff --git a/chapters/explanation/content.en-GB.md b/chapters/explanation/content.en-GB.md
index 7a7edc59..578c74a2 100644
--- a/chapters/explanation/content.en-GB.md
+++ b/chapters/explanation/content.en-GB.md
@@ -41,7 +41,7 @@ There we go. x/y coordinates, linked through some mystery value y coordinate in terms of an x coordinate, like normal functions do, but they instead link the values to a "control" variable. If we vary the value of t, then with every change we get two values, which we can use as (x,y) coordinates in a graph. The above set of functions, for instance, generates points on a circle: We can range t from negative to positive infinity, and the resulting (x,y) coordinates will always lie on a circle with radius 1 around the origin (0,0). If we plot it for t from 0 to 5, we get this (use your up and down arrow keys to change the plot end value):
-
+
Bézier curves are just one out of the many classes of parametric functions, and are characterised by using the same base function for all of the output values. In the example we saw above, the x and y values were generated by different functions (one uses a sine, the other a cosine); but Bézier curves use the "binomial polynomial" for both the x and y outputs. So what are binomial polynomials?
diff --git a/chapters/explanation/content.ja-JP.md b/chapters/explanation/content.ja-JP.md
index 79aeadb0..01aa1164 100644
--- a/chapters/explanation/content.ja-JP.md
+++ b/chapters/explanation/content.ja-JP.md
@@ -39,7 +39,7 @@
というわけで、普通の関数ではy座標をx座標によって定義しますが、パラメトリック曲線ではそうではなく、座標の値を「制御」変数と結びつけます。tの値を変化させるたびに2つの値が変化するので、これをグラフ上の座標 (x,y)として使うことができます。例えば、先ほどの関数の組は円周上の点を生成します。負の無限大から正の無限大へとtを動かすと、得られる座標(x,y)は常に中心(0,0)・半径1の円の上に乗ります。tを0から5まで変化させてプロットした場合は、このようになります(上下キーでプロットの上限を変更できます)。
-
+
ベジエ曲線はパラメトリック関数の一種であり、どの次元に対しても同じ基底関数を使うという点で特徴づけられます。先ほどの例では、xの値とyの値とで異なる関数(正弦関数と余弦関数)を使っていましたが、ベジエ曲線ではxとyの両方で「二項係数多項式」を使います。では、二項係数多項式とは何でしょう?
diff --git a/chapters/explanation/content.zh-CN.md b/chapters/explanation/content.zh-CN.md
index 1feaec2a..c16ff7d9 100644
--- a/chapters/explanation/content.zh-CN.md
+++ b/chapters/explanation/content.zh-CN.md
@@ -39,7 +39,7 @@
所以,参数曲线不像一般函数那样,通过x坐标来定义y坐标,而是用一个“控制”变量将它们连接起来。如果改变t的值,每次变化时我们都能得到两个值,这可以作为图形中的(x,y)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使t在正负极值间变化,得到的输出(x,y)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出t从0到5时的值,将得到如下图像(你可以用上下键来改变画的点和值):
-
+
贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中x值和y值使用了不同的方程,与此不同的是,贝塞尔曲线的x和y都用了“二项多项式”。那什么是二项多项式呢?
diff --git a/chapters/introduction/content.en-GB.md b/chapters/introduction/content.en-GB.md
index 557d3337..c2e52e45 100644
--- a/chapters/introduction/content.en-GB.md
+++ b/chapters/introduction/content.en-GB.md
@@ -3,8 +3,8 @@
Let's start with the good stuff: when we're talking about Bézier curves, we're talking about the things that you can see in the following graphics. They run from some start point to some end point, with their curvature influenced by one or more "intermediate" control points. Now, because all the graphics on this page are interactive, go manipulate those curves a bit: click-drag the points, and see how their shape changes based on what you do.
-
-
+
+
These curves are used a lot in computer aided design and computer aided manufacturing (CAD/CAM) applications, as well as in graphic design programs like Adobe Illustrator and Photoshop, Inkscape, GIMP, etc. and in graphic technologies like scalable vector graphics (SVG) and OpenType fonts (TTF/OTF). A lot of things use Bézier curves, so if you want to learn more about them... prepare to get your learn on!
diff --git a/chapters/introduction/content.ja-JP.md b/chapters/introduction/content.ja-JP.md
index 5b2d004b..b3719637 100644
--- a/chapters/introduction/content.ja-JP.md
+++ b/chapters/introduction/content.ja-JP.md
@@ -3,8 +3,8 @@
まずは良い例から始めましょう。ベジエ曲線というのは、下の図に表示されているもののことです。ベジエ曲線はある始点からある終点へと延びており、その曲率は1個以上の「中間」制御点に左右されています。さて、このページの図はどれもインタラクティブになっていますので、ここで曲線をちょっと操作してみましょう。点をドラッグしたとき、曲線の形がそれに応じてどう変化するのか、確かめてみてください。
-
-
+
+
ベジエ曲線は、CAD(computer aided designやCAM(computer aided manufacturing)のアプリケーションで多用されています。もちろん、Adobe Illustrator・Photoshop・Inkscape・Gimp などのグラフィックデザインアプリケーションや、SVG(scalable vector graphics)・OpenTypeフォント(otf/ttf)のようなグラフィック技術でも利用されています。ベジエ曲線はたくさんのものに使われていますので、これについてもっと詳しく学びたいのであれば……さあ、準備しましょう!
diff --git a/chapters/introduction/content.zh-CN.md b/chapters/introduction/content.zh-CN.md
index 4a09912b..5e1aaabe 100644
--- a/chapters/introduction/content.zh-CN.md
+++ b/chapters/introduction/content.zh-CN.md
@@ -3,8 +3,8 @@
让我们有个好的开始:当我们在谈论贝塞尔曲线的时候,所指的就是你在如下图像看到的东西。它们从某些起点开始,到终点结束,并且受到一个或多个的“中间”控制点的影响。本页面上的图形都是可交互的,你可以拖动这些点,看看这些形状在你的操作下会怎么变化。
-
-
+
+
这些曲线在计算机辅助设计和计算机辅助制造应用(CAD/CAM)中用的很多。在图形设计软件中也常用到,像Adobe Illustrator, Photoshop, Inkscape, Gimp等等。还可以应用在一些图形技术中,像矢量图形(SVG)和OpenType字体(ttf/otf)。许多东西都用到贝塞尔曲线,如果你想更了解它们...准备好继续往下学吧!
diff --git a/chapters/toc.js b/chapters/toc.js
index e6fd1f63..7e729dca 100644
--- a/chapters/toc.js
+++ b/chapters/toc.js
@@ -1,10 +1,5 @@
/**
- * This is an ordered list of all sections used in the Bezier primer.
- *
- * The ordering you see here reflects the ordering in which sections
- '* are present on the Primer page',
- * a REALLY good reason to =)
- *
+ * This is the section ordering used in the Primer.
*/
export default [
'preface',
@@ -61,7 +56,7 @@ export default [
// "things made of more than on curve"
'polybezier',
'shapes',
- // 'drawing',
+ // 'drawing', // still just waiting to be finished......
// curve offsetting
'projections',
diff --git a/index.html b/index.html
index 91bf5893..88eedd03 100644
--- a/index.html
+++ b/index.html
@@ -912,21 +912,54 @@ function Bezier(3,t):
curve-defining point at a specific t value.
@@ -967,11 +1000,22 @@ function Bezier(3,t):
loading="lazy"
/>
Which gives us the curve we saw at the top of the article:
-
+ width="275"
+ height="275"
+ src="./chapters/control/../introduction/cubic.js"
+ >
+
+
+ Scripts are disabled. Showing fallback image.
+
What else can we do with Bézier curves? Quite a lot, actually. The
diff --git a/lib/custom-element/api/base-api.js b/lib/custom-element/api/base-api.js
index 3dbe01cc..c2d70c4a 100644
--- a/lib/custom-element/api/base-api.js
+++ b/lib/custom-element/api/base-api.js
@@ -63,6 +63,8 @@ class BaseAPI {
this.cursor = {};
+ const release = typeof document !== "undefined" ? document : canvas;
+
[`touchstart`, `mousedown`].forEach((evtName) =>
canvas.addEventListener(evtName, (evt) => this.onMouseDown(evt))
);
@@ -72,7 +74,7 @@ class BaseAPI {
);
[`touchend`, `mouseup`].forEach((evtName) =>
- canvas.addEventListener(evtName, (evt) => this.onMouseUp(evt))
+ release.addEventListener(evtName, (evt) => this.onMouseUp(evt))
);
this.keyboard = {};
@@ -82,7 +84,7 @@ class BaseAPI {
);
[`keyup`].forEach((evtName) =>
- canvas.addEventListener(evtName, (evt) => this.onKeyUp(evt))
+ release.addEventListener(evtName, (evt) => this.onKeyUp(evt))
);
}
@@ -91,13 +93,13 @@ class BaseAPI {
*/
getCursorCoords(evt) {
const left = evt.target.offsetLeft,
- top = evt.target.offsetTop;
+ top = evt.target.offsetTop;
let x, y;
if (evt.targetTouches) {
const touch = evt.targetTouches;
- for (let i=0; i this.forceFocus();
+ canvas._force_listener = () => {
+ // I have NO idea why forceFocus() causes a scroll, but
+ // I don't have time to dig into what is no doubt deep
+ // black spec magic, so: check where we are, force the
+ // focus() state, and then force the scroll position to
+ // what it should be.
+ const x = Math.round(window.scrollX);
+ const y = Math.round(window.scrollY);
+ // Oh yeah... using round() because apparently scrollTo
+ // is not NOT idempotent and rounding errors cause it to
+ // drift. IT'S FUCKING HILARIOUS
+ this.forceFocus();
+ window.scrollTo(x, y);
+ };
[`touchstart`, `mousedown`].forEach((evtName) =>
canvas.addEventListener(evtName, canvas._force_listener)
);
@@ -254,6 +269,7 @@ function enhanceContext(ctx) {
strokeStyle: ctx.strokeStyle,
fillStyle: ctx.fillStyle,
lineWidth: ctx.lineWidth,
+ textAlign: ctx.textAlign,
transform: [m.a, m.b, m.c, m.d, m.e, m.f],
};
styles.push(e);
diff --git a/lib/custom-element/api/graphics-api.js b/lib/custom-element/api/graphics-api.js
index 5222b890..44d36bb8 100644
--- a/lib/custom-element/api/graphics-api.js
+++ b/lib/custom-element/api/graphics-api.js
@@ -7,12 +7,30 @@ import { BaseAPI } from "./base-api.js";
const MOUSE_PRECISION_ZONE = 5;
const TOUCH_PRECISION_ZONE = 30;
+let CURRENT_HUE = 0;
+
/**
* Our Graphics API, which is the "public" side of the API.
*/
class GraphicsAPI extends BaseAPI {
static get constants() {
- return [`POINTER`, `HAND`, `PI`, `TAU`, `POLYGON`, `CURVE`, `BEZIER`];
+ return [
+ `POINTER`,
+ `HAND`,
+ `PI`,
+ `TAU`,
+ `POLYGON`,
+ `CURVE`,
+ `BEZIER`,
+ `CENTER`,
+ `LEFT`,
+ `RIGHT`,
+ ];
+ }
+
+ draw() {
+ CURRENT_HUE = 0;
+ super.draw();
}
get PI() {
@@ -36,11 +54,22 @@ class GraphicsAPI extends BaseAPI {
get BEZIER() {
return Shape.BEZIER;
}
+ get CENTER() {
+ return `center`;
+ }
+ get LEFT() {
+ return `left`;
+ }
+ get RIGHT() {
+ return `right`;
+ }
onMouseDown(evt) {
super.onMouseDown(evt);
- const cdist = evt.targetTouches ? TOUCH_PRECISION_ZONE : MOUSE_PRECISION_ZONE;
+ const cdist = evt.targetTouches
+ ? TOUCH_PRECISION_ZONE
+ : MOUSE_PRECISION_ZONE;
for (let i = 0, e = this.moveable.length, p, d; i < e; i++) {
p = this.moveable[i];
@@ -92,6 +121,51 @@ class GraphicsAPI extends BaseAPI {
this.ctx.rotate(angle);
}
+ /**
+ * transforms: scale
+ */
+ scale(x, y) {
+ y = y ?? x;
+ this.ctx.scale(x, y);
+ }
+ /**
+ * transforms: screen to world
+ */
+ screenToWorld(x, y) {
+ if (y === undefined) {
+ y = x.y;
+ x = x.x;
+ }
+
+ let M = this.ctx.getTransform().invertSelf();
+
+ let ret = {
+ x: x * M.a + y * M.c + M.e,
+ y: x * M.b + y * M.d + M.f,
+ };
+
+ return ret;
+ }
+
+ /**
+ * transforms: world to screen
+ */
+ worldToScreen(x, y) {
+ if (y === undefined) {
+ y = x.y;
+ x = x.x;
+ }
+
+ let M = this.ctx.getTransform();
+
+ let ret = {
+ x: x * M.a + y * M.c + M.e,
+ y: x * M.b + y * M.d + M.f,
+ };
+
+ return ret;
+ }
+
/**
* transforms: reset
*/
@@ -131,6 +205,14 @@ class GraphicsAPI extends BaseAPI {
this.canvas.style.cursor = type;
}
+ /**
+ * Get a random color
+ */
+ randomColor(a = 1.0) {
+ CURRENT_HUE = (CURRENT_HUE + 73) % 360;
+ return `hsla(${CURRENT_HUE},50%,50%,${a})`;
+ }
+
/**
* Set the context fillStyle
*/
@@ -209,12 +291,18 @@ class GraphicsAPI extends BaseAPI {
/**
* Draw text on the canvas
*/
- text(str, x, y) {
+ text(str, x, y, alignment) {
if (y === undefined) {
y = x.y;
x = x.x;
}
+ const ctx = this.ctx;
+ if (alignment) {
+ ctx.cacheStyle();
+ ctx.textAlign = alignment;
+ }
this.ctx.fillText(str, x, y);
+ if (alignment) ctx.restoreStyle();
}
/**
@@ -225,6 +313,25 @@ class GraphicsAPI extends BaseAPI {
this.ctx.strokeRect(x, y, w, h);
}
+ /**
+ * Draw a function plot from [start] to [end] in [steps] steps.
+ * Returns the plot shape so that it can be cached for redrawing.
+ */
+ plot(fn, start = 0, end = 1, steps = 24) {
+ const ctx = this.ctx;
+ ctx.cacheStyle();
+ ctx.fillStyle = `transparent`;
+ const interval = end - start;
+ this.start();
+ for (let i = 0, e = steps - 1, v; i < steps; i++) {
+ v = fn(start + (interval * i) / e);
+ this.vertex(v.x, v.y);
+ }
+ this.end();
+ ctx.restoreStyle();
+ return this.currentShape;
+ }
+
/**
* A signal for starting a complex shape
*/
@@ -246,6 +353,14 @@ class GraphicsAPI extends BaseAPI {
this.currentShape.vertex({ x, y });
}
+ /**
+ * Draw a previously created shape
+ */
+ drawShape(shape) {
+ this.currentShape = shape;
+ this.end();
+ }
+
/**
* A signal to draw the current complex shape
*/
@@ -254,12 +369,56 @@ class GraphicsAPI extends BaseAPI {
let { x, y } = this.currentShape.first;
this.ctx.moveTo(x, y);
this.currentShape.segments.forEach((s) =>
- this[`draw${s.type}`](this.ctx, s.points, s.factor)
+ this[`draw${s.type}`](s.points, s.factor)
);
this.ctx.fill();
this.ctx.stroke();
}
+ /**
+ * Polygon draw function
+ */
+ drawPolygon(points) {
+ points.forEach((p) => this.ctx.lineTo(p.x, p.y));
+ }
+
+ /**
+ * Curve draw function, which draws a CR curve as a series of Beziers
+ */
+ drawCatmullRom(points, f) {
+ // invent a virtual first and last point
+ const f0 = points[0],
+ f1 = points[1],
+ fn = f0.reflect(f1),
+ l1 = points[points.length - 2],
+ l0 = points[points.length - 1],
+ ln = l0.reflect(l1),
+ cpoints = [fn, ...points, ln];
+
+ // four point sliding window over the segment
+ for (let i = 0, e = cpoints.length - 3; i < e; i++) {
+ let [c1, c2, c3, c4] = cpoints.slice(i, i + 4);
+ let p2 = {
+ x: c2.x + (c3.x - c1.x) / (6 * f),
+ y: c2.y + (c3.y - c1.y) / (6 * f),
+ };
+ let p3 = {
+ x: c3.x - (c4.x - c2.x) / (6 * f),
+ y: c3.y - (c4.y - c2.y) / (6 * f),
+ };
+ this.ctx.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, c3.x, c3.y);
+ }
+ }
+
+ /**
+ * Curve draw function, which assumes Bezier coordinates
+ */
+ drawBezier(points) {
+ for (let i = 0, e = points.length; i < e; i += 3) {
+ let [p1, p2, p3] = points.slice(i, i + 3);
+ this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
+ }
+ }
/**
* Yield a snapshot of the current shape.
*/
@@ -267,14 +426,6 @@ class GraphicsAPI extends BaseAPI {
return this.currentShape;
}
- /**
- * Draw a previously created shape
- */
- drawShape(shape) {
- this.currentShape = shape;
- this.end();
- }
-
/**
* convenient grid drawing function
*/
@@ -287,6 +438,30 @@ class GraphicsAPI extends BaseAPI {
}
}
+ /**
+ * convenient axis drawing function
+ *
+ * api.drawAxes(pad, "t",0,1, "S","0%","100%");
+ *
+ */
+ drawAxes(hlabel, hs, he, vlabel, vs, ve) {
+ const h = this.height;
+ const w = this.width;
+
+ this.line(0, 0, w, 0);
+ this.line(0, 0, 0, h);
+
+ const hpos = 0 - 5;
+ this.text(`${hlabel} →`, this.width / 2, hpos, this.CENTER);
+ this.text(hs, 0, hpos, this.CENTER);
+ this.text(he, w, hpos, this.CENTER);
+
+ const vpos = -10;
+ this.text(`${vlabel}\n↓`, vpos, this.height / 2, this.RIGHT);
+ this.text(vs, vpos, 0 + 5, this.RIGHT);
+ this.text(ve, vpos, h, this.RIGHT);
+ }
+
/**
* math functions
*/
diff --git a/lib/custom-element/api/types/bezier.js b/lib/custom-element/api/types/bezier.js
index fbae1100..8ecb6ced 100644
--- a/lib/custom-element/api/types/bezier.js
+++ b/lib/custom-element/api/types/bezier.js
@@ -5,11 +5,11 @@ import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
*/
class Bezier extends Original {
static defaultQuadratic(apiInstance) {
- return new Bezier(apiInstance, 70,250, 20,110, 220,60);
+ return new Bezier(apiInstance, 70, 250, 20, 110, 220, 60);
}
static defaultCubic(apiInstance) {
- return new Bezier(apiInstance, 110,150, 25,190, 210,250, 210,30);
+ return new Bezier(apiInstance, 110, 150, 25, 190, 210, 250, 210, 30);
}
constructor(apiInstance, ...coords) {
diff --git a/lib/custom-element/api/types/vector.js b/lib/custom-element/api/types/vector.js
index 591dbfcd..c67832ca 100644
--- a/lib/custom-element/api/types/vector.js
+++ b/lib/custom-element/api/types/vector.js
@@ -1,80 +1,77 @@
class Vector {
- constructor(x, y, z) {
- if (arguments.length === 1) {
- z = x.z;
- y = x.y;
- x = x.x;
- }
- this.x = x;
- this.y = y;
- if (z !== undefined) {
- this.z = z;
- }
+ constructor(x, y, z) {
+ if (arguments.length === 1) {
+ z = x.z;
+ y = x.y;
+ x = x.x;
}
- dist(other, y, z = 0) {
- if (y !== undefined) other = { x: other, y, z };
- let sum = 0;
- sum += (this.x - other.x) ** 2;
- sum += (this.y - other.y) ** 2;
- let z1 = this.z ?? 0;
- let z2 = other.z ?? 0;
- sum += (z1 - z2) ** 2;
- return sum ** 0.5;
- }
- normalize(f) {
- let mag = this.dist(0, 0, 0);
- return new Vector(
- (f * this.x) / mag,
- (f * this.y) / mag,
- (f * this.z) / mag
- );
- }
- getAngle() {
- return -Math.atan2(this.y, this.x);
- }
- reflect(other) {
- let p = new Vector(
- other.x - this.x,
- other.y - this.y
- );
- if (other.z !== undefined) {
- p.z = other.z
- if (this.z !== undefined) {
- p.z -= this.z;
- }
- }
- return this.subtract(p);
- }
- add(other) {
- let p = new Vector(this.x + other.x, this.y + other.y);
- if (this.z !== undefined) {
- p.z = this.z;
- if (other.z !== undefined) {
- p.z += other.z;
- }
- }
- return p;
- }
- subtract(other) {
- let p = new Vector(this.x - other.x, this.y - other.y);
- if (this.z !== undefined) {
- p.z = this.z;
- if (other.z !== undefined) {
- p.z -= other.z;
- }
- }
- return p;
- }
- scale(f = 1) {
- if (f === 0) {
- return new Vector(0, 0, this.z === undefined ? undefined : 0);
- }
- let p = new Vector(this.x * f, this.y * f);
- if (this.z !== undefined) {
- p.z = this.z * f;
- }
- return p;
+ this.x = x;
+ this.y = y;
+ if (z !== undefined) {
+ this.z = z;
}
}
+ dist(other, y, z = 0) {
+ if (y !== undefined) other = { x: other, y, z };
+ let sum = 0;
+ sum += (this.x - other.x) ** 2;
+ sum += (this.y - other.y) ** 2;
+ let z1 = this.z ?? 0;
+ let z2 = other.z ?? 0;
+ sum += (z1 - z2) ** 2;
+ return sum ** 0.5;
+ }
+ normalize(f) {
+ let mag = this.dist(0, 0, 0);
+ return new Vector(
+ (f * this.x) / mag,
+ (f * this.y) / mag,
+ (f * this.z) / mag
+ );
+ }
+ getAngle() {
+ return -Math.atan2(this.y, this.x);
+ }
+ reflect(other) {
+ let p = new Vector(other.x - this.x, other.y - this.y);
+ if (other.z !== undefined) {
+ p.z = other.z;
+ if (this.z !== undefined) {
+ p.z -= this.z;
+ }
+ }
+ return this.subtract(p);
+ }
+ add(other) {
+ let p = new Vector(this.x + other.x, this.y + other.y);
+ if (this.z !== undefined) {
+ p.z = this.z;
+ if (other.z !== undefined) {
+ p.z += other.z;
+ }
+ }
+ return p;
+ }
+ subtract(other) {
+ let p = new Vector(this.x - other.x, this.y - other.y);
+ if (this.z !== undefined) {
+ p.z = this.z;
+ if (other.z !== undefined) {
+ p.z -= other.z;
+ }
+ }
+ return p;
+ }
+ scale(f = 1) {
+ if (f === 0) {
+ return new Vector(0, 0, this.z === undefined ? undefined : 0);
+ }
+ let p = new Vector(this.x * f, this.y * f);
+ if (this.z !== undefined) {
+ p.z = this.z * f;
+ }
+ return p;
+ }
+}
- export { Vector };
+export { Vector };
diff --git a/lib/custom-element/api/util/shape.js b/lib/custom-element/api/util/shape.js
index 2e5f10b7..fd9c64d2 100644
--- a/lib/custom-element/api/util/shape.js
+++ b/lib/custom-element/api/util/shape.js
@@ -1,17 +1,3 @@
-/**
- * A shape subpath
- */
-class Segment {
- constructor(type, factor) {
- this.type = type;
- this.factor = factor;
- this.points = [];
- }
- add(p) {
- this.points.push(p);
- }
-}
-
/**
* A complex shape, represented as a collection of paths
* that can be either polygon, Catmull-Rom curves, or
@@ -40,4 +26,18 @@ Shape.POLYGON = `Polygon`;
Shape.CURVE = `CatmullRom`;
Shape.BEZIER = `Bezier`;
+/**
+ * A shape subpath
+ */
+class Segment {
+ constructor(type, factor) {
+ this.type = type;
+ this.factor = factor;
+ this.points = [];
+ }
+ add(p) {
+ this.points.push(p);
+ }
+}
+
export { Shape, Segment };
diff --git a/lib/custom-element/custom-element.js b/lib/custom-element/custom-element.js
index a7cddb16..a6abf7cf 100644
--- a/lib/custom-element/custom-element.js
+++ b/lib/custom-element/custom-element.js
@@ -42,14 +42,14 @@ class CustomElement extends HTMLElement {
super();
if (!customElements.resolveScope) {
- customElements.resolveScope = function(scope) {
+ customElements.resolveScope = function (scope) {
try {
return scope.getRootNode().host;
} catch (e) {
console.warn(e);
}
return window;
- }
+ };
}
this._options = options;
@@ -63,7 +63,11 @@ class CustomElement extends HTMLElement {
this.render();
},
attributes: (record) => {
- this.handleAttributeChange(record.attributeName, record.oldValue, this.getAttribute(record.attributeName));
+ this.handleAttributeChange(
+ record.attributeName,
+ record.oldValue,
+ this.getAttribute(record.attributeName)
+ );
this.render();
},
};
diff --git a/lib/custom-element/graphics-element.js b/lib/custom-element/graphics-element.js
index 4dbe1f5e..58977d6c 100644
--- a/lib/custom-element/graphics-element.js
+++ b/lib/custom-element/graphics-element.js
@@ -35,7 +35,8 @@ class GraphicsElement extends CustomElement {
:host style { display: none; }
:host canvas { display: block; margin: auto; border-radius: 0; }
:host canvas:focus { ${
- this.getAttribute(`focus-css`) || `border: 1px solid red !important; margin: -1px; `
+ this.getAttribute(`focus-css`) ||
+ `border: 1px solid red !important; margin: -1px; `
} }
:host a.view-source { float: left; font-size: 60%; text-decoration: none; }
:host label { display: block; font-style:italic; font-size: 0.9em; text-align: right; }
@@ -160,7 +161,9 @@ class GraphicsElement extends CustomElement {
const script = (this.script = document.createElement(`script`));
script.type = "module";
- script.src = `data:application/javascript;charset=utf-8,${encodeURIComponent(this.code)}`;
+ script.src = `data:application/javascript;charset=utf-8,${encodeURIComponent(
+ this.code
+ )}`;
if (rerender) this.render();
}
@@ -219,10 +222,12 @@ class GraphicsElement extends CustomElement {
if (this.canvas) slotParent.insertBefore(this.canvas, this._slot);
if (this.label) slotParent.insertBefore(this.label, this._slot);
- const a = document.createElement('a');
- a.classList.add('view-source');
+ const a = document.createElement("a");
+ a.classList.add("view-source");
a.textContent = `view source`;
- a.href = new URL(`data:text/plain;charset=utf-8,${encodeURIComponent(this.rawCode)}`);
+ a.href = new URL(
+ `data:text/plain;charset=utf-8,${encodeURIComponent(this.rawCode)}`
+ );
a.target = `_blank`;
if (this.label) slotParent.insertBefore(a, this.label);
}
diff --git a/lib/custom-element/lib/perform-code-surgery.js b/lib/custom-element/lib/perform-code-surgery.js
index d83c1512..c6ff6c19 100644
--- a/lib/custom-element/lib/perform-code-surgery.js
+++ b/lib/custom-element/lib/perform-code-surgery.js
@@ -1,7 +1,6 @@
import { GraphicsAPI } from "../api/graphics-api.js";
export default function performCodeSurgery(code) {
-
// 1. ensure that anything that needs to run by first calling its super function, does so.
GraphicsAPI.superCallers.forEach((name) => {
diff --git a/lib/custom-element/lib/split-code-sections.js b/lib/custom-element/lib/split-code-sections.js
index c603a18e..f3c96785 100644
--- a/lib/custom-element/lib/split-code-sections.js
+++ b/lib/custom-element/lib/split-code-sections.js
@@ -3,37 +3,37 @@
* We're going to regexp our way to flawed victory here.
*/
export default function splitCodeSections(code) {
- const re = /\b[\w\W][^\s]*?\([^)]*\)[\r\n\s]*{/;
- const cuts = [];
- for(let result = code.match(re); result; result=code.match(re)) {
- result = result[0];
+ const re = /\b[\w\W][^\s]*?\([^)]*\)[\r\n\s]*{/;
+ const cuts = [];
+ for (let result = code.match(re); result; result = code.match(re)) {
+ result = result[0];
- let start = code.indexOf(result);
- let end = start + result.length;
- let depth = 0;
- let slice = Array.from(code).slice(start + result.length);
+ let start = code.indexOf(result);
+ let end = start + result.length;
+ let depth = 0;
+ let slice = Array.from(code).slice(start + result.length);
- slice.some((c,pos) => {
- if (c === `{`) {
- depth++;
+ slice.some((c, pos) => {
+ if (c === `{`) {
+ depth++;
+ return false;
+ }
+ if (c === `}`) {
+ if (depth > 0) {
+ depth--;
return false;
}
- if (c === `}`) {
- if (depth > 0) {
- depth--;
- return false;
- }
- end += pos + 1;
- return true;
- }
- });
+ end += pos + 1;
+ return true;
+ }
+ });
- let cut = code.slice(start, end);
- cuts.push(cut);
- code = code.replace(cut, ``);
- }
- return {
- quasiGlobal: code,
- classCode: cuts.join(`\n`)
- };
- }
\ No newline at end of file
+ let cut = code.slice(start, end);
+ cuts.push(cut);
+ code = code.replace(cut, ``);
+ }
+ return {
+ quasiGlobal: code,
+ classCode: cuts.join(`\n`),
+ };
+}
diff --git a/lib/site/referrer.js b/lib/site/referrer.js
index 1118d060..6884c395 100644
--- a/lib/site/referrer.js
+++ b/lib/site/referrer.js
@@ -24,16 +24,28 @@
*
*/
(function referrer(l) {
- var page = l.substring(l.lastIndexOf('/')+1).replace(".html",'');
+ var page = l.substring(l.lastIndexOf("/") + 1).replace(".html", "");
page = page || "index.html";
// we don't care about file or localhost, for obvious reasons
var loc = window.location.toString();
- if(loc.indexOf("file:///")!==-1) return;
- if(loc.indexOf("localhost")!==-1) return;
+ if (loc.indexOf("file:///") !== -1) return;
+ if (loc.indexOf("localhost") !== -1) return;
// right, continue
var url = "http://pomax.nihongoresources.com/pages/bezierinfo/logger.php";
var xhr = new XMLHttpRequest();
- xhr.open("GET", url + "?" + "referrer=" + encodeURIComponent(document.referrer) + "&for=" + page, true);
- try { xhr.send(null); }
- catch(e) { /* you don't care about this error, and I can't see it, so why would we do anything with it? */ }
-}(window.location.toString()));
+ xhr.open(
+ "GET",
+ url +
+ "?" +
+ "referrer=" +
+ encodeURIComponent(document.referrer) +
+ "&for=" +
+ page,
+ true
+ );
+ try {
+ xhr.send(null);
+ } catch (e) {
+ /* you don't care about this error, and I can't see it, so why would we do anything with it? */
+ }
+})(window.location.toString());
diff --git a/lib/site/social-updater.js b/lib/site/social-updater.js
index 45d862a4..89c52095 100644
--- a/lib/site/social-updater.js
+++ b/lib/site/social-updater.js
@@ -4,7 +4,6 @@
* base article itself.
*/
(function tryToBind() {
-
if (!document.querySelector(`map[name="rhtimap"]`)) {
return setTimeout(tryToBind, 300);
}
@@ -13,33 +12,37 @@
constructor() {
this.section = false;
this.hash = false;
- this.socials = ["rdt","hn","twt"];
+ this.socials = ["rdt", "hn", "twt"];
}
update(data) {
this.section = data.section;
this.hash = data.hash;
- this.socials.forEach(social => {
+ this.socials.forEach((social) => {
var area = document.querySelector(`map area.sclnk-${social}`);
area.href = this[`get_${social}`]();
});
}
get url() {
- return encodeURIComponent(`https://pomax.github.io/bezierinfo${this.hash ? this.hash : ''}`);
+ return encodeURIComponent(
+ `https://pomax.github.io/bezierinfo${this.hash ? this.hash : ""}`
+ );
}
getTitle() {
var title = `A Primer on Bézier Curves`;
if (this.section) {
- title = `${this.section}-${title}`;
+ title = `${this.section}-${title}`;
}
return encodeURIComponent(title);
}
get_rdt() {
var title = this.getTitle();
- var text = encodeURIComponent(`A free, online book for when you really need to know how to do Bézier things.`);
+ var text = encodeURIComponent(
+ `A free, online book for when you really need to know how to do Bézier things.`
+ );
return `https://www.reddit.com/submit?url=${this.url}&title=${title}&text=${text}`;
}
@@ -49,32 +52,40 @@
}
get_twt() {
- var text = encodeURIComponent(`Reading "${this.section}" by @TheRealPomax over on `) + this.url;
+ var text =
+ encodeURIComponent(
+ `Reading "${this.section}" by @TheRealPomax over on `
+ ) + this.url;
return `https://twitter.com/intent/tweet?original_referer=${this.url}&text=${text}&hashtags=bezier,curves,maths`;
}
- };
-
+ }
// we set the section and fragmentid based on which ever section's heading is nearest
// the top of the screen, either just off-screen or in-screen
var tracker = new Tracker();
var anchors = Array.from(document.querySelectorAll("section h2 a"));
- var sections = anchors.map(a => a.parentNode);
- var sectionData = sections.map(section => {
+ var sections = anchors.map((a) => a.parentNode);
+ var sectionData = sections.map((section) => {
return {
section: section.textContent,
- hash: section.querySelector("a").hash
+ hash: section.querySelector("a").hash,
};
});
- window.addEventListener("scroll", function(evt) {
- var min = 99999999999999999;
- var element = false;
- sections.forEach( (s,pos) => {
- var v = Math.abs(s.getBoundingClientRect().top);
- if (v < min) { min = v; element = pos; }
- });
- tracker.update(sectionData[element]);
- }, { passive: true });
-
-}());
+ window.addEventListener(
+ "scroll",
+ function (evt) {
+ var min = 99999999999999999;
+ var element = false;
+ sections.forEach((s, pos) => {
+ var v = Math.abs(s.getBoundingClientRect().top);
+ if (v < min) {
+ min = v;
+ element = pos;
+ }
+ });
+ tracker.update(sectionData[element]);
+ },
+ { passive: true }
+ );
+})();
diff --git a/package-lock.json b/package-lock.json
index c9afac72..91d9938f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -280,10 +280,6 @@
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
- "codesurgeon": {
- "version": "file:lib/custom-element",
- "dev": true
- },
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
diff --git a/package.json b/package.json
index 065dc162..db113c39 100644
--- a/package.json
+++ b/package.json
@@ -8,19 +8,21 @@
"homepage": "https://pomax.github.io/bezierinfo",
"repository": {
"type": "git",
- "url": "git+https://github.com/Pomax/bezierinfo.git"
+ "url": "git+https://github.com/Pomax/BezierInfo-2.git"
},
"bugs": {
- "url": "https://github.com/Pomax/bezierinfo/issues"
+ "url": "https://github.com/Pomax/BezierInfo-2/issues"
},
"scripts": {
- "time": "node ./mark.js",
- "start": "run-s time lint build time",
- "lint": "prettier ./tools --write",
- "build": "node ./tools/build.js",
+ "start": "rm -f .timing && run-s time lint:* build time",
"test": "run-p server browser",
+ "---": "---",
+ "browser": "open-cli http://localhost:8000",
+ "build": "node ./tools/build.js",
+ "lint:tools": "prettier ./tools --write",
+ "lint:lib": "prettier ./lib --write",
"server": "http-server -p 8000 --cors",
- "browser": "open-cli http://localhost:8000"
+ "time": "node ./mark.js"
},
"keywords": [
"Bezier",
@@ -33,7 +35,6 @@
"devDependencies": {
"bezier-js": "^2.6.1",
"canvas": "^2.6.1",
- "codesurgeon": "file:./lib/custom-element",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"http-server": "^0.12.3",
diff --git a/tools/build.js b/tools/build.js
index fb56f05b..fe6cf128 100644
--- a/tools/build.js
+++ b/tools/build.js
@@ -1,7 +1,7 @@
import LocaleStrings from "./locale-strings.js";
-import getAllChapterFiles from "./build/get-all-chapter-files.js";
-import processLocale from "./build/process-locale.js";
-import createIndexPages from "./build/create-index-page.js";
+import { getAllChapterFiles } from "./build/get-all-chapter-files.js";
+import { processLocale } from "./build/process-locale.js";
+import { createIndexPages } from "./build/create-index-page.js";
/**
* main entry point:
@@ -10,6 +10,7 @@ import createIndexPages from "./build/create-index-page.js";
*/
getAllChapterFiles().then((chapterFiles) => {
const languageCodes = Object.keys(chapterFiles);
+
languageCodes.forEach(async (locale) => {
const localeStrings = new LocaleStrings(locale);
const chapters = await processLocale(locale, localeStrings, chapterFiles);
diff --git a/tools/build/create-index-page.js b/tools/build/create-index-page.js
index ec5bb450..aaf36dce 100644
--- a/tools/build/create-index-page.js
+++ b/tools/build/create-index-page.js
@@ -11,11 +11,7 @@ nunjucks.configure(".", { autoescape: false });
/**
* ...docs go here...
*/
-export default async function createIndexPages(
- locale,
- localeStrings,
- chapters
-) {
+async function createIndexPages(locale, localeStrings, chapters) {
const defaultLocale = localeStrings.getDefaultLocale();
const base = locale !== defaultLocale ? `` : ``;
const langSwitcher = generateLangSwitcher(localeStrings);
@@ -70,3 +66,5 @@ export default async function createIndexPages(
fs.writeFileSync(path.join(locale, `index.html`), data, `utf8`);
}
}
+
+export { createIndexPages };
diff --git a/tools/build/get-all-chapter-files.js b/tools/build/get-all-chapter-files.js
index acf1a55d..7c3ac581 100644
--- a/tools/build/get-all-chapter-files.js
+++ b/tools/build/get-all-chapter-files.js
@@ -9,7 +9,8 @@ const BASEDIR = path.join(__dirname, "..", "..");
/**
* ...docs go here...
*/
-export default /* async */ function getAllChapterFiles() {
+
+/* async */ function getAllChapterFiles() {
return new Promise((resolve, reject) => {
glob(path.join(BASEDIR, `chapters/**/content*md`), (err, files) => {
if (err) reject(err);
@@ -28,3 +29,5 @@ export default /* async */ function getAllChapterFiles() {
});
});
}
+
+export { getAllChapterFiles };
diff --git a/tools/build/rewrite-graphics-element.js b/tools/build/graphics/generate-graphics-module.js
similarity index 70%
rename from tools/build/rewrite-graphics-element.js
rename to tools/build/graphics/generate-graphics-module.js
index f3af449f..2164fb6c 100644
--- a/tools/build/rewrite-graphics-element.js
+++ b/tools/build/graphics/generate-graphics-module.js
@@ -1,8 +1,11 @@
-import splitCodeSections from "../../lib/custom-element/lib/split-code-sections.js";
-import performCodeSurgery from "../../lib/custom-element/lib/perform-code-surgery.js";
+import splitCodeSections from "../../../lib/custom-element/lib/split-code-sections.js";
+import performCodeSurgery from "../../../lib/custom-element/lib/perform-code-surgery.js";
import prettier from "prettier";
-export default function rewriteGraphicsElement(code, width, height) {
+/**
+ * ...docs go here...
+ */
+function generateGraphicsModule(code, width, height) {
const split = splitCodeSections(code);
const globalCode = split.quasiGlobal;
const classCode = performCodeSurgery(split.classCode);
@@ -10,7 +13,7 @@ export default function rewriteGraphicsElement(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 } from "../../../lib/custom-element/api/graphics-api.js";
const noop = (()=>{});
@@ -37,3 +40,5 @@ export default function rewriteGraphicsElement(code, width, height) {
{ parser: `babel` }
);
}
+
+export { generateGraphicsModule };
diff --git a/tools/build/generate-placeholders.js b/tools/build/graphics/generate-placeholders.js
similarity index 77%
rename from tools/build/generate-placeholders.js
rename to tools/build/graphics/generate-placeholders.js
index 8bb50462..2c8bb881 100644
--- a/tools/build/generate-placeholders.js
+++ b/tools/build/graphics/generate-placeholders.js
@@ -1,11 +1,14 @@
import fs from "fs-extra";
import path from "path";
-import rewriteGraphicsElement from "./rewrite-graphics-element.js";
+import { generateGraphicsModule } from "./generate-graphics-module.js";
const moduleURL = new URL(import.meta.url);
const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``));
-export default async function generatePlaceHolders(localeStrings, markdown) {
+/**
+ * ...docs go here...
+ */
+async function generatePlaceHolders(localeStrings, markdown) {
const locale = localeStrings.getCurrentLocale();
if (locale !== localeStrings.getDefaultLocale()) return;
@@ -36,20 +39,27 @@ export default async function generatePlaceHolders(localeStrings, markdown) {
sourcePaths.map(async (srcPath, i) => {
try {
// Get the sketch code
- const sourcePath = path.join(__dirname, "..", "..", srcPath);
- const code = fs.readFileSync(sourcePath).toString(`utf8`);
+ const sourcePath = path.join(__dirname, "..", "..", "..", srcPath);
+ let code;
+ try {
+ code = fs.readFileSync(sourcePath).toString(`utf8`);
+ } catch (e) {
+ console.log(srcPath, sourcePath);
+ throw e;
+ }
const width = elements[keys[i]].match(`width="([^"]+)"`)[1];
const height = elements[keys[i]].match(`height="([^"]+)"`)[1];
// Convert this to a valid JS module code and write this to
// a temporary file so we can import it.
- const nodeCode = rewriteGraphicsElement(code, width, height);
+ 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`);
// Import our entirely valid JS module, which will run the
- // sketch code and
+ // sketch code and export a canvas instance that we can turn
+ // into an actual image file.
const { canvas } = await import(fileName);
fs.unlinkSync(tempFile);
@@ -59,6 +69,7 @@ export default async function generatePlaceHolders(localeStrings, markdown) {
const imageData = Buffer.from(dataURI.substring(start), `base64`);
const destPath = path.join(__dirname, "..", "..", "images", srcPath);
const filename = destPath.replace(`.js`, `.png`);
+
// console.log(`Writing placeholder to ${filename}`);
fs.ensureDirSync(path.dirname(destPath));
fs.writeFileSync(filename, imageData);
@@ -68,3 +79,5 @@ export default async function generatePlaceHolders(localeStrings, markdown) {
})
);
}
+
+export { generatePlaceHolders };
diff --git a/tools/build/convert-markdown.js b/tools/build/markdown/convert-markdown.js
similarity index 52%
rename from tools/build/convert-markdown.js
rename to tools/build/markdown/convert-markdown.js
index e0f7211b..ff095db4 100644
--- a/tools/build/convert-markdown.js
+++ b/tools/build/markdown/convert-markdown.js
@@ -1,27 +1,30 @@
import marked from "marked";
-import latexToSVG from "./latex/latex-to-svg.js";
-import injectGraphicsFallback from "./markdown/inject-fallback.js";
-import extractLaTeX from "./markdown/extract-latex.js";
+import latexToSVG from "../latex/latex-to-svg.js";
+import preprocessGraphicsElement from "./preprocess-graphics-element.js";
+import extractLaTeX from "./extract-latex.js";
import nunjucks from "nunjucks";
nunjucks.configure(".", { autoescape: false });
/**
* ...docs go here...
*/
-export default async function convertMarkDown(
- chapter,
- localeStrings,
- markdown
-) {
- markdown = injectGraphicsFallback(chapter, localeStrings, markdown);
+async function convertMarkDown(chapter, localeStrings, markdown) {
+ markdown = preprocessGraphicsElement(chapter, localeStrings, markdown);
+ // This yields the original markdown with all LaTeX blocked replaced with
+ // uniquely named templating variables, referencing keys in the `latex` array.
const { data, latex } = extractLaTeX(markdown);
await Promise.all(
- Object.keys(latex).map(async (key, pos) => {
- const svg = await latexToSVG(latex[key], chapter, localeStrings, pos + 1);
- return (latex[key] = svg);
- })
+ Object.keys(latex).map(
+ async (key, pos) =>
+ (latex[key] = await latexToSVG(
+ latex[key],
+ chapter,
+ localeStrings,
+ pos + 1
+ ))
+ )
);
let converted = marked(data, {
@@ -44,3 +47,5 @@ export default async function convertMarkDown(
return nunjucks.renderString(converted, latex);
}
+
+export { convertMarkDown };
diff --git a/tools/build/markdown/inject-fallback.js b/tools/build/markdown/inject-fallback.js
deleted file mode 100644
index e3a9326a..00000000
--- a/tools/build/markdown/inject-fallback.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * ...docs go here...
- */
-export default function injectGraphicsFallback(
- chapter,
- localeStrings,
- markdown
-) {
- const translate = localeStrings.translate;
-
- let pos = -1,
- data = markdown,
- startmark = ``;
-
- do {
- pos = data.indexOf(startmark, pos);
- if (pos !== -1) {
- let endpos = data.indexOf(endmark, pos) + endmark.length;
- let slice = data.slice(pos, endpos);
- let updated = slice.replace(
- /width="([^"]+)"\s+height="([^"]+)"\s+src="([^"]+)"\s*>/,
- (_, width, height, src) => {
- src = src.replace(`./`, `./chapters/${chapter}/`);
- let img = src.replace(`./`, `./images/`).replace(`.js`, `.png`);
- return `width="${width}" height="${height}" src="${src}">
-
-
- ${translate`disabledMessage`}
- `;
- }
- );
- data = data.replace(slice, updated);
- pos += updated.length;
- }
- } while (pos !== -1);
-
- return data;
-}
diff --git a/tools/build/markdown/preprocess-graphics-element.js b/tools/build/markdown/preprocess-graphics-element.js
new file mode 100644
index 00000000..85aa5a7b
--- /dev/null
+++ b/tools/build/markdown/preprocess-graphics-element.js
@@ -0,0 +1,67 @@
+/**
+ * ...docs go here...
+ */
+function preprocessGraphicsElement(chapter, localeStrings, markdown) {
+ const translate = localeStrings.translate;
+
+ let pos = -1,
+ data = markdown,
+ startmark = ``;
+
+ do {
+ pos = data.indexOf(startmark, pos);
+ if (pos !== -1) {
+ // extract a ... segment
+ let endpos = data.indexOf(endmark, pos) + endmark.length;
+ let slice = data.slice(pos, endpos);
+ let updated = slice;
+
+ // if there are no width/height attributes, inject them
+
+ // FIXME: This will not work if there is UI html that
+ // TODO: uses width/height attributes, of course!
+
+ if (updated.indexOf(`width=`) === -1)
+ updated = updated.replace(
+ /title="([^"]+)"\s*/,
+ `title="$1" width="275" `
+ );
+
+ if (updated.indexOf(`height=`) === -1)
+ updated = updated.replace(
+ /width="(\d+)\s*"/,
+ `width="$1" height="275" `
+ );
+
+ // Then add in the fallback code
+ updated = updated.replace(
+ /width="([^"]+)"\s+height="([^"]+)"\s+src="([^"]+)"\s*>/,
+ (_, width, height, src) => {
+ if (src.indexOf(`../`) === 0) src = `./chapters/${chapter}/${src}`;
+ else {
+ if (src[0] !== `.`) src = `./${src}`;
+ src = src.replace(`./`, `./chapters/${chapter}/`);
+ }
+
+ let img = src.replace(`./`, `./images/`).replace(`.js`, `.png`);
+
+ // TODO: generate fallback image right here, since this is where we need
+ // to know what the code-hash is so we can properly link images.
+
+ return `width="${width}" height="${height}" src="${src}">
+
+
+ ${translate`disabledMessage`}
+ `;
+ }
+ );
+ data = data.replace(slice, updated);
+ pos += updated.length;
+ }
+ } while (pos !== -1);
+
+ return data;
+}
+
+export default preprocessGraphicsElement;
diff --git a/tools/build/process-locale.js b/tools/build/process-locale.js
index c0f64a02..07242ac0 100644
--- a/tools/build/process-locale.js
+++ b/tools/build/process-locale.js
@@ -1,7 +1,7 @@
import fs from "fs-extra";
import path from "path";
-import convertMarkDown from "./convert-markdown.js";
-import generatePlaceHolders from "./generate-placeholders.js";
+import { convertMarkDown } from "./markdown/convert-markdown.js";
+import { generatePlaceHolders } from "./graphics/generate-placeholders.js";
import nunjucks from "nunjucks";
import toc from "../../chapters/toc.js";
@@ -23,13 +23,8 @@ nunjucks.configure(".", { autoescape: false });
/**
* ...docs go here...
*/
-export default async function processLocale(
- locale,
- localeStrings,
- chapterFiles
-) {
+async function processLocale(locale, localeStrings, chapterFiles) {
const defaultLocale = localeStrings.getDefaultLocale();
- const translate = localeStrings.translate;
const localeFiles = chapterFiles[locale];
let localized = 0;
@@ -80,3 +75,5 @@ export default async function processLocale(
return chapters;
}
+
+export { processLocale };
diff --git a/tools/images/chapters/control/lerp-cubic.png b/tools/images/chapters/control/lerp-cubic.png
new file mode 100644
index 0000000000000000000000000000000000000000..104451e78008037c1ae9e557917203ebe9e605bb
GIT binary patch
literal 16566
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
literal 0
HcmV?d00001
diff --git a/tools/images/chapters/control/lerp-quadratic.png b/tools/images/chapters/control/lerp-quadratic.png
new file mode 100644
index 0000000000000000000000000000000000000000..78a2fe515dc9af52ac31866b77dd241c7d7f8ff2
GIT binary patch
literal 13980
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