1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-02 04:42:43 +02:00
This commit is contained in:
Pomax
2020-08-09 17:16:50 -07:00
committed by GitHub
parent fc30421eae
commit c97b1b635e
43 changed files with 812 additions and 465 deletions

View File

@@ -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 <i>t</i> value.
<div class="figure">
<Graphic inline={true} title="Quadratic interpolations" draw={this.drawQuadraticLerp}/>
<Graphic inline={true} title="Cubic interpolations" draw={this.drawCubicLerp}/>
<Graphic inline={true} title="15th degree interpolations" draw={this.draw15thLerp}/>
<graphics-element title="Quadratic interpolations" src="./lerp-quadratic.js"></graphics-element>
<graphics-element title="Cubic interpolations" src="./lerp-cubic.js"></graphics-element>
<graphics-element title="15th degree interpolations" src="./lerp-fifteenth.js"></graphics-element>
</div>
Also shown is the interpolation function for a 15<sup>th</sup> 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:
<Graphic title="Our cubic Bézier curve" setup={this.drawCubic} draw={this.drawCurve}/>
<graphics-element title="Our cubic Bézier curve" src="../introduction/cubic.js"></graphics-element>
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.

View File

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

View File

@@ -1,3 +0,0 @@
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("control", handler);

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ There we go. <i>x</i>/<i>y</i> coordinates, linked through some mystery value <i
So, parametric curves don't define a <i>y</i> coordinate in terms of an <i>x</i> coordinate, like normal functions do, but they instead link the values to a "control" variable. If we vary the value of <i>t</i>, then with every change we get <strong>two</strong> values, which we can use as (<i>x</i>,<i>y</i>) coordinates in a graph. The above set of functions, for instance, generates points on a circle: We can range <i>t</i> from negative to positive infinity, and the resulting (<i>x</i>,<i>y</i>) coordinates will always lie on a circle with radius 1 around the origin (0,0). If we plot it for <i>t</i> from 0 to 5, we get this (use your up and down arrow keys to change the plot end value):
<graphics-element title="A (partial) circle: x=sin(t), y=cos(t)" width="275" height="275" src="./circle.js"></graphics-element>
<graphics-element title="A (partial) circle: x=sin(t), y=cos(t)" src="./circle.js"></graphics-element>
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 <i>x</i> and <i>y</i> 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 <i>x</i> and <i>y</i> outputs. So what are binomial polynomials?

View File

@@ -39,7 +39,7 @@
というわけで、普通の関数では<i>y</i>座標を<i>x</i>座標によって定義しますが、パラメトリック曲線ではそうではなく、座標の値を「制御」変数と結びつけます。<i>t</i>の値を変化させるたびに<strong>2つ</strong>の値が変化するので、これをグラフ上の座標 (<i>x</i>,<i>y</i>)として使うことができます。例えば、先ほどの関数の組は円周上の点を生成します。負の無限大から正の無限大へと<i>t</i>を動かすと、得られる座標(<i>x</i>,<i>y</i>)は常に中心(0,0)・半径1の円の上に乗ります。<i>t</i>を0から5まで変化させてプロットした場合は、このようになります上下キーでプロットの上限を変更できます
<graphics-element title="(部分)円 x=sin(t), y=cos(t)" width="275" height="275" src="./circle.js"></graphics-element>
<graphics-element title="(部分)円 x=sin(t), y=cos(t)" src="./circle.js"></graphics-element>
ベジエ曲線はパラメトリック関数の一種であり、どの次元に対しても同じ基底関数を使うという点で特徴づけられます。先ほどの例では、<i>x</i>の値と<i>y</i>の値とで異なる関数(正弦関数と余弦関数)を使っていましたが、ベジエ曲線では<i>x</i><i>y</i>の両方で「二項係数多項式」を使います。では、二項係数多項式とは何でしょう?

View File

