1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-12 11:44:22 +02:00

start of revision control

This commit is contained in:
Pomax
2015-12-20 15:19:50 -08:00
commit 2e0a7c68d5
77 changed files with 29859 additions and 0 deletions

3
.babelrc Normal file
View File

@@ -0,0 +1,3 @@
{
presets: ['es2015', 'react']
}

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

23059
article.js Normal file

File diff suppressed because it is too large Load Diff

6
components/App.jsx Normal file
View File

@@ -0,0 +1,6 @@
var React = require("react");
var ReactDOM = require("react-dom");
var Article = require("./Article.jsx");
var style = require("../stylesheets/style.less");
ReactDOM.render(<Article/>, document.getElementById("article"));

21
components/Article.jsx Normal file
View File

@@ -0,0 +1,21 @@
var React = require("react");
var ReactDOM = require("react-dom");
var Article = React.createClass({
getInitialState: function() {
return {
sections: require("./sections")
};
},
render: function() {
var sections = Object.keys(this.state.sections).map((name, entry) => {
var Type = this.state.sections[name];
return <Type key={name} number={1+entry} />;
});
return <div>{ sections }</div>;
}
});
module.exports = Article;

188
components/Figure.jsx Normal file
View File

@@ -0,0 +1,188 @@
function bindDrawFunctions(idx) {
var figure = find("figure")[idx];
var cvs = document.createElement("canvas");
cvs.width = 200;
cvs.height = 200;
var ctx = cvs.getContext("2d");
figure.appendChild(cvs);
var button = figure.querySelector("button");
if(button) { figure.appendChild(button); }
return {
getCanvas: function() { return cvs; },
reset: function() {
cvs.width = cvs.width;
ctx.strokeStyle = "black";
ctx.fillStyle = "none";
},
setColor: function(c) {
ctx.strokeStyle = c;
},
noColor: function(c) {
ctx.strokeStyle = "transparent";
},
setRandomColor: function() {
var r = ((255*Math.random())|0),
g = ((255*Math.random())|0),
b = ((255*Math.random())|0);
ctx.strokeStyle = "rgb("+r+","+g+","+b+")";
},
setRandomFill: function(a) {
a = (typeof a === "undefined") ? 1 : a;
var r = ((255*Math.random())|0),
g = ((255*Math.random())|0),
b = ((255*Math.random())|0);
ctx.fillStyle = "rgba("+r+","+g+","+b+","+a+")";
},
setFill: function(c) {
ctx.fillStyle = c;
},
noFill: function() {
ctx.fillStyle = "transparent";
},
drawSkeleton: function(curve, offset) {
offset = offset || { x:0, y:0 };
var pts = curve.points;
ctx.strokeStyle = "lightgrey";
this.drawLine(pts[0], pts[1], offset);
if(pts.length === 3) { this.drawLine(pts[1], pts[2], offset); }
else {this.drawLine(pts[2], pts[3], offset); }
ctx.strokeStyle = "black";
this.drawPoints(pts, offset);
},
drawCurve: function(curve, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x;
var oy = offset.y;
ctx.beginPath();
var p = curve.points, i;
ctx.moveTo(p[0].x + ox, p[0].y + oy);
if(p.length === 3) {
ctx.quadraticCurveTo(
p[1].x + ox, p[1].y + oy,
p[2].x + ox, p[2].y + oy
);
}
if(p.length === 4) {
ctx.bezierCurveTo(
p[1].x + ox, p[1].y + oy,
p[2].x + ox, p[2].y + oy,
p[3].x + ox, p[3].y + oy
);
}
ctx.stroke();
ctx.closePath();
},
drawLine: function(p1, p2, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x;
var oy = offset.y;
ctx.beginPath();
ctx.moveTo(p1.x + ox,p1.y + oy);
ctx.lineTo(p2.x + ox,p2.y + oy);
ctx.stroke();
},
drawPoint: function(p, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x;
var oy = offset.y;
ctx.beginPath();
ctx.arc(p.x + ox, p.y + oy, 5, 0, 2*Math.PI);
ctx.stroke();
},
drawPoints: function(points, offset) {
offset = offset || { x:0, y:0 };
points.forEach(function(p) {
this.drawCircle(p, 3, offset);
}.bind(this));
},
drawArc: function(p, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x;
var oy = offset.y;
ctx.beginPath();
ctx.moveTo(p.x + ox, p.y + oy);
ctx.arc(p.x + ox, p.y + oy, p.r, p.s, p.e);
ctx.lineTo(p.x + ox, p.y + oy);
ctx.fill();
ctx.stroke();
},
drawCircle: function(p, r, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x;
var oy = offset.y;
ctx.beginPath();
ctx.arc(p.x + ox, p.y + oy, r, 0, 2*Math.PI);
ctx.stroke();
},
drawbbox: function(bbox, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x;
var oy = offset.y;
ctx.beginPath();
ctx.moveTo(bbox.x.min + ox, bbox.y.min + oy);
ctx.lineTo(bbox.x.min + ox, bbox.y.max + oy);
ctx.lineTo(bbox.x.max + ox, bbox.y.max + oy);
ctx.lineTo(bbox.x.max + ox, bbox.y.min + oy);
ctx.closePath();
ctx.stroke();
},
drawShape: function(shape, offset) {
offset = offset || { x:0, y:0 };
var order = shape.forward.points.length - 1;
ctx.beginPath();
ctx.moveTo(offset.x + shape.startcap.points[0].x, offset.y + shape.startcap.points[0].y);
ctx.lineTo(offset.x + shape.startcap.points[3].x, offset.y + shape.startcap.points[3].y);
if(order === 3) {
ctx.bezierCurveTo(
offset.x + shape.forward.points[1].x, offset.y + shape.forward.points[1].y,
offset.x + shape.forward.points[2].x, offset.y + shape.forward.points[2].y,
offset.x + shape.forward.points[3].x, offset.y + shape.forward.points[3].y
);
} else {
ctx.quadraticCurveTo(
offset.x + shape.forward.points[1].x, offset.y + shape.forward.points[1].y,
offset.x + shape.forward.points[2].x, offset.y + shape.forward.points[2].y
);
}
ctx.lineTo(offset.x + shape.endcap.points[3].x, offset.y + shape.endcap.points[3].y);
if(order === 3) {
ctx.bezierCurveTo(
offset.x + shape.back.points[1].x, offset.y + shape.back.points[1].y,
offset.x + shape.back.points[2].x, offset.y + shape.back.points[2].y,
offset.x + shape.back.points[3].x, offset.y + shape.back.points[3].y
);
} else {
ctx.quadraticCurveTo(
offset.x + shape.back.points[1].x, offset.y + shape.back.points[1].y,
offset.x + shape.back.points[2].x, offset.y + shape.back.points[2].y
);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
},
drawText: function(text, offset) {
offset = offset || { x:0, y:0 };
ctx.fillText(text, offset.x, offset.y);
}
};
}

316
components/Graphic.jsx Normal file
View File

@@ -0,0 +1,316 @@
var React = require("react");
var ReactDOM = require("react-dom");
var fix = function(e) {
e = e || window.event;
var target = e.target || e.srcElement,
rect = target.getBoundingClientRect();
e.offsetX = e.clientX - rect.left;
e.offsetY = e.clientY - rect.top;
};
var defaultWidth = 275;
var defaultHeight = 275;
var Graphic = React.createClass({
Bezier: require("bezier-js"),
curve: false,
mx:0,
my:0,
cx:0,
cy:0,
mp: { x: 0, y: 0},
render: function() {
return (
<figure>
<canvas ref="canvas"
onMouseDown={this.mouseDown}
onMouseMove={this.mouseMove}
onMouseUp={this.mouseUp}
onClick={this.mouseClick}
/>
<figcaption>{this.props.title}</figcaption>
</figure>
);
},
componentDidMount: function() {
var cvs = this.refs.canvas;
cvs.width = defaultWidth;
cvs.height = defaultHeight;
this.cvs = cvs;
var ctx = cvs.getContext("2d");
this.ctx = ctx;
this.props.setup(this);
this.props.draw(this,this.curve);
},
mouseDown: function(evt) {
fix(evt);
this.mx = evt.offsetX;
this.my = evt.offsetY;
this.lpts.forEach(p => {
if(Math.abs(this.mx - p.x)<10 && Math.abs(this.my - p.y)<10) {
this.moving = true;
this.mp = p;
this.cx = p.x;
this.cy = p.y;
}
});
},
mouseMove: function(evt) {
fix(evt);
var found = false;
this.lpts.forEach(p => {
var mx = evt.offsetX;
var my = evt.offsetY;
if(Math.abs(mx-p.x)<10 && Math.abs(my-p.y)<10) {
found = found || true;
}
});
this.cvs.style.cursor = found ? "pointer" : "default";
if(!this.moving) return;
this.ox = evt.offsetX - this.mx;
this.oy = evt.offsetY - this.my;
this.mp.x = this.cx + this.ox;
this.mp.y = this.cy + this.oy;
this.curve.update();
this.props.draw(this, this.curve);
},
mouseUp: function(evt) {
if(!this.moving) return;
this.moving = false;
this.mp = false;
},
mouseClick: function(evt) {
fix(evt);
this.mx = evt.offsetX;
this.my = evt.offsetY;
},
/**
* API for curve drawing.
*/
reset: function() {
this.refs.canvas.width = this.refs.canvas.width;
this.ctx.strokeStyle = "black";
this.ctx.lineWidth = 1;
this.ctx.fillStyle = "none";
this.offset = {x:0, y:0};
},
getPanelWidth: function() {
return defaultWidth;
},
getPanelHeight: function() {
return defaultHeight;
},
getDefaultQuadratic: function() {
return new this.Bezier(70,250,20,110,250,60);
},
getDefaultCubic: function() {
return new this.Bezier(120,160,35,200,220,260,220,40);
},
setPanelCount: function(c) {
var cvs = this.refs.canvas;
cvs.width = c*defaultWidth;
},
setCurve: function(c) {
this.curve = c;
this.lpts = c.points;
},
setOffset: function(f) {
this.offset = f;
},
setColor: function(c) {
this.ctx.strokeStyle = c;
},
setWeight: function(c) {
this.ctx.lineWidth = c;
},
noColor: function(c) {
this.ctx.strokeStyle = "transparent";
},
setRandomColor: function() {
var r = ((255*Math.random())|0),
g = ((255*Math.random())|0),
b = ((255*Math.random())|0);
this.ctx.strokeStyle = "rgb("+r+","+g+","+b+")";
},
setRandomFill: function(a) {
a = (typeof a === "undefined") ? 1 : a;
var r = ((255*Math.random())|0),
g = ((255*Math.random())|0),
b = ((255*Math.random())|0);
this.ctx.fillStyle = "rgba("+r+","+g+","+b+","+a+")";
},
setFill: function(c) {
this.ctx.fillStyle = c;
},
noFill: function() {
this.ctx.fillStyle = "transparent";
},
drawSkeleton: function(curve, offset) {
offset = offset || { x:0, y:0 };
var pts = curve.points;
this.ctx.strokeStyle = "lightgrey";
this.drawLine(pts[0], pts[1], offset);
if(pts.length === 3) { this.drawLine(pts[1], pts[2], offset); }
else {this.drawLine(pts[2], pts[3], offset); }
this.ctx.strokeStyle = "black";
this.drawPoints(pts, offset);
},
drawCurve: function(curve, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
var p = curve.points, i;
this.ctx.moveTo(p[0].x + ox, p[0].y + oy);
if(p.length === 3) {
this.ctx.quadraticCurveTo(
p[1].x + ox, p[1].y + oy,
p[2].x + ox, p[2].y + oy
);
}
if(p.length === 4) {
this.ctx.bezierCurveTo(
p[1].x + ox, p[1].y + oy,
p[2].x + ox, p[2].y + oy,
p[3].x + ox, p[3].y + oy
);
}
this.ctx.stroke();
this.ctx.closePath();
},
drawLine: function(p1, p2, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(p1.x + ox,p1.y + oy);
this.ctx.lineTo(p2.x + ox,p2.y + oy);
this.ctx.stroke();
},
drawPoint: function(p, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.arc(p.x + ox, p.y + oy, 5, 0, 2*Math.PI);
this.ctx.stroke();
},
drawPoints: function(points, offset) {
offset = offset || { x:0, y:0 };
points.forEach(function(p) {
this.drawCircle(p, 3, offset);
}.bind(this));
},
drawArc: function(p, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(p.x + ox, p.y + oy);
this.ctx.arc(p.x + ox, p.y + oy, p.r, p.s, p.e);
this.ctx.lineTo(p.x + ox, p.y + oy);
this.ctx.fill();
this.ctx.stroke();
},
drawCircle: function(p, r, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.arc(p.x + ox, p.y + oy, r, 0, 2*Math.PI);
this.ctx.stroke();
},
drawbbox: function(bbox, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(bbox.x.min + ox, bbox.y.min + oy);
this.ctx.lineTo(bbox.x.min + ox, bbox.y.max + oy);
this.ctx.lineTo(bbox.x.max + ox, bbox.y.max + oy);
this.ctx.lineTo(bbox.x.max + ox, bbox.y.min + oy);
this.ctx.closePath();
this.ctx.stroke();
},
drawShape: function(shape, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
var order = shape.forward.points.length - 1;
this.ctx.beginPath();
this.ctx.moveTo(ox + shape.startcap.points[0].x, oy + shape.startcap.points[0].y);
this.ctx.lineTo(ox + shape.startcap.points[3].x, oy + shape.startcap.points[3].y);
if(order === 3) {
this.ctx.bezierCurveTo(
ox + shape.forward.points[1].x, oy + shape.forward.points[1].y,
ox + shape.forward.points[2].x, oy + shape.forward.points[2].y,
ox + shape.forward.points[3].x, oy + shape.forward.points[3].y
);
} else {
this.ctx.quadraticCurveTo(
ox + shape.forward.points[1].x, oy + shape.forward.points[1].y,
ox + shape.forward.points[2].x, oy + shape.forward.points[2].y
);
}
this.ctx.lineTo(ox + shape.endcap.points[3].x, oy + shape.endcap.points[3].y);
if(order === 3) {
this.ctx.bezierCurveTo(
ox + shape.back.points[1].x, oy + shape.back.points[1].y,
ox + shape.back.points[2].x, oy + shape.back.points[2].y,
ox + shape.back.points[3].x, oy + shape.back.points[3].y
);
} else {
this.ctx.quadraticCurveTo(
ox + shape.back.points[1].x, oy + shape.back.points[1].y,
ox + shape.back.points[2].x, oy + shape.back.points[2].y
);
}
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
},
drawText: function(text, offset) {
offset = offset || { x:0, y:0 };
if (this.offset) {
offset.x += this.offset.x;
offset.y += this.offset.y;
}
this.ctx.fillText(text, offset.x, offset.y);
}
});
module.exports = Graphic;

35
components/LaTeX.jsx Normal file
View File

@@ -0,0 +1,35 @@
var React = require("react");
var ReactDOM = require("react-dom");
var MathJax = (typeof window !== "undefined" ? window.MathJax : false);
var noop = function() {};
// fallback will simply do nothing when typesetting.
if(!MathJax){MathJax={Hub:{Queue:noop}};}
var LaTeX = React.createClass({
mixins: [
require("react-component-visibility")
],
componentDidMount: function() {
this.setComponentVisbilityRateLimit(200);
},
componentVisibilityChanged: function() {
var visible = this.state.visible;
if (visible) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.refs.latex, false]);
this.componentVisibilityChanged = noop;
}
},
render: function() {
var data = this.props.children;
if (!data.forEach) data = [data];
return <p ref="latex" dangerouslySetInnerHTML={{__html: data.join('') }} />;
}
});
module.exports = LaTeX;

View File

@@ -0,0 +1,10 @@
var React = require("react");
var ReactDOM = require("react-dom");
var SectionHeader = React.createClass({
render: function() {
return <h2 data-num={this.props.number}>{this.props.children}</h2>;
}
});
module.exports = SectionHeader;

View File

@@ -0,0 +1,38 @@
/*
void drawFunction() {
pushStyle();
translate(dim/2,dim/2);
stroke(150);
line(-dim,0,dim,0);
line(0,-dim,0,dim);
stroke(0);
float r = dim/3;
for(float t=0; t<=5; t+=0.01) {
point(r * sin(t), -r * cos(t));
}
fill(0);
for(float i=0; i<=5; i+=0.5) {
ellipse(r * sin(i), -r * cos(i),3,3);
}
textAlign(CENTER,CENTER);
float x=0, y=-r*1.2, nx, ny;
for(float i=0; i<=5; i+=0.5) {
nx = x*cos(i) - y*sin(i);
ny = x*sin(i) + y*cos(i);
text("t="+i, nx, ny);
}
fill(150);
text("0,0",-10,10);
text("1",r+10,15);
text("-1",-10,r+10);
text("-1",-r-10,15);
text("1",-10,-r-10);
popStyle();
}
*/

View File

@@ -0,0 +1,231 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var LaTeX = require("../../LaTeX.jsx");
var Explanation = React.createClass({
setup: function(api) {
},
draw: function(api, curve) {
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>The basics of Bézier curves?</SectionHeader>
<p>Bézier curves are a form of "parametric" function. Mathematically speaking, parametric
functions are cheats: a "function" is actually a well defined term representing a mapping
from any number of inputs to a <strong>single</strong> output. Numbers go in, a single
number comes out. Change the numbers that go in, and the number that comes out is still
a single number. Parametric functions cheat. They basically say "alright, well, we want
multiple values coming out, so we'll just use more than one function". An illustration:
Let's say we have a function that maps some value, let's call it <i>x</i>, to
some other value, using some kind of number manipulation:</p>
<LaTeX>\[
f(x) = \sin(x)
\]</LaTeX>
<p>The notation <i>f(x)</i> is the standard way to show that it's a function (by convention
called <i>f</i> if we're only listing one) and its output changes based on one variable
(in this case, <i>x</i>). Change <i>x</i>, and the output for <i>f(x)</i> changes.</p>
<p>So far so good. Now, let's look at parametric functions, and how they cheat.
Let's take the following two functions:</p>
<LaTeX>\[\begin{matrix}
f(a) = \sin(a) \\
f(b) = \cos(b)
\end{matrix}\]</LaTeX>
<p>There's nothing really remarkable about them, they're just a sine and cosine function,
but you'll notice the inputs have different names. If we change the value for <i>a</i>,
we're not going to change the output value for <i>f(b)</i>, since <i>a</i> isn't used
in that function. Parametric functions cheat by changing that. In a parametric function
all the different functions share a variable, like this:</p>
<LaTeX>\[
\left \{ \begin{matrix}
f_a(t) = \sin(t) \\
f_b(t) = \cos(t)
\end{matrix} \right. \]</LaTeX>
<p>Multiple functions, but only one variable. If we change the value for <i>t</i>,
we change the outcome of both <i>f<sub>a</sub>(t)</i> and <i>f<sub>b</sub>(t)</i>.
You might wonder how that's useful, and the answer is actually pretty simple: if
we change the labels <i>f<sub>a</sub>(t)</i> and <i>f<sub>b</sub>(t)</i> with what
we usually mean with them for parametric curves, things might be a lot more obvious:</p>
<LaTeX>\[
\left \{ \begin{matrix}
x = \sin(t) \\
y = \cos(t)
\end{matrix} \right. \]</LaTeX>
<p>There we go. <i>x</i>/<i>y</i> coordinates, linked through some mystery value <i>t</i>.</p>
<p>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:</p>
<Graphic preset="empty" title="A (partial) circle: x=sin(t), y=cos(t)" setup={this.setup} draw={this.draw}/>
<p>Bézier curves are (one in many classes of) parametric functions, and are characterised
by using the same base function for all its dimensions. Unlike the above example,
where the <i>x</i> and <i>y</i> values use different functions (one uses a sine, the other
a cosine), Bézier curves use the "binomial polynomial" for both <i>x</i> and <i>y</i>.
So what are binomial polynomials?</p>
<p>You may remember polynomials from high school, where they're those sums that look like:</p>
<LaTeX>\[
f(x) = a \cdot x^3 + b \cdot x^2 + c \cdot x + d
\]</LaTeX>
<p>If they have a highest order term <i></i> they're called "cubic" polynomials, if it's
<i></i> it's a "square" polynomial, if it's just <i>x</i> it's a line (and if there aren't
even any terms with <i>x</i> it's not a polynomial!)</p>
<p>Bézier curves are polynomials of <i>t</i>, rather than <i>x</i>, with the value for <i>t</i>
fixed being between 0 and 1, with coefficients <i>a</i>, <i>b</i> etc. taking the "binomial"
form, which sounds fancy but is actually a pretty simple description for mixing values:</p>
<LaTeX>\[ \begin{align*}
linear &= (1-t) + t \\
square &= (1-t)^2 + 2 \cdot (1-t) \cdot t + t^2 \\
cubic &= (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3
\end{align*} \]</LaTeX>
<p>I know what you're thinking: that doesn't look too simple, but if we remove <i>t</i> and
add in "times one", things suddenly look pretty easy. Check out these binomial terms:</p>
<LaTeX>\[ \begin{align*}
linear &= \hskip{2.5em} 1 + 1 \\
square &= \hskip{1.7em} 1 + 2 + 1\\
cubic &= \hskip{0.85em} 1 + 3 + 3 + 1\\
hypercubic &= 1 + 4 + 6 + 4 + 1
\end{align*} \]</LaTeX>
<p>Notice that 2 is the same as 1+1, and 3 is 2+1 and 1+2, and 6 is 3+3... As you
can see, each time we go up a dimension, we simply start and end with 1, and everything
in between is just "the two numbers above it, added together". Now <i>that's</i> easy
to remember.</p>
<p>There's an equally simple way to figure out how the polynomial terms work:
if we rename <i>(1-t)</i> to <i>a</i> and <i>t</i> to <i>b</i>, and remove the weights
for a moment, we get this:</p>
<LaTeX>\[ \begin{align*}
linear &= BLUE[a] + RED[b] \\
square &= BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot RED[b] + RED[b] \cdot RED[b] \\
cubic &= BLUE[a] \cdot BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot BLUE[a] \cdot RED[b] + BLUE[a] \cdot RED[b] \cdot RED[b] + RED[b] \cdot RED[b] \cdot RED[b]\\
\end{align*} \]</LaTeX>
<p>It's basically just a sum of "every combination of <i>a</i> and <i>b</i>", progressively
replacing <i>a</i>'s with <i>b</i>'s after every + sign. So that's actually pretty simple
too. So now you know binomial polynomials, and just for completeness I'm going to show
you the generic function for this:</p>
<LaTeX>\[
Bézier(n,t) = \sum_{i=0}^{n}
\underset{binomial\ term}{\underbrace{\binom{n}{i}}}
\cdot\
\underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
\]</LaTeX>
<p>And that's the full description for Bézier curves. Σ in this function indicates that this is
a series of additions (using the variable listed below the Σ, starting at ...=&lt;value&gt; and ending
at the value listed on top of the Σ).</p>
<div className="howtocode">
<h3>How to implement the basis function</h3>
<p>We could naively implement the basis function as a mathematical construct,
using the function as our guide, like this:</p>
<pre>function Bezier(n,t):
sum = 0
for(k=0; k&lt;n; k++):
sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
return sum</pre>
<p>I say we could, because we're not going to: the factorial function is <em>incredibly</em>
expensive. And, as we can see from the above explanation, we can actually create Pascal's
triangle quite easily without it: just start at [1], then [1,1], then [1,2,1], then [1,3,3,1],
and so on, with each next row fitting 1 more number than the previous row, starting and
ending with "1", with all the numbers in between being the sum of the previous row's
elements on either side "above" the one we're computing.</p>
<p>We can generate this as a list of lists lightning fast, and then never have to compute
the binomial terms because we have a lookup table:</p>
<pre>lut = [ [1], // n=0
[1,1], // n=1
[1,2,1], // n=2
[1,3,3,1], // n=3
[1,4,6,4,1], // n=4
[1,5,10,10,5,1], // n=5
[1,6,15,20,15,6,1]] // n=6
binomial(n,k):
while(n &gt;= lut.length):
s = lut.length
nextRow = new array(size=s+1)
nextRow[0] = 1
for(i=1, prev=s-1; i&ltprev; i++):
nextRow[i] = lut[prev][i-1] + lut[prev][i]
nextRow[s] = 1
lut.add(nextRow)
return lut[n][k]</pre>
<p>So what's going on here? First, we declare a lookup table with a size that's reasonably
large enough to accommodate most lookups. Then, we declare a function to get us the values
we need, and we make sure that if an n/k pair is requested that isn't in the LUT yet, we
expand it first. Our basis function now looks like this:</p>
<pre>function Bezier(n,t):
sum = 0
for(k=0; k&lt;n; k++):
sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
return sum</pre>
<p>Perfect. Of course, we can optimize further. For most computer graphics purposes, we
don't need arbitrary curves. We need quadratic and cubic curves (this primer actually
does do arbitrary curves, so you'll find code similar to shown here), which means we can
drastically simplify the code:</p>
<pre>function Bezier(2,t):
t2 = t * t
mt = 1-t
mt2 = mt * mt
return mt2 + 2*mt*t + t2
function Bezier(3,t):
t2 = t * t
t3 = t2 * t
mt = 1-t
mt2 = mt * mt
mt3 = mt2 * mt
return mt3 + 3*mt2*t + 3*mt*t2 + t3</pre>
<p>And now we know how to program the basis function. Exellent.</p>
</div>
<p>So, now we know what the base function(s) look(s) like, time to add in the magic that makes
Bézier curves so special: control points.</p>
</section>
);
}
});
module.exports = Explanation;

View File

@@ -0,0 +1,55 @@
/**
* This is an ordered list of all sections for the article
* @type {Object}
*/
module.exports = {
preface: require("./preface"),
introduction: require("./introduction"),
whatis: require("./whatis"),
explanation: require("./explanation")
};
/*
control: require("./control.jsx"),
matrix: require("./matrix.jsx"),
decasteljau: require("./decasteljau.jsx"),
flattening: require("./flattening.jsx"),
splitting: require("./splitting.jsx"),
matrixsplit: require("./matrixsplit.jsx"),
reordering: require("./reordering.jsx"),
derivatives: require("./derivatives.jsx"),
pointvectors: require("./pointvectors.jsx"),
components: require("./components.jsx"),
extremities: require("./extremities.jsx"),
boundingbox: require("./boundingbox.jsx"),
aligning: require("./aligning.jsx"),
tightbounds: require("./tightbounds.jsx"),
canonical: require("./canonical.jsx"),
arclength: require("./arclength.jsx"),
arclengthapprox: require("./arclengthapprox.jsx"),
tracing: require("./tracing.jsx"),
intersections: require("./intersections.jsx"),
curveintersection: require("./curveintersection.jsx"),
moulding: require("./moulding.jsx"),
pointcurves: require("./pointcurves.jsx"),
catmullconv: require("./catmullconv.jsx"),
catmullmoulding: require("./catmullmoulding.jsx"),
polybezier: require("./polybezier.jsx"),
shapes: require("./shapes.jsx"),
projections: require("./projections.jsx"),
offsetting: require("./offsetting.jsx"),
graduatedoffset: require("./graduatedoffset.jsx"),
circles: require("./circles.jsx"),
circles_cubic: require("./circles_cubic.jsx"),
arcapproximation: require("./arcapproximation.jsx")
*/

View File

@@ -0,0 +1,48 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var Introduction = React.createClass({
drawQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
drawCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
drawCurve: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>A lightning introduction</SectionHeader>
<p>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.</p>
<Graphic title="Quadratic Bézier curves" setup={ this.drawQuadratic } draw={ this.drawCurve }/>
<Graphic title="Cubic Bézier curves" setup={ this.drawCubic } draw={ this.drawCurve }/>
<p>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,
the 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!</p>
</section>
);
}
});
module.exports = Introduction;

View File

@@ -0,0 +1,43 @@
whatis: require("./whatis.jsx")
explanation: require("./explanation.jsx"),
control: require("./control.jsx"),
matrix: require("./matrix.jsx"),
decasteljau: require("./decasteljau.jsx"),
flattening: require("./flattening.jsx"),
splitting: require("./splitting.jsx"),
matrixsplit: require("./matrixsplit.jsx"),
reordering: require("./reordering.jsx"),
derivatives: require("./derivatives.jsx"),
pointvectors: require("./pointvectors.jsx"),
components: require("./components.jsx"),
extremities: require("./extremities.jsx"),
boundingbox: require("./boundingbox.jsx"),
aligning: require("./aligning.jsx"),
tightbounds: require("./tightbounds.jsx"),
canonical: require("./canonical.jsx"),
arclength: require("./arclength.jsx"),
arclengthapprox: require("./arclengthapprox.jsx"),
tracing: require("./tracing.jsx"),
intersections: require("./intersections.jsx"),
curveintersection: require("./curveintersection.jsx"),
moulding: require("./moulding.jsx"),
pointcurves: require("./pointcurves.jsx"),
catmullconv: require("./catmullconv.jsx"),
catmullmoulding: require("./catmullmoulding.jsx"),
polybezier: require("./polybezier.jsx"),
shapes: require("./shapes.jsx"),
projections: require("./projections.jsx"),
offsetting: require("./offsetting.jsx"),
graduatedoffset: require("./graduatedoffset.jsx"),
circles: require("./circles.jsx"),
circles_cubic: require("./circles_cubic.jsx"),
arcapproximation: require("./arcapproximation.jsx")

