control (#261)
@@ -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.
|
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">
|
<div class="figure">
|
||||||
<Graphic inline={true} title="Quadratic interpolations" draw={this.drawQuadraticLerp}/>
|
<graphics-element title="Quadratic interpolations" src="./lerp-quadratic.js"></graphics-element>
|
||||||
<Graphic inline={true} title="Cubic interpolations" draw={this.drawCubicLerp}/>
|
<graphics-element title="Cubic interpolations" src="./lerp-cubic.js"></graphics-element>
|
||||||
<Graphic inline={true} title="15th degree interpolations" draw={this.draw15thLerp}/>
|
<graphics-element title="15th degree interpolations" src="./lerp-fifteenth.js"></graphics-element>
|
||||||
</div>
|
</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.
|
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:
|
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.
|
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.
|
||||||
|
|
||||||
|
@@ -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)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,3 +0,0 @@
|
|||||||
var handler = require("./handler.js");
|
|
||||||
var generateBase = require("../../generate-base");
|
|
||||||
module.exports = generateBase("control", handler);
|
|
62
chapters/control/lerp-cubic.js
Normal 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();
|
||||||
|
}
|
81
chapters/control/lerp-fifteenth.js
Normal 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();
|
||||||
|
}
|
61
chapters/control/lerp-quadratic.js
Normal 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();
|
||||||
|
}
|
@@ -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):
|
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?
|
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?
|
||||||
|
|
||||||
|
@@ -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まで変化させてプロットした場合は、このようになります(上下キーでプロットの上限を変更できます)。
|
というわけで、普通の関数では<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>の両方で「二項係数多項式」を使います。では、二項係数多項式とは何でしょう?
|
ベジエ曲線はパラメトリック関数の一種であり、どの次元に対しても同じ基底関数を使うという点で特徴づけられます。先ほどの例では、<i>x</i>の値と<i>y</i>の値とで異なる関数(正弦関数と余弦関数)を使っていましたが、ベジエ曲線では<i>x</i>と<i>y</i>の両方で「二項係数多項式」を使います。では、二項係数多項式とは何でしょう?
|
||||||
|
|
||||||
|
@@ -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时的值,将得到如下图像(你可以用上下键来改变画的点和值):
|
所以,参数曲线不像一般函数那样,通过<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>都用了“二项多项式”。那什么是二项多项式呢?
|
贝塞尔曲线是(一种)参数方程,并在它的多个维度上使用相同的基本方程。在上述的例子中<i>x</i>值和<i>y</i>值使用了不同的方程,与此不同的是,贝塞尔曲线的<i>x</i>和<i>y</i>都用了“二项多项式”。那什么是二项多项式呢?
|
||||||
|
|
||||||
|
@@ -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.
|
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">
|
<div class="figure">
|
||||||
<graphics-element title="A quadratic Bézier curve" width="275" height="275" src="./quadratic.js"></graphics-element>
|
<graphics-element title="A quadratic Bézier curve" 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 cubic Bézier curve" src="./cubic.js"></graphics-element>
|
||||||
</div>
|
</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!
|
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!
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
まずは良い例から始めましょう。ベジエ曲線というのは、下の図に表示されているもののことです。ベジエ曲線はある始点からある終点へと延びており、その曲率は1個以上の「中間」制御点に左右されています。さて、このページの図はどれもインタラクティブになっていますので、ここで曲線をちょっと操作してみましょう。点をドラッグしたとき、曲線の形がそれに応じてどう変化するのか、確かめてみてください。
|
まずは良い例から始めましょう。ベジエ曲線というのは、下の図に表示されているもののことです。ベジエ曲線はある始点からある終点へと延びており、その曲率は1個以上の「中間」制御点に左右されています。さて、このページの図はどれもインタラクティブになっていますので、ここで曲線をちょっと操作してみましょう。点をドラッグしたとき、曲線の形がそれに応じてどう変化するのか、確かめてみてください。
|
||||||
|
|
||||||
<div class="figure">
|
<div class="figure">
|
||||||
<graphics-element title="2次のベジエ曲線" width="275" height="275" src="./quadratic.js"></graphics-element>
|
<graphics-element title="2次のベジエ曲線" src="./quadratic.js"></graphics-element>
|
||||||
<graphics-element title="3次のベジエ曲線" width="275" height="275" src="./cubic.js"></graphics-element>
|
<graphics-element title="3次のベジエ曲線" src="./cubic.js"></graphics-element>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
ベジエ曲線は、CAD(computer aided designやCAM(computer aided manufacturing)のアプリケーションで多用されています。もちろん、Adobe Illustrator・Photoshop・Inkscape・Gimp などのグラフィックデザインアプリケーションや、SVG(scalable vector graphics)・OpenTypeフォント(otf/ttf)のようなグラフィック技術でも利用されています。ベジエ曲線はたくさんのものに使われていますので、これについてもっと詳しく学びたいのであれば……さあ、準備しましょう!
|
ベジエ曲線は、CAD(computer aided designやCAM(computer aided manufacturing)のアプリケーションで多用されています。もちろん、Adobe Illustrator・Photoshop・Inkscape・Gimp などのグラフィックデザインアプリケーションや、SVG(scalable vector graphics)・OpenTypeフォント(otf/ttf)のようなグラフィック技術でも利用されています。ベジエ曲線はたくさんのものに使われていますので、これについてもっと詳しく学びたいのであれば……さあ、準備しましょう!
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
让我们有个好的开始:当我们在谈论贝塞尔曲线的时候,所指的就是你在如下图像看到的东西。它们从某些起点开始,到终点结束,并且受到一个或多个的“中间”控制点的影响。本页面上的图形都是可交互的,你可以拖动这些点,看看这些形状在你的操作下会怎么变化。
|
让我们有个好的开始:当我们在谈论贝塞尔曲线的时候,所指的就是你在如下图像看到的东西。它们从某些起点开始,到终点结束,并且受到一个或多个的“中间”控制点的影响。本页面上的图形都是可交互的,你可以拖动这些点,看看这些形状在你的操作下会怎么变化。
|
||||||
|
|
||||||
<div class="figure">
|
<div class="figure">
|
||||||
<graphics-element title="二次贝塞尔曲线" width="275" height="275" src="./quadratic.js"></graphics-element>
|
<graphics-element title="二次贝塞尔曲线" src="./quadratic.js"></graphics-element>
|
||||||
<graphics-element title="三次贝塞尔曲线" width="275" height="275" src="./cubic.js"></graphics-element>
|
<graphics-element title="三次贝塞尔曲线" src="./cubic.js"></graphics-element>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
这些曲线在计算机辅助设计和计算机辅助制造应用(CAD/CAM)中用的很多。在图形设计软件中也常用到,像Adobe Illustrator, Photoshop, Inkscape, Gimp等等。还可以应用在一些图形技术中,像矢量图形(SVG)和OpenType字体(ttf/otf)。许多东西都用到贝塞尔曲线,如果你想更了解它们...准备好继续往下学吧!
|
这些曲线在计算机辅助设计和计算机辅助制造应用(CAD/CAM)中用的很多。在图形设计软件中也常用到,像Adobe Illustrator, Photoshop, Inkscape, Gimp等等。还可以应用在一些图形技术中,像矢量图形(SVG)和OpenType字体(ttf/otf)。许多东西都用到贝塞尔曲线,如果你想更了解它们...准备好继续往下学吧!
|
||||||
|
@@ -1,10 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* This is an ordered list of all sections used in the Bezier primer.
|
* This is the section ordering used in the Primer.
|
||||||
*
|
|
||||||
* The ordering you see here reflects the ordering in which sections
|
|
||||||
'* are present on the Primer page',
|
|
||||||
* a REALLY good reason to =)
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
export default [
|
export default [
|
||||||
'preface',
|
'preface',
|
||||||
@@ -61,7 +56,7 @@ export default [
|
|||||||
// "things made of more than on curve"
|
// "things made of more than on curve"
|
||||||
'polybezier',
|
'polybezier',
|
||||||
'shapes',
|
'shapes',
|
||||||
// 'drawing',
|
// 'drawing', // still just waiting to be finished......
|
||||||
|
|
||||||
// curve offsetting
|
// curve offsetting
|
||||||
'projections',
|
'projections',
|
||||||
|
68
index.html
@@ -912,21 +912,54 @@ function Bezier(3,t):
|
|||||||
curve-defining point at a specific <i>t</i> value.
|
curve-defining point at a specific <i>t</i> value.
|
||||||
</p>
|
</p>
|
||||||
<div class="figure">
|
<div class="figure">
|
||||||
<Graphic
|
<graphics-element
|
||||||
inline="{true}"
|
|
||||||
title="Quadratic interpolations"
|
title="Quadratic interpolations"
|
||||||
draw="{this.drawQuadraticLerp}"
|
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"
|
||||||
/>
|
/>
|
||||||
<Graphic
|
Scripts are disabled. Showing fallback image.
|
||||||
inline="{true}"
|
</fallback-image></graphics-element
|
||||||
|
>
|
||||||
|
<graphics-element
|
||||||
title="Cubic interpolations"
|
title="Cubic interpolations"
|
||||||
draw="{this.drawCubicLerp}"
|
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"
|
||||||
/>
|
/>
|
||||||
<Graphic
|
Scripts are disabled. Showing fallback image.
|
||||||
inline="{true}"
|
</fallback-image></graphics-element
|
||||||
|
>
|
||||||
|
<graphics-element
|
||||||
title="15th degree interpolations"
|
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>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -967,11 +1000,22 @@ function Bezier(3,t):
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<p>Which gives us the curve we saw at the top of the article:</p>
|
<p>Which gives us the curve we saw at the top of the article:</p>
|
||||||
<Graphic
|
<graphics-element
|
||||||
title="Our cubic Bézier curve"
|
title="Our cubic Bézier curve"
|
||||||
setup="{this.drawCubic}"
|
width="275"
|
||||||
draw="{this.drawCurve}"
|
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>
|
<p>
|
||||||
What else can we do with Bézier curves? Quite a lot, actually. The
|
What else can we do with Bézier curves? Quite a lot, actually. The
|
||||||
|
@@ -63,6 +63,8 @@ class BaseAPI {
|
|||||||
|
|
||||||
this.cursor = {};
|
this.cursor = {};
|
||||||
|
|
||||||
|
const release = typeof document !== "undefined" ? document : canvas;
|
||||||
|
|
||||||
[`touchstart`, `mousedown`].forEach((evtName) =>
|
[`touchstart`, `mousedown`].forEach((evtName) =>
|
||||||
canvas.addEventListener(evtName, (evt) => this.onMouseDown(evt))
|
canvas.addEventListener(evtName, (evt) => this.onMouseDown(evt))
|
||||||
);
|
);
|
||||||
@@ -72,7 +74,7 @@ class BaseAPI {
|
|||||||
);
|
);
|
||||||
|
|
||||||
[`touchend`, `mouseup`].forEach((evtName) =>
|
[`touchend`, `mouseup`].forEach((evtName) =>
|
||||||
canvas.addEventListener(evtName, (evt) => this.onMouseUp(evt))
|
release.addEventListener(evtName, (evt) => this.onMouseUp(evt))
|
||||||
);
|
);
|
||||||
|
|
||||||
this.keyboard = {};
|
this.keyboard = {};
|
||||||
@@ -82,7 +84,7 @@ class BaseAPI {
|
|||||||
);
|
);
|
||||||
|
|
||||||
[`keyup`].forEach((evtName) =>
|
[`keyup`].forEach((evtName) =>
|
||||||
canvas.addEventListener(evtName, (evt) => this.onKeyUp(evt))
|
release.addEventListener(evtName, (evt) => this.onKeyUp(evt))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ class BaseAPI {
|
|||||||
|
|
||||||
if (evt.targetTouches) {
|
if (evt.targetTouches) {
|
||||||
const touch = 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;
|
if (!touch[i] || !touch[i].pageX) continue;
|
||||||
x = touch[i].pageX - left;
|
x = touch[i].pageX - left;
|
||||||
y = touch[i].pageY - top;
|
y = touch[i].pageY - top;
|
||||||
@@ -180,7 +182,20 @@ class BaseAPI {
|
|||||||
const canvas = this.canvas;
|
const canvas = this.canvas;
|
||||||
canvas.setAttribute(`tabIndex`, 0);
|
canvas.setAttribute(`tabIndex`, 0);
|
||||||
canvas.classList.add(`focus-enabled`);
|
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) =>
|
[`touchstart`, `mousedown`].forEach((evtName) =>
|
||||||
canvas.addEventListener(evtName, canvas._force_listener)
|
canvas.addEventListener(evtName, canvas._force_listener)
|
||||||
);
|
);
|
||||||
@@ -254,6 +269,7 @@ function enhanceContext(ctx) {
|
|||||||
strokeStyle: ctx.strokeStyle,
|
strokeStyle: ctx.strokeStyle,
|
||||||
fillStyle: ctx.fillStyle,
|
fillStyle: ctx.fillStyle,
|
||||||
lineWidth: ctx.lineWidth,
|
lineWidth: ctx.lineWidth,
|
||||||
|
textAlign: ctx.textAlign,
|
||||||
transform: [m.a, m.b, m.c, m.d, m.e, m.f],
|
transform: [m.a, m.b, m.c, m.d, m.e, m.f],
|
||||||
};
|
};
|
||||||
styles.push(e);
|
styles.push(e);
|
||||||
|
@@ -7,12 +7,30 @@ import { BaseAPI } from "./base-api.js";
|
|||||||
const MOUSE_PRECISION_ZONE = 5;
|
const MOUSE_PRECISION_ZONE = 5;
|
||||||
const TOUCH_PRECISION_ZONE = 30;
|
const TOUCH_PRECISION_ZONE = 30;
|
||||||
|
|
||||||
|
let CURRENT_HUE = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Our Graphics API, which is the "public" side of the API.
|
* Our Graphics API, which is the "public" side of the API.
|
||||||
*/
|
*/
|
||||||
class GraphicsAPI extends BaseAPI {
|
class GraphicsAPI extends BaseAPI {
|
||||||
static get constants() {
|
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() {
|
get PI() {
|
||||||
@@ -36,11 +54,22 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
get BEZIER() {
|
get BEZIER() {
|
||||||
return Shape.BEZIER;
|
return Shape.BEZIER;
|
||||||
}
|
}
|
||||||
|
get CENTER() {
|
||||||
|
return `center`;
|
||||||
|
}
|
||||||
|
get LEFT() {
|
||||||
|
return `left`;
|
||||||
|
}
|
||||||
|
get RIGHT() {
|
||||||
|
return `right`;
|
||||||
|
}
|
||||||
|
|
||||||
onMouseDown(evt) {
|
onMouseDown(evt) {
|
||||||
super.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++) {
|
for (let i = 0, e = this.moveable.length, p, d; i < e; i++) {
|
||||||
p = this.moveable[i];
|
p = this.moveable[i];
|
||||||
@@ -92,6 +121,51 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
this.ctx.rotate(angle);
|
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
|
* transforms: reset
|
||||||
*/
|
*/
|
||||||
@@ -131,6 +205,14 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
this.canvas.style.cursor = type;
|
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
|
* Set the context fillStyle
|
||||||
*/
|
*/
|
||||||
@@ -209,12 +291,18 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
/**
|
/**
|
||||||
* Draw text on the canvas
|
* Draw text on the canvas
|
||||||
*/
|
*/
|
||||||
text(str, x, y) {
|
text(str, x, y, alignment) {
|
||||||
if (y === undefined) {
|
if (y === undefined) {
|
||||||
y = x.y;
|
y = x.y;
|
||||||
x = x.x;
|
x = x.x;
|
||||||
}
|
}
|
||||||
|
const ctx = this.ctx;
|
||||||
|
if (alignment) {
|
||||||
|
ctx.cacheStyle();
|
||||||
|
ctx.textAlign = alignment;
|
||||||
|
}
|
||||||
this.ctx.fillText(str, x, y);
|
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);
|
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
|
* A signal for starting a complex shape
|
||||||
*/
|
*/
|
||||||
@@ -246,6 +353,14 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
this.currentShape.vertex({ x, y });
|
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
|
* A signal to draw the current complex shape
|
||||||
*/
|
*/
|
||||||
@@ -254,12 +369,56 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
let { x, y } = this.currentShape.first;
|
let { x, y } = this.currentShape.first;
|
||||||
this.ctx.moveTo(x, y);
|
this.ctx.moveTo(x, y);
|
||||||
this.currentShape.segments.forEach((s) =>
|
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.fill();
|
||||||
this.ctx.stroke();
|
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.
|
* Yield a snapshot of the current shape.
|
||||||
*/
|
*/
|
||||||
@@ -267,14 +426,6 @@ class GraphicsAPI extends BaseAPI {
|
|||||||
return this.currentShape;
|
return this.currentShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a previously created shape
|
|
||||||
*/
|
|
||||||
drawShape(shape) {
|
|
||||||
this.currentShape = shape;
|
|
||||||
this.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convenient grid drawing function
|
* 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
|
* math functions
|
||||||
*/
|
*/
|
||||||
|
@@ -5,11 +5,11 @@ import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
|
|||||||
*/
|
*/
|
||||||
class Bezier extends Original {
|
class Bezier extends Original {
|
||||||
static defaultQuadratic(apiInstance) {
|
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) {
|
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) {
|
constructor(apiInstance, ...coords) {
|
||||||
|
@@ -33,12 +33,9 @@ class Vector {
|
|||||||
return -Math.atan2(this.y, this.x);
|
return -Math.atan2(this.y, this.x);
|
||||||
}
|
}
|
||||||
reflect(other) {
|
reflect(other) {
|
||||||
let p = new Vector(
|
let p = new Vector(other.x - this.x, other.y - this.y);
|
||||||
other.x - this.x,
|
|
||||||
other.y - this.y
|
|
||||||
);
|
|
||||||
if (other.z !== undefined) {
|
if (other.z !== undefined) {
|
||||||
p.z = other.z
|
p.z = other.z;
|
||||||
if (this.z !== undefined) {
|
if (this.z !== undefined) {
|
||||||
p.z -= this.z;
|
p.z -= this.z;
|
||||||
}
|
}
|
||||||
@@ -75,6 +72,6 @@ class Vector {
|
|||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Vector };
|
export { Vector };
|
||||||
|
@@ -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
|
* A complex shape, represented as a collection of paths
|
||||||
* that can be either polygon, Catmull-Rom curves, or
|
* that can be either polygon, Catmull-Rom curves, or
|
||||||
@@ -40,4 +26,18 @@ Shape.POLYGON = `Polygon`;
|
|||||||
Shape.CURVE = `CatmullRom`;
|
Shape.CURVE = `CatmullRom`;
|
||||||
Shape.BEZIER = `Bezier`;
|
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 };
|
export { Shape, Segment };
|
||||||
|
@@ -42,14 +42,14 @@ class CustomElement extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
if (!customElements.resolveScope) {
|
if (!customElements.resolveScope) {
|
||||||
customElements.resolveScope = function(scope) {
|
customElements.resolveScope = function (scope) {
|
||||||
try {
|
try {
|
||||||
return scope.getRootNode().host;
|
return scope.getRootNode().host;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
return window;
|
return window;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this._options = options;
|
this._options = options;
|
||||||
@@ -63,7 +63,11 @@ class CustomElement extends HTMLElement {
|
|||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
attributes: (record) => {
|
attributes: (record) => {
|
||||||
this.handleAttributeChange(record.attributeName, record.oldValue, this.getAttribute(record.attributeName));
|
this.handleAttributeChange(
|
||||||
|
record.attributeName,
|
||||||
|
record.oldValue,
|
||||||
|
this.getAttribute(record.attributeName)
|
||||||
|
);
|
||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -35,7 +35,8 @@ class GraphicsElement extends CustomElement {
|
|||||||
:host style { display: none; }
|
:host style { display: none; }
|
||||||
:host canvas { display: block; margin: auto; border-radius: 0; }
|
:host canvas { display: block; margin: auto; border-radius: 0; }
|
||||||
:host canvas:focus { ${
|
: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 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; }
|
: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`));
|
const script = (this.script = document.createElement(`script`));
|
||||||
script.type = "module";
|
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();
|
if (rerender) this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +222,12 @@ class GraphicsElement extends CustomElement {
|
|||||||
if (this.canvas) slotParent.insertBefore(this.canvas, this._slot);
|
if (this.canvas) slotParent.insertBefore(this.canvas, this._slot);
|
||||||
if (this.label) slotParent.insertBefore(this.label, this._slot);
|
if (this.label) slotParent.insertBefore(this.label, this._slot);
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.classList.add('view-source');
|
a.classList.add("view-source");
|
||||||
a.textContent = `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`;
|
a.target = `_blank`;
|
||||||
if (this.label) slotParent.insertBefore(a, this.label);
|
if (this.label) slotParent.insertBefore(a, this.label);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { GraphicsAPI } from "../api/graphics-api.js";
|
import { GraphicsAPI } from "../api/graphics-api.js";
|
||||||
|
|
||||||
export default function performCodeSurgery(code) {
|
export default function performCodeSurgery(code) {
|
||||||
|
|
||||||
// 1. ensure that anything that needs to run by first calling its super function, does so.
|
// 1. ensure that anything that needs to run by first calling its super function, does so.
|
||||||
|
|
||||||
GraphicsAPI.superCallers.forEach((name) => {
|
GraphicsAPI.superCallers.forEach((name) => {
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
export default function splitCodeSections(code) {
|
export default function splitCodeSections(code) {
|
||||||
const re = /\b[\w\W][^\s]*?\([^)]*\)[\r\n\s]*{/;
|
const re = /\b[\w\W][^\s]*?\([^)]*\)[\r\n\s]*{/;
|
||||||
const cuts = [];
|
const cuts = [];
|
||||||
for(let result = code.match(re); result; result=code.match(re)) {
|
for (let result = code.match(re); result; result = code.match(re)) {
|
||||||
result = result[0];
|
result = result[0];
|
||||||
|
|
||||||
let start = code.indexOf(result);
|
let start = code.indexOf(result);
|
||||||
@@ -13,7 +13,7 @@ export default function splitCodeSections(code) {
|
|||||||
let depth = 0;
|
let depth = 0;
|
||||||
let slice = Array.from(code).slice(start + result.length);
|
let slice = Array.from(code).slice(start + result.length);
|
||||||
|
|
||||||
slice.some((c,pos) => {
|
slice.some((c, pos) => {
|
||||||
if (c === `{`) {
|
if (c === `{`) {
|
||||||
depth++;
|
depth++;
|
||||||
return false;
|
return false;
|
||||||
@@ -34,6 +34,6 @@ export default function splitCodeSections(code) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
quasiGlobal: code,
|
quasiGlobal: code,
|
||||||
classCode: cuts.join(`\n`)
|
classCode: cuts.join(`\n`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -24,16 +24,28 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
(function referrer(l) {
|
(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";
|
page = page || "index.html";
|
||||||
// we don't care about file or localhost, for obvious reasons
|
// we don't care about file or localhost, for obvious reasons
|
||||||
var loc = window.location.toString();
|
var loc = window.location.toString();
|
||||||
if(loc.indexOf("file:///")!==-1) return;
|
if (loc.indexOf("file:///") !== -1) return;
|
||||||
if(loc.indexOf("localhost")!==-1) return;
|
if (loc.indexOf("localhost") !== -1) return;
|
||||||
// right, continue
|
// right, continue
|
||||||
var url = "http://pomax.nihongoresources.com/pages/bezierinfo/logger.php";
|
var url = "http://pomax.nihongoresources.com/pages/bezierinfo/logger.php";
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open("GET", url + "?" + "referrer=" + encodeURIComponent(document.referrer) + "&for=" + page, true);
|
xhr.open(
|
||||||
try { xhr.send(null); }
|
"GET",
|
||||||
catch(e) { /* you don't care about this error, and I can't see it, so why would we do anything with it? */ }
|
url +
|
||||||
}(window.location.toString()));
|
"?" +
|
||||||
|
"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());
|
||||||
|
@@ -4,7 +4,6 @@
|
|||||||
* base article itself.
|
* base article itself.
|
||||||
*/
|
*/
|
||||||
(function tryToBind() {
|
(function tryToBind() {
|
||||||
|
|
||||||
if (!document.querySelector(`map[name="rhtimap"]`)) {
|
if (!document.querySelector(`map[name="rhtimap"]`)) {
|
||||||
return setTimeout(tryToBind, 300);
|
return setTimeout(tryToBind, 300);
|
||||||
}
|
}
|
||||||
@@ -13,20 +12,22 @@
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.section = false;
|
this.section = false;
|
||||||
this.hash = false;
|
this.hash = false;
|
||||||
this.socials = ["rdt","hn","twt"];
|
this.socials = ["rdt", "hn", "twt"];
|
||||||
}
|
}
|
||||||
|
|
||||||
update(data) {
|
update(data) {
|
||||||
this.section = data.section;
|
this.section = data.section;
|
||||||
this.hash = data.hash;
|
this.hash = data.hash;
|
||||||
this.socials.forEach(social => {
|
this.socials.forEach((social) => {
|
||||||
var area = document.querySelector(`map area.sclnk-${social}`);
|
var area = document.querySelector(`map area.sclnk-${social}`);
|
||||||
area.href = this[`get_${social}`]();
|
area.href = this[`get_${social}`]();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get url() {
|
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() {
|
getTitle() {
|
||||||
@@ -39,7 +40,9 @@
|
|||||||
|
|
||||||
get_rdt() {
|
get_rdt() {
|
||||||
var title = this.getTitle();
|
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}`;
|
return `https://www.reddit.com/submit?url=${this.url}&title=${title}&text=${text}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,32 +52,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_twt() {
|
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`;
|
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
|
// 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
|
// the top of the screen, either just off-screen or in-screen
|
||||||
var tracker = new Tracker();
|
var tracker = new Tracker();
|
||||||
var anchors = Array.from(document.querySelectorAll("section h2 a"));
|
var anchors = Array.from(document.querySelectorAll("section h2 a"));
|
||||||
var sections = anchors.map(a => a.parentNode);
|
var sections = anchors.map((a) => a.parentNode);
|
||||||
var sectionData = sections.map(section => {
|
var sectionData = sections.map((section) => {
|
||||||
return {
|
return {
|
||||||
section: section.textContent,
|
section: section.textContent,
|
||||||
hash: section.querySelector("a").hash
|
hash: section.querySelector("a").hash,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("scroll", function(evt) {
|
window.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
function (evt) {
|
||||||
var min = 99999999999999999;
|
var min = 99999999999999999;
|
||||||
var element = false;
|
var element = false;
|
||||||
sections.forEach( (s,pos) => {
|
sections.forEach((s, pos) => {
|
||||||
var v = Math.abs(s.getBoundingClientRect().top);
|
var v = Math.abs(s.getBoundingClientRect().top);
|
||||||
if (v < min) { min = v; element = pos; }
|
if (v < min) {
|
||||||
|
min = v;
|
||||||
|
element = pos;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
tracker.update(sectionData[element]);
|
tracker.update(sectionData[element]);
|
||||||
}, { passive: true });
|
},
|
||||||
|
{ passive: true }
|
||||||
}());
|
);
|
||||||
|
})();
|
||||||
|
4
package-lock.json
generated
@@ -280,10 +280,6 @@
|
|||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"codesurgeon": {
|
|
||||||
"version": "file:lib/custom-element",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"color-convert": {
|
"color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
17
package.json
@@ -8,19 +8,21 @@
|
|||||||
"homepage": "https://pomax.github.io/bezierinfo",
|
"homepage": "https://pomax.github.io/bezierinfo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Pomax/bezierinfo.git"
|
"url": "git+https://github.com/Pomax/BezierInfo-2.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Pomax/bezierinfo/issues"
|
"url": "https://github.com/Pomax/BezierInfo-2/issues"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"time": "node ./mark.js",
|
"start": "rm -f .timing && run-s time lint:* build time",
|
||||||
"start": "run-s time lint build time",
|
|
||||||
"lint": "prettier ./tools --write",
|
|
||||||
"build": "node ./tools/build.js",
|
|
||||||
"test": "run-p server browser",
|
"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",
|
"server": "http-server -p 8000 --cors",
|
||||||
"browser": "open-cli http://localhost:8000"
|
"time": "node ./mark.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Bezier",
|
"Bezier",
|
||||||
@@ -33,7 +35,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bezier-js": "^2.6.1",
|
"bezier-js": "^2.6.1",
|
||||||
"canvas": "^2.6.1",
|
"canvas": "^2.6.1",
|
||||||
"codesurgeon": "file:./lib/custom-element",
|
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"http-server": "^0.12.3",
|
"http-server": "^0.12.3",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import LocaleStrings from "./locale-strings.js";
|
import LocaleStrings from "./locale-strings.js";
|
||||||
import getAllChapterFiles from "./build/get-all-chapter-files.js";
|
import { getAllChapterFiles } from "./build/get-all-chapter-files.js";
|
||||||
import processLocale from "./build/process-locale.js";
|
import { processLocale } from "./build/process-locale.js";
|
||||||
import createIndexPages from "./build/create-index-page.js";
|
import { createIndexPages } from "./build/create-index-page.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* main entry point:
|
* main entry point:
|
||||||
@@ -10,6 +10,7 @@ import createIndexPages from "./build/create-index-page.js";
|
|||||||
*/
|
*/
|
||||||
getAllChapterFiles().then((chapterFiles) => {
|
getAllChapterFiles().then((chapterFiles) => {
|
||||||
const languageCodes = Object.keys(chapterFiles);
|
const languageCodes = Object.keys(chapterFiles);
|
||||||
|
|
||||||
languageCodes.forEach(async (locale) => {
|
languageCodes.forEach(async (locale) => {
|
||||||
const localeStrings = new LocaleStrings(locale);
|
const localeStrings = new LocaleStrings(locale);
|
||||||
const chapters = await processLocale(locale, localeStrings, chapterFiles);
|
const chapters = await processLocale(locale, localeStrings, chapterFiles);
|
||||||
|
@@ -11,11 +11,7 @@ nunjucks.configure(".", { autoescape: false });
|
|||||||
/**
|
/**
|
||||||
* ...docs go here...
|
* ...docs go here...
|
||||||
*/
|
*/
|
||||||
export default async function createIndexPages(
|
async function createIndexPages(locale, localeStrings, chapters) {
|
||||||
locale,
|
|
||||||
localeStrings,
|
|
||||||
chapters
|
|
||||||
) {
|
|
||||||
const defaultLocale = localeStrings.getDefaultLocale();
|
const defaultLocale = localeStrings.getDefaultLocale();
|
||||||
const base = locale !== defaultLocale ? `<base href="..">` : ``;
|
const base = locale !== defaultLocale ? `<base href="..">` : ``;
|
||||||
const langSwitcher = generateLangSwitcher(localeStrings);
|
const langSwitcher = generateLangSwitcher(localeStrings);
|
||||||
@@ -70,3 +66,5 @@ export default async function createIndexPages(
|
|||||||
fs.writeFileSync(path.join(locale, `index.html`), data, `utf8`);
|
fs.writeFileSync(path.join(locale, `index.html`), data, `utf8`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { createIndexPages };
|
||||||
|
@@ -9,7 +9,8 @@ const BASEDIR = path.join(__dirname, "..", "..");
|
|||||||
/**
|
/**
|
||||||
* ...docs go here...
|
* ...docs go here...
|
||||||
*/
|
*/
|
||||||
export default /* async */ function getAllChapterFiles() {
|
|
||||||
|
/* async */ function getAllChapterFiles() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
glob(path.join(BASEDIR, `chapters/**/content*md`), (err, files) => {
|
glob(path.join(BASEDIR, `chapters/**/content*md`), (err, files) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
@@ -28,3 +29,5 @@ export default /* async */ function getAllChapterFiles() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { getAllChapterFiles };
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
import splitCodeSections from "../../lib/custom-element/lib/split-code-sections.js";
|
import splitCodeSections from "../../../lib/custom-element/lib/split-code-sections.js";
|
||||||
import performCodeSurgery from "../../lib/custom-element/lib/perform-code-surgery.js";
|
import performCodeSurgery from "../../../lib/custom-element/lib/perform-code-surgery.js";
|
||||||
import prettier from "prettier";
|
import prettier from "prettier";
|
||||||
|
|
||||||
export default function rewriteGraphicsElement(code, width, height) {
|
/**
|
||||||
|
* ...docs go here...
|
||||||
|
*/
|
||||||
|
function generateGraphicsModule(code, width, height) {
|
||||||
const split = splitCodeSections(code);
|
const split = splitCodeSections(code);
|
||||||
const globalCode = split.quasiGlobal;
|
const globalCode = split.quasiGlobal;
|
||||||
const classCode = performCodeSurgery(split.classCode);
|
const classCode = performCodeSurgery(split.classCode);
|
||||||
@@ -10,7 +13,7 @@ export default function rewriteGraphicsElement(code, width, height) {
|
|||||||
return prettier.format(
|
return prettier.format(
|
||||||
`
|
`
|
||||||
import CanvasBuilder from 'canvas';
|
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 = (()=>{});
|
const noop = (()=>{});
|
||||||
|
|
||||||
@@ -37,3 +40,5 @@ export default function rewriteGraphicsElement(code, width, height) {
|
|||||||
{ parser: `babel` }
|
{ parser: `babel` }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { generateGraphicsModule };
|
@@ -1,11 +1,14 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
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 moduleURL = new URL(import.meta.url);
|
||||||
const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``));
|
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();
|
const locale = localeStrings.getCurrentLocale();
|
||||||
|
|
||||||
if (locale !== localeStrings.getDefaultLocale()) return;
|
if (locale !== localeStrings.getDefaultLocale()) return;
|
||||||
@@ -36,20 +39,27 @@ export default async function generatePlaceHolders(localeStrings, markdown) {
|
|||||||
sourcePaths.map(async (srcPath, i) => {
|
sourcePaths.map(async (srcPath, i) => {
|
||||||
try {
|
try {
|
||||||
// Get the sketch code
|
// Get the sketch code
|
||||||
const sourcePath = path.join(__dirname, "..", "..", srcPath);
|
const sourcePath = path.join(__dirname, "..", "..", "..", srcPath);
|
||||||
const code = fs.readFileSync(sourcePath).toString(`utf8`);
|
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 width = elements[keys[i]].match(`width="([^"]+)"`)[1];
|
||||||
const height = elements[keys[i]].match(`height="([^"]+)"`)[1];
|
const height = elements[keys[i]].match(`height="([^"]+)"`)[1];
|
||||||
|
|
||||||
// Convert this to a valid JS module code and write this to
|
// Convert this to a valid JS module code and write this to
|
||||||
// a temporary file so we can import it.
|
// 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 fileName = `./nodecode.${Date.now()}.${Math.random()}.js`;
|
||||||
const tempFile = path.join(__dirname, fileName);
|
const tempFile = path.join(__dirname, fileName);
|
||||||
fs.writeFileSync(tempFile, nodeCode, `utf8`);
|
fs.writeFileSync(tempFile, nodeCode, `utf8`);
|
||||||
|
|
||||||
// Import our entirely valid JS module, which will run the
|
// 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);
|
const { canvas } = await import(fileName);
|
||||||
|
|
||||||
fs.unlinkSync(tempFile);
|
fs.unlinkSync(tempFile);
|
||||||
@@ -59,6 +69,7 @@ export default async function generatePlaceHolders(localeStrings, markdown) {
|
|||||||
const imageData = Buffer.from(dataURI.substring(start), `base64`);
|
const imageData = Buffer.from(dataURI.substring(start), `base64`);
|
||||||
const destPath = path.join(__dirname, "..", "..", "images", srcPath);
|
const destPath = path.join(__dirname, "..", "..", "images", srcPath);
|
||||||
const filename = destPath.replace(`.js`, `.png`);
|
const filename = destPath.replace(`.js`, `.png`);
|
||||||
|
|
||||||
// console.log(`Writing placeholder to ${filename}`);
|
// console.log(`Writing placeholder to ${filename}`);
|
||||||
fs.ensureDirSync(path.dirname(destPath));
|
fs.ensureDirSync(path.dirname(destPath));
|
||||||
fs.writeFileSync(filename, imageData);
|
fs.writeFileSync(filename, imageData);
|
||||||
@@ -68,3 +79,5 @@ export default async function generatePlaceHolders(localeStrings, markdown) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { generatePlaceHolders };
|
@@ -1,27 +1,30 @@
|
|||||||
import marked from "marked";
|
import marked from "marked";
|
||||||
import latexToSVG from "./latex/latex-to-svg.js";
|
import latexToSVG from "../latex/latex-to-svg.js";
|
||||||
import injectGraphicsFallback from "./markdown/inject-fallback.js";
|
import preprocessGraphicsElement from "./preprocess-graphics-element.js";
|
||||||
import extractLaTeX from "./markdown/extract-latex.js";
|
import extractLaTeX from "./extract-latex.js";
|
||||||
import nunjucks from "nunjucks";
|
import nunjucks from "nunjucks";
|
||||||
nunjucks.configure(".", { autoescape: false });
|
nunjucks.configure(".", { autoescape: false });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ...docs go here...
|
* ...docs go here...
|
||||||
*/
|
*/
|
||||||
export default async function convertMarkDown(
|
async function convertMarkDown(chapter, localeStrings, markdown) {
|
||||||
chapter,
|
markdown = preprocessGraphicsElement(chapter, localeStrings, markdown);
|
||||||
localeStrings,
|
|
||||||
markdown
|
|
||||||
) {
|
|
||||||
markdown = injectGraphicsFallback(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);
|
const { data, latex } = extractLaTeX(markdown);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(latex).map(async (key, pos) => {
|
Object.keys(latex).map(
|
||||||
const svg = await latexToSVG(latex[key], chapter, localeStrings, pos + 1);
|
async (key, pos) =>
|
||||||
return (latex[key] = svg);
|
(latex[key] = await latexToSVG(
|
||||||
})
|
latex[key],
|
||||||
|
chapter,
|
||||||
|
localeStrings,
|
||||||
|
pos + 1
|
||||||
|
))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let converted = marked(data, {
|
let converted = marked(data, {
|
||||||
@@ -44,3 +47,5 @@ export default async function convertMarkDown(
|
|||||||
|
|
||||||
return nunjucks.renderString(converted, latex);
|
return nunjucks.renderString(converted, latex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { convertMarkDown };
|
@@ -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;
|
|
||||||
}
|
|
67
tools/build/markdown/preprocess-graphics-element.js
Normal 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;
|
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import convertMarkDown from "./convert-markdown.js";
|
import { convertMarkDown } from "./markdown/convert-markdown.js";
|
||||||
import generatePlaceHolders from "./generate-placeholders.js";
|
import { generatePlaceHolders } from "./graphics/generate-placeholders.js";
|
||||||
import nunjucks from "nunjucks";
|
import nunjucks from "nunjucks";
|
||||||
import toc from "../../chapters/toc.js";
|
import toc from "../../chapters/toc.js";
|
||||||
|
|
||||||
@@ -23,13 +23,8 @@ nunjucks.configure(".", { autoescape: false });
|
|||||||
/**
|
/**
|
||||||
* ...docs go here...
|
* ...docs go here...
|
||||||
*/
|
*/
|
||||||
export default async function processLocale(
|
async function processLocale(locale, localeStrings, chapterFiles) {
|
||||||
locale,
|
|
||||||
localeStrings,
|
|
||||||
chapterFiles
|
|
||||||
) {
|
|
||||||
const defaultLocale = localeStrings.getDefaultLocale();
|
const defaultLocale = localeStrings.getDefaultLocale();
|
||||||
const translate = localeStrings.translate;
|
|
||||||
|
|
||||||
const localeFiles = chapterFiles[locale];
|
const localeFiles = chapterFiles[locale];
|
||||||
let localized = 0;
|
let localized = 0;
|
||||||
@@ -80,3 +75,5 @@ export default async function processLocale(
|
|||||||
|
|
||||||
return chapters;
|
return chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { processLocale };
|
||||||
|
BIN
tools/images/chapters/control/lerp-cubic.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
tools/images/chapters/control/lerp-fifteenth.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
tools/images/chapters/control/lerp-quadratic.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
tools/images/chapters/explanation/circle.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
tools/images/chapters/introduction/cubic.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
tools/images/chapters/introduction/quadratic.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
tools/images/chapters/whatis/interpolation.png
Normal file
After Width: | Height: | Size: 29 KiB |