@@ -39,7 +39,7 @@
所以,参数曲线不像一般函数那样,通过<i>x</i>坐标来定义<i>y</i>坐标,而是用一个“控制”变量将它们连接起来。如果改变<i>t</i>的值,每次变化时我们都能得到<strong>两个</strong>值,这可以作为图形中的(<i>x</i>,<i>y</i>)坐标。比如上面的方程组,生成位于一个圆上的点:我们可以使<i>t</i>在正负极值间变化,得到的输出(<i>x</i>,<i>y</i>)都会位于一个以原点(0,0)为中心且半径为1的圆上。如果我们画出<i>t</i>从0到5时的值将得到如下图像你可以用上下键来改变画的点和值
<graphics-element title="(一部分的)圆: x=sin(t), y=cos(t)" width="275" height="275" src="./circle.js"></graphics-element>
<graphics-element title="(一部分的)圆: x=sin(t), y=cos(t)" src="./circle.js"></graphics-element>
贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中<i>x</i>值和<i>y</i>值使用了不同的方程,与此不同的是,贝塞尔曲线的<i>x</i><i>y</i>都用了“二项多项式”。那什么是二项多项式呢?

View File

@@ -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.
<div class="figure">
<graphics-element title="A quadratic Bézier curve" width="275" height="275" src="./quadratic.js"></graphics-element>
<graphics-element title="A cubic Bézier curve" width="275" height="275" src="./cubic.js"></graphics-element>
<graphics-element title="A quadratic Bézier curve" src="./quadratic.js"></graphics-element>
<graphics-element title="A cubic Bézier curve" src="./cubic.js"></graphics-element>
</div>
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!

View File

@@ -3,8 +3,8 @@
まずは良い例から始めましょう。ベジエ曲線というのは、下の図に表示されているもののことです。ベジエ曲線はある始点からある終点へと延びており、その曲率は1個以上の「中間」制御点に左右されています。さて、このページの図はどれもインタラクティブになっていますので、ここで曲線をちょっと操作してみましょう。点をドラッグしたとき、曲線の形がそれに応じてどう変化するのか、確かめてみてください。
<div class="figure">
<graphics-element title="2次のベジエ曲線" width="275" height="275" src="./quadratic.js"></graphics-element>
<graphics-element title="3次のベジエ曲線" width="275" height="275" src="./cubic.js"></graphics-element>
<graphics-element title="2次のベジエ曲線" src="./quadratic.js"></graphics-element>
<graphics-element title="3次のベジエ曲線" src="./cubic.js"></graphics-element>
</div>
ベジエ曲線は、CADcomputer aided designやCAMcomputer aided manufacturingのアプリケーションで多用されています。もちろん、Adobe Illustrator・Photoshop・Inkscape・Gimp などのグラフィックデザインアプリケーションや、SVGscalable vector graphics・OpenTypeフォントotf/ttfのようなグラフィック技術でも利用されています。ベジエ曲線はたくさんのものに使われていますので、これについてもっと詳しく学びたいのであれば……さあ、準備しましょう

View File

@@ -3,8 +3,8 @@
让我们有个好的开始:当我们在谈论贝塞尔曲线的时候,所指的就是你在如下图像看到的东西。它们从某些起点开始,到终点结束,并且受到一个或多个的“中间”控制点的影响。本页面上的图形都是可交互的,你可以拖动这些点,看看这些形状在你的操作下会怎么变化。
<div class="figure">
<graphics-element title="二次贝塞尔曲线" width="275" height="275" src="./quadratic.js"></graphics-element>
<graphics-element title="三次贝塞尔曲线" width="275" height="275" src="./cubic.js"></graphics-element>
<graphics-element title="二次贝塞尔曲线" src="./quadratic.js"></graphics-element>
<graphics-element title="三次贝塞尔曲线" src="./cubic.js"></graphics-element>
</div>
这些曲线在计算机辅助设计和计算机辅助制造应用CAD/CAM中用的很多。在图形设计软件中也常用到像Adobe Illustrator, Photoshop, Inkscape, Gimp等等。还可以应用在一些图形技术中像矢量图形SVG和OpenType字体ttf/otf。许多东西都用到贝塞尔曲线如果你想更了解它们...准备好继续往下学吧!

View File

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

View File