View File

@@ -0,0 +1,79 @@
var React = require("react");
var Preface = React.createClass({
render: function() {
return (
<section>
<p>In order to draw things in 2D, we usually rely on lines, which typically get classified
into two categories: straight lines, and curves. The first of these are as easy to draw as they
are easy to make a computer draw. Give a computer the first and last point in the line, and
BAM! straight line. No questions asked.</p>
<p>Curves, however, are a much bigger problem. While we can draw curves with ridiculous ease
freehand, computers are a bit handicapped in that they can't draw curves unless there is a
mathematical function that describes how it should be drawn. In fact, they even need this for
straight lines, but the function is ridiculously easy, so we tend to ignore that as far as
computers are concerned, all lines are "functions", regardless of whether they're straight
or curves. However, that does mean that we need to come up with fast-to-compute functions that
lead to nice looking curves on a computer. There's a number of these, and in this article
we'll focus on a particular function that has received quite a bit of attention, and is used
in pretty much anything that can draw curves: "Bézier" curves</p>
<p>They're named after <a href="https://en.wikipedia.org/wiki/Pierre_B%C3%A9zier">Pierre
Bézier</a>, who is principally responsible for getting them known to the world as a curve
well-suited for design work (working for Renault and publishing his investigations in 1962),
although he was not the first, or only one, to "invent" these type of curves.
One might be tempted to say that the mathematician <a href="https://en.wikipedia.org/wiki/Paul_de_Casteljau">Paul
de Casteljau</a> was first, investigating the nature of these curves in 1959 while working at
Citroën, coming up with a really elegant way of figuring out how to draw them. However, de
Casteljau did not publish his work, making the question "who was first" hard to answer in
any absolute sense. Or is it? Bézier curves are, at their core, "Bernstein polynomials", a family
of mathematical functions investigated by
<a href="https://en.wikipedia.org/wiki/Sergei_Natanovich_Bernstein">Sergei Natanovich Bernstein</a>,
with publications on them at least as far back as 1912. Anyway, that's mostly trivia, what
you are more likely to care about is that these curves are handy: you can link up multiple
Bézier curves so that the combination looks like a single curve. If you've ever drawn Photoshop
"paths" or worked with vector drawing programs like Flash, Illustrator or Inkscape, those curves
you've been drawing are Bézier curves.</p>
<p>So, what if you need to program them yourself? What are the pitfalls? How do you draw them?
What are the bounding boxes, how do you determine intersections, how can you extrude a curve,
in short: how do you do everything that you might want when you do with these curves? That's
what this page is for. Prepare to be mathed.</p>
<div>
<h2>All Bézier graphics are interactive.</h2>
<p>This page uses interactive examples, as well as "real" maths (in LaTeX form) which
is typeset using the most excellent <a href="http://MathJax.org">MathJax</a> library.
All the examples also have a "view source" option, which lets you see how things were
implemented using the Bezier.js library.</p>
<h2>How complicated is the maths going to be?</h2>
<p>Most of the mathematics in this Primer are early high school maths. If you understand basic
arithmetic, and you know how to read English, you should be able to get by just fine. There
will at times be <em>far</em> more complicated maths, but if you don't feel like digesting
them, you can safely skip over them by either skipping over the "detail boxes" in section
or by just jumping to the end of a section with maths that looks too involving. The end of
sections typically simply list the conclusions so you can just work with those values directly.</p>
<h2>Questions, comments:</h2>
If you have suggestions for new sections, hit up the <a href="https://github.com/pomax/bezierinfo/issues">github
issue tracker</a> (also reachable from the repo linked to in the upper right). If you have
questions about the material, there's currently no comment section while I'm doing the rewrite,
but you can use the issue tracker for that as well. Once the rewrite is done, I'll add a general
comment section back in, and maybe a more topical "select this section of text and hit the
'question' button to ask a question about it" system. We'll see.
<p>Pomax (or in the tweetworld, <a href="https://twitter.com/TheRealPomax">@TheRealPomax</a>)</p>
</div>
</section>
);
}
});
module.exports = Preface;

View File

@@ -0,0 +1,139 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var LaTeX = require("../../LaTeX.jsx");
var Whatis = React.createClass({
setup: function(api) {
this.offset = 20;
var curve = api.getDefaultQuadratic();
api.setPanelCount(3);
api.setCurve(curve);
this.dim = api.getPanelWidth();
},
draw: function(api, curve) {
var pts = curve.points;
var p1 = pts[0], p2=pts[1], p3 = pts[2];
var p1e = {
x: p1.x + 0.2 * (p2.x - p1.x),
y: p1.y + 0.2 * (p2.y - p1.y)
};
var p2e = {
x: p2.x + 0.2 * (p3.x - p2.x),
y: p2.y + 0.2 * (p3.y - p2.y)
};
var m = {
x: p1e.x + 0.2 * (p2e.x - p1e.x),
y: p1e.y + 0.2 * (p2e.y - p1e.y)
}
api.reset();
api.setColor("black");
api.setFill("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
// draw 20% off-start points and struts
api.setColor("blue");
api.setWeight(2);
api.drawLine(p1, p1e);
api.drawLine(p2, p2e);
api.drawCircle(p1e,3);
api.drawCircle(p2e,3);
api.drawText("linear interpolation distance: " + this.offset + "%", {x:5, y:15});
api.drawText("linear interpolation between the first set of points", {x:5, y:this.dim-5});
// next panel
api.setColor("black");
api.setWeight(1);
api.setOffset({x:this.dim, y:0});
api.drawLine({x:0, y:0}, {x:0, y:this.dim});
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("lightgrey");
api.drawLine(p1e, p2e);
api.drawCircle(p1e,3);
api.drawCircle(p2e,3);
api.setColor("blue");
api.setWeight(2);
api.drawLine(p1e, m);
api.drawCircle(m,3);
api.drawText("same linear interpolation distance: " + this.offset + "%", {x:5, y:15});
api.drawText("linear interpolation between the second set of points", {x:5, y:this.dim-5});
// next panel
api.setColor("black");
api.setWeight(1);
api.setOffset({x:2*this.dim, y:0});
api.drawLine({x:0, y:0}, {x:0, y:this.dim});
api.drawSkeleton(curve);
api.drawCurve(curve);
api.drawCircle(m,3);
api.drawText("the second interpolation turns out to be a curve point!", {x:5, y:this.dim-5});
},
render: function() {
return (
<section>
<SectionHeader {...this.props}>What is a Bézier Curve?</SectionHeader>
<p>Playing with the points for curves may have given you a feel for how Bézier curves behaves, but
what <em>are</em> Bézier curves, really?</p>
<p>There are two ways to explain what a Bézier curve is, and they turn out to be the entirely equivalent,
but one of them uses complicated maths, and the other uses really simple maths. So... let's start
with the simple explanation:</p>
<p>Bezier curves are the result of <a href="https://en.wikipedia.org/wiki/Linear_interpolation">linear
interpolations</a>. That sounds complicated but you've been doing linear interpolation since you were
very young: any time you had to point at something between two other things, you've been applying
linear interpolation. It's simply "picking a point between two, points". If we know the distance
between those two points, and we want a new point that is, say, 20% the distance away from
the first point (and thus 80% the distance away from the second point) then we can compute that
really easily:</p>
<LaTeX>\[
p_1 = some\ point, \\
p_2 = some\ other\ point, \\
distance = (p_2 - p_1), \\
ratio = \frac{percentage}{100}, \\
new\ point = p_1 + distance \cdot ratio
\]</LaTeX>
<p>So let's look at that in action: the following graphic is interactive in that you can use your
'+' and '-' keys to increase or decrease the interpolation distance, to see what happens. We start
with three points, which gives us two lines. Linear interpolation over those lines gives use two
points, between which we can again perform linear interpolation, yielding a single point. And that
point, and all points we can form in this way for all distances taken together, form our Bézier curve:</p>
<Graphic preset="threepanel" title="Linear Interpolation leading to Bézier curves" setup={this.setup} draw={this.draw}/>
<p>And that brings us to the complicated maths: calculus. While it doesn't look like that's what we've just done,
we actually just drew a quadratic curve, in steps, rather than in a single go. One of the fascinating parts
about Bézier curves is that they can both be described in terms of polynomial functions, as well as in terms
of very simple interpolations of interpolations of [...]. That it turn means we can look at what these curves
can do based on both "real maths" (by examining the functions, their derivatives, and all that stuff), as well
as by looking at the "mechanical" composition (which tells us that a curve will never extend beyond the points
we used to construct it, for instance)</p>
<p>So let's start looking at Bézier curves a bit more in depth. Their mathematical expressions, the properties we
can derive from those, and the various things we can do to, and with, Bézier curves.</p>
</section>
);
}
});
module.exports = Whatis;

90
data/aligning.jsx Normal file
View File

@@ -0,0 +1,90 @@
<p>While there are an incredible number of curves we can define by varying the x- and y-coordinates for
the control points, not all curves are actually distinct. For instance, if we define a curve, and then
rotate it 90 degrees, it's still the same curve, and we'll find its extremities in the same spots, just
at different draw coordinates. As such, one way to make sure we're working with a "unique" curve is to
"axis-align" it.</p>
<p>Aligning also simplifies a curve's functions. We can translate (move) the curve so that the first
point lies on (0,0), which turns our <i>n</i> term polynomial functions into <i>n-1</i> term functions.
The order stays the same, but we have less terms. Then, we can rotate the curves so that the last point
always lies on the x-axis, too, making its coordinate (...,0). This further simplifies the function for
the y-component to an <i>n-2</i> term function. For instance, if we have a cubic curve such as this:</p>
<p>\[
\left \{ \begin{matrix}
x = BLUE[120] \cdot (1-t)^3 BLUE[+ 35] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 220] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 220] \cdot t^3 \\
y = BLUE[160] \cdot (1-t)^3 BLUE[+ 200] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 260] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 40] \cdot t^3
\end{matrix} \right. \]</p>
<p>Then translating it so that the first coordinate lies on (0,0), moving all <i>x</i> coordinates
by -120, and all <i>y</i> coordinates by -160, gives us:</p>
<p>\[
\left \{ \begin{matrix}
x = BLUE[0] \cdot (1-t)^3 BLUE[- 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 100] \cdot t^3 \\
y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 100] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 120] \cdot t^3
\end{matrix} \right. \]</p>
<p>If we then rotate the curve so that its end point lies on the x-axis, the coordinates (integer-rounded
for illustrative purposes here) become:</p>
<p>\[
\left \{ \begin{matrix}
x = BLUE[0] \cdot (1-t)^3 BLUE[+ 85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 12] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\
y = BLUE[0] \cdot (1-t)^3 BLUE[+ 40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 140] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[+ 0] \cdot t^3
\end{matrix} \right. \]</p>
<p>If we drop all the zero-terms, this gives us:</p>
<p>\[
\left \{ \begin{array}{l}
x = BLUE[85] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[+ 13] \cdot 3 \cdot (1-t) \cdot t^2 BLUE[- 156] \cdot t^3 \\
y = BLUE[40] \cdot 3 \cdot (1-t)^2 \cdot t BLUE[- 141] \cdot 3 \cdot (1-t) \cdot t^2
\end{array} \right. \]</p>
<p>We can see that our original curve definition has been simplified considerably. The following graphics
illustrate the result of aligning our example curves to the x-axis, with the cubic case using
the coordinates that were just used in the example formulae:</p>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Aligning a quadratic curve">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
line(0,0,0,dim);
stroke(0,50);
translate(3*dim/4,dim/2);
line(-3*dim/4,0,dim/4,0);
line(0,-dim/2,0,dim/2);
curve.align().draw(color(150));
}</textarea>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Aligning a cubic curve">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
line(0,0,0,dim);
stroke(0,50);
translate(3*dim/4,dim/2);
line(-3*dim/4,0,dim/4,0);
line(0,-dim/2,0,dim/2);
curve.align().draw(color(150));
}</textarea>

163
data/arcapproximation.jsx Normal file
View File

@@ -0,0 +1,163 @@
<p>Let's look at converting Bézier curves into sequences of circular arcs. We already saw in the
section on circle approximation that this will never yield a perfect equivalent, but sometimes
you need circular arcs, such as when you're working with fabrication machinery, or simple vector
languages that understand lines and circles, but not much else.</p>
<p>The approach is fairly simple: pick a starting point on the curve, and pick two points that are
further along the curve. Determine the circle that goes through those three points, and see if
it fits the part of the curve we're trying to approximate. Decent fit? Try spacing the points
further apart. Bad fit? Try spacing the points closer together. Keep doing this until you've
found the "good approximation/bad approximation" boundary, record the "good" arc, and then move
the starting point up to overlap the end point we previously found. Rinse and repeat until we've
covered the entire curve.</p>
<p>So: step 1, how do we find a circle through three points? That part is actually really simple.
You may remember (if you ever learned it!) that a line between two points on a circle is called
a <a href="https://en.wikipedia.org/wiki/Chord_%28geometry%29">chord</a>, and one property of
chords is that the line from the center of the chord, perpendicular to the chord, passes through
the center of the circle. So: if we have have three points, we have two (different) chords, and
consequently, two (different) lines that go from those chords through the center of the circle:
find the centers of the chords, find the perpendicular lines, find the intersection of those lines,
find the center of the circle that goes through all three points.</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Finding a circle through three points">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
curve.drawPoints();
CircleAbstractor ca = new CircleAbstractor(curve);
Point[] p = curve.points;
CircleAbstractor.Point cp = ca.getCCenter(p[0], p[1], p[2]);
stroke(0,100);
noFill();
ellipse(cp.x, cp.y, cp.r*2, cp.r*2);
ellipse(cp.x, cp.y, 5, 5);
fill(0);
text((int)cp.x+","+(int)cp.y, cp.x + 5, cp.y+5);
stroke(200,0,0);
line(cp.x, cp.y, (p[0].x+p[1].x)/2, (p[0].y+p[1].y)/2);
line(p[0].x,p[0].y,p[1].x,p[1].y);
stroke(0,0,255);
line(cp.x, cp.y, (p[1].x+p[2].x)/2, (p[1].y+p[2].y)/2);
line(p[2].x,p[2].y,p[1].x,p[1].y);
}</textarea>
<p>So, with the procedure on how to find a circle through three points, finding the arc through those points
is straight-forward. Let's apply this to a Bezier curve:</p>
<ul>
<li>Start at <em>t=0</em></li>
<li>Pick two points further down the curve at some value <em>m = t + n</em> and <em>e = t + 2n</em></li>
<li>Find the arc that these points define</li>
<li>Determine how close the found arc is to the curve:
<ul>
<li>Pick two additional points <em>e1 = t + n/2</em> and <em>e2 = t + n + n/2</em>.</li>
<li>These points, if the arc is a good approximation of the curve interval chosen, should
lie <em>on</em> the circle, so their distance to the center of the circle should be the
same as the distance from any of the three other points to the center.</li>
<li>For point points, determine the (absolute) error between the radius of the circle, and the
<em>actual</em> distance from the center of the circle to the point on the curve.</li>
<li>If this error is too high, we consider the arc bad, and try a smaller interval.</li>
</ul>
</li>
</ul>
<p>The result of this is shown in the next graphic: we start at a guaranteed failure: s=0, e=1. That's
the entire curve. The midpoint is simply at <em>t=0.5</em>, and then we start performing a
<a href="https://en.wikipedia.org/wiki/Binary_search_algorithm">Binary Search</a>.</p>
<ol>
<li>We start with {0, 0.5, 1}</li>
<li>That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}</li>
<ul>
<li>If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.</li>
<li>However, if the arc was still bad, we move <em>down</em> by half the distance: {0, 0.125, 0.25}.</li>
</ul>
<li>We keep doing this over and over until we have two arcs found in sequence of which the first arc is good, and
the second arc is bad. When we find that pair, we've found the boundary between a good approximation and a
bad approximation, and we pick the former</li>
</ol>
<p>The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that
if an arc is off by a <em>combined</em> half pixel over both verification points, then we treat the arc as bad.
This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive,
and you can use your '+' and '-' keys to increase to decrease the error threshold, to see what the effect
of a smaller or larger error threshold is.</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Arc approximation of a Bézier curve">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 5;
}
void drawCurve(BezierCurve curve) {
double threshold = 0.5;
if (offset < 1) offset = 1;
if (0 < offset && offset < 10) threshold = offset/10;
else if (10 < offset && offset < 110) threshold = offset-10;
else if (110 < offset) threshold = 100 + (offset-110)*10;
curve.draw();
CircleAbstractor ca = new CircleAbstractor(curve, threshold);
stroke(255,0,0);
fill(0,50);
for (CircleAbstractor.Point c : ca.getCircles()) {
arc(c.x, c.y, 2*c.r, 2*c.r, c.s, c.e);
break;
}
fill(0);
text("error threshold: "+ca.errorThreshold, 5, 15);
}</textarea>
<p>With that in place, all that's left now is to "restart" the procedure by treating the found arc's
end point as the new to-be-determined arc's starting point, and using points further down the curve. We
keep trying this until the found end point is for <em>t=1</em>, at which point we are done. Again,
the following graphic allows for '+' and '-' key input to increase or decrease the error threshold,
so you can see how picking a different threshold changes the number of arcs that are necessary to
reasonably approximate a curve:</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Arc approximation of a Bézier curve">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 5;
}
void drawCurve(BezierCurve curve) {
double threshold = 0.5;
if (offset < 1) offset = 1;
if (0 < offset && offset < 10) threshold = offset/10;
else if (10 < offset && offset < 110) threshold = offset-10;
else if (110 < offset) threshold = 100 + (offset-110)*10;
CircleAbstractor ca = new CircleAbstractor(curve, threshold);
stroke(255,0,0);
fill(0,50);
ArrayList<CircleAbstractor.Point> circles = ca.getCircles();
for (CircleAbstractor.Point c : circles) {
arc(c.x, c.y, 2*c.r, 2*c.r, c.s, c.e);
}
curve.drawControlLines();
curve.drawPoints();
fill(0);
text("error threshold: "+ca.errorThreshold, 5, 15);
text("Approximated the curve using " + circles.size() + " arcs.", 5, dim-5);
}</textarea>
<p>So... what is this good for? Obviously, If you're working with technologies that can't do curves,
but can do lines and circles, then the answer is pretty straight-forward, but what else? There are
some reasons why you might need this technique: using circular arcs means you can determine whether
a coordinate lies "on" your curve really easily: simply compute the distance to each circular arc
center, and if any of those are close to the arc radii, at an angle betwee the arc start and end:
bingo, this point can be treated as lying "on the curve". Another benefit is that this approximation
is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially
compute the arc length of the approximated curve (it's a bit like curve flattening). The only
thing to bear in mind is that this is a lossy equivalence: things that you compute based on the
approximation are guaranteed "off" by some small value, and depending on how much precision you
need, arc approximation is either going to be super useful, or completely useless. It's up to you
to decide which, based on your application!</p>

197
data/arclength.jsx Normal file
View File

@@ -0,0 +1,197 @@
<p>How long is a Bézier curve? As it turns out, that's not actually an easy question, because the answer
requires maths that —much like root finding— cannot generally be solved the traditional way. If we
have a parametric curve with <i>f<sub>x</sub>(t)</i> and <i>f<sub>y</sub>(t)</i>, then the length of the
curve, measured from start point to some point <i>t = z</i>, is computed using the following seemingly
straight forward (if a bit overwhelming) formula:</p>
<p>\[
\int_{0}^{z}\sqrt{f_x'(t)^2+f_y'(t)^2} dt
\]</p>
<p>or, more commonly written using Leibnitz notation as:</p>
<p>\[
length = \int_{0}^{z}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\]</p>
<p>This formula says that the length of a parametric curve is in fact equal to the <b>area</b> underneath a function that
looks a remarkable amount like Pythagoras' rule for computing the diagonal of a straight angled triangle. This sounds
pretty simple, right? Sadly, it's far from simple... cutting straight to after the chase is over: for quadratic curves,
this formula generates an <a href="http://www.wolframalpha.com/input/?i=antiderivative+for+sqrt((2*(1-t)*t*B+%2b+t^2*C)'^2+%2b+(2*(1-t)*t*E)'^2)&incParTime=true">unwieldy computation</a>,
and we're simply not going to implement things that way. For cubic Bézier curves, things get even more fun, because there
is no "closed form" solution, meaning that due to the way calculus works, there is no generic formula that allows you to
calculate the arc length. Let me just repeat this, because it's fairly crucial: <strong><em>for cubic and higher Bézier curves,
there is no way to solve this function if you want to use it "for all possible coordinates".</em></strong></p>
<p>Seriously: <a href="https://en.wikipedia.org/wiki/Abel%E2%80%93Ruffini_theorem">It cannot be done.</a>.</p>
<p>So we turn to numerical approaches again. The method we'll look at here is the
<a href="http://www.youtube.com/watch?v=unWguclP-Ds&feature=BFa&list=PLC8FC40C714F5E60F&index=1">Gauss
quadrature</a>. This approximation is a really neat trick, because for any <i>n<sup>th</sup></i> degree polynomial
it finds approximated values for an integral really efficiently. Explaining this procedure in length is way beyond
the scope of this page, so if you're interested in finding out why it works, I can recommend the University of
South Florida video lecture on the procedure, linked in this very paragraph. The general solution we're looking
for is the following:</p>
<p>\[
\int_{-1}^{1}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\left [ C_1 \cdot f\left(t_1\right)
\ +\ ...
\ +\ C_n \cdot f\left(t_n\right)
\right ]
=
\sum_{i=1}^{n}{C_i \cdot f\left(t_i\right)}
\]</p>
<p>In plain text: an integral function can always be treated as the sum of an (infinite) number of
(infinitely thin) rectangular strips sitting "under" the function's plotted graph. To illustrate
this idea, the following graph shows the integral for a sinoid function. If we pick thin enough
strips, we'd get a "perfect" fit for all the strips from the midline to the actual function values:</p>
<textarea class="sketch-code" data-sketch-preset="empty" data-sketch-title="A function's approximated integral">
void drawFunction() {
float x=0, y=dim/2, nx, ny;
stroke(0);
for(float t=0; t<=2*PI; t+=0.05) {
nx = map(t,0,2*PI,0,dim);
ny = map(sin(t), -1,1, pad,dim-pad);
line(x,y,nx,ny);
x=nx;y=ny;
}
stroke(255);
fill(0,0,255,100);
x=0;
y=dim/2;
for(float t=0; t<=2*PI+0.2; t+=0.2) {
nx = map(t,0,2*PI,0,dim);
ny = map(sin(t), -1,1, pad,dim-pad);
rect(x-(nx-x)/2,dim/2,nx-x,y-(dim/2));
x=nx;y=ny;
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="empty" data-sketch-title="A better approximation">
void drawFunction() {
float x=0, y=dim/2, nx, ny;
stroke(0);
for(float t=0; t<=2*PI; t+=0.05) {
nx = map(t,0,2*PI,0,dim);
ny = map(sin(t), -1,1, pad,dim-pad);
line(x,y,nx,ny);
x=nx;y=ny;
}
stroke(255);
fill(0,0,255,100);
x=0;
y=dim/2;
for(float t=0; t<=2*PI+0.1; t+=0.1) {
nx = map(t,0,2*PI,0,dim);
ny = map(sin(t), -1,1, pad,dim-pad);
rect(x-(nx-x)/2,dim/2,nx-x,y-(dim/2));
x=nx;y=ny;
}
}</textarea>
<p>Now, infinitely many terms to sum and infinitely thin rectangles are not something that computers
can work with, so instead we're going to approximate the infinite summation by using a sum of a finite
number of "just thin" rectangular strips. As long as we use a high enough number of thin enough rectangular
strips, this will give us an approximation that is pretty close to what the real value is.</p>
<p>So, the trick is to come up with useful rectangular strips. A naive way is to simply create <i>n</i>
strips, all with the same width, but there is a far better way using special values for <i>C</i> and
<i>f(t)</i> depending on the value of <i>n</i>, which indicates how many strips we'll use.</p>
<div class="note">
<p>Note that one requirement for the approach we'll use is that the integral must run from -1 to 1. That's no good, because
we're dealing with Bézier curves, and the length of a section of curve applies to values which run from 0 to "some
value smaller than or equal to 1" (let's call that value <i>z</i>). Thankfully, we can quite easily transform any
integral interval to any other integral interval, by shifting and scaling the inputs. Doing so, we get the
following:</p>
<p>\[\begin{array}{l}
\int_{0}^{z}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\\
\
\frac{z}{2} \cdot \left [ C_1 \cdot f\left(\frac{z}{2} \cdot t_1 + \frac{z}{2}\right)
+ ...
+ C_n \cdot f\left(\frac{z}{2} \cdot t_n + \frac{z}{2}\right)
\right ]
\\
= \
\frac{z}{2} \cdot \sum_{i=1}^{n}{C_i \cdot f\left(\frac{z}{2} \cdot t_i + \frac{z}{2}\right)}
\end{array}\]</p>
<p>That may look a bit more complicated, but the fraction involving <i>z</i> is a fixed number,
so the summation, and the evaluation of the <i>f(t)</i> values are still pretty simple.</p>
<p>So, what do we need to perform this calculation? For one, we'll need an explicit formula for <i>f(t)</i>,
because that derivative notation is handy on paper, but not when we have to implement it. We'll also
need to know what these <i>C<sub>i</sub></i> and <i>t<sub>i</sub></i> values should be. Luckily, that's
less work because there are actually many tables available that give these values, for any <i>n</i>,
so if we want to approximate our integral with only two terms (which is a bit low, really) then
<a href="legendre-gauss.html">these tables</a> would tell us that for <i>n=2</i> we must use the following values:</p>
<p>\[\begin{array}{l}
C_1 = 1 \\
C_2 = 1 \\
t_1 = - \frac{1}{\sqrt{3}} \\
t_2 = + \frac{1}{\sqrt{3}}
\end{array}\]</p>
<p>Which means that in order for us to approximate the integral, we must plug these values into the approximate
function, which gives us:</p>
<p>\[
\int_{0}^{z}\sqrt{ \left (dx/dt \right )^2+\left (dy/dt \right )^2} dt
\frac{z}{2} \cdot \left [ f\left( \frac{z}{2} \cdot \frac{-1}{\sqrt{3}} + \frac{z}{2} \right)
+ f\left( \frac{z}{2} \cdot \frac{1}{\sqrt{3}} + \frac{z}{2} \right)
\right ]
\]</p>
<p>We can program that pretty easily, provided we have that <i>f(t)</i> available, which we do,
as we know the full description for the Bézier curve functions B<sub>x</sub>(t) and B<sub>y</sub>(t).</p>
</div>
<p>If we use the Legendre-Gauss values for our <i>C</i> values (thickness for each strip) and <i>t</i>
values (location of each strip), we can determine the approximate length of a Bézier curve by computing the
Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and
change the curve, to see how its length changes (the sketch allows curve order elevation and lowering,
in case you want to try more complex, or simpler, curves), and use your "+" and "-" keys to change the number
of intervals the Legendre-Gauss integral is approximated with (to a maximum of 23, with the sketch always
telling you the result of a 24 interval arc length computation).</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Arc length for a Bézier curve">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 20;
}
void drawCurve(BezierCurve curve) {
if(offset<1) { offset = 1; }
if(offset>23) { offset = 23; }
int resolution = 100000;
curve.draw();
pushStyle();
fill(0);
float ref = curve.getCurveLength();
float f = curve.getCurveLength(offset);
if (f<0) {
curves.clear();
curves.add(curve.lower());
redraw();
return;
}
float fo = int(resolution*f)/resolution;
text("curve length using "+offset+" intervals: "+fo+"px", 5, 15);
float fn = int(resolution*ref)/resolution;
float fe = 100*abs(ref-f)/ref;
fe = int(resolution*fe)/resolution;
text("24 interval arc length: "+fn+" (error: "+fe+"%)", 5, dim-5);
popStyle();
}</textarea>

84
data/arclengthapprox.jsx Normal file
View File

@@ -0,0 +1,84 @@
<p>Sometimes, we don't actually need the precision of a true arc length, and we can get away with simply computing the approximate arc length instead. The by far fastest way to do this is to flatten the curve and then simply calculate the linear distance from point to point. This will come with an error, but this can be made arbitrarily small by increasing the segment count.</p>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Approximate quadratic curve arc length">
void setupCurve() {
setupDefaultQuadratic();
offsetting();
offset = 16;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
float x = curve.getXValue(0),
y = curve.getYValue(0),
x2, y2, step = 1/offset, t,
length=0;
for(int i=1; i<=offset; i++) {
t = i*step;
x2 = curve.getXValue(t);
y2 = curve.getYValue(t);
line(x,y,x2,y2);
length += dist(x,y,x2,y2);
x = x2;
y = y2;
}
float arclength = curve.getCurveLength();
float error = 100 * (arclength - length) / arclength;
length = nfc(length, 3, 3);
arclength = nfc(arclength, 3, 3);
error = nfc(error, 3, 3);
if(error.indexOf(".")===0) { error = "0" + error; }
fill(0);
text("Approximate arc length based on "+offset+" segments: " + length, -dim/4, dim-20);
text("True length: " + arclength + ", error: " + error + "%", -dim/4, dim-5);
}</textarea>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Approximate cubic curve arc length">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 24;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
float x = curve.getXValue(0),
y = curve.getYValue(0),
x2, y2, step = 1/offset, t,
length=0;
for(int i=1; i<=offset; i++) {
t = i*step;
x2 = curve.getXValue(t);
y2 = curve.getYValue(t);
line(x,y,x2,y2);
length += dist(x,y,x2,y2);
x = x2;
y = y2;
}
float arclength = curve.getCurveLength();
float error = 100 * (arclength - length) / arclength;
length = nfc(length, 3, 3);
arclength = nfc(arclength, 3, 3);
error = nfc(error, 3, 3);
if(error.indexOf(".")===0) { error = "0" + error; }
fill(0);
text("Approximate arc length based on "+offset+" segments: " + length, -dim/4, dim-20);
text("True length: " + arclength + ", error: " + error + "%", -dim/4, dim-5);
}</textarea>
<p>Try clicking on the sketch and using your '+' and '-' keys to lower the number of segments for both the quadratic and cubic curve. You may notice that the error in length is actually pretty significant, even if the percentage is fairly low: if the number of segments used yields an error of 0.1% or higher, the flattened curve already looks fairly obviously flattened. And of course, the longer the curve, the more significant the error will be.</p>

76
data/boundingbox.jsx Normal file
View File

@@ -0,0 +1,76 @@
<p>If we have the extremities, and the start/end points, a simple for loop that tests for min/max values for
x and y means we have the four values we need to box in our curve:
<p id="bounds_p"><i>Computing the bounding box for a Bézier curve</i></p>
<ol>
<li>Find all <i>t</i> value(s) for the curve derivative's x- and y-roots.</li>
<li>Discard any <i>t</i> value that's lower than 0 or higher than 1, because Bézier curves only use the interval [0,1].</li>
<li>Determine the lowest and highest value when plugging the values <i>t=0</i>, <i>t=1</i> and each of the found
roots into the original functions: the lowest value is the lower bound, and the highest value is the upper
bound for the bounding box we want to construct.</li>
</ol>
<p>Applying this approach to our previous root finding, we get the following bounding boxes (with curve
extremities coloured the same as in the root finding graphics):</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Quadratic Bézier bounding box">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
stroke(255,0,0);
BezierCurve x_only = curve.justX(dim-2*pad);
float[] tx = x_only.getInflections();
for(float t: tx) {
if(t==0 || t==1) continue;
Point p = curve.getPoint(t);
ellipse(p.x,p.y,5,5);
}
stroke(255,0,255);
BezierCurve y_only = curve.justY(dim-2*pad);
float[] ty = y_only.getInflections();
for(float t: ty) {
if(t==0 || t==1) continue;
Point p = curve.getPoint(t);
ellipse(p.x,p.y,5,5);
}
drawBoundingBox(curve.generateBoundingBox());
}</textarea>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Cubic Bézier bounding box">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
stroke(255,0,0);
BezierCurve x_only = curve.justX(dim-2*pad);
float[] tx = x_only.getInflections();
for(float t: tx) {
if(t==0 || t==1) continue;
Point p = curve.getPoint(t);
ellipse(p.x,p.y,5,5);
}
stroke(255,0,255);
BezierCurve y_only = curve.justY(dim-2*pad);
float[] ty = y_only.getInflections();
for(float t: ty) {
if(t==0 || t==1) continue;
Point p = curve.getPoint(t);
ellipse(p.x,p.y,5,5);
}
drawBoundingBox(curve.generateBoundingBox());
}</textarea>
<p>We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis,
but in order to do so we first need to look at how aligning works.</p>

334
data/canonical.jsx Normal file
View File

@@ -0,0 +1,334 @@
<p>While quadratic curves are relatively simple curves to analyze, the same cannot be said of the cubic curve.
As a curvature controlled by more than one control points, it exhibits all kinds of features like loops,
cusps, odd colinear features, and up to two inflection points because the curvature can change direction
up to three times. Now, knowing what kind of curve we're dealing with means that some algorithms can be
run more efficiently than if we have to implement them as generic solvers, so is there a way to determine
the curve type without lots of work?</p>
<p>As it so happens, the answer is yes and the solution we're going to look at was presented by Maureen C. Stone
from Xerox PARC and Tony D. deRose from the University of Washington in their joint paper
<a href="http://graphics.pixar.com/people/derose/publications/CubicClassification/paper.pdf">"A Geometric
Characterization of Parametric Cubic curves"</a>. It was published in 1989, and defines curves as having a
"canonical" form (i.e. a form that all curves can be reduces to) from which we can immediately tell which
features a curve will have. So how does it work?</p>
<p>The first observation that makes things work is that if we have a cubic curve with four points, we can apply
a linear transformation to these points so that three of the points end up on (0,0), (0,1) and (1,1), with the
last point then being "somewhere". After applying that transformation, where that last point can then tell us
what kind of curve we're dealing with. Specifically, we see the following breakdown:</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="The canonical curve map">
PGraphics mapping;
void setupCurve() {
setupDefaultCubic();
mapping = CanonicalLayout.create(sketch,width,height,width/5)
}
void drawCurve(BezierCurve curve) {
image(mapping,0,0,width,height);
}</textarea>
<p>This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0),
(0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have
certain features. Namely, if the fourth point is...</p>
<ol>
<li>anywhere inside of the red zone, the curve will have a loop. We won't know <i>where</i> that loop
is (in terms of <i>t</i> values), but we are guaranteed that there is one.</li>
<li>on the edges of the red zone, we know the loop will be at <i>t</i>=0 or <i>t</i>=1, depending on
whether it's on the long right edge or the short round right edge, respectively. If it's on the left
edge, we know the curve has a cusp point rather than a loop.</li>
<li>on the left of the red zone, the curve will have two inflections, meaning its curvature has two
concave/convex switches.</li>
<li>inside the green zone, the curve will have a single inflection, switching concave/convex once.</li>
<li>on the right of the red zone, the curve will have no inflections, it'll just be a well-behaved arch.</li>
</ol>
<p>Of course, this map is fairly small, but the regions extend to infinity, with well defined boundaries.</p>
<div class="note">
<h3>Wait, where do those lines come from?</h3>
<p>Without repeating the paper mentioned at the top of this section, the loop-boundaries come from
rewriting the curve into canonical form, and then solving the formulae for which constraints must
hold for which possible curve properties. In the paper these functions yield formulae for where
you will find cusp points, or loops where we know t=0 or t=1, but those functions are derived
for the full cubic expression, meaning they apply to t=- to t=... For Bézier curves we only care
about the "clipped interval" t=0 to t=1, so some of the properties that apply when you look at
the curve over an infinite interval simply don't apply to the Bézier curve interval.</p>
<p>The right bound for the loop region, indicating where the curve switches from "having inflections"
to "having a loop", for the general cubic curve, is actually mirrored over x=1, but for Bézier curves
this right half doesn't apply, so we don't need to pay attention to it. Similarly, the boundaries for
t=0 and t=1 loops are also nice clean curves but get "cut off" when we only look at what the general
curve does over the internval t=0 to t=1.</p>
<p>For the full details, head over to the paper and read through sections 3 and 4. If you still remember
your high school precalculus, you can probably follow along with this paper, although you might have
to read it a few times before all the bits "click".</p>
</div>
<p>So now the question becomes: how do we manipulate our curve so that it fits this canonical form,
with three fixed points, and one "free" point? Enter linear algerba. Don't worry, I'll be doing all
the math for you, as well as show you what the effect is on our curves, but basically we're going
to be using linear algebra, rather than calculus, because "it's way easier". Sometimes a calculus
approach is very hard to work with, when the equivalent geometrical solution is super obvious.</p>
<p>The approach is going to start with a curve that doesn't have all-colinear points (so we
need to make sure the points don't all fall on a straight line), and then applying four graphics
operations that you will probably have heard of: translation (moving all points by some fixed x-
and y-distance), scaling (multiplying all points by some x and y scale factor), and shearing (an
operation that turns rectangles into parallelograms).</p>
<p>Step 1: we translate any curve by -p1.x and -p1.y, so that the curve starts at (0,0). We're going
to make use of an interesting trick here, by pretending our 2D coordinates are 3D, with the <i>z</i>
coordinate simply always being 1. This is an old trick in graphics to overcome the limitations of 2D
transformations: without it, we can only turn (x,y) coordinates into new coordinates of the form
(ax + by, cx + dy), which means we can't do translation, since that requires we end up with some kind
of (x + a, y + b). If we add a bogus <i>z</i> coordinate that is always 1, then we can suddenly add
arbitrary values. For example:</p>
<p>\[
\left [ \begin{array}
01 & 0 & a \\\\
0 & 1 & b \\\\
0 & 0 & 1
\end{array} \right ]
\cdot
\left [
\begin{matrix}
x \\\\
y \\\\
z=1
\end{matrix}
\right ]
=
\left [
\begin{matrix}
1 \cdot x + 0 \cdot y + a \cdot z \\\\
0 \cdot x + 1 \cdot y + b \cdot z \\\\
0 \cdot x + 0 \cdot y + 1 \cdot z
\end{matrix}
\right ]
=
\left [
\begin{matrix}
x + a \cdot 1 \\\\
y + b \cdot 1 \\\\
1 \cdot z
\end{matrix}
\right ]
=
\left [
\begin{matrix}
x + a \\\\
y + b \\\\
z=1
\end{matrix}
\right ]
\]</p>
<p>Sweet! <i>z</i> stays 1, so we can effectively ignore it entirely, but we added some plain values
to our x and y coordinates. So, if we want to subtract p1.x and p1.y, we use:</p>
<p>\[ T_1 =
\left [ \begin{array}
01 & 0 & -{P_1}_x \\\\
0 & 1 & -{P_1}_y \\\\
0 & 0 & 1
\end{array} \right ]
\cdot
\left [
\begin{matrix}
x \\\\
y \\\\
1
\end{matrix}
\right ]
=
\left [
\begin{matrix}
1 \cdot x + 0 \cdot y - {P_1}_x \cdot 1 \\\\
0 \cdot x + 1 \cdot y - {P_1}_y \cdot 1 \\\\
0 \cdot x + 0 \cdot y + 1 \cdot 1
\end{matrix}
\right ]
=
\left [
\begin{matrix}
x - {P_1}_x \\\\
y - {P_1}_y \\\\
1
\end{matrix}
\right ]
\]</p>
<p>Running all our coordinates through this transformation gives a new set of coordinates, let's call those <b>U</b>, where the first coordinate lies on (0,0), and the rest is still somewhat free. Our next job is to make sure point 2 ends up lying on the <i>x=0</i> line, so what we want is a transformation matrix that, when we run it, subtracts <i>x</i> from whatever <i>x</i> we currently have. This is called <a href="https://en.wikipedia.org/wiki/Shear_matrix">shearing</a>, and the typical x-shear matrix and its transformation looks like this:</p>
<p>\[
\left [
\begin{matrix}
1 & S & 0 \\\\
0 & 1 & 0 \\\\
0 & 0 & 1
\end{matrix}
\right ]
\cdot
\left [
\begin{matrix}
x \\\\
y \\\\
1
\end{matrix}
\right ]
=
\left [
\begin{matrix}
x + S \cdot y \\\\
y \\\\
1
\end{matrix}
\right ]
\]</p>
<p>So we want some shearing value that, when multiplied by <i>y</i>, yields <i>-x</i>, so our x coordinate becomes zero. That value is simpy <i>-x/y</i>, because <i>-x/y * y = -x</i>. Done:</p>
<p>\[ T_2 =
\left [
\begin{matrix}
1 & -\frac{ {U_2}_x }{ {U_2}_y } & 0 \\\\
0 & 1 & 0 \\\\
0 & 0 & 1
\end{matrix}
\right ]
\]</p>
<p>Now, running this on all our points generates a new set of coordinates, let's call those V, which now have point 1 on (0,0) and point 2 on (0, some-value), and we wanted it at (0,1), so we need to [do some scaling](https://en.wikipedia.org/wiki/Scaling_%28geometry%29) to make sure it ends up at (0,1). Additionally, we want point 3 to end up on (1,1), so we can also scale x to make sure its x-coordinate will be 1 after we run the transform. That means we'll be x-scaling by 1/point3<sub>x</sub>, and y-scaling by point2<sub>y</sub>. This is really easy:</p>
<p>\[ T_3 =
\left [
\begin{matrix}
\frac{1}{ {V_3}_x } & 0 & 0 \\\\
0 & \frac{1}{ {V_2}_y } & 0 \\\\
0 & 0 & 1
\end{matrix}
\right ]
\]</p>
<p>Then, finally, this generates a new set of coordinates, let's call those W, of which point 1 lies on (0,0), point 2 lies on (0,1), and point three lies on (1, ...) so all that's left is to make sure point 3 ends up at (1,1) - but we can't scale! Point 2 is already in the right place, and y-scaling would move it out of (0,1) again, so our only option is to y-shear point three, just like how we x-sheared point 2 earlier. In this case, we do the same trick, but with `y/x` rather than `x/y` because we're not x-shearing but y-shearing. Additionally, we don't actually want to end up at zero (which is what we did before) so we need to shear towards an offset, in this case 1:</p>
<p>\[ T_4 =
\left [
\begin{matrix}
1 & 0 & 0 \\\\
\frac{1 - {W_3}_y}{ {W_3}_x } & 1 & 0 \\\\
0 & 0 & 1
\end{matrix}
\right ]
\]</p>
<p>And this generates our final set of four coordinates. Of these, we already know that points 1 through 3 are (0,0), (0,1) and (1,1), and only the last coordinate is "free". In fact, given any four starting coordinates, the resulting "transformation mapped" coordinate will be:</p>
<p>\[
mapped_4 = \left (
\begin{matrix}
x = \left (
\frac
{
-x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2}
}
{
-x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2}
}
\right )
\\\\
y = \left (
\frac{(-y_1+y_4)}{-y_1+y_2}
+
\frac
{
\left ( 1 - \frac{-y_1+y_3}{-y_1+y_2} \right )
\left ( -x_1 + x_4 - \frac{(-x_1+x_2)(-y_1+y_4)}{-y_1+y_2} \right )
}
{
-x_1+x_3-\frac{(-x_1+x_2)(-y_1+y_3)}{-y_1+y_2}
}
\right )
\end{matrix}
\right )
\]</p>
<p>That looks very complex, but notice that every coordinate value is being offset by the initial translation, and a lot of terms in there repeat: it's pretty easy to calculate this fast, since there's so much we can cache and reuse while we compute this mapped coordinate!</p>
<p>First, let's just do that translation step as a "preprocessing" operation so we don't have to subtract the values all the time. What does that leave?</p>
<p>\[
... = \left (
\begin{matrix}
x = \left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right )
\\\\
y =
\frac{y_4}{y_2}
+
\left ( 1 - \frac{y_3}{y_2} \right )
\cdot
\left ( x_4 - \frac{x_2 \cdot y_4}{y_2} \middle/ x_3-\frac{x_2 \cdot y_3}{y_2} \right )
\end{matrix}
\right )
\]</p>
<p>Suddenly things look a lot simpler: the mapped x is fairly straight forward to compute, and we see that the mapped
y actually contains the mapped x in its entirety, so we'll have that part already available when we need to evaluate
it. In fact, let's pull out all those common factors to see just how simple this is:</p>
<p>\[
... = \left (
\begin{matrix}
x = (x_4 - x_2 \cdot f_{42}) / ( x_3- x_2 \cdot f_{32} )
\\\\
y =
f_{42}
+
\left ( 1 - f_{32} \right )
\cdot
x
\end{matrix}
\right ), f_{32} = \frac{y_3}{y_2}, f_{42} = \frac{y_4}{y_2}
\]</p>
<p>That's kind of super-simple to write out in code, I think you'll agree. Coding math tends to be easier than the formulae initially make it look!</p>
<div class="note">
<h3>How do you track all that?</h3>
<p>Doing maths can be a pain, so whenever possible, I like to make computers do the work for me. Especially for things like this, I simply use <a href="http://www.wolfram.com/mathematica">Mathematica</a>. Tracking all this math by hand is insane, and we invented computers, literally, to do this for us. I have no reason to use pen and paper when I can write out what I want to do in a program, and have the program do the math for me. And real math, too, with symbols, not with numbers. In fact, <a href="http://pomax.github.io/gh-weblog/downloads/canonical-curve.nb">here's</a> the Mathematica notebook if you want to see how this works for yourself.</p>
<p>Now, I know, you're thinking "but Mathematica is super expensive!" and that's true, it's <a href="http://www.wolfram.com/mathematica-home-edition">$295 for home use</a>, but it's <strong>also</strong> <a href="http://www.wolfram.com/raspberry-pi">free when you buy a $35 raspberry pi</a>. Obviously, I bought a raspberry pi, and I encourage you to do the same. With that, as long as you know what you want to <em>do</em>, Mathematica can just do it for you. And we don't have to be geniusses to work out what the maths looks like. That's what we have computers for.</p>
</div>
<p>So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our
canonical map, so that we can immediately tell which features our curve must have, based on where the fourth
coordinate is located on the map:</p>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Canonical curve mapping">
PGraphics mapping;
float s;
void setupCurve() {
setupDefaultCubic();
s = dim/5;
mapping = CanonicalLayout.create(sketch,dim,dim,s);
}
void drawCurve(BezierCurve curve) { try {
curve.draw();
nextPanel();
image(mapping,0,0,dim,dim);
int w=dim/2, h=dim/2;
translate(w,h);
Point[] p = curve.points;
Point np4 = CanonicalLayout.forwardTransform(p[0], p[1], p[2], p[3], s);
bezier(0,0, 0,s, s,s, np4.x, np4.y);
ellipse(np4.x, np4.y, 4, 4);
String label = nf(np4.x/s,1,3) + "," + nf(np4.y/s,1,3);
text(label, np4.x, np4.y);
} catch(e) {}}</textarea>

397
data/catmullconv.jsx Normal file
View File

@@ -0,0 +1,397 @@
<p>Taking an excursion to different splines, the other common design curve is the
<a href="https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom
spline</a>. Now, a Catmull-Rom spline is a form of cubic Hermite spline, and as it so happens
the cubic Bézier curve is also a cubic Hermite spline, so maybe... maybe we can convert one
into the other, and back, with some simple substitutions?</p>
<p>Unlike Bézier curves, Catmull-Rom splines pass through each point used to define the curve,
except the first and last, which makes sense if you read the "natural language" description
for how a Catmull-Rom spline works: a Catmull-Rom spline is a curve that, at each point
P<sub>x</sub>, has a tangent along the line P<sub>x-1</sub> to P<sub>x+1</sub>. The curve
runs from points P<sub>2</sub> to P<sub>n-1</sub>, and has a "tension" that determines
how fast the curve passes through each point. The lower the tension, the faster the curve
goes through each point, and the bigger its local tangent is.</p>
<!-- interactive Catmull Rom curve example goes here -->
<p>I'll be showing the conversion to and from Catmull-Rom curves for the tension that the
Processing language uses for its Catmull-Rom algorithm.</p>
<p>We start with showing the Catmull-Rom matrix form:</p>
<p>\[
CatmullRom(t) =
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 \\
-3 & 3 & -2 & -1 \\
2 & -2 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
V_1 \\ V_2 \\ V'_1 \\ V'_2
\end{bmatrix}
\]
</p>
<p>However, there's something funny going on here: the coordinate column matrix looks weird.
The reason is that Catmull-Rom curves are actually curve segments that are described by two
points, and two tangents; the curve leaves a point V1 (if we have four coordinates instead,
this is coordinate 2), arriving at a point V2 (coordinate 3), with the curve departing V1
with a tangent vector V'1 (equal to the tangent from coordinate 1 to coordinate 3) and
arriving at V2 with tangent vector V'2 (equal to the tangent from coordinate 2 to coordinate
4). So if we want to express this as a matrix form based on four coordinates, we get this
representation instead:</p>
<p>\[
\begin{bmatrix}
V_1 \\ V_2 \\ V'_1 \\ V'_2
\end{bmatrix}
=
T
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
P_2 \\ P_3 \\ \frac{P_3 - P_1}{2} \\ \frac{P_4 - P_2}{2}
\end{bmatrix}
\ \Rightarrow \
T
=
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
0 & 0 & 2 & 0 \\
-1 & 0 & 1 & 0 \\
0 & -1 & 0 & 1
\end{bmatrix}
\]</p>
<div class="note">
<h2>Where did that 2 come from?</h2>
<p>Catmull-Rom splines are based on the concept of tension: the higher the tensions,
the shorter the tangents at the departure and arrival points. The basic Catmull-Rom
curve arrives and departs with tangents equal to half the distance between the two
adjacent points, so that's where that 2 came from.</p>
<p>However, the "real" matrix is this:</p>
<p>\[
T
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
P_2 \\ P_3 \\ \frac{P_3 - P_1}{2 \cdot τ} \\ \frac{P_4 - P_2}{2 \cdot τ}
\end{bmatrix}
\Rightarrow \
T
=
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
0 & 0 & 2 & 0 \\
-τ & 0 & τ & 0 \\
0 & -τ & 0 & τ
\end{bmatrix}
\]</p>
<p>This bakes in the tension factor τ explicitly.</p>
</div>
<p>Plugging this into the "two coordinates and two tangent vectors" matrix form,
we get:</p>
<p>\[
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 \\
-3 & 3 & -2 & -1 \\
2 & -2 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
V_1 \\ V_2 \\ V'_1 \\ V'_2
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 \\
-3 & 3 & -2 & -1 \\
2 & -2 & 1 & 1
\end{bmatrix}
\cdot
\left (
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
0 & 0 & 2 & 0 \\
-τ & 0 & τ & 0 \\
0 & -τ & 0 & τ
\end{bmatrix}
\right )
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
-τ & 0 & τ & 0 \\
2τ & τ-6 & -2(τ-3) & -τ \\
-τ & 4-τ & τ-4 & τ
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>So let's find out which transformation matrix we need in order to convert from Catmull-Rom to Bézier:</p>
<p>\[
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
-τ & 0 & τ & 0 \\
2τ & τ-6 & -2(τ-3) & -τ \\
-τ & 4-τ & τ-4 & τ
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
A
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>The difference is somewhere in the actual hermite matrix, since the <em>t</em> and coordinate
values are identical, so let's solve that matrix equasion:</p>
<p>\[
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
-τ & 0 & τ & 0 \\
2τ & τ-6 & -2(τ-3) & -τ \\
-τ & 4-τ & τ-4 & τ
\end{bmatrix}
=
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
A
\]</p>
<p>We left-multiply both sides by the inverse of the Bézier matrix, to get rid of the
Bézier matrix on the right side of the equals sign:</p>
<p>\[
{
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
}^{-1}
\cdot
\frac{1}{2}
\cdot
\begin{bmatrix}
0 & 2 & 0 & 0 \\
-τ & 0 & τ & 0 \\
2τ & τ-6 & -2(τ-3) & -τ \\
-τ & 4-τ & τ-4 & τ
\end{bmatrix}
=
{
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
}^{-1}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
A
\ =\
I \cdot A
\ =\
A
\]
</p>
<p>Which gives us:</p>
<p>\[
\frac{1}{6}
\cdot
\begin{bmatrix}
0 & 6 & 0 & 0 \\
-τ & 6 & τ & 0 \\
0 & τ & 0 & -τ \\
0 & 0 & 6 & 0
\end{bmatrix}
=
A
\]</p>
<p>Multiplying this <strong><em>A</em></strong> with our coordinates
will give us a proper Bézier matrix expression again:<p>
<p>\[
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\frac{1}{6}
\cdot
\begin{bmatrix}
0 & 6 & 0 & 0 \\
-τ & 6 & τ & 0 \\
0 & τ & 0 & -τ \\
0 & 0 & 6 & 0
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_2 \\
P_2 + \frac{P_3-P_1}{6 \cdot τ} \\
P_3 - \frac{P_4-P_2}{6 \cdot τ} \\
P_3
\end{bmatrix}
\]</p>
<p>So a Catmull-Rom to Bézier conversion, based on coordinates, requires turning
the Catmull-Rom coordinates on the left into the Bézier coordinates on the right
(with τ being our tension factor):</p>
<p>\[
\begin{bmatrix}
P_1 \\
P_2 \\
P_3 \\
P_4
\end{bmatrix}_{CatmullRom}
\Rightarrow
\begin{bmatrix}
P_2 \\
P_2 + \frac{P_3-P_1}{6 \cdot τ} \\
P_3 - \frac{P_4-P_2}{6 \cdot τ} \\
P_3
\end{bmatrix}_{Bézier}
\]</p>
<p>And the other way around, a Bézier to Catmull-Rom conversion requires turning
the Bézier coordinates on the left this time into the Catmull-Rom coordinates
on the right. Note that there is no tension this time, because Bézier curves
don't have any. Converting from Bézier to Catmull-Rom is simply a default-tension
Catmull-Rom curve:</p>
<p>\[
\begin{bmatrix}
P_1 \\
P_2 \\
P_3 \\
P_4
\end{bmatrix}_{Bézier}
\Rightarrow
\begin{bmatrix}
P_4 + 6 \cdot (P_1 - P_2) \\
P_1 \\
P_4 \\
P_1 + 6 \cdot (P_4 - P_3)
\end{bmatrix}_{CatmullRom}
\]</p>
<p>Done. We can now draw the curves we want using either Bézier curves or Catmull-Rom
splines, the choice mostly being which drawing algorithms we have natively available,

150
data/catmullmoulding.jsx Normal file
View File

@@ -0,0 +1,150 @@
<p>Now, if Catmull-Rom curves go through points, can't we just use those to do curve fitting, instead?
As a matter of fact, we can, but there's a difference between the kind of curve fitting we did in the
previous section, and the kind of curve fitting that we can do with Catmull-Rom curves. In the previous
section we came up with a single curve that goes through three points. There was a decent amount of
maths and computation involved, and the end result was four coordinates that described a single curve.</p>
<p>Using Catmull-Rom curves, we need virtually no computation, but we're going to end up with two curves
that together describe a single curvature from point 1 through point 2 to point 3. Rather than three points,
we end up needing eight points, describing not one but two curves.</p>
<p>Much like for Bézier curves, we'll have to "clamp" some free parameters, but there are some default values
we can pick that lead to æsthetic curves.
<p>In the following graphic, we see our three points to draw a curvature through on the left, with a
triangle that connects them shown. In the second panel, we see some of our options: In order to
draw the Catmull-Rom curves we want, we need to determine a "virtual" starting point and end point,
so that at points p1 and p3 we have tangents based on the points before and after them. The naive
choice would be to simply mirror p2 over p1 and p3, which are the lines that flare out from the
triangle. However, if we follow those, the curve would be very weird: heading towards and away from
p2 at p1 and p3 respectively, but parallel to the line p1-p3 at point p2.</p>
<p>Another option would be to make sure our curve always departs from p1 and arrives at p3 perpendicular
to the line p1-p3. To achieve this, we project P2 onto the line P1--P3. How we do this is freeform, so
for this example let's project it based on "half the incidence angle":</p>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Catmull-Rom curve fitting">
Point p1, p2, p3;
float f = 1.0,
ratio = 0,
st = -0.5,
TAU = 2*PI;
void setupCurve() {
p1 = new Point(90, 265);
p2 = new Point(95, 125);
p3 = new Point(195, 80);
Point[] pts = {p1, p2,p3};
BezierCurve c = new BezierCurve(pts);
curves.add(c);
}
float[] angle123(Point p, Point o1, Point o2) {
float a1 = (atan2(o1.y-p.y, o1.x-p.x) + TAU) % TAU;
float a2 = (atan2(o2.y-p.y, o2.x-p.x) + TAU) % TAU;
return new float[] { a1, a2, abs(a2-a1) };
}
void drawCurve(BezierCurve curve) {
Point[] pts = curve.points;
p1 = pts[0];
p2 = pts[1];
p3 = pts[2];
curve.drawPoints();
stroke(0);
line(dim,0,dim,dim);
nextPanel();
// visualise the angles
Point[] points = {null, p1, p2, p3, null};
float[] angles = {0,
angle123(p1,p2,p3),
angle123(p2,p3,p1),
angle123(p3,p1,p2),
0};
stroke(127);
fill(0,20);
float start, end;
boolean flip;
Point m2;
start = angles[2][0] < angles[2][1] ? angles[2][0] : angles[2][1],
end = start + angles[2][2];
flip = angles[2][2] > PI;
if(flip) { float _tmp = start; start = end; end = _tmp + TAU; }
arc(points[2].x, points[2].y, 40, 40, start, end);
float phi = start + (end-start)/2;
m2 = new Point(100 * cos(phi) + p2.x, 100 * sin(phi) + p2.y);
stroke(128);
fill(0,5);
triangle(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
m2 = lli(new Points[]{p2, m2, p1, p3});
float dx = m2.x - p2.x,
dy = m2.y - p2.y;
Point m = new Point( p2.x + dx + f*dx, p2.y + dy + f*dy);
Point p0 = new Point( p1.x - f*(p2.x-p1.x), p1.y - f*(p2.y-p1.y));
Point p4 = new Point( p3.x + f*(p3.x-p2.x), p3.y + f*(p3.y-p2.y));
line(p2.x, p2.y, m.x, m.y);
float x0 = ratio * p0.x + (1-ratio) * m.x,
y0 = ratio * p0.y + (1-ratio) * m.y,
x4 = ratio * p4.x + (1-ratio) * m.x,
y4 = ratio * p4.y + (1-ratio) * m.y;
stroke(200);
line(x0, y0, p1.x, p1.y);
line(p3.x, p3.y, x4, y4);
stroke(0);
line(dim,0,dim,dim);
fill(0,0,200);
m.draw("virtual p0 / p4\n");
curve.drawPoints();
nextPanel();
curve.drawPoints();
stroke(0,0,100);
noFill();
beginShape();
curveTightness(st);
curveVertex( x0, y0);
curveVertex(p1.x, p1.y);
curveVertex(p2.x, p2.y);
curveVertex(p3.x, p3.y);
curveVertex( x4, y4);
endShape();
}
Point lli(Point[] pts) {
float x1=pts[0].x, y1=pts[0].y,
x2=pts[1].x, y2=pts[1].y,
x3=pts[2].x,y3=pts[2].y,
x4=pts[3].x,y4=pts[3].y,
nx=(x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4),
ny=(x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4),
d=(x1-x2)*(y3-y4)-(y1-y2)*(x3-x4);
if(d==0) { return null; }
return new Point(nx/d, ny/d);
}
</textarea>
<p>In the right panel, we see the Catmull-Rom curvature, consisting of two curves (one from p1 to p2, one
from p2 to p3, using the same virtual start as end point, where the grey lines meet), with several
tweakable parameters fixed: the curve tension has been chosen, the tangent at p1 has been chosen,
and the tangent at p3 has been chosen. Changing any of these three values will yield a different curve,
and the art of curve fitting is, again, in finding appropriate values to make a decent looking curve.</p>
<p>Now, can we no convert this to a Bézier curve? Short answer no. Longer answer: no, but we can convert this
to <strong>two</strong> Bézier curves, since our curvature consists of two Catmull-Rom curves. Do we need to? Sometimes we do, but we already know how to convert between the two forms, so that's not all too problematic.</p>

177
data/circles.jsx Normal file
View File

@@ -0,0 +1,177 @@
<p>Circles and Bézier curves are very different beasts, and circles are infinitely easier
to work with than Bézier curves. Their formula is much simpler, and they can be drawn more
efficiently. But, sometimes you don't have the luxury of using circles, or ellipses, or
arcs. Sometimes, all you have are Bézier curves. For instance, if you're doing font design,
fonts have no concept of geometric shapes, they only know straight lines, and Bézier curves.
OpenType fonts with TrueType outlines only know quadratic Bézier curves, and OpenType fonts
with Type 2 outlines only know cubic Bézier curves. So how do you draw a circle, or an ellipse,
or an arc?</p>
<p>You approximate.</p>
<p>We already know that Bézier curves cannot model all curves that we can think of, and
this includes perfect circles, as well as ellipses, and their arc counterparts. However,
we can certainly approximate them to a degree that is visually acceptable. Quadratic and cubic
curves offer us different curvature control, so in order to approximate a circle we will
first need to figure out what the error is if we try to approximate arcs of increasing degree
with quadratic and cubic curves, and where the coordinates even lie.</p>
<p>Since arcs are mid-point-symmetrical, we need the control points to set up a symmetrical
curve. For quadratic curves this means that the control point will be somewhere on a line
that intersects the baseline at a right angle. And we don't get any choice on where that
will be, since the derivatives at the start and end point have to line up, so our control
point will lie at the intersection of the tangents at the start and end point.</p>
<p>First, let's try to fit the quadratic curve onto a circular arc. In the following sketch
you can move the mouse around over a unit circle, to see how well, or poorly, a quadratic
curve can approximate the arc from (1,0) to where your mouse cursor is:</p>
<textarea class="sketch-code" data-sketch-preset="arcfitting" data-sketch-title="Quadratic Bézier arc approximation">
void setupCurve() { order = 2; }
void checkConnect() {
if((s==0 && e>PI/2) || s<-PI/2) { connect(); }
else { noConnect(); }
}
void findArcFitting() {
Point[] points = {
new Point(dim/2 + dim/f2, dim/2),
new Point(dim/2, dim/2),
new Point(dim/2 + dim/f2*cos(ax), dim/2 + dim/f2*sin(ay))
};
Point c = comp.lli(new Point[]{
points[0],
new Point(points[0].x, points[0].y + 10),
points[2],
new Point(points[2].x + dx, points[2].y + dy)
});
if(c==null) return;
points[1] = c;
BezierCurve bc = new BezierCurve(points);
bc.draw();
bc.getPoint(0.5).draw();
}</textarea>
<p>As you can see, things go horribly wrong quite quickly; even trying to approximate a quarter circle
using a quadratic curve is a bad idea. An eighth of a turns might look okay, but how okay is okay?
Let's apply some maths and find out. What we're interested in is how far off our on-curve coordinates
are with respect to a circular arc, given a specific start and end angle. We'll be looking at how much
space there is between the circular arc, and the quadratic curve's midpoint.</p>
<p>We start out with our start and end point, and for convenience we will place them on a unit
circle (a circle around 0,0 with radius 1), at some angle <i>φ</i>:</p>
<p>\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]</p>
<p>What we want to find is the intersection of the tangents, so we want a point C such that:</p>
<p>\[ C = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix} \ , \ \ C = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix} \]</p>
<p>i.e. we want a point that lies on the vertical line through A (at some distance <i>a</i>
from A) and also lies on the tangent line through B (at some distance <i>b</i> from B). Solving
this gives us:</p>
<p>\[ \left\{ \begin{array}{l}
C_x = 1 = cos(φ) + b \cdot -sin(φ)\\
C_y = a = sin(φ) + b \cdot cos(φ)
\end{array} \right. \]</p>
<p>First we solve for <i>b</i>:</p>
<p>\[ \begin{array}{l}
1 = cos(φ) + b \cdot -sin(φ) \ \
1 - cos(φ) = -b \cdot sin(φ) \ \
-1 + cos(φ) = b \cdot sin(φ) \\ \\
b = \frac{-1 + cos(φ)}{sin(φ)}
\end{array} \]</p>
<p>which we can then substitute in the expression for <i>a</i>:</p>
<p>\[ \begin{align*}
a &= sin(φ) + b \cdot cos(φ) \\
.. &= sin(φ) + \frac{-1 + cos(φ)}{sin(φ)} \cdot cos(φ) \\
.. &= sin(φ) + \frac{-cos(φ) + cos^2(φ)}{sin(φ)} \\
.. &= \frac{sin^2(φ) + cos^2(φ) - cos(φ)}{sin(φ)} \\
a &= \frac{1 - cos(φ)}{sin(φ)}
\end{align*} \]</p>
<p>A quick check shows that plugging these values for <i>a</i> and <i>b</i> into the expressions
for C<sub>x</sub> and C<sub>y</sub> give the same x/y coordinates for both "<i>a</i> away from A"
and "<i>b</i> away from B", so let's continue: now that we know the coordinate values for C, we
know where our on-curve point T for <i>t=0.5</i> (or angle φ/2) is, because we can just evaluate
the Bézier polynomial, and we know where the circle arc's actual point P is for angle φ/2:</p>
<p>\[
P_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
\]</p>
<p>We compute T, observing that if <i>t=0.5</i>, the polynomial values (1-t)², 2(1-t)t, and
are 0.25, 0.5, and 0.25 respectively:</p>
<p>\[\begin{array}{l}
T = \frac{1}{4}S + \frac{2}{4}C + \frac{1}{4}E = \frac{1}{4}(S + 2C + E) \\
= \
\left\{\begin{align*}
T_x &= \frac{1}{4}(3 + cos(φ))\\
T_y &= \frac{1}{4}\left(\frac{2-2cos(φ)}{sin(φ)} + sin(φ)\right)
= \frac{1}{4}\left(2tan\left(\frac{φ}{2}\right) + sin(φ)\right)
\end{align*}\right.
\end{array}\]</p>
<p>And the distance between these two is the standard Euclidean distance:</p>
<p>\[\begin{array}{l}
d_x(φ) = T_x - P_x = \frac{1}{4}(3 + cos(φ)) - cos(\frac{φ}{2}) = 2sin^4\left(\frac{φ}{4}\right) \ , \\
d_y(φ) = T_y - P_y = \frac{1}{4}\left(2tan\left(\frac{φ}{2}\right) + sin(φ)\right) - sin(\frac{φ}{2}) \ , \\
d(φ) = \sqrt{d^2_x + d^2_y} = \ ... \ = 2sin^4(\frac{φ}{2})\sqrt{\frac{1}{cos^2(\frac{φ}{2})}}
\end{array}\]</p>
<p>So, what does this distance function look like when we plot it for a
number of ranges for the angle φ, such as a half circle, quarter circle and eighth circle?</p>
<table><tr><td>
<p><a href="http://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi"><img
src="images/arc-q-pi.gif"
></a></p>
<p>plotted for 0 φ π:</p>
</td><td>
<p><a href="http://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi%2F2"><img
src="images/arc-q-pi2.gif"></a></p>
<p>plotted for 0 φ ½π:</p>
</td><td>
<p><a href="http://www.wolframalpha.com/input/?i=plot+sqrt%28%281%2F4+*+%28sin%28x%29+%2B+2tan%28x%2F2%29%29+-+sin%28x%2F2%29%29%5E2+%2B+%282sin%5E4%28x%2F4%29%29%5E2%29+for+0+%3C%3D+x+%3C%3D+pi%2F4"><img
src="images/arc-q-pi4.gif"></a></p>
<p>plotted for 0 φ ¼π:</p>
</td></tr></table>
<p>We now see why the eighth circle arc looks decent, but the quarter circle arc doesn't:
an error of roughly 0.06 at <i>t=0.5</i> means we're 6% off the mark... we will already be
off by one pixel on a circle with pixel radius 17. Any decent sized quarter circle arc, say
with radius 100px, will be way off if approximated by a quadratic curve! For the eighth
circle arc, however, the error is only roughly 0.003, or 0.3%, which explains why it looks
so close to the actual eighth circle arc. In fact, if we want a truly tiny error, like 0.001,
we'll have to contend with an angle of (rounded) 0.593667, which equates to roughly 34 degrees.
We'd need 11 quadratic curves to form a full circle with that precision! (technically,
10 and ten seventeenth, but we can't do partial curves, so we have to round up). That's a
whole lot of curves just to get a shape that can be drawn using a simple function!</p>
<p>In fact, let's flip the function around, so that if we plug in the precision error, labelled
ε, we get back the maximum angle for that precision:</p>
<p>\[
φ = 4 \cdot arccos \left(\frac{\sqrt{2+ε-\sqrt{ε(2+ε)}}}{\sqrt{2}}\right)
\]</p>
<p>Things are starting to look, frankly, a bit ridiculous at this point, but this is as far
as we need the math to take us. If we plug in the precisions 0.1, 0.01, 0.001 and 0.0001 we
get the values 1.748, 1.038, 0.594 and 0.3356; in degrees, roughly 100 (requiring four curves),
59.5 (requiring six curves), 34 (requiring 11 curves), and 19.2 (requiring a whopping nineteen
curves). </p>
<p>The bottom line? <strong>Quadratic curves are kind of lousy</strong> if you want circular
(or elliptical, which are circles that have been squashed in one dimension) curves. We
can do better, even if it's just by raising the order of our curve once. So let's try the
same thing for cubic curves.</p>

270
data/circles_cubic.jsx Normal file
View File

@@ -0,0 +1,270 @@
<p>For cubic curves the control points must be each other's mirror around the line running
through the baseline midpoint, at a right angle, and again the derivatives at the start and
end points must agree. Again we don't have altogether that much choice: there is only one
pair of control points that guarantees correct derivatives for the start and end points,
while also making the midpoint of the curve lie on top of the curve.</p>
<p>In order to find a cubic curve, we first "guess" the curve, based on the previously outlined
curve-through-three-points procedure. This will give use a curve with correct start, mid and
end points, but incorrect derivatives for start and end, given the control points. We then
slide the control points along the line that connects them until they effect the corrected
derivative at the start and end points (you may remember that the derivative at the start
is aligned with the line from start point to control point 1, and that the derivative at the
end is aligned with the line from control point 2 to the end point).</p>
<textarea class="sketch-code" data-sketch-preset="arcfitting" data-sketch-title="Cubic Bézier arc approximation">
void setupCurve() { order = 3; }
void checkConnect() {
if(e < PI) { noConnect(); } else { connect(); }
}
void findArcFitting() {
// guess the curve based on the start/mid/end points:
Point p1 = new Point(dim/2 + dim/f2,dim/2),
p2 = new Point(dim/2 + dim/f2*cos((s+e)/2), dim/2 + dim/f2*sin((s+e)/2)),
p3 = new Point(dim/2 + dim/f2*cos(ax), dim/2 + dim/f2*sin(ay));
BezierCurve guess = comp.generateCurve(3,p1,p2,p3);
Point oc1 = guess.points[1];
drawGuess(guess);
// then, move the control points so that B'(0) and B'(1) are correct:
Point c1 = comp.lli(new Point[]{
guess.points[0],
new Point(p1.x, p1.y+10),
guess.points[1],
guess.points[2],
});
// taking advantage of symmetry, we trivially know c2 now, too:
dx = guess.points[1].x - c1.x;
dy = guess.points[1].y - c1.y;
Point c2 = new Point(guess.points[2].x + dx,
guess.points[2].y + dy);
// replace, update, and draw.
guess.points[1] = c1;
guess.points[2] = c2;
guess.update();
guess.draw();
float a = 0.211325;
Point pa = guess.getPoint(a);
pa.draw();
guess.getPoint(1-a).draw();
}</textarea>
<p>We see two curves here; very faintly the "guessed" curve, and drawn normally, the proper curve
with the control points shifted along the control line so that the derivatives at the start and end
points are correct. With this, we can see that cubic curves are actually a lot better than quadratic
curves, and don't look all that wrong until we go past a quarter circle; ⅜th starts to hint at
problems, and half a circle has an obvious "gap" between the real circle and the cubic approximation.
Anything past that just looks plain ridiculous... but quarter curves actually look pretty okay!
Again, how okay is okay? Let's apply some more maths to find out.</p>
<p>Unlike for the quadratic curve, we can't use <i>t=0.5</i> as our reference point because by its
very nature it's one of the three points that are actually guaranteed to lie on the circular curve.
Instead, we need a different <i>t</i> value. If we run some analysis on the curve we find that the
actual <i>t</i> value at which the curve is furthest from what it should be is 0.211325 (rounded),
but we don't know "why", since finding this value involves root-finding, and is nearly impossible
to do symbolically without pages and pages of math just to express one of the possible solutions.</p>
<p>So instead, let's simply take that <i>t</i> value and see what the error is for circular arcs
with an angle ranging from 0 to 2π:</p>
<table><tr><td>
<p><img src="images/arc-c-2pi.gif"></p>
<p>plotted for 0 φ 2π:</p>
</td><td>
<p><img src="images/arc-c-pi.gif"></p>
<p>plotted for 0 φ π:</p>
</td><td>
<p><img src="images/arc-c-pi2.gif"></p>
<p>plotted for 0 φ ½π:</p>
</td></tr></table>
<p>We see that cubic Bézier curves are much better when it comes to approximating circular arcs,
with an error of less than 0.027 at the two "bulge" points for a quarter circle (which had an
error of 0.06 for quadratic curves at the mid point), and an error near 0.001 for an eighth
of a circle, so we're getting less than half the error for a quarter circle, or: at a slightly
lower error, we're getting twice the arc. This makes cubic curves quite useful.
In fact, the precision of a cubic curve at a quarter circle is considered "good enough" by many
to justify using four cubic Bézier curves to fake a full circle when no circle primitives are
available; generally, people will not notice it's not a real circle unless you overlay the
actual circle so they can see the difference.</p>
<p>So if we want to use a cubic Bézier curve, where do the curve's points go?
The start and end point are the same as before:</p>
<p>\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]</p>
<p>But we now need to find two control points, rather than one:</p>
<p>\[
C_1 = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix} \ \ , \ \
C_2 = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
\]</p>
<div class="note">
<h2>Let's do this thing.</h2>
<p>Unlike for the quadratic case, we need some more information in order to compute <i>a</i> and <i>b</i>,
since they're no longer dependent variables. First, we observe that the curve is symmetrical, so whatever
values we end up finding for C<sub>1</sub> will apply to C<sub>2</sub> as well (rotated along its tangent),
so we'll focus on finding the location of C<sub>1</sub> only. So here's where we do something that you might
not expect: we're going to ignore for a moment, because we're going to have a much easier time if we just
solve this problem with geometry first, then move to calculus to solve a much simpler problem.</p>
<p>If we look at the triangle that is formed between our starting point, or initial guess C<sub>1</sub>
and our real C<sub>1</sub>, there's something funny going on: if we treat the line {start,guess} as
our opposite side, the line {guess,real} as our adjacent side, with {start,real} our hypothenuse, then
the angle for the corner hypothenuse/adjacent is half that of the arc we're covering. Try it: if you
place the end point at a quarter circle (pi/2, or 90 degrees), the angle in our triangle is half a
quarter (pi/4, or 45 degrees). With that knowledge, and a knowledge of what the length of any of
our lines segments are (as a function), we can determine where our control points are, and thus have
everything we need to find the error distance function. Of the three lines, the one we can easiest
determine is {start,guess}, so let's find out what the guessed control point is. Again geometrically,
because we have the benefit of an on-curve <i>t=0.5</i> value.</p>
<p>The distance from our guessed point to the start point is exactly the same as the projection distance
we looked at earlier. Using <i>t=0.5</i> as our point "B" in the "A,B,C" projection, then we know the
length of the line segment {C,A}, since it's d<sub>1</sub> = {A,B} + d<sub>2</sub> = {B,C}:</p>
<p>\[
||{A,C}|| = d_2 + d_1 = d_2 + d_2 \cdot ratio_3 \left(\frac{1}{2}\right) = d_2 + \frac{1}{3}d_2 = \frac{4}{3}d_2
\]</p>
<p>So that just leaves us to find the distance from <i>t=0.5</i> to the baseline for an arbitrary
angle φ, which is the distance from the centre of the circle to our <i>t=0.5</i> point, minus the
distance from the centre to the line that runs from start point to end point. The first is the
same as the point P we found for the quadratic curve:</p>
<p>\[
P_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
\]</p>
<p>And the distance from the origin to the line start/end is another application of angles,
since the triangle {origin,start,C} has known angles, and two known sides. We can find
the length of the line {origin,C}, which lets us trivially compute the coordinate for C:</p>
<p>\[\begin{array}{l}
l = cos(\frac{φ}{2}) \ , \\
\left\{\begin{array}{l}
C_x = l \cdot cos\left(\frac{φ}{2}\right) = cos^2\left(\frac{φ}{2}\right)\ , \\
C_y = l \cdot sin\left(\frac{φ}{2}\right) = cos(\frac{φ}{2}) \cdot sin\left(\frac{φ}{2}\right)\ , \\
\end{array}\right.
\end{array}\]</p>
<p>With the coordinate C, and knowledge of coordinate B, we can determine coordinate A, and get a vector
that is identical to the vector {start,guess}:</p>
<p>\[\left\{\begin{array}{l}
B_x - C_x = cos\left(\frac{φ}{2}\right) - cos^2\left(\frac{φ}{2}\right) \\
B_y - C_y = sin\left(\frac{φ}{2}\right) - cos(\frac{φ}{2}) \cdot sin\left(\frac{φ}{2}\right)
= sin\left(\frac{φ}{2}\right) - \frac{sin(φ)}{2}
\end{array}\right.\]</p>
<p>\[\left\{\begin{array}{l}
\vec{v}_x = \{C,A\}_x = \frac{4}{3} \cdot (B_x - C_x) \\
\vec{v}_y = \{C,A\}_y = \frac{4}{3} \cdot (B_y - C_y)
\end{array}\right.\]</p>
<p>Which means we can now determine the distance {start,guessed}, which is the same as the distance
{C,A}, and use that to determine the vertical distance from our start point to our C<sub>1</sub>:</p>
<p>\[\left\{\begin{array}{l}
C_{1x} = 1 \\
C_{1y} = \frac{d}{sin\left(\frac{φ}{2}\right)}
= \frac{\sqrt{\vec{v}^2_x + \vec{v}^2_y}}{sin\left(\frac{φ}{2}\right)}
= \frac{4}{3} tan \left( \frac{φ}{4} \right)
\end{array}\right.\]</p>
<p>And after this tedious detour to find the coordinate for C<sub>1</sub>, we can find C<sub>2</sub>
fairly simply, since it's lies at distance -C<sub>1y</sub> along the end point's tangent:</p>
<p>\[\begin{array}{l}
E'_x = -sin(φ) \ , \ E'_y = cos(φ) \ , \ ||E'|| = \sqrt{ (-sin(φ))^2 + cos^2(φ)} = 1 \ , \\
\left\{\begin{array}{l}
C_2x = E_x - C_{1y} \cdot \frac{E_x'}{||E'||}
= cos(φ) + C_{1y} \cdot sin(φ)
= cos(φ) + \frac{4}{3} tan \left( \frac{φ}{4} \right) \cdot sin(φ) \\
C_2y = E_y - C_{1y} \cdot \frac{E_y'}{||E'||}
= sin(φ) - C_{1y} \cdot cos(φ)
= sin(φ) - \frac{4}{3} tan \left( \frac{φ}{4} \right) \cdot cos(φ)
\end{array}\right.
\end{array}\]</p>
<p>And that's it, we have all four points now for an approximation of an arbitrary
circular arc with angle φ.</p>
</div>
<p>If you skipped the derivation for the "true" formula in the hopes of finding some useful
information, then you're in luck: there are many possible angles with which to approximate
sections of a circle, but by far the most common one is using one curve for each quarter
circle, and it turns that while deriving the function to get the right value is a bit of a
pain, the actual final values are pretty simple, even as precise functions. So, which
values do we plug in?:</p>
<p>\[\begin{array}{l}
S = (1, 0) \ , \
C_1 = \left ( 1, 4 \frac{\sqrt{2}-1}{3} \right ) \ , \
C_2 = \left ( 4 \frac{\sqrt{2}-1}{3} , 1 \right ) \ , \
E = (0, 1)
\end{array}\]</p>
<p>Which, in decimal values, rounded to six significant digits, is:</p>
<p>\[\begin{array}{l}
S = (1, 0) \ , \
C_1 = (1, 0.55228) \ , \
C_2 = (0.55228 , 1) \ , \
E = (0, 1)
\end{array}\]</p>
<p>Note that this is for a circle with radius 1, so if you have a different radius circle,
simply multiply the coordinate by the radius you need, and done! Finally, forming the
full curve is now a simple a matter of mirroring these coordinates about the origin:</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Cubic Bézier circle approximation">
void setupCurve() {
setupDefaultCubic();
noLabels();
}
void drawCurve(BezierCurve curve) {
float ox = dim/2, oy = dim/2, r = dim/2.5;
stroke(150);
line(0,dim/2,dim,dim/2);
line(dim/2,0,dim/2,dim);
(new BezierCurve(new Point[]{
new Point(ox + r*1, oy + r*0),
new Point(ox + r*1, oy + r*0.55228),
new Point(ox + r*0.55228, oy + r*1),
new Point(ox + r*0, oy + r*1)
})).draw(color(255,0,0));
(new BezierCurve(new Point[]{
new Point(ox + r*0, oy + r*1),
new Point(ox - r*0.55228, oy + r*1),
new Point(ox - r*1, oy + r*0.55228),
new Point(ox - r*1, oy + r*0)
})).draw();
(new BezierCurve(new Point[]{
new Point(ox - r*1, oy + r*0),
new Point(ox - r*1, oy - r*0.55228),
new Point(ox - r*0.55228, oy - r*1),
new Point(ox + r*0, oy - r*1)
})).draw();
(new BezierCurve(new Point[]{
new Point(ox + r*0, oy - r*1),
new Point(ox + r*0.55228, oy - r*1),
new Point(ox + r*1, oy - r*0.55228),
new Point(ox + r*1, oy + r*0)
})).draw();
}</textarea>

66
data/components.jsx Normal file
View File

@@ -0,0 +1,66 @@
<p>One of the first things people run into when they start using Bézier curves in their own programs is
"I know how to draw the curve, but how do I determine the bounding box?". It's actually reasonably straight
forward to do so, but it requires having some knowledge on exploiting math to get the values we need.
For bounding boxes, we aren't actually interested in the curve itself, but only in its "extremities": the
minimum and maximum values the curve has for its x- and y-axis values. If you remember your calculus
(provided you ever took calculus, otherwise it's going to be hard to remember) we can determine function
extremities using the first derivative of that function, but this poses a problem, since our function is
parametric: every axis has its own function.</p>
<p>The solution: compute the derivative for each axis separately, and then fit them back together in the same
way we do for the original.</p>
<p>Let's look at how a parametric Bézier curve "splits up" into two normal functions, one for the x-axis and
one for the y-axis. Note the left-most figure is again an interactive curve, without labeled axes (you
get coordinates in the graph instead). The center and right-most figures are the component functions for
computing the x-axis value, given a value for <i>t</i> (between 0 and 1 inclusive), and the y-axis value,
respectively.</p>
<p>If you move points in a curve sideways, you should only see the middle graph change; likely, moving
points vertically should only show a change in the right graph.</p>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Quadratic Bézier curve components">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
noAdditionals();
usePanelPadding();
nextPanel();
drawAxes("t",0,1, "x",0,panelDim);
BezierCurve x_only = curve.justX(dim-2*pad);
x_only.draw();
nextPanel();
drawAxes("t",0,1, "y",0,panelDim);
BezierCurve y_only = curve.justY(dim-2*pad);
y_only.draw();
}</textarea>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Cubic Bézier curve components">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
noAdditionals();
usePanelPadding();
nextPanel();
drawAxes("t",0,1, "x",0,panelDim);
BezierCurve x_only = curve.justX(dim-2*pad);
x_only.draw();
nextPanel();
drawAxes("t",0,1, "y",0,panelDim);
BezierCurve y_only = curve.justY(dim-2*pad);
y_only.draw();
}</textarea>

98
data/control.jsx Normal file
View File

@@ -0,0 +1,98 @@
<p>Bézier curves are (like all "splines") interpolation functions, meaning they take a set of
points, and generate values somewhere "between" those points. (One of the consequences of this
is that you'll never be able to generate a point that lies outside the outline for the control
points, commonly called the "hull" for the curve. Useful information!). In fact, we can visualize
how each point contributes to the value generated by the function, so we can see which points are
important, where, in the curve.</p>
<p>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.</p>
<p>Also shown is the interpolation function for a 15th order Bézier function. As you can see,
the start and end point contribute considerably more to the curve's shape than any other point
in the control point set.</p>
<textarea class="sketch-code" data-sketch-preset="ratios" data-sketch-title="Quadratic interpolations">
int order = 3;
</textarea>
<textarea class="sketch-code" data-sketch-preset="ratios" data-sketch-title="Cubic interpolations">
int order = 4;
</textarea>
<textarea class="sketch-code" data-sketch-preset="ratios" data-sketch-title="15th order interpolations">
int order = 15;
</textarea>
<p>If we want to change the curve, we need to change the weights of each point, effectively changing
the interpolations. The way to do this is about as straight forward as possible: just multiply each
point with a value that changes its strength. These values are conventionally called "Weights", and
we can add them to our original Bézier function:</p>
<p>\[
Bézier(n,t) = \sum_{i=0}^{n}
\underset{binomial\ term}{\underbrace{\binom{n}{i}}}
\cdot\
\underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
\cdot\
\underset{weight}{\underbrace{w_i}}
\]</p>
<p>That looks complicated, but as it so happens, the "weights" are actually just the coordinate values
we want our curve to have: for an <i>n<sup>th</sup></i> order curve, w<sub>0</sub> is our start coordinate,
w<sub>n</sub> is our last coordinate, and everything in between is a controlling coordinate. Say we want
a cubic curve that starts at (120,160), is controlled by (35,200) and (220,260) and ends at (220,40),
we use this Bézier curve:</p>
<p>\[
\left \{ \begin{matrix}
x = BLUE[120] \cdot (1-t)^3 + BLUE[35] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[220] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[220] \cdot t^3 \\
y = BLUE[160] \cdot (1-t)^3 + BLUE[200] \cdot 3 \cdot (1-t)^2 \cdot t + BLUE[260] \cdot 3 \cdot (1-t) \cdot t^2 + BLUE[40] \cdot t^3
\end{matrix} \right. \]</p>
<p>Which gives us the curve we saw at the top of the article:</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Our cubic Bézier curve">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
}</textarea>
<p>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.</p>
<div class="howtocode">
<h3>How to implement the weighted basis function</h3>
<p>Given that we already know how to implement basis function, adding in the control points
is remarkably easy:</p>
<pre>function Bezier(n,t,w[]):
sum = 0
for(k=0; k&lt;n; k++):
sum += w[k] * binomial(n,k) * (1-t)^(n-k) * t^(k)
return sum</pre>
<p>And for the extremely optimized versions:</p>
<pre>function Bezier(2,t,w[]):
t2 = t * t
mt = 1-t
mt2 = mt * mt
return w[0]*mt2 + w[1]*2*mt*t + w[2]*t2
function Bezier(3,t,w[]):
t2 = t * t
t3 = t2 * t
mt = 1-t
mt2 = mt * mt
mt3 = mt2 * mt
return w[0]*mt3 + 3*w[1]*mt2*t + 3*w[2]*mt*t2 + w[3]*t3</pre>
<p>And now we know how to program the weighted basis function.</p>
</div>

View File

@@ -0,0 +1,52 @@
<p>Using de Casteljau's algorithm to split the curve we can now implement curve/curve intersection
finding using a "divide and conquer" technique: take two curves <i>C<sub>1</sub></i> and <i>C<sub>2</sub></i>,
and treat them as a pair. If their bounding boxes overlap, split up each curve into two sub-curves,
<i>C<sub>1.1</sub></i>, <i>C<sub>1.2</sub></i>, <i>C<sub>2.1</sub></i> and <i>C<sub>2.2</sub></i>,
and form four new pairs (<i>C<sub>1.1</sub></i>,<i>C<sub>2.1</sub></i>), (<i>C<sub>1.1</sub></i>,
<i>C<sub>2.2</sub></i>), (<i>C<sub>1.2</sub></i>,<i>C<sub>2.1</sub></i>), and (<i>C<sub>1.2</sub></i>,
<i>C<sub>2.2</sub></i>). If their bounding boxes do not overlap, discard the pair, as there is no
intersection between this pair of curves. If there was overlap, for each of the newly formed pairs,
perform the same evaluation. Once the sub-curves we form are so small that they effectively occupy
sub-pixel areas, we consider an intersection found.</p>
<p>This algorithm will start with a single pair, "balloon" until it runs in parallel for a large
number of potential sub-pairs, and then taper back down as it homes in on intersection coordinates,
ending up with as many pairs as there are intersections.</p>
<p>The following graphic applies this algorithm to a pair of cubic curves, slowed down so that
you can see the algorithm in action. Click the button to run the algorithm, after setting up
your curves in some creative arrangement: <button id="clippingButton">detect</button></p>
<textarea class="sketch-code" data-sketch-preset="clipping" data-sketch-title="Curve/curve intersections">
void iterate() {
if(pairs.size()==0) {
iterated = false;
drawResult();
noAnimate();
return; }
fill(0);
text("iteration "+(iterationCount++), 10,20);
newPairs.clear();
for(CurvePair cp: pairs) {
cp.draw(getColor(random(999)));
if(cp.hasOverlap()) {
if(cp.smallEnough()) { finals.add(cp); }
else {
CurvePair[] expanded = cp.splitAndCombine();
for(CurvePair ncp: expanded) {
newPairs.add(ncp);
}
}
}
}
pairs.clear();
for(CurvePair cp: newPairs) { pairs.add(cp); }
}
</textarea>
<p>Self-intersection is dealt with in the same way, except we turn a curve into two or more curves first
based on the inflection points. We then form all possible curve pairs with the resultant segments, and
run exactly the same algorithm. All non-overlapping curve pairs will be removed after the first iteration,
and the remaining steps home in on the curve's self-intersection points.</p>

85
data/decasteljau.jsx Normal file
View File

@@ -0,0 +1,85 @@
<p>If we want to draw Bézier curves we can run through all values of <i>t</i> from 0 to 1 and then
compute the weighted basis function, getting the <i>x</i>/<i>y</i> values we need to plot, but the
more complex the curve gets, the more expensive this becomes. Instead, we can use "de Casteljau's
algorithm" to draw curves, which is a geometric approach to drawing curves, and really easy to
implement. So easy, in fact, you can do it by hand with a pencil and ruler.</p>
<p>Rather than using our calculus function to find <i>x</i>/<i>y</i> values for <i>t</i>, let's
do this instead:</p>
<ul>
<li>treat <i>t</i> as a ratio (which it is). t=0 is 0% along a line, t=1 is 100% along a line.</li>
<li>Take all lines between the curve's defining points. For an order <i>n</i> curve, that's <i>n</i> lines.</li>
<li>Place markers along each of these line, at distance <i>t</i>. So if <i>t</i> is 0.2, place the mark
at 20% from the start, 80% from the end.</li>
<li>Now form lines between <i>those</i> points. This gives <i>n-1</i> lines.</li>
<li>Place markers along each of these line at distance <i>t</i>.</li>
<li>Form lines between <i>those</i> points. This'll be <i>n-2</i> lines.</li>
<li>place markers, form lines, place markers, etc.</li>
<li>repeat this until you have only one line left. The point <i>t</i> on that line coincides with the
original curve point at <i>t</i>.</li>
</ul>
<div class="howtocode">
<h3>How to implement de Casteljau's algorithm</h3>
<p>Let's just use the algorithm we just specified, and implement that:</p>
<pre>function drawCurve(points[], t):
if(points.length==1):
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
newpoints[i] = (1-t) * points[i] + t * points[i+1]
drawCurve(newpoints, t)</pre>
<p>And done, that's the algorithm implemented. Except usually you don't get the luxury of
overloading the "+" operator, so let's also give the code for when you need to work with
<i>x</i> and <i>y</i> values:</p>
<pre>function drawCurve(points[], t):
if(points.length==1):
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
x = (1-t) * points[i].x + t * points[i+1].x
y = (1-t) * points[i].y + t * points[i+1].y
newpoints[i] = new point(x,y)
drawCurve(newpoints, t)</pre>
<p>So what does this do? This draws a point, if the passed list of points is only 1 point
long. Otherwise it will create a new list of points that sit at the <i>t</i> ratios (i.e.
the "markers" outlined in the above algorithm), and then call the draw function for this
new list.</p>
</div>
<p>To see this in action, click the following sketch. This sketch has a curve that can be lowered and elevated,
and will show de Casteljau's "skeleton" around the curve, showing how it's determining where to draw the curve's
point for every <i>t</i> value. To pause or resume, simply click or hit space bar (when focussed on the sketch).</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Traversing a curve using de Casteljau's algorithm">
void setupCurve() {
//setupDefaultCubic();
Point[] points = {
new Point(65,25),
new Point(5,150),
new Point(80,290),
new Point(220,235),
new Point(250,150),
new Point(135,125),
};
curves.add(new BezierCurve(points));
reorder();
animate();
pause();
span();
}
void drawCurve(BezierCurve curve) {
curve.draw();
Point p = curve.getPoint(t);
ellipse(p.x, p.y, 5, 5);
drawSpan(curve, t);
}</textarea>

179
data/derivatives.jsx Normal file
View File

@@ -0,0 +1,179 @@
<p>There's a number of useful things that you can do with Bézier curves based on their derivative,
and one of the more amusing observations about Bézier curves is that their derivatives are, in fact,
also Bézier curves. In fact, the derivation of a Bézier curve is relatively straight forward, although
we do need a bit of math. First, let's look at the derivative rule for Bézier curves, which is:</p>
<p>\[
Bézier'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot Bézier(n-1,t)_i
\]</p>
<p>which we can also write (observing that <i>b</i> in this formula is the same as our <i>w</i> weights,
and that <i>n</i> times a summation is the same as a summation where each term is multiplied by <i>n</i>)
as:</p>
<p>\[
Bézier'(n,t) = \sum_{i=0}^{n-1} Bézier(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i)
\]</p>
<p>Or, in plain text: the derivative of an n<sup>th</sup> degree Bézier curve is an (n-1)<sup>th</sup>
degree Bézier curve, with one fewer term, and new weights w'<sub>0</sub>...w'<sub>n-1</sub> derived
from the original weights as n(w<sub>i+1</sub> - w<sub>i</sub>), so for a 3rd degree curve, with four weights,
the derivative has three new weights w'<sub>0</sub>=3(w<sub>1</sub>-w<sub>0</sub>),
w'<sub>1</sub>=3(w<sub>2</sub>-w<sub>1</sub>) and w'<sub>2</sub>= 3(w<sub>3</sub>-w<sub>2</sub>).</p>
<div class="note">
<h3>"Slow down, why is that true?"</h3>
<p>Sometimes just being told "this is the derivative" is nice, but you might want to see why
this is indeed the case. As such, let's have a look at the proof for this derivative. First off,
the weights are independent of the full Bézier function, so the derivative involves only the
derivative of the polynomial basis function. So, let's find that:</p>
<p>\[
B_{n,k}(t) \frac{d}{dt} = {n \choose k} t^k (1-t)^{n-k} \frac{d}{dt}
\]</p>
<p>Applying the <a href="http://en.wikipedia.org/wiki/Product_rule">product</a> and
<a href="http://en.wikipedia.org/wiki/Chain_rule">chain</a> rules gives us:</p>
<p>\[\begin{array}{l}
... &= {n \choose k} \left (
k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1
\right )
\end{array}\]</p>
<p>Which is hard to work with, so let's expand that properly:</p>
<p>\[\begin{array}{l}
... &= \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k}
\end{array}\]</p>
<p>Now, the trick is to turn this expression into something that has binomial
coefficients again, so we want to end up with things that look like "x! over y!(x-y)!".
If we can do that in a way that involves terms of <i>n-1</i> and <i>k-1</i>, we'll
be on the right track.</p>
<p>\[\begin{array}{l}
... &= \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\
... &= n \left (
\frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k}
\right ) \\
... &= n \left (
\frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{(k-1)} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k}
\right )
\end{array}\]</p>
<p>And that's the first part done: the two components inside the parentheses are actually
regular, lower order Bezier expressions:</p>
<p>\[\begin{array}{l}
... &= n \left (
\frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k}
\right )
\ ,\ with\ x=n-1,\ y=k-1
\\
... &= n \left ( B_{(n-1),(k-1)}(t) - B_{(n-1),k}(t) \right )
\end{array}\]</p>
<p>Now to apply this to our weighted Bezier curves. We'll write out the plain curve formula that
we saw earlier, and then work our way through to its derivative:</p>
<p>\[\begin{array}{l}
Bézier_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + ... \\
Bézier_{n,k}(t) \frac{d}{dt} &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + \\
& & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + \\
& & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + \\
& & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + \\
& & ...
\end{array}\]</p>
<p>If we expand this (with some color to show how terms line up), and reorder the terms by increasing values for <i>k</i>
we see the following:</p>
<p>\[\begin{array}{l}
n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\
... &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\
... & & &
\end{array}\]</p>
<p>Two of these terms fall way: the first term falls away because there is no
-1<sup>st</sup> term in a summation. As such, it always contributes "nothing", so
we can safely completely ignore it for the purpose of finding the derivative function.
The other term is the very last term in this expansion: one involving <i>B<sub>n-1,n</sub></i>.
This term would have a binomial coefficient of [</i>i</i> choose <i>i+1</i>], which is
a non-existent binomial coefficient. Again, this term would contribute "nothing", so we
can ignore it, too. This means we're left with:</p>
<p>\[\begin{array}{l}
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& \ n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\
...
\end{array}\]</p>
<p>And that's just a summation of lower order curves:</p>
<p>\[
Bézier_{n,k}(t) \frac{d}{dt} = n \cdot B_{(n-1),BLUE[0]}(t) \cdot (w_1 - w_0)
+ n \cdot B_{(n-1),RED[1]}(t) \cdot (w_2 - w_1)
+ n \cdot B_{(n-1),MAGENTA[2]}(t) \cdot (w_3 - w_2)
\ + \ ...
\]</p>
<p>We can rewrite this as a normal summation, and we're done:</p>
<p>\[
Bézier_{n,k}(t) \frac{d}{dt} = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k)
= \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underset{derivative\ weights}
{\underbrace{n \cdot (w_{k+1} - w_k)}}
\]</p>
</div>
<p>Let's rewrite that in a form similar to our original formula, so we can see the difference. We will
first list our original formula for Bézier curves, and then the derivative:</p>
<p>\[
Bézier(n,t) = \sum_{i=0}^{n}
\underset{binomial\ term}{\underbrace{\binom{n}{i}}}
\cdot\
\underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
\cdot\
\underset{weight}{\underbrace{w_i}}
\]</p>
<p>\[
Bézier'(n,t) = \sum_{i=0}^{k}
\underset{binomial\ term}{\underbrace{\binom{k}{i}}}
\cdot\
\underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
\cdot\
\underset{derivative\ weight}{\underbrace{n \cdot (w_{i+1} - w_i)}}
{\ , \ with \ k=n-1}
\]</p>
<p>What are the differences? In terms of the actual Bézier curve, virtually nothing!
We lowered the order (rather than <i>n</i>, it's now <i>n-1</i>), but it's still the
same Bézier function. The only real difference is in how the weights change when we
derive the curve's function. If we have four points A, B, C, and D, then the derivative
will have three points, the second derivative two, and the third derivative one:</p>
<p>\[ \begin{array}{l}
B(n,t), & & w = \{A,B,C,D\} \\
B'(n,t), & n = 3, & w' = \{A',B',C'\} &= \{3 \cdot (B-A), {\ } 3 \cdot (C-B), {\ } 3 \cdot (D-C)\} \\
B''(n,t), & n = 2, & w'' = \{A'',B''\} &= \{2 \cdot (B'-A'), {\ } 2 \cdot (C'-B')\} \\
B'''(n,t), & n = 1, & w''' = \{A'''\} &= \{1 \cdot (B''-A'')\}
\end{array} \]</p>
<p>We can keep performing this trick for as long as we have more than one weight. Once
we have one weight left, the next step will see <i>k = 0</i>, and the result of our
"Bézier function" summation is zero, because we're not adding anything at all. As such,
a quadratic curve has no second derivative, a cubic curve has no third derivative, and
generalized: an <i>n<sup>th</sup></i> order curve has <i>n-1</i> (meaningful) derivatives,
with any further derivative being zero.</p>

240
data/explanation.jsx Normal file
View File

@@ -0,0 +1,240 @@
<p>Bézier curves are a form of "parametric" function. Mathematically speaking, parametric
functions are cheats: a "function" is actually a well defined term representing a mapping
from any number of inputs to a <strong>single</strong> output. Numbers go in, a single
number comes out. Change the numbers that go in, and the number that comes out is still
a single number. Parametric functions cheat. They basically say "alright, well, we want
multiple values coming out, so we'll just use more than one function". An illustration:
Let's say we have a function that maps some value, let's call it <i>x</i>, to
some other value, using some kind of number manipulation:</p>
<p>\[
f(x) = \sin(x)
\]</p>
<p>The notation <i>f(x)</i> is the standard way to show that it's a function (by convention
called <i>f</i> if we're only listing one) and its output changes based on one variable
(in this case, <i>x</i>). Change <i>x</i>, and the output for <i>f(x)</i> changes.</p>
<p>So far so good. Now, let's look at parametric functions, and how they cheat.
Let's take the following two functions:</p>
<p>\[\begin{matrix}
f(a) = \sin(a) \\
f(b) = \cos(b)
\end{matrix}\]</p>
<p>There's nothing really remarkable about them, they're just a sine and cosine function,
but you'll notice the inputs have different names. If we change the value for <i>a</i>,
we're not going to change the output value for <i>f(b)</i>, since <i>a</i> isn't used
in that function. Parametric functions cheat by changing that. In a parametric function
all the different functions share a variable, like this:</p>
<p>\[
\left \{ \begin{matrix}
f_a(t) = \sin(t) \\
f_b(t) = \cos(t)
\end{matrix} \right. \]</p>
<p>Multiple functions, but only one variable. If we change the value for <i>t</i>,
we change the outcome of both <i>f<sub>a</sub>(t)</i> and <i>f<sub>b</sub>(t)</i>.
You might wonder how that's useful, and the answer is actually pretty simple: if
we change the labels <i>f<sub>a</sub>(t)</i> and <i>f<sub>b</sub>(t)</i> with what
we usually mean with them for parametric curves, things might be a lot more obvious:</p>
<p>\[
\left \{ \begin{matrix}
x = \sin(t) \\
y = \cos(t)
\end{matrix} \right. \]</p>
<p>There we go. <i>x</i>/<i>y</i> coordinates, linked through some mystery value <i>t</i>.</p>
<p>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:</p>
<textarea class="sketch-code" data-sketch-preset="empty" data-sketch-title="A (partial) circle: x=sin(t), y=cos(t)">
void drawFunction() {
pushStyle();
translate(dim/2,dim/2);
stroke(150);
line(-dim,0,dim,0);
line(0,-dim,0,dim);
stroke(0);
float r = dim/3;
for(float t=0; t<=5; t+=0.01) {
point(r * sin(t), -r * cos(t));
}
fill(0);
for(float i=0; i<=5; i+=0.5) {
ellipse(r * sin(i), -r * cos(i),3,3);
}
textAlign(CENTER,CENTER);
float x=0, y=-r*1.2, nx, ny;
for(float i=0; i<=5; i+=0.5) {
nx = x*cos(i) - y*sin(i);
ny = x*sin(i) + y*cos(i);
text("t="+i, nx, ny);
}
fill(150);
text("0,0",-10,10);
text("1",r+10,15);
text("-1",-10,r+10);
text("-1",-r-10,15);
text("1",-10,-r-10);
popStyle();
}</textarea>
<p>Bézier curves are (one in many classes of) parametric functions, and are characterised
by using the same base function for all its dimensions. Unlike the above example,
where the <i>x</i> and <i>y</i> values use different functions (one uses a sine, the other
a cosine), Bézier curves use the "binomial polynomial" for both <i>x</i> and <i>y</i>.
So what are binomial polynomials?</p>
<p>You may remember polynomials from high school, where they're those sums that look like:</p>
<p>\[
f(x) = a \cdot x^3 + b \cdot x^2 + c \cdot x + d
\]</p>
<p>If they have a highest order term <i></i> they're called "cubic" polynomials, if it's
<i></i> it's a "square" polynomial, if it's just <i>x</i> it's a line (and if there aren't
even any terms with <i>x</i> it's not a polynomial!)</p>
<p>Bézier curves are polynomials of <i>t</i>, rather than <i>x</i>, with the value for <i>t</i>
fixed being between 0 and 1, with coefficients <i>a</i>, <i>b</i> etc. taking the "binomial"
form, which sounds fancy but is actually a pretty simple description for mixing values:</p>
<p>\[ \begin{align*}
linear &= (1-t) + t \\
square &= (1-t)^2 + 2 \cdot (1-t) \cdot t + t^2 \\
cubic &= (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3
\end{align*} \]</p>
<p>I know what you're thinking: that doesn't look too simple, but if we remove <i>t</i> and
add in "times one", things suddenly look pretty easy. Check out these binomial terms:</p>
<p>\[ \begin{align*}
linear &= \hskip{2.5em} 1 + 1 \\
square &= \hskip{1.7em} 1 + 2 + 1\\
cubic &= \hskip{0.85em} 1 + 3 + 3 + 1\\
hypercubic &= 1 + 4 + 6 + 4 + 1
\end{align*} \]</p>
<p>Notice that 2 is the same as 1+1, and 3 is 2+1 and 1+2, and 6 is 3+3... As you
can see, each time we go up a dimension, we simply start and end with 1, and everything
in between is just "the two numbers above it, added together". Now <i>that's</i> easy
to remember.</p>
<p>There's an equally simple way to figure out how the polynomial terms work:
if we rename <i>(1-t)</i> to <i>a</i> and <i>t</i> to <i>b</i>, and remove the weights
for a moment, we get this:</p>
<p>\[ \begin{align*}
linear &= BLUE[a] + RED[b] \\
square &= BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot RED[b] + RED[b] \cdot RED[b] \\
cubic &= BLUE[a] \cdot BLUE[a] \cdot BLUE[a] + BLUE[a] \cdot BLUE[a] \cdot RED[b] + BLUE[a] \cdot RED[b] \cdot RED[b] + RED[b] \cdot RED[b] \cdot RED[b]\\
\end{align*} \]</p>
<p>It's basically just a sum of "every combination of <i>a</i> and <i>b</i>", progressively
replacing <i>a</i>'s with <i>b</i>'s after every + sign. So that's actually pretty simple
too. So now you know binomial polynomials, and just for completeness I'm going to show
you the generic function for this:</p>
<p>\[
Bézier(n,t) = \sum_{i=0}^{n}
\underset{binomial\ term}{\underbrace{\binom{n}{i}}}
\cdot\
\underset{polynomial\ term}{\underbrace{(1-t)^{n-i} \cdot t^{i}}}
\]</p>
<p>And that's the full description for Bézier curves. Σ in this function indicates that this is
a series of additions (using the variable listed below the Σ, starting at ...=<value> and ending
at the value listed on top of the Σ).</p>
<div class="howtocode">
<h3>How to implement the basis function</h3>
<p>We could naively implement the basis function as a mathematical construct,
using the function as our guide, like this:</p>
<pre>function Bezier(n,t):
sum = 0
for(k=0; k&lt;n; k++):
sum += n!/(k!*(n-k)!) * (1-t)^(n-k) * t^(k)
return sum</pre>
<p>I say we could, because we're not going to: the factorial function is <em>incredibly</em>
expensive. And, as we can see from the above explanation, we can actually create Pascal's
triangle quite easily without it: just start at [1], then [1,1], then [1,2,1], then [1,3,3,1],
and so on, with each next row fitting 1 more number than the previous row, starting and
ending with "1", with all the numbers in between being the sum of the previous row's
elements on either side "above" the one we're computing.</p>
<p>We can generate this as a list of lists lightning fast, and then never have to compute
the binomial terms because we have a lookup table:</p>
<pre>lut = [ [1], // n=0
[1,1], // n=1
[1,2,1], // n=2
[1,3,3,1], // n=3
[1,4,6,4,1], // n=4
[1,5,10,10,5,1], // n=5
[1,6,15,20,15,6,1]] // n=6
binomial(n,k):
while(n &gt;= lut.length):
s = lut.length
nextRow = new array(size=s+1)
nextRow[0] = 1
for(i=1, prev=s-1; i&ltprev; i++):
nextRow[i] = lut[prev][i-1] + lut[prev][i]
nextRow[s] = 1
lut.add(nextRow)
return lut[n][k]</pre>
<p>So what's going on here? First, we declare a lookup table with a size that's reasonably
large enough to accommodate most lookups. Then, we declare a function to get us the values
we need, and we make sure that if an n/k pair is requested that isn't in the LUT yet, we
expand it first. Our basis function now looks like this:</p>
<pre>function Bezier(n,t):
sum = 0
for(k=0; k&lt;n; k++):
sum += binomial(n,k) * (1-t)^(n-k) * t^(k)
return sum</pre>
<p>Perfect. Of course, we can optimize further. For most computer graphics purposes, we
don't need arbitrary curves. We need quadratic and cubic curves (this primer actually
does do arbitrary curves, so you'll find code similar to shown here), which means we can
drastically simplify the code:</p>
<pre>function Bezier(2,t):
t2 = t * t
mt = 1-t
mt2 = mt * mt
return mt2 + 2*mt*t + t2
function Bezier(3,t):
t2 = t * t
t3 = t2 * t
mt = 1-t
mt2 = mt * mt
mt3 = mt2 * mt
return mt3 + 3*mt2*t + 3*mt*t2 + t3</pre>
<p>And now we know how to program the basis function. Exellent.</p>
</div>
<p>So, now we know what the base function(s) look(s) like, time to add in the magic that makes
Bézier curves so special: control points.<p>

256
data/extremities.jsx Normal file
View File

@@ -0,0 +1,256 @@
<p>Now that we understand (well, superficially anyway) the component functions, we can find the extremities of our
Bézier curve by finding maxima and minima on the component functions, by solving the equations B'(t) = 0 and B''(t) = 0.
Although, in the case of quadratic curves there is no B''(t), so we only need to compute B'(t) = 0. So, how do we compute the first and second derivatives? Fairly easily, actually, until our derivatives are 4th order or higher... then things get really hard. But let's start simple:</p>
<h2>Quadratic curves: linear derivatives.</h2>
<p>Finding the solution for "where is this line 0" should be trivial:</p>
<p>\[
l(x) = ax + b = 0,\\
ax + b = 0,\\
ax = -b \\
x = \frac{-b}{a}
\]</p>
<p>Done. And quadratic curves have no meaningful second derivative, so we're <em>really</em> done.</p>
<h2>Cubic curves: the quadratic formula.</h2>
<p>The derivative of a cubic curve is a quadratic curve, and finding the roots for a quadratic Bézier curve means we can apply the <a href="https://en.wikipedia.org/wiki/Quadratic_formula">Quadratic formulat</a>. If you've seen it before, you'll remember it, and if you haven't, it looks like this:</p>
<p>\[
Given\ f(t) = at^2 + bt + c,\ f(t)=0\ when\ t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
\]</p>
<p>So, if we can express a Bézier component function as a plain polynomial, we're done: we just plug in the values into the quadratic formula, check if that square root is negative or not (if it is, there are no roots) and then just compute the two values that come out (because of that plus/minus sign we get two). Any value between 0 and 1 is a root that matters for Bézier curves, anything below or above that is irrelevant (because Bézier curves are only defined over the interval [0,1]). So, how do we convert?</p>
<p>First we turn our cubic Bézier function into a quadratic one, by following the rule mentioned at the end of the <a href="#derivatives">derivatives section</a>:</p>
<p>\[
B(t)\ uses\ \{ p_1,p_2,p_3,p_4 \} \\
B'(t)\ uses\ \{ v_1.v_2,v_3 \},\ where\ v_1 = 3(p_2-p_1),\ v_2 = 3(p_3-p_2),\ v_3 = 3(p_4-p_3)
\]</p>
<p>And then, using these <em>v</em> values, we can find out what our <em>a</em>, <em>b</em>, and <em>c</em> should be:</p>
<p>\[
B'(t) = v_1(1-t)^2 + 2v_2(1-t)t + v_3t^2 \\
... = v_1(t^2 - 2t + 1) + 2v_2(t-t^2) + v_3t^2 \\
... = v_1t^2 - 2v_1t + v_1 + 2v_2t - 2v_2t^2 + v_3t^2 \\
... = v_1t^2 - 2v_2t^2 + v_3t^2 - 2v_1t + v_1 + 2v_2t \\
... = (v_1-2v_2+v_3)t^2 + 2(v_2-v_1)t + v_1
\]</p>
<p>So we can find the roots by using:</p>
<p>\[
a = v_1-2v_2+v_3 = 3(-p_1 + 3p_2 - 3p_3 + p_4) \\
b = 2(v_2-v_1) = 6(p_1 - 2p_2 + p_3) \\
c = v_1 = 3(p_2-p_1)
\]</p>
<p>Easy peasy. We also note that the second derivative of a cubic curve means computing the first derivative of a quadratic curve, and we just saw how to do that in the section above.</p>
<h2>Quartic curves: Cardano's algorithm.</h2>
<p>Quartic—fourth degree—curves have a cubic function as derivative. Now, cubic functions are a bit of a problem because they're really hard to solve. But, way back in the 16<sup>th</sup> century, <a href="https://en.wikipedia.org/wiki/Gerolamo_Cardano">Gerolamo Cardano</a> figured out that even if the general cubic function is really hard to solve, it can be rewritten to a form for which finding the roots is "easy", and then the only hard part is figuring out how to go from that form to the generic form. So:</p>
<p>\[
very\ hard:\ solve\ at^3 + bt^2 + ct + d = 0\\
easier:\ solve\ t^3 + pt + q = 0
\]</p>
<p>This is easier because for the "easier formula" we can use <a href="http://www.wolframalpha.com/input/?i=t^3+%2B+pt+%2B+q">regular calculus</a> to find the roots (as a cubic function, however, it can have up to three roots, but two of those can be complex. For the purpose of Bézier curve extremities, we can completely ignore those complex roots, since our <em>t</em> is a plain real number from 0 to 1).</p>
<p>So, the trick is to figure out how to turn the first formula into the second formula, and to then work out the maths that gives us the roots. This is explained in detail over at <a href="http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm">Ken J. Ward's page</a> for solving the cubic equation, so instead of showing the maths, I'm simply going to show the programming code for solving the cubic equation, with the complex roots getting totally ignored.</p>
<div class="note"><pre>
// A helper function to filter for values in the [0,1] interval:
function accept(t) {
return 0<=t && t <=1;
}
// A special cuberoot function, which we can use because we don't care about complex roots:
function crt(v) {
if(v<0) return -Math.pow(-v,1/3);
return Math.pow(v,1/3);
}
// Now then: given cubic coordinates pa, pb, pc, pd, find all roots.
function getCubicRoots(pa, pb, pc, pd) {
var d = (-pa + 3*pb - 3*pc + pd),
a = (3*pa - 6*pb + 3*pc) / d,
b = (-3*pa + 3*pb) / d,
c = pa / d;
var p = (3*b - a*a)/3,
p3 = p/3,
q = (2*a*a*a - 9*a*b + 27*c)/27,
q2 = q/2,
discriminant = q2*q2 + p3*p3*p3;
// and some variables we're going to use later on:
var u1,v1,root1,root2,root3;
// three possible real roots:
if (discriminant < 0) {
var mp3 = -p/3,
mp33 = mp3*mp3*mp3,
r = sqrt( mp33 ),
t = -q / (2*r),
cosphi = t<-1 ? -1 : t>1 ? 1 : t,
phi = acos(cosphi),
crtr = cuberoot(r),
t1 = 2*crtr;
root1 = t1 * cos(phi/3) - a/3;
root2 = t1 * cos((phi+2*pi)/3) - a/3;
root3 = t1 * cos((phi+4*pi)/3) - a/3;
return [root1, root2, root3].filter(accept);
}
// three real roots, but two of them are equal:
else if(discriminant === 0) {
u1 = q2 < 0 ? cuberoot(-q2) : -cuberoot(q2);
root1 = 2*u1 - a/3;
root2 = -u1 - a/3;
return [root1, root2].filter(accept);
}
// one real root, two complex roots
else {
var sd = sqrt(discriminant);
u1 = cuberoot(sd - q2);
v1 = cuberoot(sd + q2);
root1 = u1 - v1 - a/3;
return [root1].filter(accept);;
}
}</pre></div>
<p>And that's it. The maths is complicated, but the code is pretty much just "follow the maths, while caching as many values as we can to reduce recomputing things as much as possible" and now we have a way to find all roots for a cubic function and can just move on with using that to find extremities of our curves.</p>
<h2>Quintic and higher order curves: finding numerical solutions</h2>
<p>The problem with this is that as the order of the curve goes up, we can't actually solve those equations the normal
way. We can't take the function, and then work out what the solutions are. Not to mention that even solving a third
order derivative (for a fourth order curve) is already a royal pain in the backside. We need a better solution. We
need numerical approaches.</p>
<p>That's a fancy word for saying "rather than solve the function, treat the problem as a sequence of identical
operations, the performing of which gets us closer and closer to the real answer". As it turns out, there is a
really nice numerical root finding algorithm, called the <a href="http://en.wikipedia.org/wiki/Newton-Raphson">Newton-Raphson</a>
root finding method (yes, after <strong>that</strong> Newton), which we can make use of.</p>
<p>The Newton-Raphson approach consists of picking a value <i>t</i> (any will do), and getting the corresponding
value at that <i>t</i> value. For normal functions, we can treat that value as a height. If the height is zero,
we're done, we have found a root. If it's not, we take the tangent of the curve at that point, and extend
it until it passes the x-axis, which will be at some new point <i>t</i>. We then repeat the procedure with this
new value, and we keep doing this until we find our root.</p>
<p>Mathematically, this means that for some <i>t</i>, at step <i>n=1</i>, we perform the following calculation
until <i>f<sub>y</sub></i>(<i>t</i>) is zero, so that the next <i>t</i> is the same as the one we already have:</p>
<p>\[
t_{n+1} = t_n - \frac{f_y(t_n)}{f'_y(t_n)}
\]</p>
<p>(The wikipedia article has a decent animation for this process, so I'm not adding a sketch for that here)</p>
<p>Now, this works well only if we can pick good starting points, and our curve is continuously differentiable
and doesn't have oscillations. Glossing over the exact meaning of those terms, the curves we're dealing with
conform to those constraints, so as long as we pick good starting points, this will work. So the question is:
which starting points do we pick?</p>
<p>As it turns out, Newton-Raphson is so blindingly fast, so we could get away with just not picking:
we simply run the algorithm from <i>t=0</i> to <i>t=1</i> at small steps (say, 1/200<sup>th</sup>) and
the result will be all the roots we want. Of course, this may pose problems for high order Bézier
curves: 200 steps for a 200<sup>th</sup> order Bézier curve is going to go wrong, but that's okay:
there is no reason, ever, to use Bézier curves of crazy high orders. You might use a fifth order curve
to get the "nicest still remotely workable" approximation of a full circle with a single Bézier curve,
that's pretty much as high as you'll ever need to go.</p>
<h2>In conclusion:</h2>
<p>So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics:</p>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Quadratic Bézier curve components">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
noAdditionals();
usePanelPadding();
nextPanel();
drawAxes("t",0,1, "x",0,panelDim);
BezierCurve x_only = curve.justX(dim-2*pad);
x_only.draw();
stroke(255,0,0);
float[] tx = x_only.getInflections();
for(float t: tx) {
if(t==0 || t==1) continue;
Point p = x_only.getPoint(t);
ellipse(p.x,p.y,5,5);
line(p.x,p.y-3,p.x,0);
}
nextPanel();
drawAxes("t",0,1, "y",0,panelDim);
BezierCurve y_only = curve.justY(dim-2*pad);
y_only.draw();
stroke(255,0,255);
float[] ty = y_only.getInflections();
for(float t: ty) {
if(t==0 || t==1) continue;
Point p = y_only.getPoint(t);
ellipse(p.x,p.y,5,5);
line(p.x,p.y-3,p.x,0);
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Cubic Bézier curve components">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
noAdditionals();
usePanelPadding();
nextPanel();
drawAxes("t",0,1, "x",0,panelDim);
BezierCurve x_only = curve.justX(dim-2*pad);
x_only.draw();
stroke(255,0,0);
float[] tx = x_only.getInflections();
for(float t: tx) {
if(t==0 || t==1) continue;
Point p = x_only.getPoint(t);
ellipse(p.x,p.y,5,5);
line(p.x,p.y-3,p.x,0);
}
nextPanel();
drawAxes("t",0,1, "y",0,panelDim);
BezierCurve y_only = curve.justY(dim-2*pad);
y_only.draw();
stroke(255,0,255);
float[] ty = y_only.getInflections();
for(float t: ty) {
if(t==0 || t==1) continue;
Point p = y_only.getPoint(t);
ellipse(p.x,p.y,5,5);
line(p.x,p.y-3,p.x,0);
}
}</textarea>

90
data/flattening.jsx Normal file
View File

@@ -0,0 +1,90 @@
<p>We can also simplify the drawing process by "sampling" the curve at certain points, and then joining those points up with straight lines, a process known as "flattening", as we are reducing a curve to a simple sequence of straight, "flat" lines.</p>
<p>We can do this is by saying "we want X segments", and then sampling the curve at intervals that are spaced such that we end up with the number of segments we wanted. The advantage of this method is that it's fast: instead of evaluating 100 or even 1000 curve coordinates, we can sample a much lower number and still end up with a curve that sort-of-kind-of looks good enough. The disadvantage of course is that we lose the precision of working with "the real curve", so we usually can't use the flattened for for doing true intersection detection, or curvature alignment.</p>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Flattening a quadratic curve">
void setupCurve() {
setupDefaultQuadratic();
offsetting();
offset = 16;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
float x = curve.getXValue(0),
y = curve.getYValue(0),
x2, y2, step = 1/offset, t;
for(int i=1; i<=offset; i++) {
t = i*step;
x2 = curve.getXValue(t);
y2 = curve.getYValue(t);
line(x,y,x2,y2);
x = x2;
y = y2;
}
fill(0);
text("Flattened using "+offset+" segments", dim/4, dim-20);
}</textarea>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="Flattening a cubic curve">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 24;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
nextPanel();
stroke(0);
float x = curve.getXValue(0),
y = curve.getYValue(0),
x2, y2, step = 1/offset, t;
for(int i=1; i<=offset; i++) {
t = i*step;
x2 = curve.getXValue(t);
y2 = curve.getYValue(t);
line(x,y,x2,y2);
x = x2;
y = y2;
}
fill(0);
text("Flattened using "+offset+" segments", dim/4, dim-20);
}</textarea>
<p>Try clicking on the sketch and using your '+' and '-' keys to lower the number of segments for both the quadratic and cubic curve. You'll notice that for certain curvatures, a low number of segments works quite well, but for more complex curvatures (try this for the cubic curve), a higher number is required to capture the curvature changes properly.</p>
<div class="howtocode">
<h3>How to implement curve flattening</h3>
<p>Let's just use the algorithm we just specified, and implement that:</p>
<pre>function flattenCurve(curve, segmentCount):
step = 1/segmentCount;
coordinates = [curve.getXValue(0), curve.getYValue(0)]
for(i=1; i <= segmentCount; i++):
t = i*step;
coordinates.push[curve.getXValue(t), curve.getYValue(t)]
return coordinates;</pre>
<p>And done, that's the algorithm implemented. That just leaves drawing the resulting "curve" as a sequence of lines:</p>
<pre>function drawFlattenedCurve(curve, segmentCount):
coordinates = flattenCurve(curve, segmentCount)
coord = coordinates[0], _coords;
for(i=1; i < coordinates.length; i++):
_coords = coordinates[i]
line(coords, _coords)
coords = _coords</pre>
<p>We start with the first coordinate as reference point, and then just draw lines between each point and its next point.</p>
</div>

66
data/graduatedoffset.jsx Normal file
View File

@@ -0,0 +1,66 @@
<p>What if we want to do graduated offsetting, starting at some distance <i>s</i> but ending
at some other distance <i>e</i>? well, if we can compute the length of a curve (which we can
if we use the Legendre-Gauss quadrature approach) then we can also determine how far "along the
line" any point on the curve is. With that knowledge, we can offset a curve so that its offset
curve is not uniformly wide, but graduated between with two different offset widths at the
start and end.</p>
<p>Like normal offsetting we cut up our curve in sub-curves, and then check at which distance
along the original curve each sub-curve starts and ends, as well as to which point on the curve
each of the control points map. This gives us the distance-along-the-curve for each interesting
point in the sub-curve. If we call the total length of all sub-curves seen prior to seeing "the\
current" sub-curve <i>S</i> (and if the current sub-curve is the first one, <i>S</i> is zero),
and we call the full length of our original curve <i>L</i>, then we get the following graduation
values:</p>
<ul>
<li>start: map <i>S</i> from interval (<i>0,L</i>) to interval <i>(s,e)</i></li>
<li>c1: <i>map(<strong>S+d1</strong>, 0,L, s,e)</i>, d1 = distance along curve to projection of c1</li>
<li>c2: <i>map(<strong>S+d2</strong>, 0,L, s,e)</i>, d2 = distance along curve to projection of c2</li>
<li>...</li>
<li>end: <i>map(<strong>S+length(subcurve)</strong>, 0,L, s,e)</i></li>
</ul>
At each of the relevant points (start, end, and the projections of the control points onto
the curve) we know the curve's normal, so offsetting is simply a matter of taking our original
point, and moving it along the normal vector by the offset distance for each point. Doing so
will give us the following result (these have with a starting width of 0, and an end width
of 40 pixels, but can be controlled with your + and - keys):</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Graduated offsetting a quadratic Bézier curve">
void setupCurve() {
setupDefaultQuadratic();
offsetting();
offset = 20;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
if(offset>0) {
noAdditionals();
BezierCurve[] offsetCurve = curve.offset(offset, 0, 1);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
offsetCurve = curve.offset(-offset, 0, 1);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Graduated offsetting a cubic Bézier curve">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 20;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
if(offset>0) {
noAdditionals();
BezierCurve[] offsetCurve = curve.offset(offset, 0, 1);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
offsetCurve = curve.offset(-offset, 0, 1);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
}
}</textarea>

137
data/intersections.jsx Normal file
View File

@@ -0,0 +1,137 @@
<p>Let's look at some more things we will want to do with Bézier curves. Almost immediately after figuring out how to
get bounding boxes to work, people tend to run into the problem that even though the minimal bounding box (based on
rotation) is tight, it's not sufficient to perform collision detection ("<i>does curve C touch, or pass through, curve
or line L?</i>"). In order to do this, we need to know whether or not there's an intersection on the actual curve.</p>
<p>We'll do this in steps, because it's a bit of a journey to get to curve/curve intersection checking. First, let's
start simple, by implementing a line-line intersection checker. While we can solve this the traditional calculus way
(determine the functions for both lines, then compute the intersection by equating them and solving for two unknowns),
linear algebra actually offers a nicer solution:</p>
<p id="intersection_ll">if we have two line segments with two coordinates each, segments A-B and C-D, we can find the
intersection of the lines these segments are an intervals on by linear algebra, using the procedure outlined in this
<a href="http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=geometry2#line_line_intersection">top coder</a> article.
Of course, we need to make sure that the intersection isn't just on the lines our line segments lie on, but also on our
line segments themselves, so after we find the intersection we need to verify it lies without the bounds of our original
line segments.</p>
<p>The following graphic implements this intersection detection, showing a red point for an intersection on the lines
our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on
both segments (being a real intersection point).</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Line/line intersections">
Point p1, p2, p3, p4;
void setupCurve() {
p1 = new Point(50,50);
p2 = new Point(150,110);
curves.add(new BezierCurve(new Point[]{p1,p2}, false));
p3 = new Point(50,250);
p4 = new Point(170,170);
curves.add(new BezierCurve(new Point[]{p3,p4}, false));
}
void drawCurve(BezierCurve curve) {
// draw the lines through p1/p2 and p3/p4
stroke(0,50);
float dx = 10*(p2.x-p1.x), dy = 10*(p2.y-p1.y);
line(p1.x-dx,p1.y-dy,p2.x+dx,p2.y+dy);
dx = 10*(p4.x-p3.x); dy = 10*(p4.y-p3.y);
line(p3.x-dx,p3.y-dy,p4.x+dx,p4.y+dy);
// show the line segments
curves.get(0).draw();
curves.get(1).draw();
// show the intersection point
Point ntr = comp.getProjection(p1,p2,p3,p4);
// red if virtual intersection, green if real
boolean oncurves = true;
if(min(p1.x,p2.x) > ntr.x || ntr.x > max(p1.x,p2.x) ||
min(p1.y,p2.y) > ntr.y || ntr.y > max(p1.y,p2.y)) oncurves = false;
if(oncurves) {
if(min(p3.x,p4.x) > ntr.x || ntr.x > max(p3.x,p4.x) ||
min(p3.y,p4.y) > ntr.y || ntr.y > max(p3.y,p4.y)) oncurves = false; }
stroke(oncurves?0:255, oncurves?255:0, 0);
ellipse(ntr.x,ntr.y,5,5);
}</textarea>
<p>Curve/line intersection is more work, but we've already seen the techniques we need to use in order
to perform it: first we translate/rotate both the line and curve together, in such a way that the line
coincides with the x-axis. This will position the curve in a way that makes it cross the line at
points where its y-function is zero. By doing this, the problem of finding intersections between a
curve and a line has now become the problem of performing root finding on our translated/rotated curve.
One Newton-Raphson root finding round later and the intersections have been found:</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Quadratic curve/line intersections">
Point p1, p2;
void setupCurve() {
p1 = new Point(40,60);
p2 = new Point(260,200);
curves.add(new BezierCurve(new Point[]{
p1, p2
}, false));
curves.add(new BezierCurve(new Point[]{
new Point(25,150),
new Point(180,30),
new Point(230,250)
}));
}
void drawCurve(BezierCurve curve) {
curves.get(0).draw();
curves.get(1).draw();
BezierCurve aligned = curves.get(1).align(p1,p2);
float[] roots = comp.findAllRoots(0, aligned.y_values);
fill(150,0,150);
float x, y;
for(float t: roots) {
if(t<0 || t>1) continue;
x = curves.get(1).getXValue(t);
y = curves.get(1).getYValue(t);
ellipse(x,y,5,5);
text(""+round(1000*t)/1000,x+10,y);
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Cubic curve/line intersections">
Point p1, p2;
void setupCurve() {
p1 = new Point(100,20);
p2 = new Point(195,255);
curves.add(new BezierCurve(new Point[]{
p1, p2
}, false));
curves.add(new BezierCurve(new Point[]{
new Point(150,125),
new Point(40,30),
new Point(270,115),
new Point(145,200)
}));
}
void drawCurve(BezierCurve curve) {
curves.get(0).draw();
curves.get(1).draw();
BezierCurve aligned = curves.get(1).align(p1,p2);
float[] roots = comp.findAllRoots(0, aligned.y_values);
fill(150,0,150);
float x, y;
for(float t: roots) {
if(t<0 || t>1) continue;
x = curves.get(1).getXValue(t);
y = curves.get(1).getYValue(t);
ellipse(x,y,5,5);
text(""+round(1000*t)/1000,x+10,y);
}
}</textarea>
<p>Curve/curve intersection, however, is more complicated. Since we have no straight line to align to, we
can't simply align one of the curves and be left with a simple procedure. Instead, we'll need to apply two
techniques we've not covered yet: de Casteljau's algorithm, and curve splitting.</p>

132
data/matrix.jsx Normal file
View File

@@ -0,0 +1,132 @@
<p>We can also represent Bézier as matrix operations, by expressing the Bézier formula
as a polynomial basis function, the weight matrix, and the actual coordinates as matrix.
Let's look at what this means for the cubic curve :</p>
<p>\[
B(t) = P_1 \cdot (1-t)^3 + P_2 \cdot 3 \cdot (1-t)^2 \cdot t + P_3 \cdot 3 \cdot (1-t) \cdot t^2 + P_4 \cdot t^3
\]</p>
<p>Disregarding our actual coordinates for a moment, we have:</p>
<p>\[
B(t) = (1-t)^3 + 3 \cdot (1-t)^2 \cdot t + 3 \cdot (1-t) \cdot t^2 + t^3
\]</p>
<p>We can write this as a sum of four expressions:</p>
<p>\[
\begin{matrix}
... & = & (1-t)^3 \\
& + & 3 \cdot (1-t)^2 \cdot t \\
& + & 3 \cdot (1-t) \cdot t^2 \\
& + & t^3 \\
\end{matrix}
\]</p>
<p>And we can expand these expressions:</p>
<p>\[
\begin{matrix}
... & = & ( -t^3 + 3 \cdot t^2 - 3 \cdot t + 1) \\
& + & (3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t) \\
& + & (-3 \cdot t^3 + 3 \cdot t^2) \\
& + & (t^3) \\
\end{matrix}
\]</p>
<p>Furthermore, we can keep all the one and zero factors in:</p>
<p>\[
\begin{matrix}
... & = & -1 \cdot t^3 + 3 \cdot t^2 - 3 \cdot t + 1 \\
& + & +3 \cdot t^3 - 6 \cdot t^2 + 3 \cdot t + 0 \\
& + & -3 \cdot t^3 + 3 \cdot t^2 + 0 \cdot t + 0 \\
& + & +1 \cdot t^3 + 0 \cdot t^2 + 0 \cdot t + 0 \\
\end{matrix}
\]</p>
<p>And <em>that</em>, we can view as a series of four matrix operations:</p>
<p>\[
\begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-1 \\ 3 \\ -3 \\ 1\end{bmatrix}
+ \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}3 \\ -6 \\ 3 \\ 0\end{bmatrix}
+ \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}-3 \\ 3 \\ 0 \\ 0\end{bmatrix}
+ \begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}1 \\ 0 \\ 0 \\ 0\end{bmatrix}
\]</p>
<p>If we compact this into a single matrix operation, we get:</p>
<p>\[
\begin{bmatrix}t^3 & t^2 & t & 1\end{bmatrix} \cdot \begin{bmatrix}
-1 & 3 & -3 & 1 \\
3 & -6 & 3 & 0 \\
-3 & 3 & 0 & 0 \\
1 & 0 & 0 & 0
\end{bmatrix}
\]</p>
<p>This kind of polynomial basis representation is generally written with the bases in
increasing order, which means we need to flip our <em>t</em> matrix horizontally, and our
big "mixing" matrix upside down:</p>
<p>\[
\begin{bmatrix}1 & t & t^2 & t^3\end{bmatrix} \cdot \begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\]</p>
<p>And then finally, we can add in our original coordinates as a single third matrix:</p>
<p>\[
B(t) = \begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>We can perform the same trick for the quadratic curve, in which case we end up with:</p>
<p>\[
B(t) = \begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>If we plug in a <em>t</em> value, and then multiply the matrices, we will
get exactly the same values as when we evaluate the original polynomial function.
<strong>So: why would we bother with matrices?</strong> Matrix representations
allow us to discover things about functions that would otherwise be hard to tell.
It turns out that the curves form <a href="https://en.wikipedia.org/wiki/Triangular_matrix">triangular
matrices</a>, and they have a determinant equal to the product of the actual
coordinates we use for our curve. It's also invertible, which means there's
<a href="https://en.wikipedia.org/wiki/Invertible_matrix#The_invertible_matrix_theorem">a
ton of properties</a> that are all satisfied. Of course, the main question is:
"Why is this useful to us, now?", and the answer to that is that it's not
immediately useful, but you'll be seeing some instances where certain curve
properties can be either computed via function manipulation, or via clever
use of matrices, and sometimes the matrix approach can be (drastically) faster.<p>
<p>so for now, just remember that we can represent curves this way, and let's move on.</p>

611
data/matrixsplit.jsx Normal file
View File

@@ -0,0 +1,611 @@
<p>Another way to split curves is to exploit the matrix representation of
a Bézier curve. In <a href="#matrix">the section on matrices</a> we saw that
we can represent curves as matrix multiplications. Specifically, we saw these
two forms for the quadratic, and cubic curves, respectively (using the reversed
Bézier coefficients vector for legibility):</p>
<p>\[
B(t) = \begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
B(t) = \begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0\\
-3 & 3 & 0 & 0\\
3 & -6 & 3 & 0\\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>Let's say we want to split the curve at some point <em>t = z</em>, forming
two new (obviously smaller) Bézier curves. To find the coordinates for these
two Bézier curves, we can use the matrix representation and some linear algebra.
First, we split out the the actual "point on the curve" information as a new matrix
multiplication:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & (z \cdot t) & (z \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & (z \cdot t) & (z \cdot t)^2 & (z \cdot t)^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0\\
0 & z & 0 & 0\\
0 & 0 & z^2 & 0\\
0 & 0 & 0 & z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]</p>
<p>If we could compact these matrices back to a form <strong>[t values] · [bezier matrix] · [column matrix]</strong>,
with the first two staying the same, then that column matrix on the right would be the coordinates
of a new Bézier curve that describes the first segment, from <em>t = 0</em> to <em>t = z</em>.
As it turns out, we can do this quite easily, by exploiting some simple rules of linear algebra
(and if you don't care about the derivations, just skip to the end of the box for the results!).</p>
<div class="note">
<h2>Deriving new hull coordinates</h2>
<p>Deriving the two segments upon splitting a curve takes a few steps, and the higher
the curve order, the more work it is, so let's look at the quadratic curve first:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\underset{we\ turn\ this...}{\underbrace{Z \cdot M}}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\underset{...into\ this!}{\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
Q
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>We do this, because [<em>M · M<sup>-1</sup></em>] is the identity matrix (a bit like
multiplying something by x/x in calculus. It doesn't do anything to the function, but it
does allow you to rewrite it to something that may be easier to work with, or can be
broken up differently). Adding that as matrix multiplication has no effect on the total
formula, but it does allow us to change the matrix sequence [<em>something · M</em>] to
a sequence [<em>M · something</em>], and that makes a world of difference: if we know
what [<em>M<sup>-1</sup> · Z · M</em>] is, we can apply that to our coordinates, and be
left with a proper matrix representation of a quadratic Bézier curve (which is
[<em>T · M · P</em>]), with a new set of coordinates that represent the curve from
<em>t = 0</em> to <em>t = z</em>. So let's get computing:</p>
<p>\[
Q = M^{-1} \cdot Z \cdot M =
\begin{bmatrix}
1 & 0 & 0 \\
1 & \frac{1}{2} & 0 \\
1 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
=
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\]</p>
<p>Excellent! Now we can form our new quadratic curve:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot M \cdot Q \cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
\left (
Q
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\left (
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1
\end{bmatrix}
\]</p>
<p><strong><em>Brilliant</em></strong>: if we want a subcurve from <em>t = 0</em>
to <em>t = z</em>, we can keep the first coordinate the same (which makes sense),
our control point becomes a z-ratio mixture of the original control point and the start
point, and the new end point is a mixture that looks oddly similar to a bernstein
polynomial of degree two, except it uses (z-1) rather than (1-z)... These new
coordinates are actually really easy to compute directly!</p>
<p>Of course, that's only one of the two curves. Getting the section from <em>t = z</em>
to <em>t = 1</em> requires doing this again. We first observe what what we just did is
actually evaluate the general interval [0,<em>z</em>], which we wrote down simplified
becuase of that zero, but we actually evaluated this:<p>
<p>\[
B(t) =
\begin{bmatrix}
1 & ( 0 + z \cdot t) & ( 0 + z \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>If we want the interval [<em>z</em>,1], we will be evaluating this instead:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & ( z + (1-z) \cdot t) & ( z + (1-z) \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & z & z^2 \\
0 & 1-z & 2 \cdot z \cdot (1-z) \\
0 & 0 & (1-z)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]</p>
<p>We're going to do the same trick, to turn <em>[something · M]</em> into <em>[M · something]</em>:</p>
<p>\[
Q' = M^{-1} \cdot Z' \cdot M =
\begin{bmatrix}
1 & 0 & 0 \\
1 & \frac{1}{2} & 0 \\
1 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & z & z^2 \\
0 & 1-z & 2 \cdot z \cdot (1-z) \\
0 & 0 & (1-z)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
=
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\]</p>
<p>So, our final second curve looks like:</p>
<p>\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot M \cdot Q \cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
\left (
Q'
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\left (
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]</p>
<p>\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z \cdot P_3 - (z-1) \cdot P_2 \\
P_3
\end{bmatrix}
\]</p>
<p><strong><em>Nice</em></strong>: we see the same as before; can keep the last
coordinate the same (which makes sense), our control point becomes a z-ratio
mixture of the original control point and the end point, and the new start point
is a mixture that looks oddly similar to a bernstein polynomial of degree two,
except it uses (z-1) rather than (1-z). These new coordinates are <em>also</em>
really easy to compute directly!</p>
</div>
<p>So, using linear algebra rather than de Casteljau's algorithm, we have determined
that for any quadratic curve split at some value <em>t = z</em>, we get two subcurves
that are described as Bézier curves with simple-to-derive coordinates.</p>
<p>\[
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z \cdot P_3 - (z-1) \cdot P_2 \\
P_3
\end{bmatrix}
\]</p>
<p>We can do the same for cubic curves. However, I'll spare you the actual derivation
(don't let that stop you from writing that out yourself, though) and simply show you
the resulting new coordinate sets:</p>
<p>\[
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-(z-1) & z & 0 & 0 \\
(z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 & 0 \\
-(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1
\end{bmatrix}
\]</p>
<p>and</p>
<p>\[
\begin{bmatrix}
-(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1)^3 \cdot z^2 & z^3 \\
0 & (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 \\
0 & 0 & -(z-1) & z \\
0 & 0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \\
z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \\
z \cdot P_4 - (z-1) \cdot P_3 \\
P_4
\end{bmatrix}
\]</p>
<p>So, looking at our matrices, did we really need to compute the second segment matrix?
No, we didn't. Actually having one segment's matrix means we implicitly have the other:
push the values of each row in the matrix <strong><em>Q</em></strong> to the right, with
zeroes getting pushed off the right edge and appearing back on the left, and then flip
the matrix vertically. Presto, you just "calculated" <strong><em>Q'</em></strong>.</p>
<p>Implementing curve splitting this way requires less recursion, and is just straight
arithmetic with cached values, so can be cheaper on systems were recursion is expensive.
If you're doing computation with devices that are good at matrix multiplication, chopping
up a Bézier curve with this method will be a lot faster than applying de Casteljau.</p>

130
data/moulding.jsx Normal file
View File

@@ -0,0 +1,130 @@
<p>De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split
curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves
based on three points and a tangent. Particularly this last thing is really useful because it lets us "mould" a curve,
by picking it up at some point, and dragging that point around to change the curve's shape.</p>
<p>How does that work? Succinctly: we run de Casteljau's algorithm in reverse!</p>
<p>Let's start out with a pre-existing curve, defined by <i>start</i>, two control points, and <i>end</i>. We can
mould this curve by picking a point somewhere on the curve, at some <i>t</i> value, and the moving it to a new
location and reconstructing the curve that goes through <i>start</i>, our new point with the original tangent,
and </i>end</i>. In order to see how and why we can do this, let's look at some identity information for Bézier
curves. There's actually a hidden goldmine of identities that we can exploit when doing Bézier operations, and
this will only scratch the surface. But, in a good way!</p>
<p>In the following graphic, click anywhere on the curves to see the identity information that we'll
be using to run de Casteljau in reverse:</p>
<textarea class="sketch-code" data-sketch-preset="abc" data-sketch-title="Projections in a quadratic Bézier curve">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
if(Bt != -1) {
Point[] abc = curve.getABC(Bt);
drawABC(curve, abc);
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="abc" data-sketch-title="Projections in a cubic Bézier curve">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
if(Bt != -1) {
Point[] abc = curve.getABC(Bt);
drawABC(curve, abc);
}
}</textarea>
<p>So, what exactly do we see in these graphics? First off, there's the three points <i>A</i>, <i>B</i> and
<i>C</i>.</p>
<p>Point <i>B</i> is our "on curve" point, A is the first "strut" point when running de Casteljau's
algorithm in reverse; for quadratic curves, this happens to also be the curve's control point. For cubic
curves, it's the "top of the triangle" for the struts that lead to point <i>B</i>. Point
<i>C</i>, finally, is the intersection of the line that goes through <i>A</i> and <i>B</i> and the baseline,
between our start and end points.</p>
<p>There is some important identity information here: as long as we don't pick a new <i>t</i> coordinate,
the location of point <i>C</i> on the line <i>start-end</i> represents a fixed ratio distance. We can drag
around the control points as much as we like, that point won't move at all, and if we can drag around
the start or end point, C will stay at the same ratio-value. For instance, if it was located midway between
start and end, it'll stay midway between start and end, even if the line segment between start and end
becomes longer or shorter.</p>
<p>We can also see that the distances for the lines <i>d1 = A-B</i> and <i>d2 = B-C</i> may vary, but the
ratio between them, <i>d1/d2</i>, is a constant value. We can drag any of the start, end, or control points
around as much as we like, but that value also stays the same.</p>
<div class="note">
<p>In fact, because the distance ratio is a fixed value for each point <i>B</i>, which we get by picking
some <i>t</i> value on our curve, the distance ratio is actually an identity function for Bézier curves.
If we were to plot all the ratio values for all possible <i>t</i> values for quadratic and cubic curves,
we'd see two very interesting functions: asymptotic at <i>t=0</i> and <i>t=1</i>, tending towards positive
infinity, with a zero-derivative minimum at <i>t=0.5</i>.</p>
<p>Since these are ratios, we can actually express the ratio values as a function of <i>t</i>. I actually
failed at coming up with the precise functions, but thanks to some help from
<a href="http://mathoverflow.net/questions/122257/finding-the-formula-for-Bézier-curve-ratios-hull-point-point-baseline">Boris
Zbarsky</a> we can see that the ratio functions are actually remarkably simple:</p>
<table style="width:100%; border:0"><tr><td>
<p>Quadratic curves:\[
ratio(t)_2 = \left | \frac{2t^2 - 2t}{2t^2 - 2t + 1} \right |
\]</p>
</td><td>
<p>Cubic curves: \[
ratio(t)_3 = \left | \frac{t^3 + (1-t)^3}{t^3 + (1-t)^3 - 1} \right |
\]</p>
</td></tr></table>
<p>Unfortunately, this trick only works for quadratic and cubic curves. Once we hit higher order curves,
things become a lot less predictable; the "fixed point <i>C</i>" is no longer fixed, moving around as we
move the control points, and projections of <i>B</i> onto the line between start and end may actually
lie on that line before the start, or after the end, and there are no simple ratios that we can exploit.</p>
</div>
<p>So, with this knowledge, let's change a curve's shape by click-dragging some part of it. The follow
graphics let us click-drag somewhere on the curve, repositioning point <i>B</i> according to a simple
rule: we keep the original point <i>B</i>'s tangent:</p>
<textarea class="sketch-code" data-sketch-preset="moulding" data-sketch-title="Moulding a quadratic Bézier curve">
void setupCurve() {
setupDefaultQuadratic();
mould();
span();
additionals();
}
void mouldCurve(BezierCurve curve, int mx, int my) {
if(Bt != -1) {
B = new Point(mx, my);
BezierCurve newcurve = comp.generateCurve(curve.order, curve.points[0], B, curve.points[curve.order], Bt);
curves.clear();
curves.add(newcurve);
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="moulding" data-sketch-title="Moulding a cubic Bézier curve">
void setupCurve() {
setupDefaultCubic();
mould();
span();
additionals();
}
void mouldCurve(BezierCurve curve, int mx, int my) {
if(Bt != -1) {
B = new Point(mx, my);
BezierCurve newcurve = comp.generateCurve(curve.order, curve.points[0], B, curve.points[curve.order], Bt, tangents);
curves.clear();
curves.add(newcurve);
}
}</textarea>

141
data/offsetting.jsx Normal file
View File

@@ -0,0 +1,141 @@
<p>Perhaps you are like me, and you've been writing various small programs that use Bézier curves in some way or another,
and at some point you make the step to implementing path extrusion. But you don't want to do it pixel based, you want to
stay in the vector world. You find that extruding lines is relatively easy, and tracing outlines is coming along nicely
(although junction caps and fillets are a bit of a hassle), and then decide to do things properly and add Bézier curves
to the mix. Now you have a problem.</p>
<p>Unlike lines, you can't simply extrude a Bézier curve by taking a copy and moving it around, because of the curvatures;
rather than a uniform thickness you get an extrusion that looks too thin in places, if you're lucky, but more likely will
self-intersect. The trick, then, is to scale the curve, rather than simply copying it. But how do you scale a Bézier curve?</p>
<p>Bottom line: <strong>you can't</strong>. So you cheat. We're not going to do true curve scaling, or rather curve
offsetting, because that's impossible. Instead we're going to try to generate 'looks good enough' offset curves.</p>
<div class="note">
<h2>"What do you mean, you can't. Prove it."</h2>
<p>First off, when I say "you can't" what I really mean is "you can't offset a Bézier curve with another
Bézier curve". not even by using a really high order curve. You can find the function that describes the
offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which
<strong>has</strong> to be a polynomial. Let's look at why this is:</p>
<p>From a mathematical point of view, an offset curve <i>O(t)</i> is a curve such that, given our original curve
<i>B(t)</i>, any point on <i>O(t)</i> is a fixed distance <i>d</i> away from coordinate <i>B(t)</i>.
So let's math that:</p>
<p>\[
O(t) = B(t) + d
\]</p>
<p>However, we're working in 2D, and <i>d</i> is a single value, so we want to turn it into a vector. If we
want a point distance <i>d</i> "away" from the curve <i>B(t)</i> then what we really mean is that we want
a point at <i>d</i> times the "normal vector" from point <i>B(t)</i>, where the "normal" is a vector
that runs perpendicular ("at a right angle") to the tangent at <i>B(t)</i>. Easy enough:</p>
<p>\[
O(t) = B(t) + d \cdot N(t)
\]</p>
<p>Now this still isn't very useful unless we know what the formula for <i>N(t)</i> is, so let's find out.
<i>N(t)</i> runs perpendicular to the original curve tangent, and we know that the tangent is simply
<i>B'(t)</i>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure
that <i>N(t)</i> has the same magnitude for every <i>t</i>, or the offset curve won't be at a uniform
distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure
<i>N(t)</i> always has length 1, which we can achieve by dividing <i>B'(t)</i> by its magnitude:</p>
<p>\[
N(t) \perp \left ( \frac{B'(t)}{\left || B'(t) \right || } \right )
\]</p>
<p>Determining the length requires computing an arc length, and this is where things get Tricky with
a capital T. First off, to compute arc length from some start <i>a</i> to end <i>b</i>, we must use
the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p>
<p>\[
\left || f(x,y) \right || = \int^b_a \sqrt{ f_x'^2 + f_y'^2}
\]</p>
<p>So if we want the length of the tangent, we plug in <i>B'(t)</i>, with <i>t = 0</i> as start and
<i>t = 1</i> as end:</p>
<p>\[
\left || B'(t) \right || = \int^1_0 \sqrt{ B_x''(t)^2 + B_y''(t)^2}
\]</p>
<p>And that's where things go wrong. It doesn't even really matter what the second derivative for <i>B(t)</i>
is, that square root is screwing everything up, because it turns our nice polynomials into things that are no
longer polynomials.</p>
<p>There is a small class of polynomials where the square root is also a polynomial, but
they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is
also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't;
remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor
in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions
stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point.
We can already create offset curves for points, we call them circles, and they have much simpler functions
than Bézier curves.</p>
<p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which
means <i>N(t)</i> won't be a polynomial, which means that <i>d</i> times <i>N(t)</i> won't be a polynomial,
which means that, ultimately, <i>O(t)</i> won't be a polynomial, which means that even if we can determine the
function for <i>O(t)</i> just fine (and that's far from trivial!), it simply cannot be represented as a
Bézier curve.</p>
<p>And that's one reason why Bézier curves are tricky: there are actually a <i>lot</i> of curves that
cannot be represent as a Bézier curve at all. They can't even model their own offset curves. They're weird
that way. So how do all those other programs do it? Well, much like we're about to do, they cheat. We're
going to approximate an offset curve in a way that will look relatively close to what the real offset
curve would look like, if we could compute it.</p>
</div>
<p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make
that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where safe means that all the
control points are always on a single side of the baseline, and the midpoint of the curve at <i>t=0.5</i> is
roughly in the centre of the polygon defined by the curve coordinates) and then point-scale those sub-curves
with respect to the curve's scaling origin (which is the intersection of the point normals at the start
and end points).</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Offsetting a quadratic Bézier curve">
void setupCurve() {
setupDefaultQuadratic();
offsetting();
offset = 20;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
if(offset>0) {
noAdditionals();
BezierCurve[] offsetCurve = curve.offset(offset);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
offsetCurve = curve.offset(-offset);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Offsetting a cubic Bézier curve">
void setupCurve() {
setupDefaultCubic();
offsetting();
offset = 20;
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
if(offset>0) {
noAdditionals();
BezierCurve[] offsetCurve = curve.offset(offset);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
offsetCurve = curve.offset(-offset);
for(BezierCurve b: offsetCurve) { b.draw(); b.getPoint(0).draw(); b.getPoint(1).draw();}
}
}</textarea>
<p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the
curve around. This is caused by the fact that we're still performing a naive form of offsetting,
moving the control points the same distance as the start and end points. If the curve is large
enough, this may still lead to incorrect offsets.</p>

42
data/pointcurves.jsx Normal file
View File

@@ -0,0 +1,42 @@
<p>Given the preceding section on curve moulding, we can also generate quadratic and cubic
curves from any three points. However, unlike circle-fitting, which requires only three points,
Bézier curve fitting requires three points, as well as a tangent and <i>t</i> value. We can
come up with "default" values, where the <i>t</i> value for our middle point is simply 0.5,
and the tangent is identical to the baseline for quadratic curves, or half the baseline for
cubic curves.</p>
<p>Using these "default" values for curve creation, we can already get fairly respectable
curves; Click three times on each of the following sketches to set up the points
that should be used to form a quadratic and cubic curve, respectively</p>
<textarea class="sketch-code" data-sketch-preset="generate" data-sketch-title="Fitting a quadratic Bézier curve">
void setupCurve() { span(); }
void drawCurve(BezierCurve curve) {
recordPoint(mouseX,mouseY);
if(p1!=null) { p1.draw(); }
if(p2!=null) { p2.draw(); }
if(p3!=null) { p3.draw(); }
if(p1!=null && p2!=null && p3!=null) {
BezierCurve c = comp.generateCurve(2, p1, p2, p3);
c.draw();
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="generate" data-sketch-title="Fitting a cubic Bézier curve">
void setupCurve() { span(); }
void drawCurve(BezierCurve curve) {
recordPoint(mouseX,mouseY);
if(p1!=null) { p1.draw(); }
if(p2!=null) { p2.draw(); }
if(p3!=null) { p3.draw(); }
if(p1!=null && p2!=null && p3!=null) {
BezierCurve c = comp.generateCurve(3, p1, p2, p3);
c.draw();
}
}</textarea>
<p>(There are many ways to determine a combination of <i>t</i> and tangent values that lead
to a more "aesthetic" curve, but this will be left as an exercise, since there are too many,
and aesthetics are a personal choice)</p>

134
data/pointvectors.jsx Normal file
View File

@@ -0,0 +1,134 @@
<p>If you want to move objects along a curve, or "away from" a curve, the two vectors you're most interested
in are the tangent vector and normal vector for curve points. These are actually really easy to find. For
moving, and orienting, along a curve we use the tangent, which indicates the direction travel at specific
points, and is literally just the first derivative of our curve:</p>
<p>\[
\left \{ \begin{matrix}
tangent_x(t) = B'_x(t) \\
tangent_y(t) = B'_y(t)
\end{matrix} \right. \]</p>
<p>This gives us the directional vector we want. We can normalize it to give us uniform directional vectors
(having a length of 1.0) at each point, and then do whatever it is we want to do based on those directions:</p>
<p>\[
d = || tangent(t) || = \sqrt{B'_x(t)^2 + B'_y(t)^2}
\]</p>
<p>\[
\left \{ \begin{matrix}
\hat{x}(t) = || tangent_x(t) ||
=\frac{tangent_x(t)}{ || tangent(t) || }
= \frac{B'_x(t)}{d} \\
\hat{y}(t) = || tangent_y(t) ||
= \frac{tangent_y(t)}{ || tangent(t) || }
= \frac{B'_y(t)}{d}
\end{matrix} \right. \]</p>
<p>The tangent is very useful for moving along a line, but what if we want to move away from the curve instead,
perpendicular to the curve at some point <i>t</i>? In that case we want the "normal" vector. This vector runs
at a right angle to the direction of the curve, and is typically of length 1.0, so all we have to do is rotate
the normalized directional vector and we're done:</p>
<p>\[
\left \{ \begin{array}{l}
normal_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\
normal_y(t) = \underset{quarter\ circle\ rotation} {\underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }} = \hat{x}(t)
\end{array} \right. \]</p>
<div class="note">
<p>Rotating coordinates is actually very easy, if you know the rule for it. You might find
it explained as "applying a <a href="https://en.wikipedia.org/wiki/Rotation_matrix">rotation matrix</a>",
which is what we'll look at here, too. Essentially, the idea is to take the circles over
which we can rotate, and simply "sliding the coordinates" over those circles by the desired
angle. If we want a quarter circle turn, we take the coordinate, slide it along the cirle
by a quarter turn, and done.</p>
<p>To turn any point <i>(x,y)</i> into a rotated point <i>(x',y')</i> (over 0,0) by
some angle φ, we apply this nicely easy computation:</p>
<p>\[\begin{array}{l}
x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\
y' = x \cdot \sin(\phi) + y \cdot \cos(\phi)
\end{array}\]</p>
<p>Which is the "long" version of the following matrix transformation:</p>
<p>\[
\begin{bmatrix}
x' \\ y'
\end{bmatrix}
=
\begin{bmatrix}
\cos(\phi) & -\sin(\phi) \\
\sin(\phi) & \cos(\phi)
\end{bmatrix}
\begin{bmatrix}
x \\ y
\end{bmatrix}
\]</p>
<p>And that's all we need to rotate any coordinate. Note that for quarter, half
and three quarter turns these functions become even easier, since <i>sin</i> and
<i>cos</i> for these angles are, respectively: 0 and 1, -1 and 0, and 0 and -1.</p>
<p>But <strong><em>why</em></strong> does this work? Why this matrix multiplication?
<a href="http://en.wikipedia.org/wiki/Rotation_matrix#Decomposition_into_shears">wikipedia</a>
(Technically, Thomas Herter and Klaus Lott) tells us that a rotation matrix can be
treated as a sequence of three (elementary) shear operations. When we combine this into
a single matrix operation (because all matrix multiplications can be collapsed), we get
the matrix that you see above.
<a href="http://datagenetics.com/blog/august32013/index.html">DataGenetics</a> have an
excellent article about this very thing: it's really quite cool, and I strongly recommend
taking a quick break from this primer to read that article.</p>
</div>
<p>The following two graphics show the tangent and normal along a quadratic and cubic curve, with
the direction vector coloured blue, and the normal vector coloured red.</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Quadratic Bézier tangents and normals">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
curve.draw(color(0,40));
float t, d = 15;
Point p, tg, n;
for(int i=0; i<=10; i++) {
t = i/10.0;
p = curve.getPoint(t);
tg = curve.getDerivativePoint(t).normalize();
n = curve.getNormal(t);
stroke(0,0,255);
line(p.x, p.y, p.x+d*tg.x, p.y+d*tg.y);
stroke(200,0,0);
line(p.x, p.y, p.x+d*n.x, p.y+d*n.y);
stroke(0);
ellipse(p.x, p.y, 5, 5);
}
}</textarea>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Cubic Bézier tangents and normals">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
curve.draw(color(0,40));
float t, d = 15;
Point p, tg, n;
for(int i=0; i<=10; i++) {
t = i/10.0;
p = curve.getPoint(t);
tg = curve.getDerivativePoint(t).normalize();
n = curve.getNormal(t);
stroke(0,0,255);
line(p.x, p.y, p.x+d*tg.x, p.y+d*tg.y);
stroke(200,0,0);
line(p.x, p.y, p.x+d*n.x, p.y+d*n.y);
stroke(0);
ellipse(p.x, p.y, 5, 5);
}
}</textarea>

110
data/polybezier.jsx Normal file
View File

@@ -0,0 +1,110 @@
<p>Much like lines can be chained together to form polygons, Bézier curves can be chained together
to form poly-Béziers, and the only trick required is to make sure that: A) the end point of each
section is the starting point of the following section, and B) the derivatives across that
dual point line up. Unless, of course, you want discontinuities; then you don't even need (B).</p>
<p>We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind
that enforces "the outgoing derivative is the same as the incoming derivative" across sections:</p>
<p>\[
B'(1)_n = B'(0)_{n+1}
\]</p>
<p>We can actually guarantee this really easily, because we know that the vector from a curve's
last control point to its last on-curve point is equal to the derivative vector. If we want to
ensure that the first control point of the next curve matches that, all we have to do is mirror
that last control point through the last on-curve point. And mirroring any point A through any
point B is really simple:</p>
<p>\[
Mirrored = (B_x + (B_x - A_x),\ B_y + (B_y - A_y) = (2B_x - A_x,\ 2B_y - A_y)
\]</p>
<p>So let's implement that and see what it gets us. The following two graphics show a quadratic
and a cubic poly-Bézier curve; both consist of multiple sub-curves, but because of our constraint,
not all points on the curves can be moved around freely. Some points, when moved, will move other
points by virtue of changing the curve across sections.</p>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="Forming a quadratic poly-Bézier">
void setupCurve() {
setupDefaultQuadraticPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointConstrained(pt, mx, my);
}</textarea>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="Forming a cubic poly-Bézier">
void setupCurve() {
setupDefaultCubicPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointConstrained(pt, mx, my);
}</textarea>
<p>As you can see, quadratic curves are particularly ill-suited for poly-Bézier curves, as all
the control points are effectively linked. Move one of them, and you move all of them. This means
that we cannot use quadratic poly-Béziers for anything other than really, really simple shapes.
And even then, they're probably the wrong choice. Cubic curves are pretty decent, but the fact
that the derivatives are linked means we can't manipulate curves as well as we might if we
relaxed the constraints a little.</p>
<p>So: let's relax them!</p>
<p>We can change the constraint so that we still preserve the angle of the derivatives across
sections (so transitions from one section to the next will still look natural), but give up
the requirement that they should also have the same vector length. Doing so will give us
a much more a useful kind of poly-Bézier curve:</p>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="A half-constrained quadratic poly-Bézier">
void setupCurve() {
setupDefaultQuadraticPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointHalfConstrained(pt, mx, my);
}</textarea>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="A half-constrained cubic poly-Bézier">
void setupCurve() {
setupDefaultCubicPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointHalfConstrained(pt, mx, my);
}</textarea>
<p>Quadratic curves are still silly, but cubic curves are now much more controllable.</p>
<p>If we want even more control, we could just abandon the derivative constraints entirely,
and simply assure that the end point of one section is the same as the start point of the next section,
and then keep it at that. This gives us the greatest degree of freedom when it comes to modelling
shapes, but also means that our poly-Bézier constructs are no longer continuous curves. Sometimes
this is exactly what you want (because it lets you add corners to a shape, while still only using
Bézier curves).</p>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="An unconstrained quadratic poly-Bézier">
void setupCurve() {
setupDefaultQuadraticPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePoint(pvt, mx, my);
}</textarea>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="An unconstrained cubic poly-Bézier">
void setupCurve() {
setupDefaultCubicPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePoint(pvt, mx, my);
}</textarea>
<p>When doing any kind of modelling, you generally don't want a poly-Bézier that will only let you
pick one of the three forms for all your points; most graphics applications that deal with Bézier
curves will actually let you pick, per on-curve point, how to deal with the control points around it:
fully constrained, loosely constrained, or completely unconstrained. The best shape modelling comes
from having a curve that will let you pick what you need, when you need it, without having to start
a new poly-Bézier curve.</p>

57
data/projections.jsx Normal file
View File

@@ -0,0 +1,57 @@
<p>Say we have a Bézier curve and some point, not on the curve, of which we want to know which
<i>t</i> value on the curve gives us an on-curve point closest to our off-curve point. Or: say
we want to find the projection of a random point onto a curve. How do we do that?</p>
<p>If the Bézier curve is of low enough order, we might be able to <a href="http://jazzros.blogspot.ca/2011/03/projecting-point-on-bezier-curve.html">work out the maths for how
to do this</a>, and get a perfect <i>t</i> value back, but in general this is an incredibly hard
problem and the easiest solution is, really, a numerical approach again. We'll be finding our
ideal <i>t</i> value using a <a href="https://en.wikipedia.org/wiki/Binary_search_algorithm">binary
search</a>. First, we do a coarse distance-check based on <i>t</i> values associated with the
curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run
this algorithm:</p>
<ol>
<li>with the <i>t</i> value we found, start with some small interval around <i>t</i>
(1/length_of_LUT on either side is a reasonable start),</li>
<li>if the distance to <i>t ± interval/2</i> is larger than the distance to <i>t</i>,
try again with the interval reduced to half its original length.</li>
<li>if the distance to <i>t ± interval/2</i> is smaller than the distance to <i>t</i>,
replace <i>t</i> with the smaller-distance value.</li>
<li>after reducing the interval, or changing <i>t</i>, go back to step 1.</li>
</ol>
<p>We keep repeating this process until the interval is small enough to claim the difference
in precision found is irrelevant for the purpose we're trying to find <i>t</i> for. In this
case, I'm arbitrarily fixing it at 0.0001.</p>
<p>The following graphic demonstrates the result of this procedure.Simply move the cursor
around, and if it does not lie on top of the curve, you will see a line that projects the
cursor onto the curve based on an iteratively found "ideal" <i>t</i> value.</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Projecting a point onto a Bézier curve">
void setupCurve() {
int d = dim - 2*pad;
int order = 10;
int[] c = {248,188, 218,294, 45,290, 12,236, 14,82, 186,177, 221,90, 18,156, 34,57, 198,18};
Point[] points = new Point[c.length/2];
for(int i=0, e=c.length; i<e; i+=2) {
points[i/2] = new Point(c[i], c[i+1]);
}
curves.add(new BezierCurve(points));
redrawOnMove();
}
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
if(curve.over(mouseX,mouseY) == -1) {
float t = curve.getPointProjection(new Point(mouseX, mouseY));
Point p = curve.getPoint(t);
stroke(255,0,0);
line(mouseX, mouseY, p.x, p.y);
fill(150,0,0);
text("t " + (int(1000*t)/1000.0), p.x+10, p.y);
text("p: "+mouseX+"/"+mouseY, mouseX+10, mouseY);
}
}</textarea>

54
data/reordering.jsx Normal file
View File

@@ -0,0 +1,54 @@
<p>One interesting property of Bézier curves is that an <i>n<sup>th</sup></i> order curve can
always be perfectly represented by an <i>(n+1)<sup>th</sup></i> order curve, by giving the
higher order curve specific control points.</p>
<p>If we have a curve with three points, then we can create a four point curve that exactly
reproduce the original curve as long as we give it the same start and end points, and for
its two control points we pick "1/3<sup>rd</sup> start + 2/3<sup>rd</sup> control" and
"2/3<sup>rd</sup> control + 1/3<sup>rd</sup> end", and now we have exactly the same curve as
before, except represented as a cubic curve, rather than a quadratic curve.</p>
<p>The general rule for raising an <i>n<sup>th</sup></i> order curve to an <i>(n+1)<sup>th</sup></i>
order curve is as follows (observing that the start and end weights are the same as the start and
end weights for the old curve):</p>
<p>\[
Bézier(k,t) = \sum_{i=0}^{k}
\underset{binomial\ term}{\underbrace{\binom{k}{i}}}
\cdot\
\underset{polynomial\ term}{\underbrace{(1-t)^{k-i} \cdot t^{i}}}
\ \cdot \
\underset{new\ weights}{\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}}
\ ,\ with\ k = n+1
\]</p>
<p>However, this rule also has as direct consequence that you <strong>cannot</strong> generally
safely lower a curve from <i>n<sup>th</sup></i> order to <i>(n-1)<sup>th</sup></i> order, because
the control points cannot be "pulled apart" cleanly. We can try to, but the resulting curve will
not be identical to the original, and may in fact look completely different.</p>
<p>We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch
and press your up and down cursor keys to elevate or lower the curve order.</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="A tenth order Bézier curve">
void setupCurve() {
int d = dim - 2*pad;
int order = 10;
ArrayList<Point> pts = new ArrayList<Point>();
float dst = d/2.5, nx, ny, a=0, step = 2*PI/order, r;
for(a=0; a<2*PI; a+=step) {
r = random(-dst/4,dst/4);
pts.add(new Point(d/2 + cos(a) * (r+dst), d/2 + sin(a) * (r+dst)));
dst -= 1.2;
}
Point[] points = new Point[pts.size()];
for(int p=0,last=points.length; p<last; p++) { points[p] = pts.get(p); }
curves.add(new BezierCurve(points));
reorder();
}
void drawCurve(BezierCurve curve) {
curve.draw();
}</textarea>

149
data/shapes.jsx Normal file
View File

@@ -0,0 +1,149 @@
<p>We can apply the topics covered so far in this primer to effect boolean shape operations:
getting the union, intersection, or exclusion, between two or more shapes that involve Bézier
curves. For simplicity (well.. sort of, more homogeneity), we'll be looking at Poly-Bézier
shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a
simplification (although it does mean we need to write a definition for the class of shapes
that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using
those in the following examples)</p>
<p>The procedure for performing boolean operations consists, broadly, of four steps:</p>
<ol>
<li>Find the intersection points between both shapes,</li>
<li>cut up the shapes into multiple sections between these intersections,</li>
<li>discard any section that isn't part of the desired operation's resultant shape, and</li>
<li>link up the remaining sections to form the new shape.</li>
</ol>
<p>Finding all intersections between two poly-Bézier curves, or any poly-line-section shape,
is similar to the iterative algorithm discussed in the section on curve/curve intersection.
For each segment in the poly-Bézier curve we check whether its bounding box overlaps with
any of the segment bounding boxes in the other poly-Bézier curve. If so, we run normal
intersection detection.</p>
<p>After we found all intersection points, we split up our poly-Bézier curves, making sure to
record which of the newly formed poly-Bézier curves might potentially link up at the points
we split the originals up at. This will let us quickly glue poly-Bézier curves back together
after the next step.</p>
<p>Once we have all the new poly-Bézier curves, we run the first step of the desired boolean
operation.</p>
<ul>
<li>Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if
we want the union of two overlapping circles, the resulting shape is the outline.</li>
<li>Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the
two shapes. E.g. if we want the intersection of two overlapping circles, the resulting
shape is the tapered ellipse where they overlap.</li>
<li>Exclusion: none of the sections are discarded, but we will need to link the shapes back
up in a special way. Flip any section that would qualify for removal under UNION rules.</li>
</ul>
<table class="sketch"><tr><td class="labeled-image">
<img src="images/op_base.gif" height="169px">
<p>Two overlapping shapes.</p>
</td><td class="labeled-image">
<img src="images/op_union.gif" height="169px">
<p>The unified region.</p>
</td><td class="labeled-image">
<img src="images/op_intersection.gif" height="169px">
<p >Their intersection.</p>
</td><td class="labeled-image">
<img src="images/op_exclusion.gif" height="169px">
<p>Their exclusion regions.</p>
</td></tr></table>
<p>The main complication in the outlined procedure here is determining how sections qualify
in terms of being "inside" and "outside" of our shapes. For this, we need to be able to
perform point-in-shape detection, for which we'll use a classic algorithm: getting the
"crossing number" by using ray casting, and then testing for "insidedness" by applying
the <a href="http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html">even-odd
rule</a>: For any point and any shape, we can cast a ray from our point, to some point that we know
lies outside of the shape (such as a corner of our drawing surface). We then count how many
times that line crosses our shape (remember that we can perform line/curve intersection
detection quite easily). If the number of times it crosses the shape's outline is even,
the point did not actually lie inside our shape. If the number of intersections is odd,
our point did lie inside out shape. With that knowledge, we can decide whether to treat
a section that such a point lies on "needs removal" (under union rules), "needs preserving"
(under intersection rules), or "needs flipping" (under exclusion rules).</p>
<p>Applying this rule in a simple setting, the following sketch shows whether your cursor's
coordinate is considered "inside" or "outside" the given outline shape. Try changing the
outline's shape to see what happens w.r.t. "insidedness", particularly by introducing
enclosed regions or self-intersecting loops.</p>
<textarea class="sketch-code" data-sketch-preset="poly" data-sketch-title="Testing 'insidedness'">
void setupCurve() {
setupDefaultCubicPoly();
int pad = dim/3;
float k = 0.55228;
PolyBezierCurve p = polycurves.get(0);
p.addCurve(new BezierCurve(new Point[]{
ORIGIN,
ORIGIN,
new Point(dim/2-0.55228*pad, dim/2+pad),
new Point(dim/2, dim/2+pad)
}));
p.close();
}
void handleMouseMoved(PolyBezierCurve p, int mx, int my) {
int cross = p.getCrossingNumber(new Point(mx, my), new Point(0,0));
background(255);
stroke(0,50);
line(mx,my,0,0);
if(cross%2==0) {
p.draw(color(255,0,0));
} else {
p.draw(color(0,255,0));
}
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
handleMouseMoved(p, mx, my);
p.movePoint(pvt, mx, my);
}
</textarea>
<p>So, using this approach we can easily detect which parts of a shape to keep, and which to reject.
After pruning, we perform the last step: link up all the remaining sections. The following sketch
shows two shapes as well as the result of applying a union (middle) and intersection (right) operation
to the pair.</p>
<textarea class="sketch-code" data-sketch-preset="shapes" data-sketch-title="Performing Boolean shape operations">
void setupCurve() {
setupDefaultShapes();
fill(0);
textAlign(CENTER);
}
void drawShapes() {
p1.draw(color(200,0,0));
p2.draw(color(0,200,0));
text("shapes (took "+timeTaken()+"ms to set up)", dim/2, dim-10);
nextPanel();
stroke(0);
line(0,0,0,dim);
mark();
PolyBezierCurve union = bcomp.getUnion();
text("shape union (formed in "+timeTaken()+"ms)", dim/2, dim-10);
union.draw(color(0,255,0), true);
nextPanel();
stroke(0);
line(0,0,0,dim);
mark();
PolyBezierCurve intersection = bcomp.getIntersection();
text("shape intersection (formed in "+timeTaken()+"ms)", dim/2, dim-10);
intersection.draw(color(255,0,0), true);
}</textarea>
<div class="note">
This sketch is still being fixed up so that it's interactive. While boolean operations are
not inherently expensive operations (they scale linearly, so the more segments, the longer
it'll take), but setting up the intersection resolver takes fairly long. I will try to bring
it down to a reasonable amount of time (on my not-new-but-not-exactly-poor i7 950, it takes
900ms to set up the intersection resolver... that's at least 4x too slow for interaction).
So, for now, this sketch is unfortunately, and with apologies not interactive.
</div>

64
data/splitting.jsx Normal file
View File

@@ -0,0 +1,64 @@
<p>With de Casteljau's algorithm we also find all the points we need to split up a Bézier curve into two, smaller
curves, which taken together form the original curve. When we construct de Casteljau's skeleton for some value
<i>t</i>, the procedure gives us all the points we need to split a curve at that <i>t</i> value: one curve is defined
by all the inside skeleton points found prior to our on-curve point, with the other curve being defined by all the
inside skeleton points after our on-curve point.</p>
<div class="howtocode">
<h3>implementing curve splitting</h3>
<p>We can implement curve splitting by bolting some extra logging onto the de Casteljau function:</p>
<pre>left=[]
right=[]
function drawCurve(points[], t):
if(points.length==1):
left.add(points[0])
right.add(points[0])
draw(points[0])
else:
newpoints=array(points.size-1)
for(i=0; i&lt;newpoints.length; i++):
if(i==0):
left.add(points[i])
if(i==newpoints.length-1):
right.add(points[i+1])
newpoints[i] = (1-t) * points[i] + t * points[i+1]
drawCurve(newpoints, t)</pre>
<p>After running this function for some value <i>t</i>, the <i>left</i> and <i>right</i> arrays
will contain all the coordinates for two new curves - one to the "left" of our <i>t</i> value,
the other on the "right", of the same order as the original curve, and overlayed exactly on the
original curve.</p>
</div>
<p>This is best illustrated with an animated graphic:</p>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Bézier curve splitting. Curve order can be lowered/elevated.">
void setupCurve() {
setupDefaultCubic();
reorder();
animate();
pause();
span();
}
void drawCurve(BezierCurve curve) {
curve.draw();
Point p = curve.getPoint(t);
ellipse(p.x, p.y, 5, 5);
drawSpan(curve, t);
BezierCurve[] segments = curve.split(t);
usePanelPadding();
nextPanel();
drawAxes("first curve x",0,panelDim, "first\ncurve\ny",0,panelDim);
drawSpan(curve, t);
segments[0].draw();
nextPanel();
drawAxes("second curve x",0,panelDim, "second\ncurve\ny",0,panelDim);
drawSpan(curve, t);
segments[1].draw();
}</textarea>

31
data/tightbounds.jsx Normal file
View File

@@ -0,0 +1,31 @@
<p>With our knowledge of bounding boxes, and curve alignment, We can now form the "tight" bounding box for
curves. We first align our curve, recording the translation we performed, "T", and the rotation angle we
used, "R". We then determine the aligned curve's normal bounding box. Once we have that, we can map that
bounding box back to our original curve by rotating it by -R, and then translating it by -T. We now have
nice tight bounding boxes for our curves:</p>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Tight quadratic Bézier bounding box">
void setupCurve() {
setupDefaultQuadratic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
drawBoundingBox(curve.generateTightBoundingBox());
}</textarea>
<textarea class="sketch-code" data-sketch-preset="simple" data-sketch-title="Tight cubic Bézier bounding box">
void setupCurve() {
setupDefaultCubic();
}
void drawCurve(BezierCurve curve) {
curve.draw();
drawBoundingBox(curve.generateTightBoundingBox());
}</textarea>
<p>These are, strictly speaking, not necessarily the tightest possible bounding boxes. It is possible to compute
the optimal bounding box by determining which spanning lines we need to effect a minimal box area, but because
of the parametric nature of Bézier curves this is actually a rather costly operation, and the gain in bounding
precision is often not worth it. If there is high demand for it, I'll add a section on how to precisely compute
the best fit bounding box, but the maths is fairly gruelling and just not really worth spending time on.</p>

170
data/tracing.jsx Normal file
View File

@@ -0,0 +1,170 @@
<p>Say you want to draw a curve with a dashed line, rather than a solid line,
or you want to move something along the curve at fixed distance intervals over
time, like a train along a track, and you want to use Bézier curves. Now you
have a problem.</p>
<p>The reason you have a problem is that Bézier curves are parametric functions
with non-linear behaviour, whereas moving a train along a track is about as
close to a practical example of linear behaviour as you can get. The problem
we're faced with is that we can't just pick <i>t</i> values at some fixed interval
and expect the Bézier functions to generate points that are spaced a fixed distance
apart. In fact, let's look at the relation between "distance long a curve" and
"<i>t</i> value", by plotting them against one another.</p>
<p>The following graphic shows a particularly illustrative curve, and it's length/<i>t</i>
plot. While the curve itself is a cubic curve, the plot shows that the distance function
along the curve is actually a function of a much higher order than the curve itself.</p>
<textarea class="sketch-code" data-sketch-preset="twopanel" data-sketch-title="The t-for-distance function">
HashMap<Float,Float> values = new HashMap<Float,Float>();
ArrayList<Float> keys = new ArrayList<Float>();
void setupCurve() {
curves.add(new BezierCurve( new Point[]{
new Point(150,140),
new Point(215,35),
new Point(25,275),
new Point(145,160)
}));
}
float lD=0, D, d, x, y;
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
D = curve.getCurveLength();
noAdditionals();
usePanelPadding();
nextPanel();
drawAxes("t",0,1, "d",0,D);
if(lD!=D) {
lD = D;
values.clear();
keys.clear();
for(float t=0.001; t<=1.0; t+=0.001) {
d = comp.getArcLength(t, curve.x_values, curve.y_values);
x = map(t,0,1,0,panelDim);
y = map(d,0,D,0,panelDim);
values.put(x,y);
keys.add(x);
}
}
stroke(100);
for(float x: keys) {
point(x, values.get(x));
}
}</textarea>
<p>We see a function that might be invertible, but we won't be able to do so, symbolically.
You may remember from the section on arc length that we cannot actually compute the true
arc length function as an expression of <i>t</i>, which means we also can't compute the true
inverted function that gives <i>t</i> as an expression of length. So how do we fix this?</p>
<p>One way is to do what the graphic does: simply run through the curve, determine its
<i>t</i>-for-length values as a set of discrete values at some high resolution (the graphic
uses 1000 discrete points), and then use those as a basis for finding an appropriate <i>t</i>
value, given a distance along the curve. This works quite well, actually, and is fairly fast
(you can move the curve around without noticeable lag on a 2 year old computer, for instance).</p>
<p>We can use some colour to show the difference between distance-based and time based intervals:
the following graph is similar to the previous one, except it segments the curve in terms of
equal-distance intervals. This shows as regular colour intervals going down the graph, but
the mapping to <i>t</i> values is not linear, so there will be (highly) irregular intervals
along the horizontal axis. It also shows the curve in an alternating colouring based on the
t-for-distance values we find our LUT:</p>
<textarea class="sketch-code" data-sketch-preset="threepanel" data-sketch-title="Fixed-interval coloring a curve">
HashMap<Float,Float> values = new HashMap<Float,Float>();
HashMap<Float,Float> inverted = new HashMap<Float,Float>();
ArrayList<Float> t_keys = new ArrayList<Float>();
ArrayList<Float> d_keys = new ArrayList<Float>();
void setupCurve() {
curves.add(new BezierCurve( new Point[]{
new Point(150,140),
new Point(215,35),
new Point(25,275),
new Point(145,160)
}));
}
float lD=0, D, d, x, y;
void drawCurve(BezierCurve curve) {
additionals();
curve.draw();
D = curve.getCurveLength();
float section = 15;
noAdditionals();
usePanelPadding();
nextPanel();
drawAxes("t",0,1, "d",0,D);
if(lD!=D) {
lD = D;
// clear everything
values.clear();
inverted.clear();
t_keys.clear();
d_keys.clear();
// rebuild;
for(float t=0.001; t<=1.0; t+=0.001) {
d = comp.getArcLength(t, curve.x_values, curve.y_values);
values.put(t,d);
inverted.put(d,t);
t_keys.add(t);
d_keys.add(d);
}
}
float x, y, ly=0;
ArrayList<Float> markers = new ArrayList<Float>();
color lc=0, c;
for(float d: d_keys) {
y = int(map(d,0,D,0,panelDim));
x = map(inverted.get(d),0,1,0,panelDim);
if(y%section==0 && ly != y) {
ly = y;
stroke(0,0,200,100);
line(0,y,x,y);
stroke(0,100,0,100);
line(x,0,x,y);
markers.add(inverted.get(d));
}
}
stroke(0);
for(float t: t_keys) {
x = map(t,0,1,0,panelDim);
y = map(values.get(t),0,D,0,panelDim);
point(x, y);
}
nextPanel();
additionals();
curve.draw();
noAdditionals();
for(int m=1, last=markers.size(); m<last; m++) {
BezierCurve segment = curve.split(markers.get(m-1), markers.get(m));
c = (m%2==0 ? color(0,255,120) : color(0,0,255));
segment.draw(c);
}
}</textarea>
<p>However, are there better ways? One such way is discussed in "<a href="http://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf"
>Moving Along a Curve with Specified Speed</a>" by David Eberly of Geometric Tools, LLC, but
basically because we have no explicit length function (or rather, one we don't have to
constantly compute for different intervals), you may simply be better off with a traditional
lookup table (LUT).</p>
<!--
<p>That said, this is an area of Bézier curves that I've not really investigated in any
depth, so if you think I'm missing something obvious and think I should have a look at
[<i>whatever thing you think I need to have a look at</i>], get in touch and I shall
try to update this section with new information.</p>
-->

BIN
images/ajax-loader.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/arc-c-2pi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
images/arc-c-pi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
images/arc-c-pi2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
images/arc-q-pi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
images/arc-q-pi2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
images/arc-q-pi4.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
images/gq.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/lp.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/op_base.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
images/op_exclusion.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
images/op_intersection.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
images/op_union.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
images/paper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
images/ribbon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/trans.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
images/wi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

53
index.html Normal file
View File

@@ -0,0 +1,53 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>A Primer on Bézier Curves</title>
<!-- opengraph information -->
<meta property="og:title" content="A Primer on Bézier Curves">
<meta property="og:type" content="text">
<meta property="og:url" content="http://pomax.github.io/bezierinfo">
<meta property="og:description" content="A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them.">
<meta property="og:locale" content="en_GB">
<meta property="og:type" content="article">
<meta property="og:published_time" content="2013-06-13 12:00:00">
<meta property="og:author" content="Mike 'Pomax' Kamermans">
<meta property="og:section" content="Bézier Curves">
<meta property="og:tag" content="Bézier Curves">
<!-- MathJax for beautiful LaTeX functions -->
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
displayAlign: "left",
displayIndent: "2em",
TeX: {
extensions: ["color.js"]
},
skipStartupTypeset: true
});
</script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
</head>
<body>
<!-- This page lives on github -->
<img id="ribbonimg" src="images/ribbon.png" alt="This page on GitHub" border=0 usemap="#githubmap">
<map name="githubmap">
<area shape="poly" coords="30,0, 200,0, 200,114" href="http://github.com/pomax/bezierinfo" alt="This page on GitHub">
</map>
<article>
<header>
<h1>A Primer on Bézier Curves</h1>
</header>
<!-- React does its magic here -->
<div id="article"></div>
</article>
<script src="article.js"></script>
</body>
</html>

52
lib/latex-loader.js Normal file
View File

@@ -0,0 +1,52 @@
var op = "\\[";
var ed = "\\]";
/**
* Is there going to be anything to convert here?
*/
function hasLaTeX(input) {
return input.indexOf(op) + input.indexOf(ed) >= 0;
}
/**
* We look for MathJax/KaTeX style data, and make sure
* it is escaped properly so that JSX conversion will
* still work.
*/
function escapeBlockLaTeX(source) {
// we can't do this with regexp, unfortunately.
var from = 0, curr, term;
var newsource = "", latex;
for(curr = source.indexOf(op, from); curr !== -1; from = term + ed.length, curr = source.indexOf(op, from)) {
newsource += source.substring(from, curr);
term = source.indexOf(ed, from);
if(term === -1) {
// that's a problem...
throw new Error("improperly closed LaTeX encountered!");
}
latex = source.substring(curr, term + ed.length);
latex = latex.replace(/([{}])/g,"{'$1'}");
newsource += latex;
}
return newsource + source.substring(from);
}
/**
* ...
*/
function colorPreProcess(input) {
var regexp = new RegExp("([A-Z]+)\\[([^\\]]+)\\]",'g');
var output = input.replace(regexp, function(_,color,content) {
if(content.indexOf(" ")!==-1) { content = " " + content; }
return "{\\color{"+color.toLowerCase()+"}"+content.replace(/ /g,"\\ ")+"}";
});
return output;
};
module.exports = function(source) {
this.cacheable();
if (!hasLaTeX(source)) return source;
return escapeBlockLaTeX(colorPreProcess(source));
};

36
lib/pre-loader.js Normal file
View File

@@ -0,0 +1,36 @@
/**
* <pre> should preserve newlines. Damnit, JSX
*/
var op = "<pre>";
var ed = "</pre>";
function hasTokens(input) {
return input.indexOf(op) + input.indexOf(ed) >= 0;
}
function fixPreBlocks(source) {
// we can't do this with regexp, unfortunately.
var from = 0, curr, term;
var newsource = "", pre;
for(curr = source.indexOf(op, from); curr !== -1; from = term + ed.length, curr = source.indexOf(op, from)) {
newsource += source.substring(from, curr);
term = source.indexOf(ed, from);
if(term === -1) {
// that's a problem...
throw new Error("improperly closed LaTeX encountered!");
}
pre = source.substring(curr, term + ed.length);
pre = pre.replace(/\n/g,"{'\\n'}");
newsource += pre;
}
return newsource + source.substring(from);
}
module.exports = function(source) {
this.cacheable();
if (!hasTokens(source)) return source;
return fixPreBlocks(source);
};

55
package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "bezierinfo",
"version": "1.0.0",
"description": "pomax.github.io/bezierinfo",
"main": "build.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --prod",
"start": "webpack-dev-server --progress --colors --hot --inline"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Pomax/bezierinfo.git"
},
"keywords": [
"Bezier",
"curves",
"tutorial",
"article",
"primer"
],
"author": "Pomax",
"license": "SEE LICENSE IN LICENSE.md",
"bugs": {
"url": "https://github.com/Pomax/bezierinfo/issues"
},
"homepage": "https://github.com/Pomax/bezierinfo#readme",
"dependencies": {},
"devDependencies": {
"babel-core": "^6.3.17",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"bezier-js": "^1.0.8",
"css-loader": "^0.23.0",
"file-loader": "^0.8.5",
"jsmin": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"less": "^2.5.3",
"less-loader": "^2.2.2",
"less-watch-compiler": "^1.1.4",
"mocha": "^2.3.4",
"nunjucks": "^2.2.0",
"raw-loader": "^0.5.1",
"react": "^0.14.3",
"react-component-visibility": "0.0.8",
"react-dom": "^0.14.3",
"should": "^8.0.1",
"sinon": "^1.17.2",
"style-loader": "^0.13.0",
"uglify-loader": "^1.3.0",
"webpack": "^1.12.9",
"webpack-dev-server": "^1.14.0"
}
}

1
stylesheets/print.css Normal file
View File

@@ -0,0 +1 @@
html,body{margin:0;padding:0}body{background:url('images/paper.png')}header,section,footer{width:100%;margin:0}header{font-size:125%}#ribbonimg{display:none}article{width:100%;background:rgba(255,255,255,0.74)}section p{text-align:justify}section h2:before{content:"§" attr(data-num) " — "}section *+h2:before{content:""}section * h2:before{content:""}.sketch-code{display:inline-block;unicode-bidi:embed;font-family:monospace;white-space:pre;border:1px solid black;min-height:300px}.sketch{display:inline-block;border:1px solid #DDD;padding:5px;font-style:italic;font-size:80%;background:#fafacc}.sketch-title span{color:#0000c8;cursor:pointer}div.note{font-size:90%;margin:1em 2em;padding:1em;border:2px solid black}div.note *{margin:0;padding:0}div.note p{margin:1em 0}div.note div.MathJax_Display{margin:1em 0}td{display:block;border:1px solid gray;padding:1em}td+td{margin-top:1em}td p{text-align:center}a,a:visited{color:#000;text-decoration:none}span.ribbon{display:none}#navbar{display:none}#scriptblock{display:none}.viewsource{display:none}button{background:white;border:1px solid black}#article-notes,#issues,#comments{display:none}

82
stylesheets/print.less Normal file
View File

@@ -0,0 +1,82 @@
html, body {
margin: 0;
padding: 0;
}
body {
background: url('images/paper.png');
}
header, section, footer {
width: 100%;
margin: 0;
}
header { font-size: 125%; }
#ribbonimg { display: none; }
article {
width: 100%;
background: rgba(255, 255, 255, 0.74);
}
section p {
text-align: justify;
}
section h2:before {
content: "§" attr(data-num) " — ";
}
section *+h2:before {
content: "";
}
section * h2:before {
content: "";
}
.sketch-code {
display: inline-block;
unicode-bidi: embed;
font-family: monospace;
white-space: pre;
border: 1px solid black;
min-height: 300px;
}
.sketch {
display: inline-block;
border: 1px solid #DDD;
padding: 5px;
font-style: italic;
font-size: 80%;
background: rgb(250,250,204);
}
.sketch canvas {
/*background: url(attr('data-print-image'));*/
}
.sketch-title span { color: rgb(0,0,200); cursor: pointer; }
div.note {
font-size: 90%;
margin: 1em 2em;
padding: 1em;
border: 2px solid black;
}
div.note * { margin: 0; padding: 0; }
div.note p { margin: 1em 0; }
div.note div.MathJax_Display { margin: 1em 0; }
td { display: block; border: 1px solid gray; padding: 1em; }
td+td { margin-top: 1em;}
td p { text-align:center; }
a, a:visited { color: #000; text-decoration: none; }
span.ribbon { display: none; }
#navbar { display: none; }
#scriptblock { display: none; }
.viewsource { display: none; }
button { background: white; border: 1px solid black;}
#article-notes, #issues, #comments { display:none; }

1
stylesheets/style.css Normal file
View File

@@ -0,0 +1 @@
html,body{margin:0;padding:0}body{background:url('/images/paper.png')}header,section,footer{width:960px;margin:0 auto}header{font-size:125%}article{width:960px;margin:auto;background:rgba(255,255,255,0.74);border:solid rgba(255,0,0,0.35);border-width:0;border-left-width:1px;padding:1em;box-shadow:25px 0 25px 25px rgba(255,255,255,0.74)}section p{text-align:justify}section h2:before{content:"§" attr(data-num) " — "}section *+h2:before{content:""}section * h2:before{content:""}section h2{margin-top:2em;cursor:pointer}section *+h2{margin-top:0;cursor:auto}section * h2{margin-top:0;cursor:auto}canvas.loading-sketch{background:url('/images/ajax-loader.gif');background-repeat:no-repeat;background-position:50% 50%}.sketch-code{display:inline-block;unicode-bidi:embed;font-family:monospace;white-space:pre;border:1px solid black;min-height:300px}.sketch{display:inline-block;border:1px solid #DDD;padding:5px;font-style:italic;font-size:80%;background:#fafacc}.sketch canvas{margin:auto;border:1px solid black;height:300px}.sketch canvas:focus{outline:1px solid rgba(0,0,0,0)}.sketch img{margin:auto;border:1px solid black}.labeled-image p:before{content:"Image " attr(data-img-label) ". "}.labeled-image p{margin:0}.sketch canvas[data-preset=simple],.sketch canvas[data-preset=poly],.sketch canvas[data-preset=ratios],.sketch canvas[data-preset=clipping],.sketch canvas[data-preset=abc],.sketch canvas[data-preset=moulding],.sketch canvas[data-preset=generate]{width:300px}.sketch canvas[data-preset=twopanel]{width:600px}.sketch canvas[data-preset=shapes],.sketch canvas[data-preset=threepanel]{width:900px}.sketch-title span{color:#0000c8;cursor:pointer}div.note{font-size:90%;margin:1em 2em;padding:1em;border:1px solid grey;background:rgba(150,150,50,0.05)}div.note *{margin:0;padding:0}div.note p{margin:1em 0}div.note div.MathJax_Display{margin:1em 0}a,a:visited{color:#0000c8;text-decoration:none}#ribbonimg{position:fixed;top:0;right:0;z-index:999}#navbar{float:right;background:white;padding:0 .2em .5em .5em;border:1px solid black;margin:0 0 .2em 1em;font-size:80%}#navbar h3{margin:.2em 0}#navbar ol{margin:.2em;margin-left:-1em}td p{text-align:center}#scriptblock{display:none}github-issues{position:relative;display:block;width:100%}github-issue{display:block;position:relative;border:1px solid #EEE;border-left:.3em solid #e5ecf3;background:white;padding:0 .3em;font:13px Helvetica,arial,freesans,clean,sans-serif;width:95%;margin:auto;min-height:33px}github-issue+github-issue{margin-top:1em}github-issue h3{font-size:100%;background:#e5ecf3;margin:0;position:relative;left:-0.5%;width:101%;font-weight:bold;border-bottom:1px solid #999}github-issue a{position:absolute;top:2px;right:10px;padding:0 4px;color:#4183C4 !important;background:white;line-height:10px;font-size:10px}.howtocode{border:1px solid #8d94bd;padding:0 1em;margin:0 2em;overflow-x:hidden}.howtocode h3{margin:0 -1em;padding:0;background:#91bef7;padding-left:.5em;color:white;text-shadow:1px 1px 0 #000;cursor:pointer}.howtocode pre{border:1px solid #8d94bd;background:rgba(223,226,243,0.32);margin:.5em;padding:.5em}footer{font-style:italic;margin:2em 0 1em 0;background:inherit}.at-floatingbar-inner{width:10px}

243
stylesheets/style.less Normal file
View File

@@ -0,0 +1,243 @@
html, body {
margin: 0;
padding: 0;
}
body {
background: url('../images/paper.png');
}
header, section, footer {
width: 960px;
margin: 0 auto;
}
header { font-size: 125%; }
article {
width: 960px;
margin: auto;
background: rgba(255, 255, 255, 0.74);
border: solid rgba(255, 0, 0, 0.35);
border-width: 0;
border-left-width: 1px;
padding: 1em;
box-shadow: 25px 0px 25px 25px rgba(255, 255, 255, 0.74);
}
section p {
text-align: justify;
}
section h2:before { content: "§" attr(data-num) " — "; }
section *+h2:before { content: ""; }
section * h2:before { content: ""; }
section h2 { margin-top: 2em; cursor: pointer; }
section *+h2 { margin-top: 0em; cursor: auto; }
section * h2 { margin-top: 0em; cursor: auto; }
canvas.loading-sketch {
background: url('../images/ajax-loader.gif');
background-repeat: no-repeat;
background-position: 50% 50%;
}
.sketch-code {
display: inline-block;
unicode-bidi: embed;
font-family: monospace;
white-space: pre;
border: 1px solid black;
min-height: 300px;
}
.sketch {
display: inline-block;
border: 1px solid #DDD;
padding: 5px;
font-style: italic;
font-size: 80%;
background: rgb(250,250,204);
}
.sketch canvas {
margin: auto;
border: 1px solid black;
height: 300px;
}
.sketch canvas:focus {
outline: 1px solid rgba(0,0,0,0);
}
.sketch img {
margin: auto;
border: 1px solid black;
}
.labeled-image p:before {
content: "Image " attr(data-img-label) ". ";
}
.labeled-image p {
margin: 0;
}
.sketch canvas[data-preset=simple],
.sketch canvas[data-preset=poly],
.sketch canvas[data-preset=ratios],
.sketch canvas[data-preset=clipping],
.sketch canvas[data-preset=abc],
.sketch canvas[data-preset=moulding],
.sketch canvas[data-preset=generate]
{ width: 300px; }
.sketch canvas[data-preset=twopanel]
{ width: 600px; }
.sketch canvas[data-preset=shapes],
.sketch canvas[data-preset=threepanel]
{ width: 900px; }
.sketch-title span { color: rgb(0,0,200); cursor: pointer; }
div.note {
font-size: 90%;
margin: 1em 2em;
padding: 1em;
border: 1px solid grey;
background: rgba(150,150,50,0.05);
}
div.note * { margin: 0; padding: 0; }
div.note p { margin: 1em 0; }
div.note div.MathJax_Display { margin: 1em 0; }
a, a:visited { color: rgb(0,0,200); text-decoration: none; }
#ribbonimg {
position: fixed;
top: 0;
right: 0;
z-index: 999;
}
#navbar {
float: right;
background: white;
padding: 0 0.2em 0.5em 0.5em;
border: 1px solid black;
margin: 0 0 0.2em 1em;
font-size: 80%;
}
#navbar h3 { margin: 0.2em 0; }
#navbar ol { margin: 0.2em; margin-left: -1em; }
td p {
text-align:center;
}
#scriptblock { display: none; }
/**
GITHUB ISSUES
**/
github-issues {
position: relative;
display: block;
width: 100%;
}
github-issue {
display: block;
position: relative;
border: 1px solid #EEE;
border-left: 0.3em solid rgb(229, 236, 243);
/*border-radius: 7px 0 0 7px;*/
background: white;
padding: 0 0.3em;
font: 13px Helvetica, arial, freesans, clean, sans-serif;
width: 95%;
margin: auto;
min-height: 33px;
}
github-issue + github-issue {
margin-top: 1em;
}
github-issue h3 {
font-size:100%;
background: rgb(229, 236, 243);
margin: 0;
position: relative;
left: -.5%;
width: 101%;
font-weight: bold;
border-bottom: 1px solid #999;
}
github-issue a {
position: absolute;
top: 2px;
right: 10px;
padding: 0 4px;
color: #4183C4!important;
background: white;
line-height: 10px;
font-size: 10px;
}
.howtocode {
border: 1px solid rgb(141, 148, 189);
padding: 0 1em;
margin: 0 2em;
overflow-x: hidden;
}
.howtocode h3 {
margin: 0 -1em;
padding: 0;
background: rgb(145, 190, 247);
padding-left: 0.5em;
color: white;
text-shadow: 1px 1px 0 rgb(0, 0, 0);
cursor: pointer;
}
.howtocode pre {
border: 1px solid rgb(141, 148, 189);
background: rgba(223, 226, 243, 0.32);
margin: 0.5em;
padding: 0.5em;
}
footer {
font-style: italic;
margin: 2em 0 1em 0;
background: inherit;
}
/* addthis nonsense */
.at-floatingbar-inner {
width: 10px;
}
/* =========================================================== */
figure {
display: inline-block;
border: 1px solid grey;
background: #F0F0F0;
@psize: 0.5em;
padding: @psize @psize 0 @psize;
text-align: center;
canvas {
display: inline-block;
background: white;
border: 1px solid lightgrey;
}
figcaption {
text-align: center;
padding: 0.5em 0;
font-style: italic;
font-size: 90%;
}
}

32
webpack.config.js Normal file
View File

@@ -0,0 +1,32 @@
var webpack = require('webpack');
// Hot Reload server when we're in dev mode,
// otherwise build it the normal way.
var entry = ['./components/App.jsx'];
if(!process.argv.indexOf("--prod")) {
entry.push('webpack/hot/dev-server');
}
module.exports = {
entry: entry,
output: {
path: __dirname,
filename: 'article.js'
},
module: {
loaders: [
{ test: /\.txt$/, loader: "raw" },
{ test: /\.(png|gif)$/, loader: "file" },
{ test: /\.less$/, loader: "style!css!less" },
{
test: /.jsx?$/,
exclude: /node_modules/,
loaders: [
'babel-loader',
__dirname + '/lib/latex-loader',
__dirname + '/lib/pre-loader'
]
}
]
},
};