@@ -912,21 +912,54 @@ function Bezier(3,t):
curve-defining point at a specific <i>t</i> value.
</p>
<div class="figure">
<Graphic
inline="{true}"
<graphics-element
title="Quadratic interpolations"
draw="{this.drawQuadraticLerp}"
/>
<Graphic
inline="{true}"
width="275"
height="275"
src="./chapters/control/lerp-quadratic.js"
>
<fallback-image>
<img
width="275px"
height="275px"
src="./images/chapters/control/lerp-quadratic.png"
loading="lazy"
/>
Scripts are disabled. Showing fallback image.
</fallback-image></graphics-element
>
<graphics-element
title="Cubic interpolations"
draw="{this.drawCubicLerp}"
/>
<Graphic
inline="{true}"
width="275"
height="275"
src="./chapters/control/lerp-cubic.js"
>
<fallback-image>
<img
width="275px"
height="275px"
src="./images/chapters/control/lerp-cubic.png"
loading="lazy"
/>
Scripts are disabled. Showing fallback image.
</fallback-image></graphics-element
>
<graphics-element
title="15th degree interpolations"
draw="{this.draw15thLerp}"
/>
width="275"
height="275"
src="./chapters/control/lerp-fifteenth.js"
>
<fallback-image>
<img
width="275px"
height="275px"
src="./images/chapters/control/lerp-fifteenth.png"
loading="lazy"
/>
Scripts are disabled. Showing fallback image.
</fallback-image></graphics-element
>
</div>
<p>
@@ -967,11 +1000,22 @@ function Bezier(3,t):
loading="lazy"
/>
<p>Which gives us the curve we saw at the top of the article:</p>
<Graphic
<graphics-element
title="Our cubic Bézier curve"
setup="{this.drawCubic}"
draw="{this.drawCurve}"
/>
width="275"
height="275"
src="./chapters/control/../introduction/cubic.js"
>
<fallback-image>
<img
width="275px"
height="275px"
src="./images/chapters/control/../introduction/cubic.png"
loading="lazy"
/>
Scripts are disabled. Showing fallback image.
</fallback-image></graphics-element
>
<p>
What else can we do with Bézier curves? Quite a lot, actually. The

View File

@@ -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<touch.length; i++) {
for (let i = 0; i < touch.length; i++) {
if (!touch[i] || !touch[i].pageX) continue;
x = touch[i].pageX - left;
y = touch[i].pageY - top;
@@ -180,7 +182,20 @@ class BaseAPI {
const canvas = this.canvas;
canvas.setAttribute(`tabIndex`, 0);
canvas.classList.add(`focus-enabled`);
canvas._force_listener = () => 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`)
};
}
let cut = code.slice(start, end);
cuts.push(cut);
code = code.replace(cut, ``);
}
return {
quasiGlobal: code,
classCode: cuts.join(`\n`),
};
}

View File

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

View File

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

4
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -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 ? `<base href="..">` : ``;
const langSwitcher = generateLangSwitcher(localeStrings);
@@ -70,3 +66,5 @@ export default async function createIndexPages(
fs.writeFileSync(path.join(locale, `index.html`), data, `utf8`);
}
}
export { createIndexPages };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,39 +0,0 @@
/**
* ...docs go here...
*/
export default function injectGraphicsFallback(
chapter,
localeStrings,
markdown
) {
const translate = localeStrings.translate;
let pos = -1,
data = markdown,
startmark = `<graphics-element`,
endmark = `</graphics-element>`;
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}">
<fallback-image>
<img width="${width}px" height="${height}px" src="${img}" loading="lazy">
${translate`disabledMessage`}
</fallback-image>`;
}
);
data = data.replace(slice, updated);
pos += updated.length;
}
} while (pos !== -1);
return data;
}

View File

@@ -0,0 +1,67 @@
/**
* ...docs go here...
*/
function preprocessGraphicsElement(chapter, localeStrings, markdown) {
const translate = localeStrings.translate;
let pos = -1,
data = markdown,
startmark = `<graphics-element`,
endmark = `</graphics-element>`;
do {
pos = data.indexOf(startmark, pos);
if (pos !== -1) {
// extract a <graphics-element...>...</graphics-element> 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}">
<fallback-image>
<img width="${width}px" height="${height}px" src="${img}" loading="lazy">
${translate`disabledMessage`}
</fallback-image>`;
}
);
data = data.replace(slice, updated);
pos += updated.length;
}
} while (pos !== -1);
return data;
}
export default preprocessGraphicsElement;

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB