1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-01-18 06:08:20 +01:00

rewrite to handler.js format

This commit is contained in:
Pomax 2017-03-20 17:31:20 -07:00
parent 71a56be5f8
commit af96d59bb9
91 changed files with 4113 additions and 5017 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
var React = require("react");
var Locale = require("../lib/locale");
var locale = new Locale();
module.exports = function generateBase(page, handler) {
// the basic class just has a title and basic content.
var componentClass = {
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
render: function() {
return locale.getContent(page, this);
}
};
// if the content requires code bindings, ensure those exist:
if (handler) {
Object.keys(handler).forEach(key => {
componentClass[key] = handler[key];
});
}
// then build the actual React class
return React.createClass(componentClass);
};

View File

@ -0,0 +1,122 @@
module.exports = {
onClick: function(evt, api) {
api.t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7);
if (api.t < 0.05 || api.t > 0.95) api.t = false;
api.redraw();
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[0].y -= 10;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
curve.points[2].y -= 20;
api.setCurve(curve);
api.lut = curve.getLUT(100);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var h = api.getPanelHeight();
api.setColor("black");
if (api.t) {
api.drawCircle(api.curve.get(api.t),3);
api.setColor("lightgrey");
var hull = api.drawHull(curve, api.t);
var utils = api.utils;
var A, B, C;
if(hull.length === 6) {
A = curve.points[1];
B = hull[5];
C = utils.lli4(A, B, curve.points[0], curve.points[2]);
api.setColor("lightgrey");
api.drawLine(curve.points[0], curve.points[2]);
} else if(hull.length === 10) {
A = hull[5];
B = hull[9];
C = utils.lli4(A, B, curve.points[0], curve.points[3]);
api.setColor("lightgrey");
api.drawLine(curve.points[0], curve.points[3]);
}
api.setColor("#00FF00");
api.drawLine(A,B);
api.setColor("red");
api.drawLine(B,C);
api.setColor("black");
api.drawCircle(C,3);
api.setFill("black");
api.text("A", {x:10 + A.x, y: A.y});
api.text("B (t = " + api.utils.round(api.t,2) + ")", {x:10 + B.x, y: B.y});
api.text("C", {x:10 + C.x, y: C.y});
var d1 = utils.dist(A, B);
var d2 = utils.dist(B, C);
var ratio = d1/d2;
api.text("d1 (A-B): " + utils.round(d1,2) + ", d2 (B-C): "+ utils.round(d2,2) + ", ratio (d1/d2): " + utils.round(ratio,4), {x:10, y:h-7});
}
},
setCT: function(evt,api) {
api.t = evt.offsetX / api.getPanelWidth();
},
drawCTgraph: function(api) {
api.reset();
api.setColor("black");
var w = api.getPanelWidth();
var pad = 20;
var fwh = w - 2*pad;
api.drawAxes(pad, "t",0,1, "u",0,1);
api.setColor("blue");
var uPoint = function(t) {
var value = api.u(t),
res = { x: pad + t*fwh, y: pad + value*fwh };
return res;
};
api.drawFunction(uPoint);
if (api.t) {
var v = api.u(api.t),
v1 = api.utils.round(v,3),
v2 = api.utils.round(1-v,3),
up = uPoint(api.t);
api.drawLine({x:up.x,y:pad}, up);
api.drawLine({x:pad,y:up.y}, up);
api.drawCircle(up,3);
api.setFill("blue");
api.text(" t = " + api.utils.round(api.t,3), {x:up.x+10, y:up.y-7});
api.text("u(t) = " + api.utils.round(v,3), {x:up.x+10, y:up.y+7});
api.setFill("black");
api.text("C = "+v1+" * start + "+v2+" * end", {x:w/2 - pad, y:pad+fwh});
}
},
drawQCT: function(api) {
api.u = api.u || function(t) {
var top = (t-1) * (t-1),
bottom = 2*t*t - 2*t + 1;
return top/bottom;
};
this.drawCTgraph(api);
},
drawCCT: function(api) {
api.u = api.u || function(t) {
var top = (1-t) * (1-t) * (1-t),
bottom = t*t*t + top;
return top/bottom;
};
this.drawCTgraph(api);
}
};

View File

@ -1,141 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "abc";
var ABC = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
onClick: function(evt, api) {
api.t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7);
if (api.t < 0.05 || api.t > 0.95) api.t = false;
api.redraw();
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[0].y -= 10;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
curve.points[2].y -= 20;
api.setCurve(curve);
api.lut = curve.getLUT(100);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var h = api.getPanelHeight();
api.setColor("black");
if (api.t) {
api.drawCircle(api.curve.get(api.t),3);
api.setColor("lightgrey");
var hull = api.drawHull(curve, api.t);
var utils = api.utils;
var A, B, C;
if(hull.length === 6) {
A = curve.points[1];
B = hull[5];
C = utils.lli4(A, B, curve.points[0], curve.points[2]);
api.setColor("lightgrey");
api.drawLine(curve.points[0], curve.points[2]);
} else if(hull.length === 10) {
A = hull[5];
B = hull[9];
C = utils.lli4(A, B, curve.points[0], curve.points[3]);
api.setColor("lightgrey");
api.drawLine(curve.points[0], curve.points[3]);
}
api.setColor("#00FF00");
api.drawLine(A,B);
api.setColor("red");
api.drawLine(B,C);
api.setColor("black");
api.drawCircle(C,3);
api.setFill("black");
api.text("A", {x:10 + A.x, y: A.y});
api.text("B (t = " + api.utils.round(api.t,2) + ")", {x:10 + B.x, y: B.y});
api.text("C", {x:10 + C.x, y: C.y});
var d1 = utils.dist(A, B);
var d2 = utils.dist(B, C);
var ratio = d1/d2;
api.text("d1 (A-B): " + utils.round(d1,2) + ", d2 (B-C): "+ utils.round(d2,2) + ", ratio (d1/d2): " + utils.round(ratio,4), {x:10, y:h-7});
}
},
setCT: function(evt,api) {
api.t = evt.offsetX / api.getPanelWidth();
},
drawCTgraph: function(api) {
api.reset();
api.setColor("black");
var w = api.getPanelWidth();
var pad = 20;
var fwh = w - 2*pad;
api.drawAxes(pad, "t",0,1, "u",0,1);
api.setColor("blue");
var uPoint = function(t) {
var value = api.u(t),
res = { x: pad + t*fwh, y: pad + value*fwh };
return res;
};
api.drawFunction(uPoint);
if (api.t) {
var v = api.u(api.t),
v1 = api.utils.round(v,3),
v2 = api.utils.round(1-v,3),
up = uPoint(api.t);
api.drawLine({x:up.x,y:pad}, up);
api.drawLine({x:pad,y:up.y}, up);
api.drawCircle(up,3);
api.setFill("blue");
api.text(" t = " + api.utils.round(api.t,3), {x:up.x+10, y:up.y-7});
api.text("u(t) = " + api.utils.round(v,3), {x:up.x+10, y:up.y+7});
api.setFill("black");
api.text("C = "+v1+" * start + "+v2+" * end", {x:w/2 - pad, y:pad+fwh});
}
},
drawQCT: function(api) {
api.u = api.u || function(t) {
var top = (t-1) * (t-1),
bottom = 2*t*t - 2*t + 1;
return top/bottom;
};
this.drawCTgraph(api);
},
drawCCT: function(api) {
api.u = api.u || function(t) {
var top = (1-t) * (1-t) * (1-t),
bottom = t*t*t + top;
return top/bottom;
};
this.drawCTgraph(api);
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = ABC;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("abc", handler);

View File

@ -0,0 +1,54 @@
module.exports = {
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
align: function(points, line) {
var tx = line.p1.x,
ty = line.p1.y,
a = -Math.atan2(line.p2.y-ty, line.p2.x-tx),
cos = Math.cos,
sin = Math.sin,
d = function(v) {
return {
x: (v.x-tx)*cos(a) - (v.y-ty)*sin(a),
y: (v.x-tx)*sin(a) + (v.y-ty)*cos(a)
};
};
return points.map(d);
},
draw: function(api, curve) {
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var pts = curve.points;
var line = {p1: pts[0], p2: pts[pts.length-1]};
var apts = this.align(pts, line);
var aligned = new api.Bezier(apts);
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var offset = {x:w, y:0};
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
offset.x += w/4;
offset.y += h/2;
api.setColor("grey");
api.drawLine({x:0,y:-h/2}, {x:0,y:h/2}, offset);
api.drawLine({x:-w/4,y:0}, {x:w,y:0}, offset);
api.setFill("grey");
api.setColor("black");
api.drawSkeleton(aligned, offset);
api.drawCurve(aligned, offset);
}
};

View File

@ -1,72 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "aligning";
var Aligning = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
align: function(points, line) {
var tx = line.p1.x,
ty = line.p1.y,
a = -Math.atan2(line.p2.y-ty, line.p2.x-tx),
cos = Math.cos,
sin = Math.sin,
d = function(v) {
return {
x: (v.x-tx)*cos(a) - (v.y-ty)*sin(a),
y: (v.x-tx)*sin(a) + (v.y-ty)*cos(a)
};
};
return points.map(d);
},
draw: function(api, curve) {
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var pts = curve.points;
var line = {p1: pts[0], p2: pts[pts.length-1]};
var apts = this.align(pts, line);
var aligned = new api.Bezier(apts);
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var offset = {x:w, y:0};
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
offset.x += w/4;
offset.y += h/2;
api.setColor("grey");
api.drawLine({x:0,y:-h/2}, {x:0,y:h/2}, offset);
api.drawLine({x:-w/4,y:0}, {x:w,y:0}, offset);
api.setFill("grey");
api.setColor("black");
api.drawSkeleton(aligned, offset);
api.drawCurve(aligned, offset);
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Aligning;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("aligning", handler);

View File

@ -0,0 +1,49 @@
# Approximating Bézier curves with circular arcs
Let's look at doing the exact opposite of the previous section: rather than approximating circular arc using Bézier curves, let's approximate Bézier curves using 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.
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.
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 [chord](https://en.wikipedia.org/wiki/Chord_%28geometry%29), and one property of chords is that the line from the center of any chord, perpendicular to that chord, passes through the center of the circle.
So: if we have have three points, we have three (different) chords, and consequently, three (different) lines that go from those chords through the center of the circle. So we find the centers of the chords, find the perpendicular lines, find the intersection of those lines, and thus find the center of the circle.
The following graphic shows this procedure with a different colour for each chord and its associated perpendicular through the center. You can move the points around as much as you like, those lines will always meet!
<Graphic preset="simple" title="Finding a circle through three points" setup={this.setupCircle} draw={this.drawCircle} />
So, with the procedure on how to find a circle through three points, finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, over the remaining point, to the end point.
So how can we convert a Bezier curve into a (sequence of) circular arc(s)?
- Start at <em>t=0</em>
- Pick two points further down the curve at some value <em>m = t + n</em> and <em>e = t + 2n</em>
- Find the arc that these points define
- Determine how close the found arc is to the curve:
- Pick two additional points <em>e1 = t + n/2</em> and <em>e2 = t + n + n/2</em>.
- 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.
- 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.
- If this error is too high, we consider the arc bad, and try a smaller interval.
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 [Binary Search](https://en.wikipedia.org/wiki/Binary_search_algorithm).
1. We start with {0, 0.5, 1}
2. That'll fail, so we retry with the interval halved: {0, 0.25, 0.5}
- If that arc's good, we move back up by half distance: {0, 0.375, 0.75}.
- However, if the arc was still bad, we move <em>down</em> by half the distance: {0, 0.125, 0.25}.
3. 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.
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 up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is.
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} />
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 up and down arrow 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:
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawArcs} onKeyDown={this.props.onKeyDown} />
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!

View File

@ -0,0 +1,160 @@
var atan2 = Math.atan2, PI = Math.PI, TAU = 2*PI, cos = Math.cos, sin = Math.sin;
module.exports = {
statics: {
keyHandlingOptions: {
propName: "error",
values: {
"38": 0.1, // up arrow
"40": -0.1 // down arrow
},
controller: function(api) {
if (api.error < 0.1) {
api.error = 0.1;
}
}
}
},
setupCircle: function(api) {
var curve = new api.Bezier(70,70, 140,40, 240,130);
api.setCurve(curve);
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.error = 0.5;
},
getCCenter: function(api, p1, p2, p3) {
// deltas
var dx1 = (p2.x - p1.x),
dy1 = (p2.y - p1.y),
dx2 = (p3.x - p2.x),
dy2 = (p3.y - p2.y);
// perpendiculars (quarter circle turned)
var dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2),
dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2),
dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2),
dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2);
// chord midpoints
var mx1 = (p1.x + p2.x)/2,
my1 = (p1.y + p2.y)/2,
mx2 = (p2.x + p3.x)/2,
my2 = (p2.y + p3.y)/2;
// midpoint offsets
var mx1n = mx1 + dx1p,
my1n = my1 + dy1p,
mx2n = mx2 + dx2p,
my2n = my2 + dy2p;
// intersection of these lines:
var i = api.utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n);
var r = api.utils.dist(i,p1);
// arc start/end values, over mid point
var s = atan2(p1.y - i.y, p1.x - i.x),
m = atan2(p2.y - i.y, p2.x - i.x),
e = atan2(p3.y - i.y, p3.x - i.x);
// determine arc direction (cw/ccw correction)
var __;
if (s<e) {
// if s<m<e, arc(s, e)
// if m<s<e, arc(e, s + TAU)
// if s<e<m, arc(e, s + TAU)
if (s>m || m>e) { s += TAU; }
if (s>e) { __=e; e=s; s=__; }
} else {
// if e<m<s, arc(e, s)
// if m<e<s, arc(s, e + TAU)
// if e<s<m, arc(s, e + TAU)
if (e<m && m<s) { __=e; e=s; s=__; } else { e += TAU; }
}
// assign and done.
i.s = s;
i.e = e;
i.r = r;
return i;
},
drawCircle: function(api, curve) {
api.reset();
var pts = curve.points;
// get center
var C = this.getCCenter(api, pts[0], pts[1], pts[2]);
// outer circle
api.setColor("grey");
api.drawCircle(C, api.utils.dist(C,pts[0]));
// controllable points
api.setColor("black");
pts.forEach(p => api.drawCircle(p,3));
// chords and perpendicular lines
var m;
api.setColor("blue");
api.drawLine(pts[0], pts[1]);
m = {x: (pts[0].x + pts[1].x)/2, y: (pts[0].y + pts[1].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
api.setColor("red");
api.drawLine(pts[1], pts[2]);
m = {x: (pts[1].x + pts[2].x)/2, y: (pts[1].y + pts[2].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
api.setColor("green");
api.drawLine(pts[2], pts[0]);
m = {x: (pts[2].x + pts[0].x)/2, y: (pts[2].y + pts[0].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
// center
api.setColor("black");
api.drawPoint(C);
api.setFill("black");
api.text("Intersection point", C, {x:-25, y:10});
},
drawSingleArc: function(api, curve) {
api.reset();
var arcs = curve.arcs(api.error);
api.drawSkeleton(curve);
api.drawCurve(curve);
var a = arcs[0];
api.setColor("red");
api.setFill("rgba(255,0,0,0.2)");
api.debug = true;
api.drawArc(a);
api.setFill("black");
api.text("Arc approximation with total error " + api.utils.round(api.error,1), {x:10, y:15});
},
drawArcs: function(api, curve) {
api.reset();
var arcs = curve.arcs(api.error);
api.drawSkeleton(curve);
api.drawCurve(curve);
arcs.forEach(a => {
api.setRandomColor(0.3);
api.setFill(api.getColor());
api.drawArc(a);
});
api.setFill("black");
api.text("Arc approximation with total error " + api.utils.round(api.error,1) + " per arc segment", {x:10, y:15});
}
};

View File

@ -1,281 +1,4 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var atan2 = Math.atan2, PI = Math.PI, TAU = 2*PI, cos = Math.cos, sin = Math.sin;
var Introduction = React.createClass({
statics: {
keyHandlingOptions: {
propName: "error",
values: {
"38": 0.1, // up arrow
"40": -0.1 // down arrow
},
controller: function(api) {
if (api.error < 0.1) {
api.error = 0.1;
}
}
}
},
getDefaultProps: function() {
return {
title: "Approximating Bézier curves with circular arcs"
};
},
setupCircle: function(api) {
var curve = new api.Bezier(70,70, 140,40, 240,130);
api.setCurve(curve);
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.error = 0.5;
},
getCCenter: function(api, p1, p2, p3) {
// deltas
var dx1 = (p2.x - p1.x),
dy1 = (p2.y - p1.y),
dx2 = (p3.x - p2.x),
dy2 = (p3.y - p2.y);
// perpendiculars (quarter circle turned)
var dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2),
dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2),
dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2),
dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2);
// chord midpoints
var mx1 = (p1.x + p2.x)/2,
my1 = (p1.y + p2.y)/2,
mx2 = (p2.x + p3.x)/2,
my2 = (p2.y + p3.y)/2;
// midpoint offsets
var mx1n = mx1 + dx1p,
my1n = my1 + dy1p,
mx2n = mx2 + dx2p,
my2n = my2 + dy2p;
// intersection of these lines:
var i = api.utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n);
var r = api.utils.dist(i,p1);
// arc start/end values, over mid point
var s = atan2(p1.y - i.y, p1.x - i.x),
m = atan2(p2.y - i.y, p2.x - i.x),
e = atan2(p3.y - i.y, p3.x - i.x);
// determine arc direction (cw/ccw correction)
var __;
if (s<e) {
// if s<m<e, arc(s, e)
// if m<s<e, arc(e, s + TAU)
// if s<e<m, arc(e, s + TAU)
if (s>m || m>e) { s += TAU; }
if (s>e) { __=e; e=s; s=__; }
} else {
// if e<m<s, arc(e, s)
// if m<e<s, arc(s, e + TAU)
// if e<s<m, arc(s, e + TAU)
if (e<m && m<s) { __=e; e=s; s=__; } else { e += TAU; }
}
// assign and done.
i.s = s;
i.e = e;
i.r = r;
return i;
},
drawCircle: function(api, curve) {
api.reset();
var pts = curve.points;
// get center
var C = this.getCCenter(api, pts[0], pts[1], pts[2]);
// outer circle
api.setColor("grey");
api.drawCircle(C, api.utils.dist(C,pts[0]));
// controllable points
api.setColor("black");
pts.forEach(p => api.drawCircle(p,3));
// chords and perpendicular lines
var m;
api.setColor("blue");
api.drawLine(pts[0], pts[1]);
m = {x: (pts[0].x + pts[1].x)/2, y: (pts[0].y + pts[1].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
api.setColor("red");
api.drawLine(pts[1], pts[2]);
m = {x: (pts[1].x + pts[2].x)/2, y: (pts[1].y + pts[2].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
api.setColor("green");
api.drawLine(pts[2], pts[0]);
m = {x: (pts[2].x + pts[0].x)/2, y: (pts[2].y + pts[0].y)/2};
api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)});
// center
api.setColor("black");
api.drawPoint(C);
api.setFill("black");
api.text("Intersection point", C, {x:-25, y:10});
},
drawSingleArc: function(api, curve) {
api.reset();
var arcs = curve.arcs(api.error);
api.drawSkeleton(curve);
api.drawCurve(curve);
var a = arcs[0];
api.setColor("red");
api.setFill("rgba(255,0,0,0.2)");
api.debug = true;
api.drawArc(a);
api.setFill("black");
api.text("Arc approximation with total error " + api.utils.round(api.error,1), {x:10, y:15});
},
drawArcs: function(api, curve) {
api.reset();
var arcs = curve.arcs(api.error);
api.drawSkeleton(curve);
api.drawCurve(curve);
arcs.forEach(a => {
api.setRandomColor(0.3);
api.setFill(api.getColor());
api.drawArc(a);
});
api.setFill("black");
api.text("Arc approximation with total error " + api.utils.round(api.error,1) + " per arc segment", {x:10, y:15});
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<p>Let's look at doing the exact opposite of the previous section: rather than approximating
circular arc using Bézier curves, let's approximate Bézier curves using circular arcs.</p>
<p>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 any chord, perpendicular to that chord, passes through
the center of the circle.</p>
<p>So: if we have have three points, we have three (different) chords, and consequently, three
(different) lines that go from those chords through the center of the circle. So we find the
centers of the chords, find the perpendicular lines, find the intersection of those lines,
and thus find the center of the circle.</p>
<p>The following graphic shows this procedure with a different colour for each chord and its
associated perpendicular through the center. You can move the points around as much as you
like, those lines will always meet!</p>
<Graphic preset="simple" title="Finding a circle through three points" setup={this.setupCircle} draw={this.drawCircle} />
<p>So, with the procedure on how to find a circle through three points, finding the arc through those points
is straight-forward: pick one of the three points as start point, pick another as an end point, and
the arc has to necessarily go from the start point, over the remaining point, to the end point.</p>
<p>So how can we convert a Bezier curve into a (sequence of) circular arc(s)?</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 up and down arrow keys keys to increase or decrease the error threshold,
to see what the effect of a smaller or larger error threshold is.</p>
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} />
<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 up and down arrow 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>
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawArcs} onKeyDown={this.props.onKeyDown} />
<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>
</section>
);
}
});
module.exports = keyHandling(Introduction);
module.exports = keyHandling(generateBase("arcapproximation", handler));

View File

@ -0,0 +1,91 @@
var sin = Math.sin;
var tau = Math.PI*2;
module.exports = {
setup: function(api) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var generator;
if (!this.generator) {
generator = ((v,scale) => {
scale = scale || 1;
return {
x: v*w/tau,
y: scale * sin(v)
};
});
generator.start = 0;
generator.end = tau;
generator.step = 0.1;
generator.scale = h/3;
this.generator = generator;
}
},
drawSine: function(api, dheight) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var generator = this.generator;
generator.dheight = dheight;
api.setColor("black");
api.drawLine({x:0,y:h/2}, {x:w,y:h/2});
api.drawFunction(generator, {x:0, y:h/2});
},
drawSlices: function(api, steps) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var f = w/tau;
var area = 0;
var c = steps <= 25 ? 1 : 0;
api.reset();
api.setColor("transparent");
api.setFill("rgba(150,150,255, 0.4)");
for (var step=tau/steps, i=step/2, v, p1, p2; i<tau+step/2; i+=step) {
v = this.generator(i);
p1 = {x:v.x - f*step/2 + c, y: 0};
p2 = {x:v.x + f*step/2 - c, y: v.y * this.generator.scale};
if (!c) { api.setFill("rgba(150,150,255,"+(0.4 + 0.3*Math.random())+")"); }
api.drawRect(p1, p2, {x:0, y:h/2});
area += step * Math.abs(v.y * this.generator.scale);
}
api.setFill("black");
var trueArea = ((100 * 4 * h/3)|0)/100;
var currArea = ((100 * area)|0)/100;
api.text("Approximating with "+steps+" strips (true area: "+trueArea+"): " + currArea, {x: 10, y: h-15});
},
drawCoarseIntegral: function(api) {
api.reset();
this.drawSlices(api, 10);
this.drawSine(api);
},
drawFineIntegral: function(api) {
api.reset();
this.drawSlices(api, 24);
this.drawSine(api);
},
drawSuperFineIntegral: function(api) {
api.reset();
this.drawSlices(api, 99);
this.drawSine(api);
},
setupCurve: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
drawCurve: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var len = curve.length();
api.setFill("black");
api.text("Curve length: "+len+" pixels", {x:10, y:15});
}
};

View File

@ -1,109 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "arclength";
var sin = Math.sin;
var tau = Math.PI*2;
var Arclength = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var generator;
if (!this.generator) {
generator = ((v,scale) => {
scale = scale || 1;
return {
x: v*w/tau,
y: scale * sin(v)
};
});
generator.start = 0;
generator.end = tau;
generator.step = 0.1;
generator.scale = h/3;
this.generator = generator;
}
},
drawSine: function(api, dheight) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var generator = this.generator;
generator.dheight = dheight;
api.setColor("black");
api.drawLine({x:0,y:h/2}, {x:w,y:h/2});
api.drawFunction(generator, {x:0, y:h/2});
},
drawSlices: function(api, steps) {
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var f = w/tau;
var area = 0;
var c = steps <= 25 ? 1 : 0;
api.reset();
api.setColor("transparent");
api.setFill("rgba(150,150,255, 0.4)");
for (var step=tau/steps, i=step/2, v, p1, p2; i<tau+step/2; i+=step) {
v = this.generator(i);
p1 = {x:v.x - f*step/2 + c, y: 0};
p2 = {x:v.x + f*step/2 - c, y: v.y * this.generator.scale};
if (!c) { api.setFill("rgba(150,150,255,"+(0.4 + 0.3*Math.random())+")"); }
api.drawRect(p1, p2, {x:0, y:h/2});
area += step * Math.abs(v.y * this.generator.scale);
}
api.setFill("black");
var trueArea = ((100 * 4 * h/3)|0)/100;
var currArea = ((100 * area)|0)/100;
api.text("Approximating with "+steps+" strips (true area: "+trueArea+"): " + currArea, {x: 10, y: h-15});
},
drawCoarseIntegral: function(api) {
api.reset();
this.drawSlices(api, 10);
this.drawSine(api);
},
drawFineIntegral: function(api) {
api.reset();
this.drawSlices(api, 24);
this.drawSine(api);
},
drawSuperFineIntegral: function(api) {
api.reset();
this.drawSlices(api, 99);
this.drawSine(api);
},
setupCurve: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
drawCurve: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var len = curve.length();
api.setFill("black");
api.text("Curve length: "+len+" pixels", {x:10, y:15});
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Arclength;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("arclength", handler);

View File

@ -0,0 +1,58 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 10;
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 16;
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var pts = curve.getLUT(api.steps);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
var len = curve.length();
var alen = 0;
for(var i=0,p1,dx,dy; i<pts.length-1; i++) {
p0 = pts[i];
p1 = pts[i+1];
dx = p1.x-p0.x;
dy = p1.y-p0.y;
alen += Math.sqrt(dx*dx+dy*dy);
}
alen = ((100*alen)|0)/100;
len = ((100*len)|0)/100;
api.text("Approximate length, "+api.steps+" steps: "+alen+" (true: "+len+")", {x:10, y: 15});
}
};

View File

@ -1,78 +1,4 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "arclengthapprox";
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var ArclengthApprox = React.createClass({
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 10;
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 16;
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var pts = curve.getLUT(api.steps);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
var len = curve.length();
var alen = 0;
for(var i=0,p1,dx,dy; i<pts.length-1; i++) {
p0 = pts[i];
p1 = pts[i+1];
dx = p1.x-p0.x;
dy = p1.y-p0.y;
alen += Math.sqrt(dx*dx+dy*dy);
}
alen = ((100*alen)|0)/100;
len = ((100*len)|0)/100;
api.text("Approximate length, "+api.steps+" steps: "+alen+" (true: "+len+")", {x:10, y: 15});
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = keyHandling(ArclengthApprox);
module.exports = keyHandling(generateBase("arclengthapprox", handler));

View File

@ -0,0 +1,24 @@
module.exports = {
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.setColor("#00FF00");
api.drawbbox(curve.bbox());
api.setColor("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("red");
curve.extrema().values.forEach(t => {
api.drawCircle(curve.get(t), 3);
});
}
};

View File

@ -1,42 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "boundingbox";
var BoundingBox = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.setColor("#00FF00");
api.drawbbox(curve.bbox());
api.setColor("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("red");
curve.extrema().values.forEach(t => {
api.drawCircle(curve.get(t), 3);
});
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = BoundingBox;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("boundingbox", handler);

View File

@ -0,0 +1,64 @@
# B-Spline derivatives
One last section specific to B-Splines: in order to apply the same procedures to B-Splines as we've looked at for BEzier curves, we'll need to know the first and second derivative. But... what is the derivative of a B-Spline?
Thankfully, much like as was the case for Bezier curves, the derivative of a B-Spline is itself a (lower order) B-Spline. The following two functions specify the general B-Spline formula for a B-Spline of degree <em>d</em> with <em>n</em> points, and knot vector of length <em>d+n+1</em>, and its derivative:
\[
C(t) = \sum_{i=0}^n P_i \cdot N_{i,k}(t)
\]
\[
C'(t) = \sum_{i=0}^{n-1} P_i \prime \cdot N_{i+1,k-1}(t)
\]
where
\[
P_i \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
\]
So, much as for Bezier derivatives, we see a derivative function that is simply a new interpolation function, with interpolated weights. With this information, we can do things like draw tangents and normals, as well as determine the curvature function, draw inflection points, and all those lovely things.
As a concrete example, let's look at cubic (=degree 3) B-Spline with five coordinates, and with uniform knot vector of length 3 + 5 + 1 = 9:
\[\begin{array}{l}
d = 3, \\
P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\
knots = {0,1,2,3,4,5,6,7,8}
\end{array}\]
<BSplineGraphic sketch={require('./demonstrator')} />
Applying the above knowledge, we end up with a new B-Spline of degree <em>d-1</em>, with four points <em>P'</em>:
\[\begin{array}{l}
P_0 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{4} - knot_{1}} (P_1 - P_0)
= \frac{3}{3} (P_1 - P_0)
= (135, -210) \\
P_1 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{5} - knot_{2}} (P_2 - P_1)
= \frac{3}{3} (P_2 - P_1)
= (135, 105) \\
P_2 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{6} - knot_{3}} (P_3 - P_2)
= \frac{3}{3} (P_3 - P_2)
= (135, -110) \\
P_3 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{7} - knot_{4}} (P_4 - P_3)
= \frac{3}{3} (P_4 - P_3)
= (105, 230) \\
\end{array}\]
So, we end up with a derivative that has as parameters:
\[\begin{array}{l}
d = 3, \\
P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\
knots = {0,1,2,3,4,5,6,7,8}
\end{array}\]
<BSplineGraphic sketch={require('./derived')} />

View File

@ -1,106 +1,3 @@
var React = require("react");
var SectionHeader = require("../../SectionHeader.jsx");
var BSplineGraphic = require("../../BSplineGraphic.jsx");
var BoundingBox = React.createClass({
getDefaultProps: function() {
return {
title: "B-Spline derivatives"
};
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<p>
One last section specific to B-Splines: in order to apply the same procedures
to B-Splines as we've looked at for BEzier curves, we'll need to know the first
and second derivative. But... what is the derivative of a B-Spline?
</p>
<p>
Thankfully, much like as was the case for Bezier curves, the derivative of a
B-Spline is itself a (lower order) B-Spline. The following two functions specify
the general B-Spline formula for a B-Spline of degree <em>d</em> with <em>n</em>
points, and knot vector of length <em>d+n+1</em>, and its derivative:
</p>
<p>\[
C(t) = \sum_{i=0}^n P_i \cdot N_{i,k}(t)
\]</p>
<p>\[
C'(t) = \sum_{i=0}^{n-1} P_i \prime \cdot N_{i+1,k-1}(t)
\]</p>
<p>where</p>
<p>\[
P_i \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
\]</p>
<p>
So, much as for Bezier derivatives, we see a derivative function that is simply
a new interpolation function, with interpolated weights. With this information, we
can do things like draw tangents and normals, as well as determine the curvature
function, draw inflection points, and all those lovely things.
</p>
<p>
As a concrete example, let's look at cubic (=degree 3) B-Spline with five coordinates,
and with uniform knot vector of length 3 + 5 + 1 = 9:
</p>
<p>\[\begin{array}{l}
d = 3, \\
P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\
knots = {0,1,2,3,4,5,6,7,8}
\end{array}\]</p>
<BSplineGraphic sketch={require('./demonstrator')} />
<p>
Applying the above knowledge, we end up with a new B-Spline of degree <em>d-1</em>,
with four points <em>P'</em>:
</p>
<p>\[\begin{array}{l}
P_0 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{4} - knot_{1}} (P_1 - P_0)
= \frac{3}{3} (P_1 - P_0)
= (135, -210) \\
P_1 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{5} - knot_{2}} (P_2 - P_1)
= \frac{3}{3} (P_2 - P_1)
= (135, 105) \\
P_2 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{6} - knot_{3}} (P_3 - P_2)
= \frac{3}{3} (P_3 - P_2)
= (135, -110) \\
P_3 \prime = \frac{d}{knot_{i+d+1} - knot_{i+1}} (P_{i+1} - P_i)
= \frac{3}{knot_{7} - knot_{4}} (P_4 - P_3)
= \frac{3}{3} (P_4 - P_3)
= (105, 230) \\
\end{array}\]</p>
<p>
So, we end up with a derivative that has as parameters:
</p>
<p>\[\begin{array}{l}
d = 3, \\
P = {(50,240), (185,30), (320,135), (455,25), (560,255)}, \\
knots = {0,1,2,3,4,5,6,7,8}
\end{array}\]</p>
<BSplineGraphic sketch={require('./derived')} />
</section>
);
}
});
module.exports = BoundingBox;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("bsplined", handler);

View File

@ -0,0 +1,224 @@
# B-Splines
No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference, which we'll be looking at in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves (that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them, and how to draw them based on a number of parameters that you can pick for individual B-Splines.
First off: B-Splines are [piecewise polynomial interpolation curves](https://en.wikipedia.org/wiki/Piecewise), where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.
What do they look like? They look like this! .. okay that's an empty graph, but simply click to place some point, with the stipulation that you need at least four point to see any curve. More than four points simply draws a longer B-Spline curve:
<BSplineGraphic sketch={this.basicSketch} />
The important part to notice here is that we are **not** doing the same thing with B-Splines that we do for poly-Béziers or Catmull-Rom curves: both of the latter simply define new sections as literally "new sections based on new points", so a 12 point cubic poly-Bézier curve is actually impossible, because we start with a four point curve, and then add three more points for each section that follows, so we can only have 4, 7, 10, 13, 16, etc point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single points, this addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other hand, are smooth interpolations of *each possible curve involving four consecutive points*, such that at any point along the curve except for our start and end points, our on-curve coordinate is defined by four control points.
Consider the difference to be this:
- for Bézier curves, the curve is defined as an interpolation of points, but:
- for B-Splines, the curve is defined as an interpolation of *curves*.
In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work.
## How to compute a B-Spline curve: some maths
Given a B-Spline of degree `d` and thus order `k=d+1` (so a quadratic B-Spline is degree 2 and order 3, a cubic B-Spline is degree 3 and order 4, etc) and `n` control points `P<sub>0</sub>` through `P<sub>n-1</sub>`, we can compute a point on the curve for some value `t` in the interval [0,1] (where 0 is the start of the curve, and 1 the end, just like for Bézier curves), by evaluting the following function:
\[
Point(t) = \sum^n_{i=0} P_i \cdot N_{i,k}(t)
\]
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve is defined as "a mix of all the control points, weighted somehow", where the weighting is achieved through the *N(...)* function, subscipted with an obvious parameter `i`, which comes from our summation, and some magical parameter `k`. So we need to know two things: 1. what does N(t) do, and 2. what is that `k`? Let's cover both, in reverse order.
The parameter `k` represents the "knot interval" over which a section of curve is defined. As we learned earlier, a B-Spline curve is itself an interpoliation of curves, and we can treat each transition where a control point starts or tops influencing the total curvature as a "knot on the curve".
Doing so for a degree `d` B-Spline with `n` control point gives us `d + n + 1` knots, defining `d + n` intervals along the curve, and it is these intervals that the above `k` subscript to the N() function applies to.
Then the N() function itself. What does it look like?
\[
N_{i,k}(t) = \left ( \frac{t-knot_i}{knot_{(i+k-1)} - knot_i}\right ) \cdot N_{i,k-1}(t) + \left ( \frac{knot_{(i+k)}-t}{knot_{(i+k)} - knot_{(i+1)}} \right ) \cdot N_{i+1,k-1}(t)
\]
So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation, on a specific knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that this is a recursive iteration where `i` goes up, and `k` goes down, so it seem reasonable to expect that this recursion has to stop at some point; obviously, it does, and specifically it does so for the following `i`/`k` values:
\[
N_{i,1}(t) = \left\{\begin{matrix}
1& \text{if } t \in [knot_i,knot_{i+1}) \\
0& \text{otherwise}
\end{matrix}\right.
\]
And this function finally has a straight up evaluation: if a `t` value lies within a knot-specific interval once we reach a `k=1` value, it "counts", otherwise it doesn't. We did cheat a little, though, because for all these values we need to scale our `t` value first, so that it lies in the interval bounded by `knots[d]` and `knots[n]`, which are the start point and end point where curvature is controlled by exactly `order` control points. For instance, for degree 3 (=order 4) and 7 control points, with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map `t` from [the interval 0,1] to the interval [4,8], and then use that value in the functions above, instead.
## Can we simplify that?
We can, yes.
People far smarter than us have looked at this work, and two in particular — [Maurice Cox](http://www.npl.co.uk/people/maurice-cox) and [Carl de Boor](https://en.wikipedia.org/wiki/Carl_R._de_Boor) — came to a mathematically pleasing solution: to compute a point P(t), we can compute this point by evaluating *d(t)* on a curve section between knots *i* and *i+1*:
\[
d^k_i(t) = \alpha_{i,k} \cdot d^{k-1}_i(t) + (1-\alpha_{i,k}) \cdot d^{k-1}_{i-1}(t)
\]
This is another recursive function, with *k* values decreasing from the curve order to 1, and the value *α* (alpha) defined by:
\[
\alpha_{i,k} = \frac{t - knots[i]}{knots[i+1+n-k] - knots[i]}
\]
That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha". Computing this d() function is thus simply a matter of "computing simple arithmetics but with recursion", which might be computationally expensive because we're doing "a lot of" steps, but is also computationally cheap because each step only involves very simple maths. Of course as before the recursion has to stop:
\[
d^k_0(t) = 0, \ d^0_i(t) = N_{i,1}(t) =
\left\{\begin{matrix}
1& \text{if } t \in [knot_i,knot_{i+1}) \\
0& \text{otherwise}
\end{matrix}\right.
\]
So, we see two stopping conditions: either `i` becomes 0, in which case d() is zero, or `k` becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:
\[
\begin{array}{ccccccc}
d^3_3 && d^2_3 && d^1_3 && d^0_3 (= 0 \text{ or } 1) \\
&+^{α^3_3 \times …}_{(1-{α^3_3}) \times …}& &+^{α^2_3 \times …}_{(1-{α^2_3}) \times …}& &+^{α^1_3 \times …}_{(1-{α^1_3}) \times …}&\\
&& && && \\
& & d^2_2 && d^1_2 && d^0_2 (= 0 \text{ or } 1) \\
& & &+^{α^2_2 \times …}_{(1-{α^2_2}) \times …}& &+^{α^1_2 \times …}_{(1-{α^1_2}) \times …}&\\
& & && && \\
& & & & d^1_1 && d^0_1 (= 0 \text{ or } 1) \\
& & & & &+^{α^1_1 \times …}_{(1-{α^1_1}) \times …}&\\
& & & & && \\
& & & & & & d^0_0 (= 0)
\end{array}
\]
That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!
One thing we need to keep in mind is that we're working with a spline that is contrained by its control points, so even though the `d(..., k)` values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point vector and then works its way up to that single point: that's pretty essential!
If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate the terminating d() values first, then compute the a() constants, perform our multiplcations, generate the previous step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers we start with and simply update them as we move up the triangle. So, let's implement that!
## Cool, cool... but I don't know what to do with that information
I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by one function each) influences the total curvature, given our knot values. And, because exploration is the key to discovery, let's make the knot vector a thing we can actually manipulate. Normally a proper knot vector has a constraint that any value is strictly equal to, or larger than the previous ones, but screw it this is programming, let's ignore that hard restriction and just mess with the knots however we like.
<div className="two-column">
<KnotController ref="interpolation-graph" />
<BSplineGraphic sketch={this.interpolationGraph} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/>
</div>
Changing the values in the knot vector changes how much each point influences the total curvature (with some clever knot value manipulation, we can even make the influence of certain points disappear entirely!), so we can see that while the control points define the hull inside of which we're going to be drawing a curve, it is actually the knot vector that determines the actual *shape* of the curve inside that hull.
After reading the rest of this section you may want to come back here to try some specific knot vectors, and see if the resulting interpolation landscape makes sense given what you will now think should happen!
## Running the computation
Unlike the de Casteljau algorithm, where the `t` value stays the same at every iteration, for B-Splines that is not the case, and so we end having to (for each point we evaluate) run a fairly involving bit of recursive computation. The algorithm is discussed on [this Michigan Tech](http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/de-Boor.html) page, but an easier to read version is implemented by [b-spline.js](https://github.com/thibauts/b-spline/blob/master/index.js#L59-L71), so we'll look at its code.
Given an input value `t`, we first map the input to a value from the domain [0,1] to the domain [knots[degree], knots[knots.length - 1 - degree]. Then, we find the section number `s` that this mapped `t` value lies on:
```
for(s=domain[0]; s < domain[1]; s++) {
if(knots[s] <= t && t <= knots[s+1]) break;
}
```
after running this code, `s` is the index for the section the point will lie on. We then run the algorithm mentioned on the MU page (updated to use this description's variable names):
```
let v = copy of control points
for(let L = 1; L <= order; L++) {
for(let i=s; i > s + L - order; i--) {
let numerator = t - knots[i]
let denominator = knots[i - L + order] - knots[i]
let alpha = numerator / denominator
let v[i] = alpha * v[i] + (1-alpha) * v[i-1]
}
}
```
(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at `i=s` at each level of the interpolation, and we stop when `i = s - order + level`, so we always end up with a value for `i` such that those `v[i-1]` don't try to use an array index that doesn't exist)
## Open vs. closed paths
Much like poly-Béziers, B-Splines can be either open, running from the first point to the last point, or closed, where the first and last point are *the same point*. However, because B-Splines are an interpolation of curves, not just point, we can't simply make the first and last point the same, we need to link a few point point: for an order `d` B-Spline, we need to make the last `d` point the same as the first `d` points. And the easiest way to do this is to simply append `points.splice(0,d)` to `points`. Done!
Of course if we want to manipulate these kind of curves we need to make sure to mark them as "closed" so that we know the coordinate for `points[0]` and `points[n-k]` etc. are the same coordinate, and manipulating one will equally manipulate the other, but programming generally makes this really easy by storing references to coordinates (or other linked values such as coordinate weights, discussed in the NURBS section) rather than separate coordinate objects.
## Manipulating the curve through the knot vector
The most important thing to understand when it comes to B-Splines is that they work *because* of the concept of a knot vector. As mentioned above, knots represent "where individual control points start/stop influencing the curve", but we never looked at the *values* that go in the knot vector. If you look back at the N() and a() functions, you see that interpolations are based on intervals in the knot vector, rather than the actual values in the knot vector, and we can exploit this to do some pretty interesting things with clever manipulation of the knot vector. Specifically there are four things we can do that are worth looking at:
1. we can use a uniform knot vector, with equally spaced intervals,
2. we can use a non-uniform knot vector, without enforcing equally spaced internvals,
3. we can collapse sequential knots to the same value, locally lowering curve complexity using "null" intervals, and
4. we can form a special case non-uniform vector, by combining (1) and (3) to for a vector with collapsed start and end knots, with a uniform vector in between.
### Uniform B-Splines
The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines *the same intervals*, or even [0,2,3,...,18,20,22], which also defines *the same intervals*, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to the curvature.
<div className="two-column">
<KnotController ref="uniform-spline" />
<BSplineGraphic sketch={this.uniformBSpline} controller={(owner, knots) => this.bindKnots(owner, knots, "uniform-spline")}/>
</div>
This is an important point: the intervals that the knot vector defines are *relative* intervals, so it doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve.
The problem with uniform knot vectors is that, as we need `order` control points before we have any curve with which we can perform interpolation, the curve does not "start" at the first point, nor "ends" at the last point. Instead there are "gaps". We can get rid of these, by being clever about how we apply the following uniformity-breaking approach instead...
### Reducing local curve complexity by collapsing intervals
By collapsing knot intervals by making two or more consecutive knots have the same value, we can reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for ever interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing `order` knots creates a situation where all continuity is lost and the curve "kinks".
<div className="two-column">
<KnotController ref="center-cut-bspline" />
<BSplineGraphic sketch={this.centerCutBSpline} controller={(owner, knots) => this.bindKnots(owner, knots, "center-cut-bspline")}/>
</div>
### Open-Uniform B-Splines
By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we can overcome the problem of the curve not starting and ending where we'd kind of like it to:
For any curve of degree `D` with control points `N`, we can define a knot vector of length `N+D+1` in which the values `0 ... D+1` are the same, the values `D+1 ... N+1` follow the "uniform" pattern, and the values `N+1 ... N+D+1` are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.
<div className="two-column">
<KnotController ref="open-uniform-bspline" />
<BSplineGraphic sketch={this.openUniformBSpline} controller={(owner, knots) => this.bindKnots(owner, knots, "open-uniform-bspline")}/>
</div>
### Non-uniform B-Splines
This is essentialy the "free form" version of a B-Spline, and also the least interesting to look at, as without any specific reason to pick specific knot intervals, there is nothing particularly interesting going on. There is one constraint to the knot vector, and that is that any value `knots[k+1]` should be equal to, or greater than `knots[k]`.
## One last thing: Rational B-Splines
While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a control point carries, the close to that point the spline curve will lie, a bit like turning up the gravity of a control point.
<div className="two-column">
{
// <KnotController ref="rational-uniform-bspline" />
}
<WeightController ref="rational-uniform-bspline-weights" />
<BSplineGraphic scrolling={true} sketch={this.rationalUniformBSpline} controller={(owner, knots, weights, closed) => {
// this.bindKnots(owner, knots, "rational-uniform-bspline");
this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights");
}} />
</div>
Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS are an important type of curve in computer-facilitated design, used a lot in 3D modelling (as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.
While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which has the useful property of starting the curve at the first control point, and ending it at the last.
## Extending our implementation to cover rational splines
The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and the extension to work in the control point weights is fairly simple: we extend each control point from a point in its original number of dimensions (2D, 3D, etc) to one dimension higher, scaling the original dimensions by the control point's weight, and then assigning that weight as its value for the extended dimension.
For example, a 2D point `(x,y)` with weight `w` becomes a 3D point `(w * x, w * y, w)`.
We then run the same algorithm as before, which will automaticall perform weight interpolation in addition to regular coordinate interpolation, because all we've done is pretended we have coordinates in a higher dimension. The algorithm doesn't really care about how many dimensions it needs to interpolate.
In order to recover our "real" curve point, we take the final result of the point generation algorithm, and "unweigh" it: we take the final point's derived weight `w'` and divide all the regular coordinate dimensions by it, then throw away the weight information.
Based on our previous example, we take the final 3D point `(x', y', w')`, which we then turn back into a 2D point by computing `(x'/w', y'/w')`. And that's it, we're done!

View File

@ -0,0 +1,16 @@
module.exports = {
basicSketch: require('./basic-sketch'),
interpolationGraph: require('./interpolation-graph'),
uniformBSpline: require('./uniform-bspline'),
centerCutBSpline: require('./center-cut-bspline'),
openUniformBSpline: require('./open-uniform-bspline'),
rationalUniformBSpline: require('./rational-uniform-bspline'),
bindKnots: function(owner, knots, ref) {
this.refs[ref].bindKnots(owner, knots);
},
bindWeights: function(owner, weights, closed, ref) {
this.refs[ref].bindWeights(owner, weights, closed);
}
};

View File

@ -1,468 +1,3 @@
var React = require("react");
var BSplineGraphic = require("../../BSplineGraphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var KnotController = require("../../KnotController.jsx");
var WeightController = require("../../WeightController.jsx");
var BoundingBox = React.createClass({
getDefaultProps: function() {
return {
title: "B-Splines"
};
},
bindKnots: function(owner, knots, ref) {
this.refs[ref].bindKnots(owner, knots);
},
bindWeights: function(owner, weights, closed, ref) {
this.refs[ref].bindWeights(owner, weights, closed);
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<p>No discussion on Bézier curves is complete without also giving mention of that other
beast in the curve design space: B-Splines. Easily confused to mean Bézier splines, that's
not actually what they are; they are "basis function" splines, which makes a lot of difference,
which we'll be looking at in this section. We're not going to dive as deep into B-Splines as
we have for Bézier curves (that would be an entire primer on its own) but we'll be looking at
how B-Splines work, what kind of maths is involved in computing them, and how to draw them
based on a number of parameters that you can pick for individual B-Splines.</p>
<p>First off: B-Splines are <a href="https://en.wikipedia.org/wiki/Piecewise">"piecewise polynomial interpolation curves"</a>, where the "single curve"
is built by performing polynomial interpolation over a set of points, using a sliding window
of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will
have its curve built by evaluating the polynomial interpolation of four points, and the curve
can be treated as a lot of different sections, each controlled by four points at a time, such
that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5},
..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.</p>
<p>What do they look like? They look like this! .. okay that's an empty graph, but simply
click to place some point, with the stipulation that you need at least four point to see
any curve. More than four points simply draws a longer B-Spline curve:</p>
<BSplineGraphic sketch={require('./basic-sketch')} />
<p>The important part to notice here is that we are <strong>not</strong> doing the same thing
with B-Splines that we do for poly-Béziers or Catmull-Rom curves: both of the latter simply
define new sections as literally "new sections based on new points", so a 12 point cubic
poly-Bézier curve is actually impossible, because we start with a four point curve, and then
add three more points for each section that follows, so we can only have 4, 7, 10, 13, 16, etc
point Poly-Béziers. Similarly, while Catmull-Rom curves can grow by adding single points, this
addition of a single point introduces three implicit Bézier points. Cubic B-Splines, on the other
hand, are smooth interpolations of <em>each possible curve involving four consecutive points</em>,
such that at any point along the curve except for our start and end points, our on-curve
coordinate is defined by four control points.</p>
<p>Consider the difference to be this:</p>
<ul>
<li>for Bézier curves, the curve is defined as an interpolation of points, but:</li>
<li>for B-Splines, the curve is defined as an interpolation of <em>curves</em>.</li>
</ul>
<p>In order to make this interpolation of curves work, the maths is necessarily more complex
than the maths for Bézier curves, so let's have a look at how things work.</p>
<h2>
How to compute a B-Spline curve: some maths
</h2>
<p>
Given a B-Spline of degree <code>d</code> and thus order <code>k=d+1</code> (so a quadratic
B-Spline is degree 2 and order 3, a cubic B-Spline is degree 3 and order 4, etc) and <code>n</code> control
points <code>P<sub>0</sub></code> through <code>P<sub>n-1</sub></code>, we can compute a
point on the curve for some value <code>t</code> in the interval [0,1] (where 0 is the start
of the curve, and 1 the end, just like for Bézier curves), by evaluting the following function:
</p>
<p>\[
Point(t) = \sum^n_{i=0} P_i \cdot N_{i,k}(t)
\]</p>
<p>
Which, honestly, doesn't tell us all that much. All we can see is that a point on a B-Spline curve
is defined as "a mix of all the control points, weighted somehow", where the weighting is achieved
through the <em>N(...)</em> function, subscipted with an obvious parameter <code>i</code>, which
comes from our summation, and some magical parameter <code>k</code>. So we need to know two things:
1. what does N(t) do, and 2. what is that <code>k</code>? Let's cover both, in reverse order.
</p>
<p>The parameter <code>k</code> represents the "knot interval" over which a section of curve is defined.
As we learned earlier, a B-Spline curve is itself an interpoliation of curves, and we can treat each
transition where a control point starts or tops influencing the total curvature as a "knot on the curve".
Doing so for a degree <code>d</code> B-Spline with <code>n</code> control point gives us <code>d + n +
1</code> knots, defining <code>d + n</code> intervals along the curve, and it is these intervals that
the above <code>k</code> subscript to the N() function applies to.</p>
<p>Then the N() function itself. What does it look like?</p>
<p>\[
N_{i,k}(t) = \left ( \frac{t-knot_i}{knot_{(i+k-1)} - knot_i}\right ) \cdot N_{i,k-1}(t) + \left ( \frac{knot_{(i+k)}-t}{knot_{(i+k)} - knot_{(i+1)}} \right ) \cdot N_{i+1,k-1}(t)
\]</p>
<p>
So this is where we see the interpolation: N(t) for an (i,k) pair (that is, for a step in the above summation,
on a specific knot interval) is a mix between N(t) for (i,k-1) and N(t) for (i+1,k-1), so we see that this is
a recursive iteration where <code>i</code> goes up, and <code>k</code> goes down, so it seem reasonable to
expect that this recursion has to stop at some point; obviously, it does, and specifically it does so for
the following <code>i</code>/<code>k</code> values:
</p>
<p>\[
N_{i,1}(t) = \left\{\begin{matrix}
1& \text{if } t \in [knot_i,knot_{i+1}) \\
0& \text{otherwise}
\end{matrix}\right.
\]</p>
<p>
And this function finally has a straight up evaluation: if a <code>t</code> value lies within a knot-specific
interval once we reach a <code>k=1</code> value, it "counts", otherwise it doesn't. We did cheat a little, though,
because for all these values we need to scale our <code>t</code> value first, so that it lies in the interval
bounded by <code>knots[d]</code> and <code>knots[n]</code>, which are the start point and end point where curvature
is controlled by exactly <code>order</code> control points. For instance, for degree 3 (=order 4) and 7 control points,
with knot vector [1,2,3,4,5,6,7,8,9,10,11], we map <code>t</code> from [the interval 0,1] to the interval [4,8],
and then use that value in the functions above, instead.
</p>
<h2>
Can we simplify that?
</h2>
<p>
We can, yes.
</p>
<p>
People far smarter than us have looked at this work, and two in particular <a href="http://www.npl.co.uk/people/maurice-cox">Maurice Cox</a> and <a href="https://en.wikipedia.org/wiki/Carl_R._de_Boor">Carl de Boor</a> came
to a mathematically pleasing solution: to compute a point P(t), we can compute this point by
evaluating <em>d(t)</em> on a curve section between knots <em>i</em> and <em>i+1</em>:
</p>
<p>\[
d^k_i(t) = \alpha_{i,k} \cdot d^{k-1}_i(t) + (1-\alpha_{i,k}) \cdot d^{k-1}_{i-1}(t)
\]</p>
This is another recursive function, with <em>k</em> values decreasing from the curve order to 1,
and the value <em>α</em> (alpha) defined by:
<p>\[
\alpha_{i,k} = \frac{t - knots[i]}{knots[i+1+n-k] - knots[i]}
\]</p>
<p>
That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers
and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha".
Computing this d() function is thus simply a matter of "computing simple arithmetics
but with recursion", which might be computationally expensive because we're doing "a lot of" steps, but
is also computationally cheap because each step only involves very simple maths. Of course as before
the recursion has to stop:
</p>
<p>\[
d^k_0(t) = 0, \ d^0_i(t) = N_{i,1}(t) =
\left\{\begin{matrix}
1& \text{if } t \in [knot_i,knot_{i+1}) \\
0& \text{otherwise}
\end{matrix}\right.
\]</p>
<p>
So, we see two stopping conditions: either <code>i</code> becomes 0, in which case d() is zero,
or <code>k</code> becomes zero, in which case we get the same "either 1 or 0" that we saw in the N()
function above.</p>
<p>
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute
a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:
</p>
<p>\[\begin{array}{ccccccc}
d^3_3 && d^2_3 && d^1_3 && d^0_3 (= 0 \text{ or } 1) \\
&+^{α^3_3 \times }_{(1-{α^3_3}) \times }& &+^{α^2_3 \times }_{(1-{α^2_3}) \times }& &+^{α^1_3 \times }_{(1-{α^1_3}) \times }&\\
&& && && \\
& & d^2_2 && d^1_2 && d^0_2 (= 0 \text{ or } 1) \\
& & &+^{α^2_2 \times }_{(1-{α^2_2}) \times }& &+^{α^1_2 \times }_{(1-{α^1_2}) \times }&\\
& & && && \\
& & & & d^1_1 && d^0_1 (= 0 \text{ or } 1) \\
& & & & &+^{α^1_1 \times }_{(1-{α^1_1}) \times }&\\
& & & & && \\
& & & & & & d^0_0 (= 0)
\end{array}\]</p>
<p>
That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and
we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!
</p>
<p>
One thing we need to keep in mind is that we're working with a spline that is contrained by its control points,
so even though the <code>d(..., k)</code> values are zero or one at the lowest level, they are really "zero or one, times their
respective control point", so in the next section you'll see the algorithm for running through the computation in
a way that starts with a copy of the control point vector and then works its way up to that single point:
that's pretty essential!
</p>
<p>
If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a
few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate
the terminating d() values first, then compute the a() constants, perform our multiplcations, generate the previous
step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the
top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers
we start with and simply update them as we move up the triangle. So, let's implement that!
</p>
<h2>
Cool, cool... but I don't know what to do with that information
</h2>
<p>I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change
the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector
itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points
with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by
one function each) influences the total curvature, given our knot values. And, because exploration is the key to
discovery, let's make the knot vector a thing we can actually manipulate. Normally a proper knot vector has a constraint
that any value is strictly equal to, or larger than the previous ones, but screw it this is programming, let's
ignore that hard restriction and just mess with the knots however we like.</p>
<div className="two-column">
<KnotController ref="interpolation-graph" />
<BSplineGraphic sketch={require('./interpolation-graph')} controller={(owner, knots) => this.bindKnots(owner, knots, "interpolation-graph")}/>
</div>
<p>Changing the values in the knot vector changes how much each point influences the total curvature
(with some clever knot value manipulation, we can even make the influence of certain points disappear
entirely!), so we can see that while the control points define the hull inside of which we're going
to be drawing a curve, it is actually the knot vector that determines the actual <em>shape</em> of
the curve inside that hull.</p>
<p>After reading the rest of this section you may want to come back here to try some specific knot vectors,
and see if the resulting interpolation landscape makes sense given what you will now think should happen!</p>
<h2>
Running the computation
</h2>
<p>
Unlike the de Casteljau algorithm, where the <code>t</code> value stays the same at every iteration, for B-Splines that
is not the case, and so we end having to (for each point we evaluate) run a fairly involving bit of recursive computation.
The algorithm is discussed on <a href="http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/de-Boor.html">this Michigan
Tech</a> page, but an easier to read version is implemented
by <a href="https://github.com/thibauts/b-spline/blob/master/index.js#L59-L71">b-spline.js</a>, so we'll look at its code.
</p>
<p>
Given an input value <code>t</code>, we first map the input to a value from the domain [0,1] to the domain [knots[degree],
knots[knots.length - 1 - degree]. Then, we find the section number <code>s</code> that this mapped <code>t</code> value lies on:
</p>
<pre>
for(s=domain[0]; s < domain[1]; s++) {
if(knots[s] <= t && t <= knots[s+1]) break;
}</pre>
<p>
after running this code, <code>s</code> is the index for the section the point will lie on. We then run the algorithm mentioned on the MU page (updated to use this description's variable names):
</p>
<pre>
let v = copy of control points
for(let L = 1; L <= order; L++) {
for(let i=s; i > s + L - order; i--) {
let numerator = t - knots[i]
let denominator = knots[i - L + order] - knots[i]
let alpha = numerator / denominator
let v[i] = alpha * v[i] + (1-alpha) * v[i-1]
}
}</pre>
<p>
(A nice bit of behaviour in this code is that we work the interpolation "backwards", starting at <code>i=s</code> at
each level of the interpolation, and we stop when <code>i = s - order + level</code>, so we always end up with a
value for <code>i</code> such that those <code>v[i-1]</code> don't try to use an array index that doesn't exist)
</p>
<h2>
Open vs. closed paths
</h2>
<p>
Much like poly-Béziers, B-Splines can be either open, running from the first point to the last point, or closed,
where the first and last point are <em>the same point</em>. However, because B-Splines are an interpolation of
curves, not just point, we can't simply make the first and last point the same, we need to link a few point point:
for an order <code>d</code> B-Spline, we need to make the last <code>d</code> point the same as the first <code>d</code> points.
And the easiest way to do this is to simply append <code>points.splice(0,d)</code> to <code>points</code>. Done!
</p>
<p>
Of course if we want to manipulate these kind of curves we need to make sure to mark them as "closed" so that
we know the coordinate for <code>points[0]</code> and <code>points[n-k]</code> etc. are the same coordinate,
and manipulating one will equally manipulate the other, but programming generally makes this really easy by
storing references to coordinates (or other linked values such as coordinate weights, discussed in the NURBS
section) rather than separate coordinate objects.
</p>
<h2>
Manipulating the curve through the knot vector
</h2>
<p>
The most important thing to understand when it comes to B-Splines is that they work <em>because</em> of the concept
of a knot vector. As mentioned above, knots represent "where individual control points start/stop influencing
the curve", but we never looked at the <em>values</em> that go in the knot vector. If you look back at the N() and
a() functions, you see that interpolations are based on intervals in the knot vector, rather than the actual values
in the knot vector, and we can exploit this to do some pretty interesting things with clever manipulation of the
knot vector. Specifically there are four things we can do that are worth looking at:
</p>
<ol>
<li>we can use a uniform knot vector, with equally spaced intervals,</li>
<li>we can use a non-uniform knot vector, without enforcing equally spaced internvals,</li>
<li>we can collapse sequential knots to the same value, locally lowering curve complexity using "null" intervals, and</li>
<li>we can form a special case non-uniform vector, by combining (1) and (3) to for a vector with collapsed start and end knots, with a uniform vector in between.</li>
</ol>
<h3>Uniform B-Splines</h3>
<p>
The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed
uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform
knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines <em>the same intervals</em>,
or even [0,2,3,...,18,20,22], which also defines <em>the same intervals</em>, just scaled by a constant factor,
which becomes normalised during interpolation and so does not contribute to the curvature.
</p>
<div className="two-column">
<KnotController ref="uniform-spline" />
<BSplineGraphic sketch={require('./uniform-bspline')} controller={(owner, knots) => this.bindKnots(owner, knots, "uniform-spline")}/>
</div>
<p>
This is an important point: the intervals that the knot vector defines are <em>relative</em> intervals, so it
doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what
shapes any particular curve.
</p>
<p>
The problem with uniform knot vectors is that, as we need <code>order</code> control points before we have any
curve with which we can perform interpolation, the curve does not "start" at the first point, nor "ends" at
the last point. Instead there are "gaps". We can get rid of these, by being clever about how we apply
the following uniformity-breaking approach instead...
</p>
<h3>Reducing local curve complexity by collapsing intervals</h3>
<p>
By collapsing knot intervals by making two or more consecutive knots have the same value, we can reduce the
curve complexity in the sections that are affected by the knots involved. This can have drastic effects:
for ever interval collapse, the curve order goes down, and curve continuity goes down, to the point where
collapsing <code>order</code> knots creates a situation where all continuity is lost and the curve "kinks".
</p>
<div className="two-column">
<KnotController ref="center-cut-bspline" />
<BSplineGraphic sketch={require('./center-cut-bspline')} controller={(owner, knots) => this.bindKnots(owner, knots, "center-cut-bspline")}/>
</div>
<h3>Open-Uniform B-Splines</h3>
<p>
By combining knot interval collapsing at the start and end of the curve, with uniform knots in between, we
can overcome the problem of the curve not starting and ending where we'd kind of like it to:
</p>
<p>
For any curve of degree <code>D</code> with control points <code>N</code>, we can define a knot vector of
length <code>N+D+1</code> in which the values <code>0 ... D+1</code> are the same, the values <code>D+1 ... N+1</code> follow
the "uniform" pattern, and the values <code>N+1 ... N+D+1</code> are the same again. For example, a
cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the
"identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.
</p>
<div className="two-column">
<KnotController ref="open-uniform-bspline" />
<BSplineGraphic sketch={require('./open-uniform-bspline')} controller={(owner, knots) => this.bindKnots(owner, knots, "open-uniform-bspline")}/>
</div>
<h3>Non-uniform B-Splines</h3>
<p>This is essentialy the "free form" version of a B-Spline, and also the least interesting to look at,
as without any specific reason to pick specific knot intervals, there is nothing particularly interesting
going on. There is one constraint to the knot vector, and that is that any value <code>knots[k+1]</code>
should be equal to, or greater than <code>knots[k]</code>.</p>
<h2>One last thing: Rational B-Splines</h2>
<p>While it is true that this section on B-Splines is running quite long already, there is one more thing
we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative
weights, of the control points themselves. By introducing a ratio vector with weights to apply to each
control point, we greatly increase our influence over the final curve shape: the more weight a control
point carries, the close to that point the spline curve will lie, a bit like turning up the gravity
of a control point.</p>
<div className="two-column">
{
// <KnotController ref="rational-uniform-bspline" />
}
<WeightController ref="rational-uniform-bspline-weights" />
<BSplineGraphic scrolling={true} sketch={require('./rational-uniform-bspline')} controller={(owner, knots, weights, closed) => {
// this.bindKnots(owner, knots, "rational-uniform-bspline");
this.bindWeights(owner, weights, closed, "rational-uniform-bspline-weights");
}} />
</div>
<p>Of course this brings us to the final topic that any text on B-Splines must touch on before calling it
a day: the NURBS, or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands
for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS are an
important type of curve in computer-facilitated design, used a lot in 3D modelling (as NURBS surfaces)
as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.
</p>
<p>While a true non-uniform rational B-Spline would be hard to work with, when we talk about NURBS we
typically mean the Open-Uniform Rational B-Spline, or OURBS, but that doesn't roll off the tongue nearly
as nicely, and so remember that when people talk about NURBS, they typically mean open-uniform, which
has the useful property of starting the curve at the first control point, and ending it at the last.</p>
<h2>Extending our implementation to cover rational splines</h2>
<p>
The algorithm for working with Rational B-Splines is virtually identical to the regular algorithm, and
the extension to work in the control point weights is fairly simple:
we extend each control point from a point in its original number of dimensions (2D, 3D, etc) to one
dimension higher, scaling the original dimensions by the control point's weight, and then
assigning that weight as its value for the extended dimension.
</p>
<p>
For example, a 2D point <code>(x,y)</code> with weight <code>w</code> becomes
a 3D point <code>(w * x, w * y, w)</code>.
</p>
<p>
We then run the same algorithm as before, which will automaticall perform weight interpolation in
addition to regular coordinate interpolation, because all we've done is pretended we have coordinates
in a higher dimension. The algorithm doesn't really care about how many dimensions it needs to interpolate.
</p>
<p>
In order to recover our "real" curve point, we take the final result of the point generation algorithm,
and "unweigh" it: we take the final point's derived weight <code>w'</code> and divide all the regular
coordinate dimensions by it, then throw away the weight information.
</p>
<p>
Based on our previous example, we take the final 3D point <code>(x', y', w')</code>, which we then
turn back into a 2D point by computing <code>(x'/w', y'/w')</code>. And that's it, we're done!
</p>
</section>
);
}
});
module.exports = BoundingBox;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("bsplines", handler);

View File

@ -0,0 +1,159 @@
module.exports = {
setup: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.reset();
api._map_loaded = false;
},
draw: function(api, curve) {
var w = 400,
h = w,
unit = this.unit,
center = {x:w/2, y:h/2};
api.setSize(w,h);
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.offset.x += 400;
if (api._map_loaded) { api.image(api._map_image); }
else { setTimeout((
function() {
this.drawBase(api, curve);
this.draw(api, curve);
}
).bind(this), 100); }
api.drawLine({x:0,y:0}, {x:0, y:h});
var npts = [
{x:0, y: 0},
{x:0, y: unit},
{x:unit, y: unit},
this.forwardTransform(curve.points, unit)
];
var canonical = new api.Bezier(npts);
api.setColor("blue");
api.drawCurve(canonical, center);
api.drawCircle(npts[3], 3, center);
},
forwardTransform: function(pts, s) {
s = s || 1;
var p1 = pts[0], p2 = pts[1], p3 = pts[2], p4 = pts[3];
var xn = -p1.x + p4.x - (-p1.x+p2.x)*(-p1.y+p4.y)/(-p1.y+p2.y);
var xd = -p1.x + p3.x - (-p1.x+p2.x)*(-p1.y+p3.y)/(-p1.y+p2.y);
var np4x = s*xn/xd;
var yt1 = s*(-p1.y+p4.y) / (-p1.y+p2.y);
var yt2 = s - (s*(-p1.y+p3.y)/(-p1.y+p2.y));
var yp = yt2 * xn / xd;
var np4y = yt1 + yp;
return {x:np4x, y:np4y};
},
drawBase: function(api, curve) {
api.reset();
var w = 400,
h = w,
unit = this.unit = w/5,
center = {x:w/2, y:h/2};
api.setSize(w,h);
// axes + gridlines
api.setColor("lightgrey");
for(var x=0; x<w; x+= unit/2) { api.drawLine({x:x, y:0}, {x:x, y:h}); }
for(var y=0; y<h; y+= unit/2) { api.drawLine({x:0, y:y}, {x:w, y:y}); }
api.setColor("black");
api.drawLine({x:w/2,y:0}, {x:w/2, y:h});
api.drawLine({x:0,y:h/2}, {x:w, y:h/2});
// Inflection border:
api.setColor("green");
api.drawLine({x:-w/2,y:unit}, {x:w/2,y:unit}, center);
// the three stable points
api.setColor("black");
api.setFill("black");
api.drawCircle({x:0, y:0}, 4, center);
api.text("(0,0)", {x: 5+center.x, y:15+center.y});
api.drawCircle({x:0, y:unit}, 4, center);
api.text("(0,1)", {x: 5+center.x, y:unit+15+center.y});
api.drawCircle({x:unit, y:unit}, 4, center);
api.text("(1,1)", {x: unit+5+center.x, y:unit+15+center.y});
// cusp parabola:
api.setWeight(1.5);
api.setColor("#FF0000");
api.setFill(api.getColor());
var pts = [];
var px = 1, py = 1;
for (x=-10; x<=1; x+=0.01) {
y = (-x*x + 2*x + 3)/4;
if (x>-10) {
pts.push({x:unit*px, y:unit*py});
api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center);
}
px = x;
py = y;
}
pts.push({x:unit*px, y:unit*py});
api.text("Curve form has cusp →", {x:w/2-unit*2, y: h/2+unit/2.5});
// loop/arch transition boundary, elliptical section
api.setColor("#FF00FF");
api.setFill(api.getColor());
var sqrt = Math.sqrt;
for (x=1; x>=0; x-=0.005) {
pts.push({x:unit*px, y:unit*py});
y = 0.5 * (sqrt(3) * sqrt(4*x - x*x) - x);
api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center);
px = x;
py = y;
}
pts.push({x:unit*px, y:unit*py});
api.text("← Curve forms a loop at t = 1", {x:w/2+unit/4, y: h/2+unit/1.5});
// loop/arch transition boundary, parabolic section
api.setColor("#3300FF");
api.setFill(api.getColor());
for (x=0; x>-w; x-=0.01) {
pts.push({x:unit*px, y:unit*py});
y = (-x*x + 3*x)/3;
api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center);
px = x;
py = y;
}
pts.push({x:unit*px, y:unit*py});
api.text("← Curve forms a loop at t = 0", {x:w/2-unit+10, y: h/2-unit*1.25});
// shape fill
api.setColor("transparent");
api.setFill("rgba(255,120,100,0.2)");
api.drawPath(pts, center);
pts = [{x:-w/2,y:unit}, {x:w/2,y:unit}, {x:w/2,y:h}, {x:-w/2,y:h}];
api.setFill("rgba(0,200,0,0.2)");
api.drawPath(pts, center);
// further labels
api.setColor("black");
api.setFill(api.getColor());
api.text("← Curve form has one inflection →", {x:w/2 - unit, y: h/2 + unit*1.75});
api.text("← Plain curve ↕", {x:w/2 + unit/2, y: h/6});
api.text("↕ Double inflection", {x:10, y: h/2 - 10});
api._map_image = api.toImage();
api._map_loaded = true;
}
};

View File

@ -1,177 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "canonical";
var Canonical = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.reset();
api._map_loaded = false;
},
draw: function(api, curve) {
var w = 400,
h = w,
unit = this.unit,
center = {x:w/2, y:h/2};
api.setSize(w,h);
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.offset.x += 400;
if (api._map_loaded) { api.image(api._map_image); }
else { setTimeout((
function() {
this.drawBase(api, curve);
this.draw(api, curve);
}
).bind(this), 100); }
api.drawLine({x:0,y:0}, {x:0, y:h});
var npts = [
{x:0, y: 0},
{x:0, y: unit},
{x:unit, y: unit},
this.forwardTransform(curve.points, unit)
];
var canonical = new api.Bezier(npts);
api.setColor("blue");
api.drawCurve(canonical, center);
api.drawCircle(npts[3], 3, center);
},
forwardTransform: function(pts, s) {
s = s || 1;
var p1 = pts[0], p2 = pts[1], p3 = pts[2], p4 = pts[3];
var xn = -p1.x + p4.x - (-p1.x+p2.x)*(-p1.y+p4.y)/(-p1.y+p2.y);
var xd = -p1.x + p3.x - (-p1.x+p2.x)*(-p1.y+p3.y)/(-p1.y+p2.y);
var np4x = s*xn/xd;
var yt1 = s*(-p1.y+p4.y) / (-p1.y+p2.y);
var yt2 = s - (s*(-p1.y+p3.y)/(-p1.y+p2.y));
var yp = yt2 * xn / xd;
var np4y = yt1 + yp;
return {x:np4x, y:np4y};
},
drawBase: function(api, curve) {
api.reset();
var w = 400,
h = w,
unit = this.unit = w/5,
center = {x:w/2, y:h/2};
api.setSize(w,h);
// axes + gridlines
api.setColor("lightgrey");
for(var x=0; x<w; x+= unit/2) { api.drawLine({x:x, y:0}, {x:x, y:h}); }
for(var y=0; y<h; y+= unit/2) { api.drawLine({x:0, y:y}, {x:w, y:y}); }
api.setColor("black");
api.drawLine({x:w/2,y:0}, {x:w/2, y:h});
api.drawLine({x:0,y:h/2}, {x:w, y:h/2});
// Inflection border:
api.setColor("green");
api.drawLine({x:-w/2,y:unit}, {x:w/2,y:unit}, center);
// the three stable points
api.setColor("black");
api.setFill("black");
api.drawCircle({x:0, y:0}, 4, center);
api.text("(0,0)", {x: 5+center.x, y:15+center.y});
api.drawCircle({x:0, y:unit}, 4, center);
api.text("(0,1)", {x: 5+center.x, y:unit+15+center.y});
api.drawCircle({x:unit, y:unit}, 4, center);
api.text("(1,1)", {x: unit+5+center.x, y:unit+15+center.y});
// cusp parabola:
api.setWeight(1.5);
api.setColor("#FF0000");
api.setFill(api.getColor());
var pts = [];
var px = 1, py = 1;
for (x=-10; x<=1; x+=0.01) {
y = (-x*x + 2*x + 3)/4;
if (x>-10) {
pts.push({x:unit*px, y:unit*py});
api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center);
}
px = x;
py = y;
}
pts.push({x:unit*px, y:unit*py});
api.text("Curve form has cusp →", {x:w/2-unit*2, y: h/2+unit/2.5});
// loop/arch transition boundary, elliptical section
api.setColor("#FF00FF");
api.setFill(api.getColor());
var sqrt = Math.sqrt;
for (x=1; x>=0; x-=0.005) {
pts.push({x:unit*px, y:unit*py});
y = 0.5 * (sqrt(3) * sqrt(4*x - x*x) - x);
api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center);
px = x;
py = y;
}
pts.push({x:unit*px, y:unit*py});
api.text("← Curve forms a loop at t = 1", {x:w/2+unit/4, y: h/2+unit/1.5});
// loop/arch transition boundary, parabolic section
api.setColor("#3300FF");
api.setFill(api.getColor());
for (x=0; x>-w; x-=0.01) {
pts.push({x:unit*px, y:unit*py});
y = (-x*x + 3*x)/3;
api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center);
px = x;
py = y;
}
pts.push({x:unit*px, y:unit*py});
api.text("← Curve forms a loop at t = 0", {x:w/2-unit+10, y: h/2-unit*1.25});
// shape fill
api.setColor("transparent");
api.setFill("rgba(255,120,100,0.2)");
api.drawPath(pts, center);
pts = [{x:-w/2,y:unit}, {x:w/2,y:unit}, {x:w/2,y:h}, {x:-w/2,y:h}];
api.setFill("rgba(0,200,0,0.2)");
api.drawPath(pts, center);
// further labels
api.setColor("black");
api.setFill(api.getColor());
api.text("← Curve form has one inflection →", {x:w/2 - unit, y: h/2 + unit*1.75});
api.text("← Plain curve ↕", {x:w/2 + unit/2, y: h/6});
api.text("↕ Double inflection", {x:10, y: h/2 - 10});
api._map_image = api.toImage();
api._map_loaded = true;
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Canonical;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("canonical", handler);

View File

@ -1,19 +1,2 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "catmullconv";
var CatmullRomConversion = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = CatmullRomConversion;
var generateBase = require("../../generate-base");
module.exports = generateBase("catmullconv");

View File

@ -0,0 +1,131 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
setup: function(api) {
api.setPanelCount(3);
api.lpts = [
{x:56, y:153},
{x:144,y:83},
{x:188,y:185}
];
api.distance = 0;
},
convert: function(p1, p2, p3, p4) {
var t = 0.5;
return [
p2, {
x: p2.x + (p3.x-p1.x)/(6*t),
y: p2.y + (p3.y-p1.y)/(6*t)
}, {
x: p3.x - (p4.x-p2.x)/(6*t),
y: p3.y - (p4.y-p2.y)/(6*t)
}, p3
];
},
draw: function(api) {
api.reset();
api.setColor("lightblue");
api.drawGrid(10,10);
var pts = api.lpts;
api.setColor("black");
api.setFill("black");
pts.forEach((p,pos) => {
api.drawCircle(p, 3);
api.text("point "+(pos+1), p, {x:10, y:7});
});
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var offset = {x:w, y:0};
api.setColor("lightblue");
api.drawGrid(10,10,offset);
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
pts.forEach((p,pos) => {
api.drawCircle(p, 3, offset);
});
var p1 = pts[0], p2 = pts[1], p3 = pts[2];
var dx = p3.x - p1.x,
dy = p3.y - p1.y,
m = Math.sqrt(dx*dx + dy*dy);
dx /= m;
dy /= m;
api.drawLine(p1, p3, offset);
var p0 = {
x: p1.x + (p3.x - p2.x) - api.distance * dx,
y: p1.y + (p3.y - p2.y) - api.distance * dy
};
var p4 = {
x: p1.x + (p3.x - p2.x) + api.distance * dx,
y: p1.y + (p3.y - p2.y) + api.distance * dy
};
var center = api.utils.lli4(p1,p3,p2,{
x: (p0.x + p4.x)/2,
y: (p0.y + p4.y)/2
});
api.setColor("blue");
api.drawCircle(center, 3, offset);
api.drawLine(pts[1],center, offset);
api.setColor("#666");
api.drawLine(center, p0, offset);
api.drawLine(center, p4, offset);
api.setFill("blue");
api.text("p0", p0, {x:-20 + offset.x, y:offset.y + 2});
api.text("p4", p4, {x:+10 + offset.x, y:offset.y + 2});
// virtual point p0
api.setColor("red");
api.drawCircle(p0, 3, offset);
api.drawLine(p2, p0, offset);
api.drawLine(p1, {
x: p1.x + (p2.x - p0.x)/5,
y: p1.y + (p2.y - p0.y)/5
}, offset);
// virtual point p4
api.setColor("#00FF00");
api.drawCircle(p4, 3, offset);
api.drawLine(p2, p4, offset);
api.drawLine(p3, {
x: p3.x + (p4.x - p2.x)/5,
y: p3.y + (p4.y - p2.y)/5
}, offset);
// Catmull-Rom curve for p0-p1-p2-p3-p4
var c1 = new api.Bezier(this.convert(p0,p1,p2,p3)),
c2 = new api.Bezier(this.convert(p1,p2,p3,p4));
api.setColor("lightgrey");
api.drawCurve(c1, offset);
api.drawCurve(c2, offset);
offset.x += w;
api.setColor("lightblue");
api.drawGrid(10,10,offset);
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawCurve(c1, offset);
api.drawCurve(c2, offset);
api.drawPoints(c1.points, offset);
api.drawPoints(c2.points, offset);
api.setColor("lightgrey");
api.drawLine(c1.points[0], c1.points[1], offset);
api.drawLine(c1.points[2], c2.points[1], offset);
api.drawLine(c2.points[2], c2.points[3], offset);
}
};

View File

@ -1,151 +1,4 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "catmullmoulding";
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var CatmullRomMoulding = React.createClass({
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
api.setPanelCount(3);
api.lpts = [
{x:56, y:153},
{x:144,y:83},
{x:188,y:185}
];
api.distance = 0;
},
convert: function(p1, p2, p3, p4) {
var t = 0.5;
return [
p2, {
x: p2.x + (p3.x-p1.x)/(6*t),
y: p2.y + (p3.y-p1.y)/(6*t)
}, {
x: p3.x - (p4.x-p2.x)/(6*t),
y: p3.y - (p4.y-p2.y)/(6*t)
}, p3
];
},
draw: function(api) {
api.reset();
api.setColor("lightblue");
api.drawGrid(10,10);
var pts = api.lpts;
api.setColor("black");
api.setFill("black");
pts.forEach((p,pos) => {
api.drawCircle(p, 3);
api.text("point "+(pos+1), p, {x:10, y:7});
});
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var offset = {x:w, y:0};
api.setColor("lightblue");
api.drawGrid(10,10,offset);
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
pts.forEach((p,pos) => {
api.drawCircle(p, 3, offset);
});
var p1 = pts[0], p2 = pts[1], p3 = pts[2];
var dx = p3.x - p1.x,
dy = p3.y - p1.y,
m = Math.sqrt(dx*dx + dy*dy);
dx /= m;
dy /= m;
api.drawLine(p1, p3, offset);
var p0 = {
x: p1.x + (p3.x - p2.x) - api.distance * dx,
y: p1.y + (p3.y - p2.y) - api.distance * dy
};
var p4 = {
x: p1.x + (p3.x - p2.x) + api.distance * dx,
y: p1.y + (p3.y - p2.y) + api.distance * dy
};
var center = api.utils.lli4(p1,p3,p2,{
x: (p0.x + p4.x)/2,
y: (p0.y + p4.y)/2
});
api.setColor("blue");
api.drawCircle(center, 3, offset);
api.drawLine(pts[1],center, offset);
api.setColor("#666");
api.drawLine(center, p0, offset);
api.drawLine(center, p4, offset);
api.setFill("blue");
api.text("p0", p0, {x:-20 + offset.x, y:offset.y + 2});
api.text("p4", p4, {x:+10 + offset.x, y:offset.y + 2});
// virtual point p0
api.setColor("red");
api.drawCircle(p0, 3, offset);
api.drawLine(p2, p0, offset);
api.drawLine(p1, {
x: p1.x + (p2.x - p0.x)/5,
y: p1.y + (p2.y - p0.y)/5
}, offset);
// virtual point p4
api.setColor("#00FF00");
api.drawCircle(p4, 3, offset);
api.drawLine(p2, p4, offset);
api.drawLine(p3, {
x: p3.x + (p4.x - p2.x)/5,
y: p3.y + (p4.y - p2.y)/5
}, offset);
// Catmull-Rom curve for p0-p1-p2-p3-p4
var c1 = new api.Bezier(this.convert(p0,p1,p2,p3)),
c2 = new api.Bezier(this.convert(p1,p2,p3,p4));
api.setColor("lightgrey");
api.drawCurve(c1, offset);
api.drawCurve(c2, offset);
offset.x += w;
api.setColor("lightblue");
api.drawGrid(10,10,offset);
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawCurve(c1, offset);
api.drawCurve(c2, offset);
api.drawPoints(c1.points, offset);
api.drawPoints(c2.points, offset);
api.setColor("lightgrey");
api.drawLine(c1.points[0], c1.points[1], offset);
api.drawLine(c1.points[2], c2.points[1], offset);
api.drawLine(c2.points[2], c2.points[3], offset);
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = keyHandling(CatmullRomMoulding);
module.exports = keyHandling(generateBase("catmullmoulding", handler));

View File

@ -0,0 +1,125 @@
# Circles and quadratic Bézier curves
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?
You approximate.
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.
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.
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:
<Graphic preset="arcfitting" title="Quadratic Bézier arc approximation" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
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.
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 *φ*:
\[
S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix}
\]
What we want to find is the intersection of the tangents, so we want a point C such that:
\[
C = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix} \ , \ \ C = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
\]
i.e. we want a point that lies on the vertical line through S (at some distance *a* from S) and also lies on the tangent line through E (at some distance *b* from E). Solving this gives us:
\[
\left\{ \begin{array}{l}
C_x = 1 = cos(φ) + b \cdot -sin(φ)\\
C_y = a = sin(φ) + b \cdot cos(φ)
\end{array} \right.
\]
First we solve for *b*:
\[
\begin{array}{l}
1 = cos(φ) + b \cdot -sin(φ) \ → \
1 - cos(φ) = -b \cdot sin(φ) \ → \
-1 + cos(φ) = b \cdot sin(φ)
\end{array}
\]
which yields:
\[
b = \frac{cos(φ)-1}{sin(φ)}
\]
which we can then substitute in the expression for *a*:
\[
\begin{aligned}
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{aligned}
\]
A quick check shows that plugging these values for *a* and *b* into the expressions for C<sub>x</sub> and C<sub>y</sub> give the same x/y coordinates for both "*a* away from A" and "*b* 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 *t=0.5* (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_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
\]
We compute T, observing that if *t=0.5*, the polynomial values (1-t)², 2(1-t)t, and t² are 0.25, 0.5, and 0.25 respectively:
\[
T = \frac{1}{4}S + \frac{2}{4}C + \frac{1}{4}E = \frac{1}{4}(S + 2C + E)
\]
Which, worked out for the x and y components, gives:
\[
\begin{array}{l}
\left\{\begin{aligned}
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{aligned}\right.
\end{array}
\]
And the distance between these two is the standard Euclidean distance:
\[
\begin{aligned}
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{aligned}
\]
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?
<table><tbody><tr><td>
<img src="images/arc-q-pi.gif" height="190px"/>
plotted for 0 ≤ φ ≤ π:
</td><td>
<img src="images/arc-q-pi2.gif" height="187px"/>
plotted for 0 ≤ φ ≤ ½π:
</td><td>
{ this.props.showhref ? "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" : null }
<img src="images/arc-q-pi4.gif" height="174px"/>
plotted for 0 ≤ φ ≤ ¼π:
</td></tr></tbody></table>
We now see why the eighth circle arc looks decent, but the quarter circle arc doesn't: an error of roughly 0.06 at *t=0.5* 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!
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:
\[
φ = 4 \cdot arccos \left(\frac{\sqrt{2+ε-\sqrt{ε(2+ε)}}}{\sqrt{2}}\right)
\]
And frankly, things are starting to look a bit ridiculous at this point, we're doing way more maths than we've ever done, but thankfully this is as far as we need the maths to take us: If we plug in the precisions 0.1, 0.01, 0.001 and 0.0001 we get the radians values 1.748, 1.038, 0.594 and 0.3356; in degrees, that means we can cover roughly 100 degrees (requiring four curves), 59.5 degrees (requiring six curves), 34 degrees (requiring 11 curves), and 19.2 degrees (requiring a whopping nineteen curves).
The bottom line? **Quadratic curves are kind of lousy** 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.

View File

@ -0,0 +1,57 @@
var sin = Math.sin,
cos = Math.cos;
module.exports = {
setup: function(api) {
api.w = api.getPanelWidth();
api.h = api.getPanelHeight();
api.pad = 20;
api.r = api.w/2 - api.pad;
api.mousePt = false;
api.angle = 0;
var spt = { x: api.w-api.pad, y: api.h/2 };
api.setCurve(new api.Bezier(spt, spt, spt));
},
draw: function(api, curve) {
api.reset();
api.setColor("lightgrey");
api.drawGrid(1,1);
api.setColor("red");
api.drawCircle({x:api.w/2,y:api.h/2},api.r);
api.setColor("transparent");
api.setFill("rgba(100,255,100,0.4)");
var p = {
x: api.w/2,
y: api.h/2,
r: api.r,
s: api.angle < 0 ? api.angle : 0,
e: api.angle < 0 ? 0 : api.angle
};
api.drawArc(p);
api.setColor("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
},
onMouseMove: function(evt, api) {
var x = evt.offsetX - api.w/2,
y = evt.offsetY - api.h/2;
var angle = Math.atan2(y,x);
var pts = api.curve.points;
// new control
var r = api.r,
b = (cos(angle) - 1) / sin(angle);
pts[1] = {
x: api.w/2 + r * (cos(angle) - b * sin(angle)),
y: api.w/2 + r * (sin(angle) + b * cos(angle))
};
// new endpoint
pts[2] = {
x: api.w/2 + api.r * cos(angle),
y: api.w/2 + api.r * sin(angle)
};
api.setCurve(new api.Bezier(pts));
api.angle = angle;
}
};

View File

@ -1,235 +1,3 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var sin = Math.sin, cos = Math.cos;
var Circles = React.createClass({
getDefaultProps: function() {
return {
title: "Circles and quadratic Bézier curves"
};
},
setup: function(api) {
api.w = api.getPanelWidth();
api.h = api.getPanelHeight();
api.pad = 20;
api.r = api.w/2 - api.pad;
api.mousePt = false;
api.angle = 0;
var spt = { x: api.w-api.pad, y: api.h/2 };
api.setCurve(new api.Bezier(spt, spt, spt));
},
draw: function(api, curve) {
api.reset();
api.setColor("lightgrey");
api.drawGrid(1,1);
api.setColor("red");
api.drawCircle({x:api.w/2,y:api.h/2},api.r);
api.setColor("transparent");
api.setFill("rgba(100,255,100,0.4)");
var p = {
x: api.w/2,
y: api.h/2,
r: api.r,
s: api.angle < 0 ? api.angle : 0,
e: api.angle < 0 ? 0 : api.angle
};
api.drawArc(p);
api.setColor("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
},
onMouseMove: function(evt, api) {
var x = evt.offsetX - api.w/2,
y = evt.offsetY - api.h/2;
var angle = Math.atan2(y,x);
var pts = api.curve.points;
// new control
var r = api.r,
b = (cos(angle) - 1) / sin(angle);
pts[1] = {
x: api.w/2 + r * (cos(angle) - b * sin(angle)),
y: api.w/2 + r * (sin(angle) + b * cos(angle))
};
// new endpoint
pts[2] = {
x: api.w/2 + api.r * cos(angle),
y: api.w/2 + api.r * sin(angle)
};
api.setCurve(new api.Bezier(pts));
api.angle = angle;
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<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>
<Graphic preset="arcfitting" title="Quadratic Bézier arc approximation" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
<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 S (at some distance <i>a</i> from S)
and also lies on the tangent line through E (at some distance <i>b</i> from E). 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(φ)
\end{array} \]</p>
<p>which yields:</p>
<p>\[
b = \frac{cos(φ)-1}{sin(φ)}
\]</p>
<p>which we can then substitute in the expression for <i>a</i>:</p>
<p>\[ \begin{aligned}
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{aligned} \]</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 t²
are 0.25, 0.5, and 0.25 respectively:</p>
<p>\[
T = \frac{1}{4}S + \frac{2}{4}C + \frac{1}{4}E = \frac{1}{4}(S + 2C + E)
\]</p>
<p>Which, worked out for the x and y components, gives:</p>
<p>\[\begin{array}{l}
\left\{\begin{aligned}
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{aligned}\right.
\end{array}\]</p>
<p>And the distance between these two is the standard Euclidean distance:</p>
<p>\[\begin{aligned}
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{aligned}\]</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><tbody><tr><td>
<p><img src="images/arc-q-pi.gif" height="190px"/></p>
<p>plotted for 0 φ π:</p>
</td><td>
<p><img src="images/arc-q-pi2.gif" height="187px"/></p>
<p>plotted for 0 φ ½π:</p>
</td><td>
{ this.props.showhref ? "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" : null }
<p><img src="images/arc-q-pi4.gif" height="174px"/></p>
<p>plotted for 0 φ ¼π:</p>
</td></tr></tbody></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>And frankly, things are starting to look a bit ridiculous at this point, we're doing way more
maths than we've ever done, but thankfully this is as far as we need the maths to take us:
If we plug in the precisions 0.1, 0.01, 0.001 and 0.0001 we get the radians values 1.748, 1.038, 0.594
and 0.3356; in degrees, that means we can cover roughly 100 degrees (requiring four curves),
59.5 degrees (requiring six curves), 34 degrees (requiring 11 curves), and 19.2 degrees (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>
</section>
);
}
});
module.exports = Circles;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("circles", handler);

View File

@ -0,0 +1,180 @@
# Circles and cubic Bézier curves
In the previous section we tried to approximate a circular arc with a quadratic curve, and it mostly made us unhappy. Cubic curves are much better suited to this task, so what do we need to do?
For cubic curves, we basically want the curve to pass through three points on the circle: the start point, the mid point at "angle/2", and the end point at "angle". We then also need to make sure the control points are such that the start and end tangent lines line up with the circle's tangent lines at the start and end point.
The first thing we can do is "guess" what the curve should look like, based on the previously outlined curve-through-three-points procedure. This will give use a curve with correct start, mid and end points, but possibly incorrect derivatives at the start and end, because the control points might not be in the right spot. We can then slide the control points along the lines that connect them to their respective end point, until they effect the corrected derivative at the start and end points. However, if you look back at the section on fitting curves through three points, the rules used were such that they optimized for a near perfect hemisphere, so using the same guess won't be all that useful: guessing the solution based on knowing the solution is not really guessing.
So have a graphical look at a "bad" guess versus the true fit, where we'll be using the bad guess and the description in the second paragraph to derive the maths for the true fit:
<Graphic preset="arcfitting" title="Cubic Bézier arc approximation" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
We see two curves here; in blue, our "guessed" curve and its control points, and in grey/black, the true curve fit, with proper control points that were shifted in, along line between our guessed control points, such that the derivatives at the start and end points are correct.
We can already seethat cubic curves are a lot better than quadratic curves, and don't look all that wrong until we go well 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!
So, maths time again: how okay is "okay"? Let's apply some more maths to find out.
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.
So instead of walking you through the derivation for that value, 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π:
<table><tbody><tr><td>
<img src="images/arc-c-2pi.gif" height="187px"/>
plotted for 0 ≤ φ ≤ 2π:
</td><td>
<img src="images/arc-c-pi.gif" height="187px"/>
plotted for 0 ≤ φ ≤ π:
</td><td>
<img src="images/arc-c-pi2.gif" height="187px"/>
plotted for 0 ≤ φ ≤ ½π:
</td></tr></tbody></table>
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 so many people that it's generally considered "just fine" to use four cubic Bézier curves to fake a full circle when no circle primitives are available; generally, people won't notice that it's not a real circle unless you also happen to overlay an actual circle, so that the difference becomes obvious.
So with the error analysis out of the way, how do we actually compute the coordinates needed to get that "true fit" cubic curve? The first observation is that we already know the start and end points, because they're the same as for the quadratic attempt:
\[ S = \begin{pmatrix} 1 \\ 0 \end{pmatrix} \ , \ \ E = \begin{pmatrix} cos(φ) \\ sin(φ) \end{pmatrix} \]
But we now need to find two control points, rather than one. If we want the derivatives at the start and end point to match the circle, then the first control point can only lie somewhere on the vertical line through S, and the second control point can only lie somewhere on the line tangent to point E, which means:
\[
C_1 = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix}
\]
where "a" is some scaling factor, and:
\[
C_2 = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
\]
where "b" is also some scaling factor.
Starting with this information, we slowly maths our way to success, but I won't lie: the maths for this is pretty trig-heavy, and it's easy to get lost if you remember (or know!) some of the core trigonoetric identities, so if you just want to see the final result just skip past the next section!
<div className="note">
## Let's do this thing.
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.
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.
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}:
\[
||{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
\]
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_x = cos(\frac{φ}{2}) \ , \ \ P_y = sin(\frac{φ}{2})
\]
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:
\[
\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}
\]
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}:
\[
\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.
\]
\[
\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.
\]
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>:
\[
\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.
\]
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:
\[
\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}
\]
And that's it, we have all four points now for an approximation of an arbitrary circular arc with angle φ.
</div>
So, to recap, given an angle φ, the new control coordinates are:
\[
C_1 = \left [ \begin{matrix}
1 \\
f
\end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
\]
and
\[
C_2 = \left [ \begin{matrix}
cos(φ) + f \cdot sin(φ) \\
sin(φ) - f \cdot cos(φ)
\end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
\]
And, because the "quarter curve" special case comes up so incredibly often, let's look at what these new control points mean for the curve coordinates of a quarter curve, by simply filling in φ = π/2:
\[
\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}
\]
Which, in decimal values, rounded to six significant digits, is:
\[
\begin{array}{l}
S = (1, 0) \ , \
C_1 = (1, 0.55228) \ , \
C_2 = (0.55228 , 1) \ , \
E = (0, 1)
\end{array}
\]
Of course, 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 then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin:
<Graphic preset="simple" title="Cubic Bézier circle approximation" draw={this.drawCircle} static={true}/>

View File

@ -0,0 +1,205 @@
var sin = Math.sin, cos = Math.cos, tan = Math.tan;
module.exports = {
setup: function(api) {
api.setSize(400,400);
api.w = api.getPanelWidth();
api.h = api.getPanelHeight();
api.pad = 80;
api.r = api.w/2 - api.pad;
api.mousePt = false;
api.angle = 0;
var spt = { x: api.w-api.pad, y: api.h/2 };
api.setCurve(new api.Bezier(spt, spt, spt, spt));
},
guessCurve: function(S, B, E) {
var C = {
x: (S.x + E.x)/2,
y: (S.y + E.y)/2
},
A = {
x: B.x + (B.x-C.x)/3, // cubic ratio at t=0.5 is 1/3
y: B.y + (B.y-C.y)/3
},
bx = (E.x-S.x)/4,
by = (E.y-S.y)/4,
e1 = {
x: B.x - bx,
y: B.y - by
},
e2 = {
x: B.x + bx,
y: B.y + by
},
v1 = {
x: A.x + (e1.x-A.x)*2,
y: A.y + (e1.y-A.y)*2
},
v2 = {
x: A.x + (e2.x-A.x)*2,
y: A.y + (e2.y-A.y)*2
},
nc1 = {
x: S.x + (v1.x-S.x)*2,
y: S.y + (v1.y-S.y)*2
},
nc2 = {
x: E.x + (v2.x-E.x)*2,
y: E.y + (v2.y-E.y)*2
};
return [nc1, nc2];
},
draw: function(api, curve) {
api.reset();
api.setColor("lightgrey");
api.drawGrid(1,1);
api.setColor("rgba(255,0,0,0.4)");
api.drawCircle({x:api.w/2,y:api.h/2},api.r);
api.setColor("transparent");
api.setFill("rgba(100,255,100,0.4)");
var p = {
x: api.w/2,
y: api.h/2,
r: api.r,
s: api.angle < 0 ? api.angle : 0,
e: api.angle < 0 ? 0 : api.angle
};
api.drawArc(p);
// guessed curve
var B = {
x: api.w/2 + api.r * cos(api.angle/2),
y: api.w/2 + api.r * sin(api.angle/2)
};
var S = curve.points[0],
E = curve.points[3],
nc = this.guessCurve(S,B,E);
var guess = new api.Bezier([S, nc[0], nc[1], E]);
api.setColor("rgb(140,140,255)");
api.drawLine(guess.points[0], guess.points[1]);
api.drawLine(guess.points[1], guess.points[2]);
api.drawLine(guess.points[2], guess.points[3]);
api.setColor("blue");
api.drawCurve(guess);
api.drawCircle(guess.points[1], 3);
api.drawCircle(guess.points[2], 3);
// real curve
api.drawSkeleton(curve);
api.setColor("black");
api.drawLine(curve.points[1], curve.points[2]);
api.drawCurve(curve);
},
onMouseMove: function(evt, api) {
var x = evt.offsetX - api.w/2,
y = evt.offsetY - api.h/2;
if (x>api.w/2) return;
var angle = Math.atan2(y,x);
if (angle < 0) {
angle = 2*Math.PI + angle;
}
var pts = api.curve.points;
// new control 1
var r = api.r,
f = (4 * tan(angle/4)) /3;
pts[1] = {
x: api.w/2 + r,
y: api.w/2 + r * f
};
// new control 2
pts[2] = {
x: api.w/2 + api.r * (cos(angle) + f*sin(angle)),
y: api.w/2 + api.r * (sin(angle) - f*cos(angle))
};
// new endpoint
pts[3] = {
x: api.w/2 + api.r * cos(angle),
y: api.w/2 + api.r * sin(angle)
};
api.setCurve(new api.Bezier(pts));
api.angle = angle;
},
drawCircle: function(api) {
api.setSize(325,325);
api.reset();
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
pad = 60,
r = w/2 - pad,
k = 0.55228,
offset = {x: -pad/2, y:-pad/4};
var curve = new api.Bezier([
{x:w/2 + r, y:h/2},
{x:w/2 + r, y:h/2 + k*r},
{x:w/2 + k*r, y:h/2 + r},
{x:w/2, y:h/2 + r}
]);
api.setColor("lightgrey");
api.drawLine({x:0,y:h/2}, {x:w+pad,y:h/2}, offset);
api.drawLine({x:w/2,y:0}, {x:w/2,y:h+pad}, offset);
var pts = curve.points;
api.setColor("red");
api.drawPoint(pts[0], offset);
api.drawPoint(pts[1], offset);
api.drawPoint(pts[2], offset);
api.drawPoint(pts[3], offset);
api.drawCurve(curve, offset);
api.setColor("rgb(255,160,160)");
api.drawLine(pts[0],pts[1],offset);
api.drawLine(pts[1],pts[2],offset);
api.drawLine(pts[2],pts[3],offset);
api.setFill("red");
api.text((pts[0].x - w/2) + "," + (pts[0].y - h/2), {x: pts[0].x + 7, y: pts[0].y + 3}, offset);
api.text((pts[1].x - w/2) + "," + (pts[1].y - h/2), {x: pts[1].x + 7, y: pts[1].y + 3}, offset);
api.text((pts[2].x - w/2) + "," + (pts[2].y - h/2), {x: pts[2].x + 7, y: pts[2].y + 7}, offset);
api.text((pts[3].x - w/2) + "," + (pts[3].y - h/2), {x: pts[3].x, y: pts[3].y + 13}, offset);
pts.forEach(p => { p.x = -(p.x - w); });
api.setColor("blue");
api.drawCurve(curve, offset);
api.drawLine(pts[2],pts[3],offset);
api.drawPoint(pts[2],offset);
api.setFill("blue");
api.text("reflected", {x: pts[2].x - pad/2, y: pts[2].y + 13}, offset);
api.setColor("rgb(200,200,255)");
api.drawLine(pts[1],pts[0],offset);
api.drawPoint(pts[1],offset);
pts.forEach(p => { p.y = -(p.y - h); });
api.setColor("green");
api.drawCurve(curve, offset);
pts.forEach(p => { p.x = -(p.x - w); });
api.setColor("purple");
api.drawCurve(curve, offset);
api.drawLine(pts[1],pts[0],offset);
api.drawPoint(pts[1],offset);
api.setFill("purple");
api.text("reflected", {x: pts[1].x + 10, y: pts[1].y + 3}, offset);
api.setColor("rgb(200,200,255)");
api.drawLine(pts[2],pts[3],offset);
api.drawPoint(pts[2],offset);
api.setColor("black");
api.setFill("black");
api.drawLine({x:w/2, y:h/2}, {x:w/2 + r -2, y:h/2}, offset);
api.drawLine({x:w/2, y:h/2}, {x:w/2, y:h/2 + r -2}, offset);
api.text("r = " + r, {x:w/2 + r/3, y:h/2 + 10}, offset);
}
};

View File

@ -1,456 +1,3 @@
var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var sin = Math.sin, cos = Math.cos, tan = Math.tan;
var CirclesCubic = React.createClass({
getDefaultProps: function() {
return {
title: "Circles and cubic Bézier curves"
};
},
setup: function(api) {
api.setSize(400,400);
api.w = api.getPanelWidth();
api.h = api.getPanelHeight();
api.pad = 80;
api.r = api.w/2 - api.pad;
api.mousePt = false;
api.angle = 0;
var spt = { x: api.w-api.pad, y: api.h/2 };
api.setCurve(new api.Bezier(spt, spt, spt, spt));
},
guessCurve: function(S, B, E) {
var C = {
x: (S.x + E.x)/2,
y: (S.y + E.y)/2
},
A = {
x: B.x + (B.x-C.x)/3, // cubic ratio at t=0.5 is 1/3
y: B.y + (B.y-C.y)/3
},
bx = (E.x-S.x)/4,
by = (E.y-S.y)/4,
e1 = {
x: B.x - bx,
y: B.y - by
},
e2 = {
x: B.x + bx,
y: B.y + by
},
v1 = {
x: A.x + (e1.x-A.x)*2,
y: A.y + (e1.y-A.y)*2
},
v2 = {
x: A.x + (e2.x-A.x)*2,
y: A.y + (e2.y-A.y)*2
},
nc1 = {
x: S.x + (v1.x-S.x)*2,
y: S.y + (v1.y-S.y)*2
},
nc2 = {
x: E.x + (v2.x-E.x)*2,
y: E.y + (v2.y-E.y)*2
};
return [nc1, nc2];
},
draw: function(api, curve) {
api.reset();
api.setColor("lightgrey");
api.drawGrid(1,1);
api.setColor("rgba(255,0,0,0.4)");
api.drawCircle({x:api.w/2,y:api.h/2},api.r);
api.setColor("transparent");
api.setFill("rgba(100,255,100,0.4)");
var p = {
x: api.w/2,
y: api.h/2,
r: api.r,
s: api.angle < 0 ? api.angle : 0,
e: api.angle < 0 ? 0 : api.angle
};
api.drawArc(p);
// guessed curve
var B = {
x: api.w/2 + api.r * cos(api.angle/2),
y: api.w/2 + api.r * sin(api.angle/2)
};
var S = curve.points[0],
E = curve.points[3],
nc = this.guessCurve(S,B,E);
var guess = new api.Bezier([S, nc[0], nc[1], E]);
api.setColor("rgb(140,140,255)");
api.drawLine(guess.points[0], guess.points[1]);
api.drawLine(guess.points[1], guess.points[2]);
api.drawLine(guess.points[2], guess.points[3]);
api.setColor("blue");
api.drawCurve(guess);
api.drawCircle(guess.points[1], 3);
api.drawCircle(guess.points[2], 3);
// real curve
api.drawSkeleton(curve);
api.setColor("black");
api.drawLine(curve.points[1], curve.points[2]);
api.drawCurve(curve);
},
onMouseMove: function(evt, api) {
var x = evt.offsetX - api.w/2,
y = evt.offsetY - api.h/2;
if (x>api.w/2) return;
var angle = Math.atan2(y,x);
if (angle < 0) {
angle = 2*Math.PI + angle;
}
var pts = api.curve.points;
// new control 1
var r = api.r,
f = (4 * tan(angle/4)) /3;
pts[1] = {
x: api.w/2 + r,
y: api.w/2 + r * f
};
// new control 2
pts[2] = {
x: api.w/2 + api.r * (cos(angle) + f*sin(angle)),
y: api.w/2 + api.r * (sin(angle) - f*cos(angle))
};
// new endpoint
pts[3] = {
x: api.w/2 + api.r * cos(angle),
y: api.w/2 + api.r * sin(angle)
};
api.setCurve(new api.Bezier(pts));
api.angle = angle;
},
drawCircle: function(api) {
api.setSize(325,325);
api.reset();
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
pad = 60,
r = w/2 - pad,
k = 0.55228,
offset = {x: -pad/2, y:-pad/4};
var curve = new api.Bezier([
{x:w/2 + r, y:h/2},
{x:w/2 + r, y:h/2 + k*r},
{x:w/2 + k*r, y:h/2 + r},
{x:w/2, y:h/2 + r}
]);
api.setColor("lightgrey");
api.drawLine({x:0,y:h/2}, {x:w+pad,y:h/2}, offset);
api.drawLine({x:w/2,y:0}, {x:w/2,y:h+pad}, offset);
var pts = curve.points;
api.setColor("red");
api.drawPoint(pts[0], offset);
api.drawPoint(pts[1], offset);
api.drawPoint(pts[2], offset);
api.drawPoint(pts[3], offset);
api.drawCurve(curve, offset);
api.setColor("rgb(255,160,160)");
api.drawLine(pts[0],pts[1],offset);
api.drawLine(pts[1],pts[2],offset);
api.drawLine(pts[2],pts[3],offset);
api.setFill("red");
api.text((pts[0].x - w/2) + "," + (pts[0].y - h/2), {x: pts[0].x + 7, y: pts[0].y + 3}, offset);
api.text((pts[1].x - w/2) + "," + (pts[1].y - h/2), {x: pts[1].x + 7, y: pts[1].y + 3}, offset);
api.text((pts[2].x - w/2) + "," + (pts[2].y - h/2), {x: pts[2].x + 7, y: pts[2].y + 7}, offset);
api.text((pts[3].x - w/2) + "," + (pts[3].y - h/2), {x: pts[3].x, y: pts[3].y + 13}, offset);
pts.forEach(p => { p.x = -(p.x - w); });
api.setColor("blue");
api.drawCurve(curve, offset);
api.drawLine(pts[2],pts[3],offset);
api.drawPoint(pts[2],offset);
api.setFill("blue");
api.text("reflected", {x: pts[2].x - pad/2, y: pts[2].y + 13}, offset);
api.setColor("rgb(200,200,255)");
api.drawLine(pts[1],pts[0],offset);
api.drawPoint(pts[1],offset);
pts.forEach(p => { p.y = -(p.y - h); });
api.setColor("green");
api.drawCurve(curve, offset);
pts.forEach(p => { p.x = -(p.x - w); });
api.setColor("purple");
api.drawCurve(curve, offset);
api.drawLine(pts[1],pts[0],offset);
api.drawPoint(pts[1],offset);
api.setFill("purple");
api.text("reflected", {x: pts[1].x + 10, y: pts[1].y + 3}, offset);
api.setColor("rgb(200,200,255)");
api.drawLine(pts[2],pts[3],offset);
api.drawPoint(pts[2],offset);
api.setColor("black");
api.setFill("black");
api.drawLine({x:w/2, y:h/2}, {x:w/2 + r -2, y:h/2}, offset);
api.drawLine({x:w/2, y:h/2}, {x:w/2, y:h/2 + r -2}, offset);
api.text("r = " + r, {x:w/2 + r/3, y:h/2 + 10}, offset);
},
render: function() {
return (
<section>
<SectionHeader {...this.props} />
<p>In the previous section we tried to approximate a circular arc with a quadratic curve,
and it mostly made us unhappy. Cubic curves are much better suited to this task, so what
do we need to do?</p>
<p>For cubic curves, we basically want the curve to pass through three points on the circle:
the start point, the mid point at "angle/2", and the end point at "angle". We then also need
to make sure the control points are such that the start and end tangent lines line up with the
circle's tangent lines at the start and end point.</p>
<p>The first thing we can do is "guess" what the curve should look like, based on the previously
outlined curve-through-three-points procedure. This will give use a curve with correct start, mid
and end points, but possibly incorrect derivatives at the start and end, because the control points
might not be in the right spot. We can then slide the control points along the lines that connect
them to their respective end point, until they effect the corrected derivative at the start and
end points. However, if you look back at the section on fitting curves through three points, the
rules used were such that they optimized for a near perfect hemisphere, so using the same guess
won't be all that useful: guessing the solution based on knowing the solution is not really guessing.</p>
<p>So have a graphical look at a "bad" guess versus the true fit, where we'll be using the
bad guess and the description in the second paragraph to derive the maths for the true fit:</p>
<Graphic preset="arcfitting" title="Cubic Bézier arc approximation" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
<p>We see two curves here; in blue, our "guessed" curve and its control points, and in grey/black,
the true curve fit, with proper control points that were shifted in, along line between our guessed
control points, such that the derivatives at the start and end points are correct.</p>
<p>We can already seethat cubic curves are a lot better than quadratic curves, and don't look all
that wrong until we go well 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!</p>
<p>So, maths time 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 of walking you through the derivation for that value, 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><tbody><tr><td>
<p><img src="images/arc-c-2pi.gif" height="187px"/></p>
<p>plotted for 0 φ 2π:</p>
</td><td>
<p><img src="images/arc-c-pi.gif" height="187px"/></p>
<p>plotted for 0 φ π:</p>
</td><td>
<p><img src="images/arc-c-pi2.gif" height="187px"/></p>
<p>plotted for 0 φ ½π:</p>
</td></tr></tbody></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!</p>
<p>In fact, the precision of a cubic curve at a quarter circle is considered "good enough" by
so many people that it's generally considered "just fine" to use four cubic Bézier curves to
fake a full circle when no circle primitives are available; generally, people won't notice
that it's not a real circle unless you also happen to overlay an actual circle, so that
the difference becomes obvious.</p>
<p>So with the error analysis out of the way, how do we actually compute the coordinates
needed to get that "true fit" cubic curve? The first observation is that we already know
the start and end points, because they're the same as for the quadratic attempt:</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. If we want the derivatives
at the start and end point to match the circle, then the first control point can only lie
somewhere on the vertical line through S, and the second control point can only lie somewhere
on the line tangent to point E, which means:</p>
<p>\[
C_1 = S + a \cdot \begin{pmatrix} 0 \\ 1 \end{pmatrix}
\]</p>
<p>where "a" is some scaling factor, and:</p>
<p>\[
C_2 = E + b \cdot \begin{pmatrix} -sin(φ) \\ cos(φ) \end{pmatrix}
\]</p>
<p>where "b" is also some scaling factor.</p>
<p>Starting with this information, we slowly maths our way to success, but I won't lie: the maths for
this is pretty trig-heavy, and it's easy to get lost if you remember (or know!) some of the core
trigonoetric identities, so if you just want to see the final result just skip past the next section!</p>
<div className="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>So, to recap, given an angle φ, the new control coordinates are:</p>
<p>\[
C_1 = \left [ \begin{matrix}
1 \\
f
\end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
\]</p>
<p>and</p>
<p>\[
C_2 = \left [ \begin{matrix}
cos(φ) + f \cdot sin(φ) \\
sin(φ) - f \cdot cos(φ)
\end{matrix} \right ],\ with\ f = \frac{4}{3} tan \left( \frac{φ}{4} \right)
\]</p>
<p>And, because the "quarter curve" special case comes up so incredibly often, let's look at what
these new control points mean for the curve coordinates of a quarter curve, by simply filling in
φ = π/2:</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>Of course, 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 then finally, forming a full curve
is now a simple a matter of mirroring these coordinates about the origin:</p>
<Graphic preset="simple" title="Cubic Bézier circle approximation" draw={this.drawCircle} static={true}/>
</section>
);
}
});
module.exports = CirclesCubic;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("circles_cubic", handler);

View File

@ -0,0 +1,39 @@
module.exports = {
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[2].x = 210;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.setPanelCount(3);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var tf = curve.order + 1,
pad = 20,
pts = curve.points,
w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = { x: w, y: 0 };
var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; });
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "x",0,w, offset);
offset.x += pad;
api.drawCurve(new api.Bezier(x_pts), offset);
offset.x += w-pad;
var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; });
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "y",0,w, offset);
offset.x += pad;
api.drawCurve(new api.Bezier(y_pts), offset);
}
};

View File

@ -1,57 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "components";
var Components = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[2].x = 210;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.setPanelCount(3);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var tf = curve.order + 1,
pad = 20,
pts = curve.points,
w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = { x: w, y: 0 };
var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; });
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "x",0,w, offset);
offset.x += pad;
api.drawCurve(new api.Bezier(x_pts), offset);
offset.x += w-pad;
var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; });
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "y",0,w, offset);
offset.x += pad;
api.drawCurve(new api.Bezier(y_pts), offset);
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Components;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("components", handler);

View File

@ -0,0 +1,159 @@
module.exports = {
drawCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
drawCurve: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
},
drawFunction: function(api, label, where, generator) {
api.setRandomColor();
api.drawFunction(generator);
api.setFill(api.getColor());
if (label) api.text(label, where);
},
drawLerpBox: function(api, dim, pad, p) {
api.noColor();
api.setFill("rgba(0,0,100,0.2)");
var p1 = {x: p.x-5, y:pad},
p2 = {x:p.x + 5, y:dim};
api.drawRect(p1, p2);
api.setColor("black");
},
drawLerpPoint: function(api, tf, pad, fwh, p) {
p.y = pad + tf*fwh;
api.drawCircle(p, 3);
api.setFill("black");
api.text(((tf*10000)|0)/100 + "%", {x:p.x+10, y:p.y+4});
api.noFill();
},
drawQuadraticLerp: function(api) {
api.reset();
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var p = api.hover;
if (p && p.x >= pad && p.x <= dim-pad) {
this.drawLerpBox(api, dim, pad, p);
var t = (p.x-pad)/fwh;
this.drawLerpPoint(api, (1-t)*(1-t), pad, fwh, p);
this.drawLerpPoint(api, 2*(1-t)*(t), pad, fwh, p);
this.drawLerpPoint(api, (t)*(t), pad, fwh, p);
}
this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (1-t) * (1-t)
};
});
this.drawFunction(api, "second term", {x: dim/2 - 1.5*pad, y: dim/2 + pad}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * 2 * (1-t) * (t)
};
});
this.drawFunction(api, "third term", {x: fwh - pad*2.5, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (t) * (t)
};
});
},
drawCubicLerp: function(api) {
api.reset();
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var p = api.hover;
if (p && p.x >= pad && p.x <= dim-pad) {
this.drawLerpBox(api, dim, pad, p);
var t = (p.x-pad)/fwh;
this.drawLerpPoint(api, (1-t)*(1-t)*(1-t), pad, fwh, p);
this.drawLerpPoint(api, 3*(1-t)*(1-t)*(t), pad, fwh, p);
this.drawLerpPoint(api, 3*(1-t)*(t)*(t), pad, fwh, p);
this.drawLerpPoint(api, (t)*(t)*(t), pad, fwh, p);
}
this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (1-t) * (1-t) * (1-t)
};
});
this.drawFunction(api, "second term", {x: dim/2 - 4*pad, y: dim/2 }, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * 3 * (1-t) * (1-t) * (t)
};
});
this.drawFunction(api, "third term", {x: dim/2 + 2*pad, y: dim/2}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * 3 * (1-t) * (t) * (t)
};
});
this.drawFunction(api, "fourth term", {x: fwh - pad*2.5, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (t) * (t) * (t)
};
});
},
draw15thLerp: function(api) {
api.reset();
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var factors = [1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1];
var p = api.hover, n;
if (p && p.x >= pad && p.x <= dim-pad) {
this.drawLerpBox(api, dim, pad, p);
for(n=0; n<=15; n++) {
var t = (p.x-pad)/fwh,
tf = factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n);
this.drawLerpPoint(api, tf, pad, fwh, p);
}
}
for(n=0; n<=15; n++) {
var label = false, position = false;
if (n===0) {
label = "first term";
position = {x: pad + 5, y: fwh};
}
if (n===15) {
label = "last term";
position = {x: dim - 3.5*pad, y: fwh};
}
this.drawFunction(api, label, position, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n)
};
});
}
}
};

View File

@ -1,177 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "control";
var Control = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
drawCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
drawCurve: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
},
drawFunction: function(api, label, where, generator) {
api.setRandomColor();
api.drawFunction(generator);
api.setFill(api.getColor());
if (label) api.text(label, where);
},
drawLerpBox: function(api, dim, pad, p) {
api.noColor();
api.setFill("rgba(0,0,100,0.2)");
var p1 = {x: p.x-5, y:pad},
p2 = {x:p.x + 5, y:dim};
api.drawRect(p1, p2);
api.setColor("black");
},
drawLerpPoint: function(api, tf, pad, fwh, p) {
p.y = pad + tf*fwh;
api.drawCircle(p, 3);
api.setFill("black");
api.text(((tf*10000)|0)/100 + "%", {x:p.x+10, y:p.y+4});
api.noFill();
},
drawQuadraticLerp: function(api) {
api.reset();
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var p = api.hover;
if (p && p.x >= pad && p.x <= dim-pad) {
this.drawLerpBox(api, dim, pad, p);
var t = (p.x-pad)/fwh;
this.drawLerpPoint(api, (1-t)*(1-t), pad, fwh, p);
this.drawLerpPoint(api, 2*(1-t)*(t), pad, fwh, p);
this.drawLerpPoint(api, (t)*(t), pad, fwh, p);
}
this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (1-t) * (1-t)
};
});
this.drawFunction(api, "second term", {x: dim/2 - 1.5*pad, y: dim/2 + pad}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * 2 * (1-t) * (t)
};
});
this.drawFunction(api, "third term", {x: fwh - pad*2.5, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (t) * (t)
};
});
},
drawCubicLerp: function(api) {
api.reset();
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var p = api.hover;
if (p && p.x >= pad && p.x <= dim-pad) {
this.drawLerpBox(api, dim, pad, p);
var t = (p.x-pad)/fwh;
this.drawLerpPoint(api, (1-t)*(1-t)*(1-t), pad, fwh, p);
this.drawLerpPoint(api, 3*(1-t)*(1-t)*(t), pad, fwh, p);
this.drawLerpPoint(api, 3*(1-t)*(t)*(t), pad, fwh, p);
this.drawLerpPoint(api, (t)*(t)*(t), pad, fwh, p);
}
this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (1-t) * (1-t) * (1-t)
};
});
this.drawFunction(api, "second term", {x: dim/2 - 4*pad, y: dim/2 }, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * 3 * (1-t) * (1-t) * (t)
};
});
this.drawFunction(api, "third term", {x: dim/2 + 2*pad, y: dim/2}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * 3 * (1-t) * (t) * (t)
};
});
this.drawFunction(api, "fourth term", {x: fwh - pad*2.5, y: fwh}, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * (t) * (t) * (t)
};
});
},
draw15thLerp: function(api) {
api.reset();
var dim = api.getPanelWidth(),
pad = 20,
fwh = dim - pad*2;
api.drawAxes(pad, "t",0,1, "S","0%","100%");
var factors = [1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1];
var p = api.hover, n;
if (p && p.x >= pad && p.x <= dim-pad) {
this.drawLerpBox(api, dim, pad, p);
for(n=0; n<=15; n++) {
var t = (p.x-pad)/fwh,
tf = factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n);
this.drawLerpPoint(api, tf, pad, fwh, p);
}
}
for(n=0; n<=15; n++) {
var label = false, position = false;
if (n===0) {
label = "first term";
position = {x: pad + 5, y: fwh};
}
if (n===15) {
label = "last term";
position = {x: dim - 3.5*pad, y: fwh};
}
this.drawFunction(api, label, position, function(t) {
return {
x: pad + t * fwh,
y: pad + fwh * factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n)
};
});
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Control;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("control", handler);

View File

@ -0,0 +1,117 @@
var abs = Math.abs;
module.exports = {
setup: function(api) {
this.api = api;
api.setPanelCount(3);
var curve1 = new api.Bezier(10,100,90,30,40,140,220,220);
var curve2 = new api.Bezier(5,150,180,20,80,250,210,190);
api.setCurve(curve1, curve2);
this.pairReset();
},
pairReset: function() {
this.prevstep = 0;
this.step = 0;
},
draw: function(api, curves) {
api.reset();
var offset = {x:0, y:0};
curves.forEach(curve => {
api.drawSkeleton(curve);
api.drawCurve(curve);
});
// next panel: iterations
var w = api.getPanelWidth();
var h = api.getPanelHeight();
offset.x += w;
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
if (this.step === 0) {
this.pairs = [{c1: curves[0], c2: curves[1]}];
}
if(this.step !== this.prevstep) {
var pairs = this.pairs;
this.pairs = [];
this.finals = [];
pairs.forEach(p => {
if(p.c1.length() < 0.6 && p.c2.length() < 0.6) {
return this.finals.push(p);
}
var s1 = p.c1.split(0.5);
api.setColor("black");
api.drawCurve(p.c1, offset);
api.setColor("red");
api.drawbbox(s1.left.bbox(), offset);
api.drawbbox(s1.right.bbox(), offset);
var s2 = p.c2.split(0.5);
api.setColor("black");
api.drawCurve(p.c2, offset);
api.setColor("blue");
api.drawbbox(s2.left.bbox(), offset);
api.drawbbox(s2.right.bbox(), offset);
if (s1.left.overlaps(s2.left)) { this.pairs.push({c1: s1.left, c2: s2.left}); }
if (s1.left.overlaps(s2.right)) { this.pairs.push({c1: s1.left, c2: s2.right}); }
if (s1.right.overlaps(s2.left)) { this.pairs.push({c1: s1.right, c2: s2.left}); }
if (s1.right.overlaps(s2.right)) { this.pairs.push({c1: s1.right, c2: s2.right}); }
});
this.prevstep = this.step;
} else {
this.pairs.forEach(p => {
api.setColor("black");
api.drawCurve(p.c1, offset);
api.drawCurve(p.c2, offset);
api.setColor("red");
api.drawbbox(p.c1.bbox(), offset);
api.setColor("blue");
api.drawbbox(p.c2.bbox(), offset);
});
}
if (this.pairs.length === 0) {
this.pairReset();
this.draw(api, curves);
}
// next panel: results
offset.x += w;
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
// get intersections as coordinates
var results = curves[0].intersects(curves[1]).map(s => {
var tvals = s.split('/').map(v => parseFloat(v));
return {t1: tvals[0], t2: tvals[1]};
});
// filter out likely duplicates
var curr = results[0], _, i, same = ((a,b) => abs(a.t1-b.t1) < 0.01 && abs(a.t2-b.t2) < 0.01);
for(i=1; i<results.length; i++) {
_ = results[i];
if (same(curr, _)) {
results.splice(i--,1);
} else { curr = _; }
}
api.setColor("lightblue");
api.drawCurve(curves[0], offset);
api.drawCurve(curves[1], offset);
api.setColor("blue");
results.forEach(tvals => {
api.drawCircle(curves[0].get(tvals.t1), 3, offset);
});
},
stepUp: function() {
this.step++;
this.api.redraw();
}
};

View File

@ -1,135 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "curveintersection";
var abs = Math.abs;
var CurveIntersections = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
this.api = api;
api.setPanelCount(3);
var curve1 = new api.Bezier(10,100,90,30,40,140,220,220);
var curve2 = new api.Bezier(5,150,180,20,80,250,210,190);
api.setCurve(curve1, curve2);
this.pairReset();
},
pairReset: function() {
this.prevstep = 0;
this.step = 0;
},
draw: function(api, curves) {
api.reset();
var offset = {x:0, y:0};
curves.forEach(curve => {
api.drawSkeleton(curve);
api.drawCurve(curve);
});
// next panel: iterations
var w = api.getPanelWidth();
var h = api.getPanelHeight();
offset.x += w;
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
if (this.step === 0) {
this.pairs = [{c1: curves[0], c2: curves[1]}];
}
if(this.step !== this.prevstep) {
var pairs = this.pairs;
this.pairs = [];
this.finals = [];
pairs.forEach(p => {
if(p.c1.length() < 0.6 && p.c2.length() < 0.6) {
return this.finals.push(p);
}
var s1 = p.c1.split(0.5);
api.setColor("black");
api.drawCurve(p.c1, offset);
api.setColor("red");
api.drawbbox(s1.left.bbox(), offset);
api.drawbbox(s1.right.bbox(), offset);
var s2 = p.c2.split(0.5);
api.setColor("black");
api.drawCurve(p.c2, offset);
api.setColor("blue");
api.drawbbox(s2.left.bbox(), offset);
api.drawbbox(s2.right.bbox(), offset);
if (s1.left.overlaps(s2.left)) { this.pairs.push({c1: s1.left, c2: s2.left}); }
if (s1.left.overlaps(s2.right)) { this.pairs.push({c1: s1.left, c2: s2.right}); }
if (s1.right.overlaps(s2.left)) { this.pairs.push({c1: s1.right, c2: s2.left}); }
if (s1.right.overlaps(s2.right)) { this.pairs.push({c1: s1.right, c2: s2.right}); }
});
this.prevstep = this.step;
} else {
this.pairs.forEach(p => {
api.setColor("black");
api.drawCurve(p.c1, offset);
api.drawCurve(p.c2, offset);
api.setColor("red");
api.drawbbox(p.c1.bbox(), offset);
api.setColor("blue");
api.drawbbox(p.c2.bbox(), offset);
});
}
if (this.pairs.length === 0) {
this.pairReset();
this.draw(api, curves);
}
// next panel: results
offset.x += w;
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
// get intersections as coordinates
var results = curves[0].intersects(curves[1]).map(s => {
var tvals = s.split('/').map(v => parseFloat(v));
return {t1: tvals[0], t2: tvals[1]};
});
// filter out likely duplicates
var curr = results[0], _, i, same = ((a,b) => abs(a.t1-b.t1) < 0.01 && abs(a.t2-b.t2) < 0.01);
for(i=1; i<results.length; i++) {
_ = results[i];
if (same(curr, _)) {
results.splice(i--,1);
} else { curr = _; }
}
api.setColor("lightblue");
api.drawCurve(curves[0], offset);
api.drawCurve(curves[1], offset);
api.setColor("blue");
results.forEach(tvals => {
api.drawCircle(curves[0].get(tvals.t1), 3, offset);
});
},
stepUp: function() {
this.step++;
this.api.redraw();
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = CurveIntersections;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("curveintersection", handler);

View File

@ -0,0 +1,36 @@
module.exports = {
setup: function(api) {
var points = [
{x: 90, y:110},
{x: 25, y: 40},
{x:230, y: 40},
{x:150, y:240}
];
api.setCurve(new api.Bezier(points));
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
if (api.hover) {
api.setColor("rgb(200,100,100)");
var dim = api.getPanelWidth();
var t = api.hover.x / dim;
var hull = api.drawHull(curve, t);
for(var i=4; i<=8; i++) {
api.drawCircle(hull[i],3);
}
var p = curve.get(t);
api.drawCircle(p, 5);
api.setFill("black");
api.drawCircle(p, 3);
var perc = (t*100)|0;
t = perc/100;
api.text("Sequential interpolation for "+perc+"% (t="+t+")", {x: 10, y:15});
}
}
};

View File

@ -1,56 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "decasteljau";
var deCasteljau = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
var points = [
{x: 90, y:110},
{x: 25, y: 40},
{x:230, y: 40},
{x:150, y:240}
];
api.setCurve(new api.Bezier(points));
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
if (api.hover) {
api.setColor("rgb(200,100,100)");
var dim = api.getPanelWidth();
var t = api.hover.x / dim;
var hull = api.drawHull(curve, t);
for(var i=4; i<=8; i++) {
api.drawCircle(hull[i],3);
}
var p = curve.get(t);
api.drawCircle(p, 5);
api.setFill("black");
api.drawCircle(p, 3);
var perc = (t*100)|0;
t = perc/100;
api.text("Sequential interpolation for "+perc+"% (t="+t+")", {x: 10, y:15});
}
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = deCasteljau;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("decasteljau", handler);

View File

@ -1,19 +1,2 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "derivatives";
var Derivatives = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Derivatives;
var generateBase = require("../../generate-base");
module.exports = generateBase("derivatives");

View File

@ -0,0 +1,52 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "step",
values: {
"38": 0.1, // up arrow
"40": -0.1 // down arrow
},
controller: function(api) {
if (api.step < 0.1) {
api.step = 0.1;
}
}
}
},
setup: function(api) {
api.step = 5;
},
draw: function(api, curve) {
var dim = api.getPanelWidth(),
w = dim,
h = dim,
w2 = w/2,
h2 = h/2,
w4 = w2/2,
h4 = h2/2;
api.reset();
api.setColor("black");
api.drawLine({x:0,y:h2},{x:w,y:h2});
api.drawLine({x:w2,y:0},{x:w2,y:h});
var offset = {x:w2, y:h2};
for(var t=0, p; t<=api.step; t+=0.1) {
p = {
x: w4 * Math.cos(t),
y: h4 * Math.sin(t)
};
api.drawPoint(p, offset);
var modulo = t % 1;
if(modulo<0.05 || modulo> 0.95) {
api.text("t = " + Math.round(t), {
x: offset.x + 1.25 * w4 * Math.cos(t) - 10,
y: offset.y + 1.25 * h4 * Math.sin(t) + 5
});
api.drawCircle(p, 2, offset);
}
}
}
};

View File

@ -1,73 +1,4 @@
var React = require("react");
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "explanation";
var Explanation = React.createClass({
statics: {
keyHandlingOptions: {
propName: "step",
values: {
"38": 0.1, // up arrow
"40": -0.1 // down arrow
},
controller: function(api) {
if (api.step < 0.1) {
api.step = 0.1;
}
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
api.step = 5;
},
draw: function(api, curve) {
var dim = api.getPanelWidth(),
w = dim,
h = dim,
w2 = w/2,
h2 = h/2,
w4 = w2/2,
h4 = h2/2;
api.reset();
api.setColor("black");
api.drawLine({x:0,y:h2},{x:w,y:h2});
api.drawLine({x:w2,y:0},{x:w2,y:h});
var offset = {x:w2, y:h2};
for(var t=0, p; t<=api.step; t+=0.1) {
p = {
x: w4 * Math.cos(t),
y: h4 * Math.sin(t)
};
api.drawPoint(p, offset);
var modulo = t % 1;
if(modulo<0.05 || modulo> 0.95) {
api.text("t = " + Math.round(t), {
x: offset.x + 1.25 * w4 * Math.cos(t) - 10,
y: offset.y + 1.25 * h4 * Math.sin(t) + 5
});
api.drawCircle(p, 2, offset);
}
}
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = keyHandling(Explanation);
module.exports = keyHandling(generateBase("explanation", handler));

View File

@ -0,0 +1,34 @@
module.exports = {
setupQuadratic: function(api) {
var curve = new api.Bezier(70, 155, 20, 110, 100,75);
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = new api.Bezier(60,105, 75,30, 215,115, 140,160);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("lightgrey");
var t, step=0.05, min=-10;
var pt = curve.get(min - step), pn;
for (t=min; t<=step; t+=step) {
pn = curve.get(t);
api.drawLine(pt, pn);
pt = pn;
}
pt = curve.get(1);
var max = 10;
for (t=1+step; t<=max; t+=step) {
pn = curve.get(t);
api.drawLine(pt, pn);
pt = pn;
}
}
};

View File

@ -1,54 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "extended";
var Explanation = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = new api.Bezier(70, 155, 20, 110, 100,75);
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = new api.Bezier(60,105, 75,30, 215,115, 140,160);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("lightgrey");
var t, step=0.05, min=-10;
var pt = curve.get(min - step), pn;
for (t=min; t<=step; t+=step) {
pn = curve.get(t);
api.drawLine(pt, pn);
pt = pn;
}
pt = curve.get(1);
var max = 10;
for (t=1+step; t<=max; t+=step) {
pn = curve.get(t);
api.drawLine(pt, pn);
pt = pn;
}
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = Explanation;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("extended", handler);

View File

@ -0,0 +1,53 @@
module.exports = {
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[2].x = 210;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.setPanelCount(3);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var tf = curve.order + 1,
pad = 20,
pts = curve.points,
w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = { x: w, y: 0 };
var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; });
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "x",0,w, offset);
offset.x += pad;
var xcurve = new api.Bezier(x_pts);
api.drawCurve(xcurve, offset);
api.setColor("red");
xcurve.extrema().y.forEach(t => {
var p = xcurve.get(t);
api.drawCircle(p, 3, offset);
});
offset.x += w-pad;
var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; });
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "y",0,w, offset);
offset.x += pad;
var ycurve = new api.Bezier(y_pts);
api.drawCurve(ycurve, offset);
api.setColor("red");
ycurve.extrema().y.forEach(t => {
var p = ycurve.get(t);
api.drawCircle(p, 3, offset);
});
}
};

View File

@ -1,71 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "extremities";
var Extremities = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
curve.points[2].x = 210;
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.setPanelCount(3);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var tf = curve.order + 1,
pad = 20,
pts = curve.points,
w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = { x: w, y: 0 };
var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; });
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "x",0,w, offset);
offset.x += pad;
var xcurve = new api.Bezier(x_pts);
api.drawCurve(xcurve, offset);
api.setColor("red");
xcurve.extrema().y.forEach(t => {
var p = xcurve.get(t);
api.drawCircle(p, 3, offset);
});
offset.x += w-pad;
var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; });
api.setColor("black");
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "y",0,w, offset);
offset.x += pad;
var ycurve = new api.Bezier(y_pts);
api.drawCurve(ycurve, offset);
api.setColor("red");
ycurve.extrema().y.forEach(t => {
var p = ycurve.get(t);
api.drawCircle(p, 3, offset);
});
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Extremities;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("extremities", handler);

View File

@ -0,0 +1,62 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 3;
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 5;
},
drawFlattened: function(api, curve) {
api.reset();
api.setColor("#DDD");
api.drawSkeleton(curve);
api.setColor("#DDD");
api.drawCurve(curve);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
api.setFill("black");
api.text("Curve approximation using "+api.steps+" segments", {x:10, y:15});
},
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
onKeyDown: function(e, api) {
var v = this.values[e.keyCode];
if(v) {
e.preventDefault();
api.steps += v;
if (api.steps < 1) {
api.steps = 1;
}
}
}
};

View File

@ -1,84 +1,4 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "flattening";
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var Flattening = React.createClass({
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.steps = 3;
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 5;
},
drawFlattened: function(api, curve) {
api.reset();
api.setColor("#DDD");
api.drawSkeleton(curve);
api.setColor("#DDD");
api.drawCurve(curve);
var step = 1 / api.steps;
var p0 = curve.points[0], pc;
for(var t=step; t<1.0+step; t+=step) {
pc = curve.get(Math.min(t,1));
api.setColor("red");
api.drawLine(p0,pc);
p0 = pc;
}
api.setFill("black");
api.text("Curve approximation using "+api.steps+" segments", {x:10, y:15});
},
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
onKeyDown: function(e, api) {
var v = this.values[e.keyCode];
if(v) {
e.preventDefault();
api.steps += v;
if (api.steps < 1) {
api.steps = 1;
}
}
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = keyHandling(Flattening);
module.exports = keyHandling(generateBase("flattening", handler));

View File

@ -0,0 +1,37 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
setup: function(api, curve) {
api.setCurve(curve);
api.distance = 20;
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
this.setup(api, curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
this.setup(api, curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("blue");
var outline = curve.outline(0,0,api.distance,api.distance);
outline.curves.forEach(c => api.drawCurve(c));
}
};

View File

@ -1,57 +1,4 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "graduatedoffset";
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var GraduatedOffsetting = React.createClass({
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api, curve) {
api.setCurve(curve);
api.distance = 20;
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
this.setup(api, curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
this.setup(api, curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("blue");
var outline = curve.outline(0,0,api.distance,api.distance);
outline.curves.forEach(c => api.drawCurve(c));
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = keyHandling(GraduatedOffsetting);
module.exports = keyHandling(generateBase("graduatedoffset", handler));

View File

@ -0,0 +1,17 @@
module.exports = {
setupCubic: function(api) {
var curve = new api.Bezier(135,25, 25, 135, 215,75, 215,240);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("red");
curve.inflections().forEach(function(t) {
api.drawCircle(curve.get(t), 5);
});
}
};

View File

@ -1,36 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "inflections";
var ABC = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupCubic: function(api) {
var curve = new api.Bezier(135,25, 25, 135, 215,75, 215,240);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("red");
curve.inflections().forEach(function(t) {
api.drawCircle(curve.get(t), 5);
});
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = ABC;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("graduatedoffset", handler);

View File

@ -0,0 +1,75 @@
var min = Math.min, max = Math.max;
module.exports = {
setupLines: function(api) {
var curve1 = new api.Bezier([50,50,150,110]);
var curve2 = new api.Bezier([50,250,170,170]);
api.setCurve(curve1, curve2);
},
drawLineIntersection: function(api, curves) {
api.reset();
var lli = api.utils.lli4;
var p = lli(
curves[0].points[0],
curves[0].points[1],
curves[1].points[0],
curves[1].points[1]
);
var mark = 0;
curves.forEach(curve => {
api.drawSkeleton(curve);
api.setColor("black");
if (p) {
var pts = curve.points,
mx = min(pts[0].x, pts[1].x),
my = min(pts[0].y, pts[1].y),
Mx = max(pts[0].x, pts[1].x),
My = max(pts[0].y, pts[1].y);
if (mx <= p.x && my <= p.y && Mx >= p.x && My >= p.y) {
api.setColor("#00FF00");
mark++;
}
}
api.drawCurve(curve);
});
if (p) {
api.setColor(mark < 2 ? "red" : "#00FF00");
api.drawCircle(p, 3);
}
},
setupQuadratic: function(api) {
var curve1 = api.getDefaultQuadratic();
var curve2 = new api.Bezier([15,250,220,20]);
api.setCurve(curve1, curve2);
},
setupCubic: function(api) {
var curve1 = new api.Bezier([100,240, 30,60, 210,230, 160,30]);
var curve2 = new api.Bezier([25,260, 230,20]);
api.setCurve(curve1, curve2);
},
draw: function(api, curves) {
api.reset();
curves.forEach(curve => {
api.drawSkeleton(curve);
api.drawCurve(curve);
});
var utils = api.utils;
var line = { p1: curves[1].points[0], p2: curves[1].points[1] };
var acpts = utils.align(curves[0].points, line);
var nB = new api.Bezier(acpts);
var roots = utils.roots(nB.points);
roots.forEach(t => {
var p = curves[0].get(t);
api.drawCircle(p, 3);
api.text("t = " + t, {x: p.x + 5, y: p.y + 10});
});
}
};

View File

@ -1,93 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "intersections";
var min = Math.min, max = Math.max;
var Intersections = React.createClass({
getDefaultProps: function() {
return {
title: "Intersections"
};
},
setupLines: function(api) {
var curve1 = new api.Bezier([50,50,150,110]);
var curve2 = new api.Bezier([50,250,170,170]);
api.setCurve(curve1, curve2);
},
drawLineIntersection: function(api, curves) {
api.reset();
var lli = api.utils.lli4;
var p = lli(
curves[0].points[0],
curves[0].points[1],
curves[1].points[0],
curves[1].points[1]
);
var mark = 0;
curves.forEach(curve => {
api.drawSkeleton(curve);
api.setColor("black");
if (p) {
var pts = curve.points,
mx = min(pts[0].x, pts[1].x),
my = min(pts[0].y, pts[1].y),
Mx = max(pts[0].x, pts[1].x),
My = max(pts[0].y, pts[1].y);
if (mx <= p.x && my <= p.y && Mx >= p.x && My >= p.y) {
api.setColor("#00FF00");
mark++;
}
}
api.drawCurve(curve);
});
if (p) {
api.setColor(mark < 2 ? "red" : "#00FF00");
api.drawCircle(p, 3);
}
},
setupQuadratic: function(api) {
var curve1 = api.getDefaultQuadratic();
var curve2 = new api.Bezier([15,250,220,20]);
api.setCurve(curve1, curve2);
},
setupCubic: function(api) {
var curve1 = new api.Bezier([100,240, 30,60, 210,230, 160,30]);
var curve2 = new api.Bezier([25,260, 230,20]);
api.setCurve(curve1, curve2);
},
draw: function(api, curves) {
api.reset();
curves.forEach(curve => {
api.drawSkeleton(curve);
api.drawCurve(curve);
});
var utils = api.utils;
var line = { p1: curves[1].points[0], p2: curves[1].points[1] };
var acpts = utils.align(curves[0].points, line);
var nB = new api.Bezier(acpts);
var roots = utils.roots(nB.points);
roots.forEach(t => {
var p = curves[0].get(t);
api.drawCircle(p, 3);
api.text("t = " + t, {x: p.x + 5, y: p.y + 10});
});
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Intersections;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("intersections", handler);

View File

@ -0,0 +1,17 @@
module.exports = {
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);
}
};

View File

@ -1,37 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "introduction";
var Introduction = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
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>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = Introduction;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("introduction", handler);

View File

@ -1,21 +1,2 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "matrix";
var Matrix = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = Matrix;
var generateBase = require("../../generate-base");
module.exports = generateBase("matrix");

View File

@ -1,21 +1,2 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "matrixsplit";
var MatrixSplit = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = MatrixSplit;
var generateBase = require("../../generate-base");
module.exports = generateBase("matrixsplit");

View File

@ -0,0 +1,199 @@
var abs = Math.abs;
module.exports = {
setupQuadratic: function(api) {
api.setPanelCount(3);
var curve = api.getDefaultQuadratic();
curve.points[2].x -= 30;
api.setCurve(curve);
},
setupCubic: function(api) {
api.setPanelCount(3);
var curve = new api.Bezier([100,230, 30,160, 200,50, 210,160]);
curve.points[2].y -= 20;
api.setCurve(curve);
api.lut = curve.getLUT(100);
},
saveCurve: function(evt, api) {
if (!api.t) return;
api.setCurve(api.newcurve);
api.t = false;
api.redraw();
},
findTValue: function(evt, api) {
var t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7);
if (t < 0.05 || t > 0.95) return false;
return t;
},
markQB: function(evt, api) {
api.t = this.findTValue(evt, api);
if(api.t) {
var t = api.t,
t2 = 2*t,
top = t2*t - t2,
bottom = top + 1,
ratio = abs(top/bottom),
curve = api.curve,
A = api.A = curve.points[1],
B = api.B = curve.get(t);
api.C = api.utils.lli4(A, B, curve.points[0], curve.points[2]);
api.ratio = ratio;
}
},
markCB: function(evt, api) {
api.t = this.findTValue(evt, api);
if(api.t) {
var t = api.t,
mt = (1-t),
t3 = t*t*t,
mt3 = mt*mt*mt,
bottom = t3 + mt3,
top = bottom - 1,
ratio = abs(top/bottom),
curve = api.curve,
hull = curve.hull(t),
A = api.A = hull[5],
B = api.B = curve.get(t);
api.db = curve.derivative(t);
api.C = api.utils.lli4(A, B, curve.points[0], curve.points[3]);
api.ratio = ratio;
}
},
drag: function(evt, api) {
if (!api.t) return;
var newB = api.newB = {
x: evt.offsetX,
y: evt.offsetY
};
// now that we know A, B, C and the AB:BC ratio, we can compute the new A' based on the desired B'
api.newA = {
x: newB.x - (api.C.x - newB.x) / api.ratio,
y: newB.y - (api.C.y - newB.y) / api.ratio
};
},
dragQB: function(evt, api) {
if (!api.t) return;
this.drag(evt, api);
api.update = [api.newA];
},
dragCB: function(evt, api) {
if (!api.t) return;
this.drag(evt,api);
// preserve struts for B when repositioning
var curve = api.curve,
hull = curve.hull(api.t),
B = api.B,
Bl = hull[7],
Br = hull[8],
dbl = { x: Bl.x - B.x, y: Bl.y - B.y },
dbr = { x: Br.x - B.x, y: Br.y - B.y },
pts = curve.points,
// find new point on s--c1
p1 = {x: api.newB.x + dbl.x, y: api.newB.y + dbl.y},
sc1 = {
x: api.newA.x - (api.newA.x - p1.x)/(1-api.t),
y: api.newA.y - (api.newA.y - p1.y)/(1-api.t)
},
// find new point on c2--e
p2 = {x: api.newB.x + dbr.x, y: api.newB.y + dbr.y},
sc2 = {
x: api.newA.x + (p2.x - api.newA.x)/(api.t),
y: api.newA.y + (p2.y - api.newA.y)/(api.t)
},
// construct new c1` based on the fact that s--sc1 is s--c1 * t
nc1 = {
x: pts[0].x + (sc1.x - pts[0].x)/(api.t),
y: pts[0].y + (sc1.y - pts[0].y)/(api.t)
},
// construct new c2` based on the fact that e--sc2 is e--c2 * (1-t)
nc2 = {
x: pts[3].x - (pts[3].x - sc2.x)/(1-api.t),
y: pts[3].y - (pts[3].y - sc2.y)/(1-api.t)
};
api.p1 = p1;
api.p2 = p2;
api.sc1 = sc1;
api.sc2 = sc2;
api.nc1 = nc1;
api.nc2 = nc2;
api.update = [nc1, nc2];
},
drawMould: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = {x:w, y:0},
round = api.utils.round;
api.setColor("black");
api.drawLine({x:0,y:0},{x:0,y:h}, offset);
api.drawLine({x:w,y:0},{x:w,y:h}, offset);
if (api.t) {
api.drawCircle(curve.get(api.t),3);
api.npts = [curve.points[0]].concat(api.update).concat([curve.points.slice(-1)[0]]);
api.newcurve = new api.Bezier(api.npts);
api.setColor("lightgrey");
api.drawCurve(api.newcurve);
var newhull = api.drawHull(api.newcurve, api.t, offset);
api.drawLine(api.npts[0], api.npts.slice(-1)[0], offset);
api.drawLine(api.newA, api.newB, offset);
api.setColor("grey");
api.drawCircle(api.newA, 3, offset);
api.setColor("blue");
api.drawCircle(api.B, 3, offset);
api.drawCircle(api.C, 3, offset);
api.drawCircle(api.newB, 3, offset);
api.drawLine(api.B, api.C, offset);
api.drawLine(api.newB, api.C, offset);
api.setFill("black");
api.text("A'", api.newA, {x:offset.x + 7, y:offset.y + 1});
api.text("start", curve.get(0), {x:offset.x + 7, y:offset.y + 1});
api.text("end", curve.get(1), {x:offset.x + 7, y:offset.y + 1});
api.setFill("blue");
api.text("B'", api.newB, {x:offset.x + 7, y:offset.y + 1});
api.text("B, at t = "+round(api.t,2), api.B, {x:offset.x + 7, y:offset.y + 1});
api.text("C", api.C, {x:offset.x + 7, y:offset.y + 1});
if(curve.order === 3) {
var hull = curve.hull(api.t);
api.drawLine(hull[7], hull[8], offset);
api.drawLine(newhull[7], newhull[8], offset);
api.drawCircle(newhull[7], 3, offset);
api.drawCircle(newhull[8], 3, offset);
api.text("e1", newhull[7], {x:offset.x + 7, y:offset.y + 1});
api.text("e2", newhull[8], {x:offset.x + 7, y:offset.y + 1});
}
offset.x += w;
api.setColor("lightgrey");
api.drawSkeleton(api.newcurve, offset);
api.setColor("black");
api.drawCurve(api.newcurve, offset);
} else {
offset.x += w;
api.drawCurve(curve, offset);
}
}
};

View File

@ -1,217 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "moulding";
var abs = Math.abs;
var Moulding = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
api.setPanelCount(3);
var curve = api.getDefaultQuadratic();
curve.points[2].x -= 30;
api.setCurve(curve);
},
setupCubic: function(api) {
api.setPanelCount(3);
var curve = new api.Bezier([100,230, 30,160, 200,50, 210,160]);
curve.points[2].y -= 20;
api.setCurve(curve);
api.lut = curve.getLUT(100);
},
saveCurve: function(evt, api) {
if (!api.t) return;
api.setCurve(api.newcurve);
api.t = false;
api.redraw();
},
findTValue: function(evt, api) {
var t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7);
if (t < 0.05 || t > 0.95) return false;
return t;
},
markQB: function(evt, api) {
api.t = this.findTValue(evt, api);
if(api.t) {
var t = api.t,
t2 = 2*t,
top = t2*t - t2,
bottom = top + 1,
ratio = abs(top/bottom),
curve = api.curve,
A = api.A = curve.points[1],
B = api.B = curve.get(t);
api.C = api.utils.lli4(A, B, curve.points[0], curve.points[2]);
api.ratio = ratio;
}
},
markCB: function(evt, api) {
api.t = this.findTValue(evt, api);
if(api.t) {
var t = api.t,
mt = (1-t),
t3 = t*t*t,
mt3 = mt*mt*mt,
bottom = t3 + mt3,
top = bottom - 1,
ratio = abs(top/bottom),
curve = api.curve,
hull = curve.hull(t),
A = api.A = hull[5],
B = api.B = curve.get(t);
api.db = curve.derivative(t);
api.C = api.utils.lli4(A, B, curve.points[0], curve.points[3]);
api.ratio = ratio;
}
},
drag: function(evt, api) {
if (!api.t) return;
var newB = api.newB = {
x: evt.offsetX,
y: evt.offsetY
};
// now that we know A, B, C and the AB:BC ratio, we can compute the new A' based on the desired B'
api.newA = {
x: newB.x - (api.C.x - newB.x) / api.ratio,
y: newB.y - (api.C.y - newB.y) / api.ratio
};
},
dragQB: function(evt, api) {
if (!api.t) return;
this.drag(evt, api);
api.update = [api.newA];
},
dragCB: function(evt, api) {
if (!api.t) return;
this.drag(evt,api);
// preserve struts for B when repositioning
var curve = api.curve,
hull = curve.hull(api.t),
B = api.B,
Bl = hull[7],
Br = hull[8],
dbl = { x: Bl.x - B.x, y: Bl.y - B.y },
dbr = { x: Br.x - B.x, y: Br.y - B.y },
pts = curve.points,
// find new point on s--c1
p1 = {x: api.newB.x + dbl.x, y: api.newB.y + dbl.y},
sc1 = {
x: api.newA.x - (api.newA.x - p1.x)/(1-api.t),
y: api.newA.y - (api.newA.y - p1.y)/(1-api.t)
},
// find new point on c2--e
p2 = {x: api.newB.x + dbr.x, y: api.newB.y + dbr.y},
sc2 = {
x: api.newA.x + (p2.x - api.newA.x)/(api.t),
y: api.newA.y + (p2.y - api.newA.y)/(api.t)
},
// construct new c1` based on the fact that s--sc1 is s--c1 * t
nc1 = {
x: pts[0].x + (sc1.x - pts[0].x)/(api.t),
y: pts[0].y + (sc1.y - pts[0].y)/(api.t)
},
// construct new c2` based on the fact that e--sc2 is e--c2 * (1-t)
nc2 = {
x: pts[3].x - (pts[3].x - sc2.x)/(1-api.t),
y: pts[3].y - (pts[3].y - sc2.y)/(1-api.t)
};
api.p1 = p1;
api.p2 = p2;
api.sc1 = sc1;
api.sc2 = sc2;
api.nc1 = nc1;
api.nc2 = nc2;
api.update = [nc1, nc2];
},
drawMould: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
offset = {x:w, y:0},
round = api.utils.round;
api.setColor("black");
api.drawLine({x:0,y:0},{x:0,y:h}, offset);
api.drawLine({x:w,y:0},{x:w,y:h}, offset);
if (api.t) {
api.drawCircle(curve.get(api.t),3);
api.npts = [curve.points[0]].concat(api.update).concat([curve.points.slice(-1)[0]]);
api.newcurve = new api.Bezier(api.npts);
api.setColor("lightgrey");
api.drawCurve(api.newcurve);
var newhull = api.drawHull(api.newcurve, api.t, offset);
api.drawLine(api.npts[0], api.npts.slice(-1)[0], offset);
api.drawLine(api.newA, api.newB, offset);
api.setColor("grey");
api.drawCircle(api.newA, 3, offset);
api.setColor("blue");
api.drawCircle(api.B, 3, offset);
api.drawCircle(api.C, 3, offset);
api.drawCircle(api.newB, 3, offset);
api.drawLine(api.B, api.C, offset);
api.drawLine(api.newB, api.C, offset);
api.setFill("black");
api.text("A'", api.newA, {x:offset.x + 7, y:offset.y + 1});
api.text("start", curve.get(0), {x:offset.x + 7, y:offset.y + 1});
api.text("end", curve.get(1), {x:offset.x + 7, y:offset.y + 1});
api.setFill("blue");
api.text("B'", api.newB, {x:offset.x + 7, y:offset.y + 1});
api.text("B, at t = "+round(api.t,2), api.B, {x:offset.x + 7, y:offset.y + 1});
api.text("C", api.C, {x:offset.x + 7, y:offset.y + 1});
if(curve.order === 3) {
var hull = curve.hull(api.t);
api.drawLine(hull[7], hull[8], offset);
api.drawLine(newhull[7], newhull[8], offset);
api.drawCircle(newhull[7], 3, offset);
api.drawCircle(newhull[8], 3, offset);
api.text("e1", newhull[7], {x:offset.x + 7, y:offset.y + 1});
api.text("e2", newhull[8], {x:offset.x + 7, y:offset.y + 1});
}
offset.x += w;
api.setColor("lightgrey");
api.drawSkeleton(api.newcurve, offset);
api.setColor("black");
api.drawCurve(api.newcurve, offset);
} else {
offset.x += w;
api.drawCurve(curve, offset);
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Moulding;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("moulding", handler);

View File

@ -53,7 +53,7 @@ And that's one reason why Bézier curves are tricky: there are actually a `lot`
</div>
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 `t=0.5` 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).
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 `t=0.5` is roughly in the centre of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).
A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for `t=0.5` is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.

View File

@ -0,0 +1,58 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
setup: function(api, curve) {
api.setCurve(curve);
api.distance = 20;
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
this.setup(api, curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
this.setup(api, curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var reduced = curve.reduce();
reduced.forEach(c => {
api.setRandomColor();
api.drawCurve(c);
api.drawCircle(c.points[0], 1);
});
var last = reduced.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
api.setColor("red");
var offset = curve.offset(api.distance);
offset.forEach(c => {
api.drawPoint(c.points[0]);
api.drawCurve(c);
});
last = offset.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
api.setColor("blue");
offset = curve.offset(-api.distance);
offset.forEach(c => {
api.drawPoint(c.points[0]);
api.drawCurve(c);
});
last = offset.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
}
};

View File

@ -1,78 +1,4 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "offsetting";
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var Offsetting = React.createClass({
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api, curve) {
api.setCurve(curve);
api.distance = 20;
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
this.setup(api, curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
this.setup(api, curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var reduced = curve.reduce();
reduced.forEach(c => {
api.setRandomColor();
api.drawCurve(c);
api.drawCircle(c.points[0], 1);
});
var last = reduced.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
api.setColor("red");
var offset = curve.offset(api.distance);
offset.forEach(c => {
api.drawPoint(c.points[0]);
api.drawCurve(c);
});
last = offset.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
api.setColor("blue");
offset = curve.offset(-api.distance);
offset.forEach(c => {
api.drawPoint(c.points[0]);
api.drawCurve(c);
});
last = offset.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = keyHandling(Offsetting);
module.exports = keyHandling(generateBase("offsetting", handler));

View File

@ -0,0 +1,164 @@
var abs = Math.abs;
module.exports = {
setup: function(api) {
api.lpts = [
{x:56, y:153},
{x:144,y:83},
{x:188,y:185}
];
},
onClick: function(evt, api) {
if (api.lpts.length==3) { api.lpts = []; }
api.lpts.push({
x: evt.offsetX,
y: evt.offsetY
});
api.redraw();
},
getQRatio: function(t) {
var t2 = 2*t,
top = t2*t - t2,
bottom = top + 1;
return abs(top/bottom);
},
getCRatio: function(t) {
var mt = (1-t),
t3 = t*t*t,
mt3 = mt*mt*mt,
bottom = t3 + mt3,
top = bottom - 1;
return abs(top/bottom);
},
drawQuadratic: function(api, curve) {
var labels = ["start","t=0.5","end"];
api.reset();
api.setColor("lightblue");
api.drawGrid(10,10);
api.setFill("black");
api.setColor("black");
api.lpts.forEach((p,i) => {
api.drawCircle(p,3);
api.text(labels[i], p, {x:5, y:2});
});
if(api.lpts.length === 3) {
var S = api.lpts[0],
E = api.lpts[2],
B = api.lpts[1],
C = {
x: (S.x + E.x)/2,
y: (S.y + E.y)/2
};
api.setColor("blue");
api.drawLine(S, E);
api.drawLine(B, C);
api.drawCircle(C, 3);
var ratio = this.getQRatio(0.5),
A = {
x: B.x + (B.x-C.x)/ratio,
y: B.y + (B.y-C.y)/ratio
};
curve = new api.Bezier([S, A, E]);
api.setColor("lightgrey");
api.drawLine(A, B);
api.drawLine(A, S);
api.drawLine(A, E);
api.setColor("black");
api.drawCircle(A, 1);
api.drawCurve(curve);
}
},
drawCubic: function(api, curve) {
var labels = ["start","t=0.5","end"];
api.reset();
api.setFill("black");
api.setColor("black");
api.lpts.forEach((p,i) => {
api.drawCircle(p,3);
api.text(labels[i], p, {x:5, y:2});
});
api.setColor("lightblue");
api.drawGrid(10,10);
if(api.lpts.length === 3) {
var S = api.lpts[0],
E = api.lpts[2],
B = api.lpts[1],
C = {
x: (S.x + E.x)/2,
y: (S.y + E.y)/2
};
api.setColor("blue");
api.drawLine(S, E);
api.drawLine(B, C);
api.drawCircle(C, 1);
var ratio = this.getCRatio(0.5),
A = {
x: B.x + (B.x-C.x)/ratio,
y: B.y + (B.y-C.y)/ratio
},
selen = api.utils.dist(S,E),
bclen_min = selen/8,
bclen = api.utils.dist(B,C),
aesthetics = 4,
be12dist = bclen_min + bclen/aesthetics,
bx = be12dist * (E.x-S.x)/selen,
by = be12dist * (E.y-S.y)/selen,
e1 = {
x: B.x - bx,
y: B.y - by
},
e2 = {
x: B.x + bx,
y: B.y + by
},
v1 = {
x: A.x + (e1.x-A.x)*2,
y: A.y + (e1.y-A.y)*2
},
v2 = {
x: A.x + (e2.x-A.x)*2,
y: A.y + (e2.y-A.y)*2
},
nc1 = {
x: S.x + (v1.x-S.x)*2,
y: S.y + (v1.y-S.y)*2
},
nc2 = {
x: E.x + (v2.x-E.x)*2,
y: E.y + (v2.y-E.y)*2
};
curve = new api.Bezier([S, nc1, nc2, E]);
api.drawLine(e1, e2);
api.setColor("lightgrey");
api.drawLine(A, C);
api.drawLine(A, v1);
api.drawLine(A, v2);
api.drawLine(S, nc1);
api.drawLine(E, nc2);
api.drawLine(nc1, nc2);
api.setColor("black");
api.drawCircle(A, 1);
api.drawCircle(nc1, 1);
api.drawCircle(nc2, 1);
api.drawCurve(curve);
}
}
};

View File

@ -1,182 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "pointcurves";
var abs = Math.abs;
var PointCurves = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
api.lpts = [
{x:56, y:153},
{x:144,y:83},
{x:188,y:185}
];
},
onClick: function(evt, api) {
if (api.lpts.length==3) { api.lpts = []; }
api.lpts.push({
x: evt.offsetX,
y: evt.offsetY
});
api.redraw();
},
getQRatio: function(t) {
var t2 = 2*t,
top = t2*t - t2,
bottom = top + 1;
return abs(top/bottom);
},
getCRatio: function(t) {
var mt = (1-t),
t3 = t*t*t,
mt3 = mt*mt*mt,
bottom = t3 + mt3,
top = bottom - 1;
return abs(top/bottom);
},
drawQuadratic: function(api, curve) {
var labels = ["start","t=0.5","end"];
api.reset();
api.setColor("lightblue");
api.drawGrid(10,10);
api.setFill("black");
api.setColor("black");
api.lpts.forEach((p,i) => {
api.drawCircle(p,3);
api.text(labels[i], p, {x:5, y:2});
});
if(api.lpts.length === 3) {
var S = api.lpts[0],
E = api.lpts[2],
B = api.lpts[1],
C = {
x: (S.x + E.x)/2,
y: (S.y + E.y)/2
};
api.setColor("blue");
api.drawLine(S, E);
api.drawLine(B, C);
api.drawCircle(C, 3);
var ratio = this.getQRatio(0.5),
A = {
x: B.x + (B.x-C.x)/ratio,
y: B.y + (B.y-C.y)/ratio
};
curve = new api.Bezier([S, A, E]);
api.setColor("lightgrey");
api.drawLine(A, B);
api.drawLine(A, S);
api.drawLine(A, E);
api.setColor("black");
api.drawCircle(A, 1);
api.drawCurve(curve);
}
},
drawCubic: function(api, curve) {
var labels = ["start","t=0.5","end"];
api.reset();
api.setFill("black");
api.setColor("black");
api.lpts.forEach((p,i) => {
api.drawCircle(p,3);
api.text(labels[i], p, {x:5, y:2});
});
api.setColor("lightblue");
api.drawGrid(10,10);
if(api.lpts.length === 3) {
var S = api.lpts[0],
E = api.lpts[2],
B = api.lpts[1],
C = {
x: (S.x + E.x)/2,
y: (S.y + E.y)/2
};
api.setColor("blue");
api.drawLine(S, E);
api.drawLine(B, C);
api.drawCircle(C, 1);
var ratio = this.getCRatio(0.5),
A = {
x: B.x + (B.x-C.x)/ratio,
y: B.y + (B.y-C.y)/ratio
},
selen = api.utils.dist(S,E),
bclen_min = selen/8,
bclen = api.utils.dist(B,C),
aesthetics = 4,
be12dist = bclen_min + bclen/aesthetics,
bx = be12dist * (E.x-S.x)/selen,
by = be12dist * (E.y-S.y)/selen,
e1 = {
x: B.x - bx,
y: B.y - by
},
e2 = {
x: B.x + bx,
y: B.y + by
},
v1 = {
x: A.x + (e1.x-A.x)*2,
y: A.y + (e1.y-A.y)*2
},
v2 = {
x: A.x + (e2.x-A.x)*2,
y: A.y + (e2.y-A.y)*2
},
nc1 = {
x: S.x + (v1.x-S.x)*2,
y: S.y + (v1.y-S.y)*2
},
nc2 = {
x: E.x + (v2.x-E.x)*2,
y: E.y + (v2.y-E.y)*2
};
curve = new api.Bezier([S, nc1, nc2, E]);
api.drawLine(e1, e2);
api.setColor("lightgrey");
api.drawLine(A, C);
api.drawLine(A, v1);
api.drawLine(A, v2);
api.drawLine(S, nc1);
api.drawLine(E, nc2);
api.drawLine(nc1, nc2);
api.setColor("black");
api.drawCircle(A, 1);
api.drawCircle(nc1, 1);
api.drawCircle(nc2, 1);
api.drawCurve(curve);
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = PointCurves;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("pointcurves", handler);

View File

@ -0,0 +1,32 @@
module.exports = {
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var i,t,p,tg,n,m,nd=20;
for(i=0; i<=10; i++) {
t = i/10.0;
p = curve.get(t);
tg = curve.derivative(t);
m = Math.sqrt(tg.x*tg.x + tg.y*tg.y);
tg = {x:tg.x/m, y:tg.y/m};
n = curve.normal(t);
api.setColor("blue");
api.drawLine(p, {x:p.x+tg.x*nd, y:p.y+tg.y*nd});
api.setColor("red");
api.drawLine(p, {x:p.x+n.x*nd, y:p.y+n.y*nd});
api.setColor("black");
api.drawCircle(p,3);
}
}
};

View File

@ -1,51 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "pointvectors";
var PointVectors = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var i,t,p,tg,n,m,nd=20;
for(i=0; i<=10; i++) {
t = i/10.0;
p = curve.get(t);
tg = curve.derivative(t);
m = Math.sqrt(tg.x*tg.x + tg.y*tg.y);
tg = {x:tg.x/m, y:tg.y/m};
n = curve.normal(t);
api.setColor("blue");
api.drawLine(p, {x:p.x+tg.x*nd, y:p.y+tg.y*nd});
api.setColor("red");
api.drawLine(p, {x:p.x+n.x*nd, y:p.y+n.y*nd});
api.setColor("black");
api.drawCircle(p,3);
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = PointVectors;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("pointvectors", handler);

View File

@ -0,0 +1,241 @@
var atan2 = Math.atan2, sqrt = Math.sqrt, sin = Math.sin, cos = Math.cos;
module.exports = {
setupQuadratic: function(api) {
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
cx = w/2, cy = h/2, pad = 40,
pts = [
// first curve:
{x:cx,y:pad}, {x:w-pad,y:pad}, {x:w-pad,y:cy},
// subsequent curve
{x:w-pad,y:h-pad}, {x:cx,y:h-pad},
// subsequent curve
{x:pad,y:h-pad}, {x:pad,y:cy},
// final curve control point
{x:pad,y:pad}
];
api.lpts = pts;
},
setupCubic: function(api) {
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
cx = w/2, cy = h/2, pad = 40,
r = (w - 2*pad)/2,
k = 0.55228,
kr = k*r,
pts = [
// first curve:
{x:cx,y:pad}, {x:cx+kr,y:pad}, {x:w-pad,y:cy-kr}, {x:w-pad,y:cy},
// subsequent curve
{x:w-pad,y:cy+kr}, {x:cx+kr,y:h-pad}, {x:cx,y:h-pad},
// subsequent curve
{x:cx-kr,y:h-pad}, {x:pad,y:cy+kr}, {x:pad,y:cy},
// final curve control point
{x:pad,y:cy-kr}, {x:cx-kr,y:pad}
];
api.lpts = pts;
},
movePointsQuadraticLD: function(api, i) {
// ...we need to move _everything_
var anchor, fixed, toMove;
for(var p=1; p<4; p++) {
anchor = i + (2*p - 2) + api.lpts.length;
anchor = api.lpts[anchor % api.lpts.length];
fixed = i + (2*p - 1);
fixed = api.lpts[fixed % api.lpts.length];
toMove = i + 2*p;
toMove = api.lpts[toMove % api.lpts.length];
toMove.x = fixed.x + (fixed.x - anchor.x);
toMove.y = fixed.y + (fixed.y - anchor.y);
}
// then, the furthest point cannot be computed properly!
toMove = i + 6;
toMove = api.lpts[toMove % api.lpts.length];
api.problem = toMove;
},
movePointsCubicLD: function(api, i) {
var toMove, fixed;
if (i%3 === 1) {
fixed = i-1;
fixed += (fixed < 0) ? api.lpts.length : 0;
toMove = i-2;
toMove += (toMove < 0) ? api.lpts.length : 0;
} else {
fixed = (i+1) % api.lpts.length;
toMove = (i+2) % api.lpts.length;
}
fixed = api.lpts[fixed];
toMove = api.lpts[toMove];
toMove.x = fixed.x + (fixed.x - api.mp.x);
toMove.y = fixed.y + (fixed.y - api.mp.y);
},
linkDerivatives: function(evt, api) {
if (api.mp) {
var quad = api.lpts.length === 8;
var i = api.mp_idx;
if (quad) {
if (i%2 !== 0) { this.movePointsQuadraticLD(api, i); }
} else {
if(i%3 !== 0) { this.movePointsCubicLD(api, i); }
}
}
},
movePointsQuadraticDirOnly: function(api, i) {
// ...we need to move _everything_ ...again
var anchor, fixed, toMove;
// move left and right
[-1,1].forEach(v => {
anchor = api.mp;
fixed = i + v + api.lpts.length;
fixed = api.lpts[fixed % api.lpts.length];
toMove = i + 2*v + api.lpts.length;
toMove = api.lpts[toMove % api.lpts.length];
var a = atan2(fixed.y - anchor.y, fixed.x - anchor.x),
dx = toMove.x - fixed.x,
dy = toMove.y - fixed.y,
d = sqrt(dx*dx + dy*dy);
toMove.x = fixed.x + d*cos(a);
toMove.y = fixed.y + d*sin(a);
});
// then, the furthest point cannot be computed properly!
toMove = i + 4;
toMove = api.lpts[toMove % api.lpts.length];
api.problem = toMove;
},
movePointsCubicDirOnly: function(api, i) {
var toMove, fixed;
if (i%3 === 1) {
fixed = i-1;
fixed += (fixed < 0) ? api.lpts.length : 0;
toMove = i-2;
toMove += (toMove < 0) ? api.lpts.length : 0;
} else {
fixed = (i+1) % api.lpts.length;
toMove = (i+2) % api.lpts.length;
}
fixed = api.lpts[fixed];
toMove = api.lpts[toMove];
var a = atan2(fixed.y - api.mp.y, fixed.x - api.mp.x),
dx = toMove.x - fixed.x,
dy = toMove.y - fixed.y,
d = sqrt(dx*dx + dy*dy);
toMove.x = fixed.x + d*cos(a);
toMove.y = fixed.y + d*sin(a);
},
linkDirection: function(evt, api) {
if (api.mp) {
var quad = api.lpts.length === 8;
var i = api.mp_idx;
if (quad) {
if(i%2 !== 0) { this.movePointsQuadraticDirOnly(api, i); }
} else {
if(i%3 !== 0) { this.movePointsCubicDirOnly(api, i); }
}
}
},
bufferPoints: function(evt, api) {
api.bpts = JSON.parse(JSON.stringify(api.lpts));
},
moveQuadraticPoint: function(api, i) {
this.moveCubicPoint(api,i);
// then move the other control points
[-1,1].forEach(v => {
var anchor = i - v + api.lpts.length;
anchor = api.lpts[anchor % api.lpts.length];
var fixed = i - 2*v + api.lpts.length;
fixed = api.lpts[fixed % api.lpts.length];
var toMove = i - 3*v + api.lpts.length;
toMove = api.lpts[toMove % api.lpts.length];
var a = atan2(fixed.y - anchor.y, fixed.x - anchor.x),
dx = toMove.x - fixed.x,
dy = toMove.y - fixed.y,
d = sqrt(dx*dx + dy*dy);
toMove.x = fixed.x + d*cos(a);
toMove.y = fixed.y + d*sin(a);
});
// then signal a problem
var toMove = i + 4;
toMove = api.lpts[toMove % api.lpts.length];
api.problem = toMove;
},
moveCubicPoint: function(api, i) {
var op = api.bpts[i],
np = api.lpts[i],
dx = np.x - op.x,
dy = np.y - op.y,
len = api.lpts.length,
l = i-1+len,
r = i+1,
// original left and right
ol = api.bpts[l % len],
or = api.bpts[r % len],
// current left and right
nl = api.lpts[l % len],
nr = api.lpts[r % len];
// update current left
nl.x = ol.x + dx;
nl.y = ol.y + dy;
// update current right
nr.x = or.x + dx;
nr.y = or.y + dy;
return {x:dx, y:dy};
},
modelCurve: function(evt, api) {
if (api.mp) {
var quad = api.lpts.length === 8;
var i = api.mp_idx;
if (quad) {
if (i%2 !== 0) { this.movePointsQuadraticDirOnly(api, i); }
else { this.moveQuadraticPoint(api, i); }
}
else {
if(i%3 !== 0) { this.movePointsCubicDirOnly(api, i); }
else { this.moveCubicPoint(api, i); }
}
}
},
draw: function(api, curves) {
api.reset();
var pts = api.lpts;
var quad = pts.length === 8;
var c1 = quad ? new api.Bezier(pts[0],pts[1],pts[2]) : new api.Bezier(pts[0],pts[1],pts[2],pts[3]);
api.drawSkeleton(c1, false, true);
api.drawCurve(c1);
var c2 = quad ? new api.Bezier(pts[2],pts[3],pts[4]) : new api.Bezier(pts[3],pts[4],pts[5],pts[6]);
api.drawSkeleton(c2, false, true);
api.drawCurve(c2);
var c3 = quad ? new api.Bezier(pts[4],pts[5],pts[6]) : new api.Bezier(pts[6],pts[7],pts[8],pts[9]);
api.drawSkeleton(c3, false, true);
api.drawCurve(c3);
var c4 = quad ? new api.Bezier(pts[6],pts[7],pts[0]) : new api.Bezier(pts[9],pts[10],pts[11],pts[0]);
api.drawSkeleton(c4, false, true);
api.drawCurve(c4);
if (api.problem) {
api.setColor("red");
api.drawCircle(api.problem,5);
}
}
};

View File

@ -1,259 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "polybezier";
var atan2 = Math.atan2, sqrt = Math.sqrt, sin = Math.sin, cos = Math.cos;
var PolyBezier = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
cx = w/2, cy = h/2, pad = 40,
pts = [
// first curve:
{x:cx,y:pad}, {x:w-pad,y:pad}, {x:w-pad,y:cy},
// subsequent curve
{x:w-pad,y:h-pad}, {x:cx,y:h-pad},
// subsequent curve
{x:pad,y:h-pad}, {x:pad,y:cy},
// final curve control point
{x:pad,y:pad}
];
api.lpts = pts;
},
setupCubic: function(api) {
var w = api.getPanelWidth(),
h = api.getPanelHeight(),
cx = w/2, cy = h/2, pad = 40,
r = (w - 2*pad)/2,
k = 0.55228,
kr = k*r,
pts = [
// first curve:
{x:cx,y:pad}, {x:cx+kr,y:pad}, {x:w-pad,y:cy-kr}, {x:w-pad,y:cy},
// subsequent curve
{x:w-pad,y:cy+kr}, {x:cx+kr,y:h-pad}, {x:cx,y:h-pad},
// subsequent curve
{x:cx-kr,y:h-pad}, {x:pad,y:cy+kr}, {x:pad,y:cy},
// final curve control point
{x:pad,y:cy-kr}, {x:cx-kr,y:pad}
];
api.lpts = pts;
},
movePointsQuadraticLD: function(api, i) {
// ...we need to move _everything_
var anchor, fixed, toMove;
for(var p=1; p<4; p++) {
anchor = i + (2*p - 2) + api.lpts.length;
anchor = api.lpts[anchor % api.lpts.length];
fixed = i + (2*p - 1);
fixed = api.lpts[fixed % api.lpts.length];
toMove = i + 2*p;
toMove = api.lpts[toMove % api.lpts.length];
toMove.x = fixed.x + (fixed.x - anchor.x);
toMove.y = fixed.y + (fixed.y - anchor.y);
}
// then, the furthest point cannot be computed properly!
toMove = i + 6;
toMove = api.lpts[toMove % api.lpts.length];
api.problem = toMove;
},
movePointsCubicLD: function(api, i) {
var toMove, fixed;
if (i%3 === 1) {
fixed = i-1;
fixed += (fixed < 0) ? api.lpts.length : 0;
toMove = i-2;
toMove += (toMove < 0) ? api.lpts.length : 0;
} else {
fixed = (i+1) % api.lpts.length;
toMove = (i+2) % api.lpts.length;
}
fixed = api.lpts[fixed];
toMove = api.lpts[toMove];
toMove.x = fixed.x + (fixed.x - api.mp.x);
toMove.y = fixed.y + (fixed.y - api.mp.y);
},
linkDerivatives: function(evt, api) {
if (api.mp) {
var quad = api.lpts.length === 8;
var i = api.mp_idx;
if (quad) {
if (i%2 !== 0) { this.movePointsQuadraticLD(api, i); }
} else {
if(i%3 !== 0) { this.movePointsCubicLD(api, i); }
}
}
},
movePointsQuadraticDirOnly: function(api, i) {
// ...we need to move _everything_ ...again
var anchor, fixed, toMove;
// move left and right
[-1,1].forEach(v => {
anchor = api.mp;
fixed = i + v + api.lpts.length;
fixed = api.lpts[fixed % api.lpts.length];
toMove = i + 2*v + api.lpts.length;
toMove = api.lpts[toMove % api.lpts.length];
var a = atan2(fixed.y - anchor.y, fixed.x - anchor.x),
dx = toMove.x - fixed.x,
dy = toMove.y - fixed.y,
d = sqrt(dx*dx + dy*dy);
toMove.x = fixed.x + d*cos(a);
toMove.y = fixed.y + d*sin(a);
});
// then, the furthest point cannot be computed properly!
toMove = i + 4;
toMove = api.lpts[toMove % api.lpts.length];
api.problem = toMove;
},
movePointsCubicDirOnly: function(api, i) {
var toMove, fixed;
if (i%3 === 1) {
fixed = i-1;
fixed += (fixed < 0) ? api.lpts.length : 0;
toMove = i-2;
toMove += (toMove < 0) ? api.lpts.length : 0;
} else {
fixed = (i+1) % api.lpts.length;
toMove = (i+2) % api.lpts.length;
}
fixed = api.lpts[fixed];
toMove = api.lpts[toMove];
var a = atan2(fixed.y - api.mp.y, fixed.x - api.mp.x),
dx = toMove.x - fixed.x,
dy = toMove.y - fixed.y,
d = sqrt(dx*dx + dy*dy);
toMove.x = fixed.x + d*cos(a);
toMove.y = fixed.y + d*sin(a);
},
linkDirection: function(evt, api) {
if (api.mp) {
var quad = api.lpts.length === 8;
var i = api.mp_idx;
if (quad) {
if(i%2 !== 0) { this.movePointsQuadraticDirOnly(api, i); }
} else {
if(i%3 !== 0) { this.movePointsCubicDirOnly(api, i); }
}
}
},
bufferPoints: function(evt, api) {
api.bpts = JSON.parse(JSON.stringify(api.lpts));
},
moveQuadraticPoint: function(api, i) {
this.moveCubicPoint(api,i);
// then move the other control points
[-1,1].forEach(v => {
var anchor = i - v + api.lpts.length;
anchor = api.lpts[anchor % api.lpts.length];
var fixed = i - 2*v + api.lpts.length;
fixed = api.lpts[fixed % api.lpts.length];
var toMove = i - 3*v + api.lpts.length;
toMove = api.lpts[toMove % api.lpts.length];
var a = atan2(fixed.y - anchor.y, fixed.x - anchor.x),
dx = toMove.x - fixed.x,
dy = toMove.y - fixed.y,
d = sqrt(dx*dx + dy*dy);
toMove.x = fixed.x + d*cos(a);
toMove.y = fixed.y + d*sin(a);
});
// then signal a problem
var toMove = i + 4;
toMove = api.lpts[toMove % api.lpts.length];
api.problem = toMove;
},
moveCubicPoint: function(api, i) {
var op = api.bpts[i],
np = api.lpts[i],
dx = np.x - op.x,
dy = np.y - op.y,
len = api.lpts.length,
l = i-1+len,
r = i+1,
// original left and right
ol = api.bpts[l % len],
or = api.bpts[r % len],
// current left and right
nl = api.lpts[l % len],
nr = api.lpts[r % len];
// update current left
nl.x = ol.x + dx;
nl.y = ol.y + dy;
// update current right
nr.x = or.x + dx;
nr.y = or.y + dy;
return {x:dx, y:dy};
},
modelCurve: function(evt, api) {
if (api.mp) {
var quad = api.lpts.length === 8;
var i = api.mp_idx;
if (quad) {
if (i%2 !== 0) { this.movePointsQuadraticDirOnly(api, i); }
else { this.moveQuadraticPoint(api, i); }
}
else {
if(i%3 !== 0) { this.movePointsCubicDirOnly(api, i); }
else { this.moveCubicPoint(api, i); }
}
}
},
draw: function(api, curves) {
api.reset();
var pts = api.lpts;
var quad = pts.length === 8;
var c1 = quad ? new api.Bezier(pts[0],pts[1],pts[2]) : new api.Bezier(pts[0],pts[1],pts[2],pts[3]);
api.drawSkeleton(c1, false, true);
api.drawCurve(c1);
var c2 = quad ? new api.Bezier(pts[2],pts[3],pts[4]) : new api.Bezier(pts[3],pts[4],pts[5],pts[6]);
api.drawSkeleton(c2, false, true);
api.drawCurve(c2);
var c3 = quad ? new api.Bezier(pts[4],pts[5],pts[6]) : new api.Bezier(pts[6],pts[7],pts[8],pts[9]);
api.drawSkeleton(c3, false, true);
api.drawCurve(c3);
var c4 = quad ? new api.Bezier(pts[6],pts[7],pts[0]) : new api.Bezier(pts[9],pts[10],pts[11],pts[0]);
api.drawSkeleton(c4, false, true);
api.drawCurve(c4);
if (api.problem) {
api.setColor("red");
api.drawCircle(api.problem,5);
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = PolyBezier;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("polybezier", handler);

View File

@ -1,21 +1,2 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "preface";
var Preface = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = Preface;
var generateBase = require("../../generate-base");
module.exports = generateBase("preface");

View File

@ -0,0 +1,55 @@
module.exports = {
setup: function(api) {
api.setSize(320,320);
var curve = new api.Bezier([
{x:248,y:188},
{x:218,y:294},
{x:45,y:290},
{x:12,y:236},
{x:14,y:82},
{x:186,y:177},
{x:221,y:90},
{x:18,y:156},
{x:34,y:57},
{x:198,y:18}
]);
api.setCurve(curve);
api._lut = curve.getLUT();
},
findClosest: function(LUT, p, dist) {
var i,
end = LUT.length,
d,
dd = dist(LUT[0],p),
f = 0;
for(i=1; i<end; i++) {
d = dist(LUT[i],p);
if(d<dd) {f = i;dd = d;}
}
return f/(end-1);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
if (api.mousePt) {
api.setColor("red");
api.setFill("red");
api.drawCircle(api.mousePt, 3);
// naive t value
var t = this.findClosest(api._lut, api.mousePt, api.utils.dist);
// no real point in refining for illustration purposes
var p = curve.get(t);
api.drawLine(p, api.mousePt);
api.drawCircle(p, 3);
api.text("t = "+api.utils.round(t,2), p, {x:10, y:3});
}
},
onMouseMove: function(evt, api) {
api.mousePt = {x: evt.offsetX, y: evt.offsetY };
api._lut = api.curve.getLUT();
}
};

View File

@ -1,73 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "projections";
var Projections = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
api.setSize(320,320);
var curve = new api.Bezier([
{x:248,y:188},
{x:218,y:294},
{x:45,y:290},
{x:12,y:236},
{x:14,y:82},
{x:186,y:177},
{x:221,y:90},
{x:18,y:156},
{x:34,y:57},
{x:198,y:18}
]);
api.setCurve(curve);
api._lut = curve.getLUT();
},
findClosest: function(LUT, p, dist) {
var i,
end = LUT.length,
d,
dd = dist(LUT[0],p),
f = 0;
for(i=1; i<end; i++) {
d = dist(LUT[i],p);
if(d<dd) {f = i;dd = d;}
}
return f/(end-1);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
if (api.mousePt) {
api.setColor("red");
api.setFill("red");
api.drawCircle(api.mousePt, 3);
// naive t value
var t = this.findClosest(api._lut, api.mousePt, api.utils.dist);
// no real point in refining for illustration purposes
var p = curve.get(t);
api.drawLine(p, api.mousePt);
api.drawCircle(p, 3);
api.text("t = "+api.utils.round(t,2), p, {x:10, y:3});
}
},
onMouseMove: function(evt, api) {
api.mousePt = {x: evt.offsetX, y: evt.offsetY };
api._lut = api.curve.getLUT();
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Projections;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("projections", handler);

View File

@ -0,0 +1,107 @@
var Reordering = {
statics: {
// Improve this based on http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
lower: function(curve) {
var pts = curve.points, q=[], n = pts.length;
pts.forEach((p,k) => {
if (!k) { return (q[k] = p); }
var f1 = k/n, f2 = 1 - f1;
q[k] = {
x: f1 * p.x + f2 * pts[k-1].x,
y: f1 * p.y + f2 * pts[k-1].y
};
});
q.splice(n-1,1);
q[n-2] = pts[n-1];
curve.points = q;
return curve;
},
keyHandlingOptions: {
values: {
"38": function(api) {
api.setCurve(api.curve.raise());
},
"40": function(api) {
api.setCurve(Reordering.lower(api.curve));
}
}
}
},
getInitialState: function() {
return {
order: 0
};
},
setup: function(api) {
var points = [];
var w = api.getPanelWidth(),
h = api.getPanelHeight();
for (var i=0; i<10; i++) {
points.push({
x: w/2 + (Math.random() * 20) + Math.cos(Math.PI*2 * i/10) * (w/2 - 40),
y: h/2 + (Math.random() * 20) + Math.sin(Math.PI*2 * i/10) * (h/2 - 40)
});
}
var curve = new api.Bezier(points);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
var pts = curve.points;
this.setState({
order: pts.length
});
var p0 = pts[0];
// we can't "just draw" this curve, since it'll be an arbitrary order,
// And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm:
for(var t=0; t<=1; t+=0.01) {
var q = JSON.parse(JSON.stringify(pts));
while(q.length > 1) {
for (var i=0; i<q.length-1; i++) {
q[i] = {
x: q[i].x + (q[i+1].x - q[i].x) * t,
y: q[i].y + (q[i+1].y - q[i].y) * t
};
}
q.splice(q.length-1, 1);
}
api.drawLine(p0, q[0]);
p0 = q[0];
}
p0 = pts[0];
api.setColor("black");
api.drawCircle(p0,3);
pts.forEach(p => {
if(p===p0) return;
api.setColor("#DDD");
api.drawLine(p0,p);
api.setColor("black");
api.drawCircle(p,3);
p0 = p;
});
},
getOrder: function() {
var order = this.state.order;
if (order%10 === 1 && order !== 11) {
order += "st";
} else if (order%10 === 2 && order !== 12) {
order += "nd";
} else if (order%10 === 3 && order !== 13) {
order += "rd";
} else {
order += "th";
}
return order;
}
};
module.exports = Reordering;

View File

@ -1,126 +1,4 @@
var React = require("react");
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "reordering";
var Reordering = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
statics: {
// Improve this based on http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/
lower: function(curve) {
var pts = curve.points, q=[], n = pts.length;
pts.forEach((p,k) => {
if (!k) { return (q[k] = p); }
var f1 = k/n, f2 = 1 - f1;
q[k] = {
x: f1 * p.x + f2 * pts[k-1].x,
y: f1 * p.y + f2 * pts[k-1].y
};
});
q.splice(n-1,1);
q[n-2] = pts[n-1];
curve.points = q;
return curve;
},
keyHandlingOptions: {
values: {
"38": function(api) {
api.setCurve(api.curve.raise());
},
"40": function(api) {
api.setCurve(Reordering.lower(api.curve));
}
}
}
},
getInitialState: function() {
return {
order: 0
};
},
setup: function(api) {
var points = [];
var w = api.getPanelWidth(),
h = api.getPanelHeight();
for (var i=0; i<10; i++) {
points.push({
x: w/2 + (Math.random() * 20) + Math.cos(Math.PI*2 * i/10) * (w/2 - 40),
y: h/2 + (Math.random() * 20) + Math.sin(Math.PI*2 * i/10) * (h/2 - 40)
});
}
var curve = new api.Bezier(points);
api.setCurve(curve);
},
draw: function(api, curve) {
api.reset();
var pts = curve.points;
this.setState({
order: pts.length
});
var p0 = pts[0];
// we can't "just draw" this curve, since it'll be an arbitrary order,
// And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm:
for(var t=0; t<=1; t+=0.01) {
var q = JSON.parse(JSON.stringify(pts));
while(q.length > 1) {
for (var i=0; i<q.length-1; i++) {
q[i] = {
x: q[i].x + (q[i+1].x - q[i].x) * t,
y: q[i].y + (q[i+1].y - q[i].y) * t
};
}
q.splice(q.length-1, 1);
}
api.drawLine(p0, q[0]);
p0 = q[0];
}
p0 = pts[0];
api.setColor("black");
api.drawCircle(p0,3);
pts.forEach(p => {
if(p===p0) return;
api.setColor("#DDD");
api.drawLine(p0,p);
api.setColor("black");
api.drawCircle(p,3);
p0 = p;
});
},
getOrder: function() {
var order = this.state.order;
if (order%10 === 1 && order !== 11) {
order += "st";
} else if (order%10 === 2 && order !== 12) {
order += "nd";
} else if (order%10 === 3 && order !== 13) {
order += "rd";
} else {
order += "th";
}
return order;
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = keyHandling(Reordering);
module.exports = keyHandling(generateBase("reordering", handler));

View File

@ -0,0 +1,89 @@
var modes;
module.exports = {
getInitialState: function() {
modes = this.modes = ["unite","intersect","exclude","subtract"];
return {
mode: modes[0]
};
},
setMode: function(mode) {
this.setState({ mode: mode });
},
formPath: function(api, mx, my, w, h) {
mx = mx || 0;
my = my || 0;
var unit = 30;
var unit2 = unit/2;
w = w || 8 * unit;
h = h || 4 * unit;
var w2 = w/2;
var h2 = h/2;
var ow3 = w2/3;
var oh3 = h2/3;
var Paper = api.Paper;
var Path = Paper.Path;
var Point = Paper.Point;
var path = new Path();
path.moveTo(
new Point(mx-w2 + unit*2, my-h2)
);
path.cubicCurveTo(
new Point(mx-w2 + unit2, my-h2 + unit2),
new Point(mx-w2 + unit2, my+h2 - unit2),
new Point(mx-w2 + unit*2, my+h2)
);
path.cubicCurveTo(
new Point(mx-ow3, my+oh3),
new Point(mx+ow3, my+oh3),
new Point(mx+w2 - unit*2, my+h2)
);
path.cubicCurveTo(
new Point(mx+w2 - unit2, my+h2 - unit2),
new Point(mx+w2 - unit2, my-h2 + unit2),
new Point(mx+w2 - unit*2, my-h2)
);
path.cubicCurveTo(
new Point(mx+ow3, my-oh3),
new Point(mx-ow3, my-oh3),
new Point(mx-w2 + unit*2, my-h2)
);
path.closePath(true);
path.strokeColor = "rgb(100,100,255)";
return path;
},
setup: function(api) {
var dim = api.getPanelWidth();
var pad = 40;
var cx = dim/2;
var cy = dim/2;
api.c1 = this.formPath(api, cx, cy);
cx += pad;
cy += pad;
api.c2 = this.formPath(api, cx, cy);
this.state.mode = modes[0];
},
onMouseMove: function(evt, api) {
var cx = evt.offsetX;
var cy = evt.offsetY;
api.c2.position = {x:cx, y:cy};
},
draw: function(api) {
if (api.c3) { api.c3.remove(); }
var c1 = api.c1,
c2 = api.c2,
fn = c1[this.state.mode].bind(c1),
c3 = api.c3 = fn(c2);
c3.strokeColor = "red";
c3.fillColor = "rgba(255,100,100,0.4)";
api.Paper.view.draw();
}
};

View File

@ -1,107 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "shapes";
var modes;
var Shapes = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
getInitialState: function() {
modes = this.modes = ["unite","intersect","exclude","subtract"];
return {
mode: modes[0]
};
},
setMode: function(mode) {
this.setState({ mode: mode });
},
formPath: function(api, mx, my, w, h) {
mx = mx || 0;
my = my || 0;
var unit = 30;
var unit2 = unit/2;
w = w || 8 * unit;
h = h || 4 * unit;
var w2 = w/2;
var h2 = h/2;
var ow3 = w2/3;
var oh3 = h2/3;
var Paper = api.Paper;
var Path = Paper.Path;
var Point = Paper.Point;
var path = new Path();
path.moveTo(
new Point(mx-w2 + unit*2, my-h2)
);
path.cubicCurveTo(
new Point(mx-w2 + unit2, my-h2 + unit2),
new Point(mx-w2 + unit2, my+h2 - unit2),
new Point(mx-w2 + unit*2, my+h2)
);
path.cubicCurveTo(
new Point(mx-ow3, my+oh3),
new Point(mx+ow3, my+oh3),
new Point(mx+w2 - unit*2, my+h2)
);
path.cubicCurveTo(
new Point(mx+w2 - unit2, my+h2 - unit2),
new Point(mx+w2 - unit2, my-h2 + unit2),
new Point(mx+w2 - unit*2, my-h2)
);
path.cubicCurveTo(
new Point(mx+ow3, my-oh3),
new Point(mx-ow3, my-oh3),
new Point(mx-w2 + unit*2, my-h2)
);
path.closePath(true);
path.strokeColor = "rgb(100,100,255)";
return path;
},
setup: function(api) {
var dim = api.getPanelWidth();
var pad = 40;
var cx = dim/2;
var cy = dim/2;
api.c1 = this.formPath(api, cx, cy);
cx += pad;
cy += pad;
api.c2 = this.formPath(api, cx, cy);
this.state.mode = modes[0];
},
onMouseMove: function(evt, api) {
var cx = evt.offsetX;
var cy = evt.offsetY;
api.c2.position = {x:cx, y:cy};
},
draw: function(api) {
if (api.c3) { api.c3.remove(); }
var c1 = api.c1,
c2 = api.c2,
fn = c1[this.state.mode].bind(c1),
c3 = api.c3 = fn(c2);
c3.strokeColor = "red";
c3.fillColor = "rgba(255,100,100,0.4)";
api.Paper.view.draw();
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Shapes;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("shapes", handler);

View File

@ -0,0 +1,91 @@
module.exports = {
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.forward = true;
},
drawSplit: function(api, curve) {
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var offset = {x:0, y:0};
var t = 0.5;
var pt = curve.get(0.5);
var split = curve.split(t);
api.drawCurve(split.left);
api.drawCurve(split.right);
api.setColor("red");
api.drawCircle(pt,3);
api.setColor("black");
offset.x = api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawCircle(pt,4);
offset.x -= 20;
offset.y -= 20;
api.drawSkeleton(split.left, offset, true);
api.drawCurve(split.left, offset);
offset.x += 40;
offset.y += 40;
api.drawSkeleton(split.right, offset, true);
api.drawCurve(split.right, offset);
},
drawAnimated: function(api, curve) {
api.setPanelCount(3);
api.reset();
var frame = api.getFrame();
var interval = 5 * api.getPlayInterval();
var t = (frame%interval)/interval;
var forward = (frame%(2*interval)) < interval;
if (forward) { t = t%1; } else { t = 1 - t%1; }
var offset = {x:0, y:0};
api.setColor("lightblue");
api.drawHull(curve, t);
api.drawSkeleton(curve);
api.drawCurve(curve);
var pt = curve.get(t);
api.drawCircle(pt, 4);
api.setColor("black");
offset.x += api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
var split = curve.split(t);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawHull(curve, t, offset);
api.setColor("black");
api.drawCurve(split.left, offset);
api.drawPoints(split.left.points, offset);
api.setFill("black");
api.text("Left side of curve split at t = " + (((100*t)|0)/100), {x: 10 + offset.x, y: 15 + offset.y});
offset.x += api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawHull(curve, t, offset);
api.setColor("black");
api.drawCurve(split.right, offset);
api.drawPoints(split.right.points, offset);
api.setFill("black");
api.text("Right side of curve split at t = " + (((100*t)|0)/100), {x: 10 + offset.x, y: 15 + offset.y});
},
togglePlay: function(evt, api) {
if (api.playing) { api.pause(); } else { api.play(); }
}
};

View File

@ -1,111 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "splitting";
var Splitting = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.forward = true;
},
drawSplit: function(api, curve) {
api.setPanelCount(2);
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var offset = {x:0, y:0};
var t = 0.5;
var pt = curve.get(0.5);
var split = curve.split(t);
api.drawCurve(split.left);
api.drawCurve(split.right);
api.setColor("red");
api.drawCircle(pt,3);
api.setColor("black");
offset.x = api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawCircle(pt,4);
offset.x -= 20;
offset.y -= 20;
api.drawSkeleton(split.left, offset, true);
api.drawCurve(split.left, offset);
offset.x += 40;
offset.y += 40;
api.drawSkeleton(split.right, offset, true);
api.drawCurve(split.right, offset);
},
drawAnimated: function(api, curve) {
api.setPanelCount(3);
api.reset();
var frame = api.getFrame();
var interval = 5 * api.getPlayInterval();
var t = (frame%interval)/interval;
var forward = (frame%(2*interval)) < interval;
if (forward) { t = t%1; } else { t = 1 - t%1; }
var offset = {x:0, y:0};
api.setColor("lightblue");
api.drawHull(curve, t);
api.drawSkeleton(curve);
api.drawCurve(curve);
var pt = curve.get(t);
api.drawCircle(pt, 4);
api.setColor("black");
offset.x += api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
var split = curve.split(t);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawHull(curve, t, offset);
api.setColor("black");
api.drawCurve(split.left, offset);
api.drawPoints(split.left.points, offset);
api.setFill("black");
api.text("Left side of curve split at t = " + (((100*t)|0)/100), {x: 10 + offset.x, y: 15 + offset.y});
offset.x += api.getPanelWidth();
api.drawLine({x:0,y:0},{x:0,y:api.getPanelHeight()}, offset);
api.setColor("lightgrey");
api.drawCurve(curve, offset);
api.drawHull(curve, t, offset);
api.setColor("black");
api.drawCurve(split.right, offset);
api.drawPoints(split.right.points, offset);
api.setFill("black");
api.text("Right side of curve split at t = " + (((100*t)|0)/100), {x: 10 + offset.x, y: 15 + offset.y});
},
togglePlay: function(evt, api) {
if (api.playing) { api.pause(); } else { api.play(); }
},
render: function() {
return (
<section>{ locale.getContent(page, this) }</section>
);
}
});
module.exports = Splitting;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("splitting", handler);

View File

@ -0,0 +1,71 @@
module.exports = {
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
align: function(points, line) {
var tx = line.p1.x,
ty = line.p1.y,
a = -Math.atan2(line.p2.y-ty, line.p2.x-tx),
cos = Math.cos,
sin = Math.sin,
d = function(v) {
return {
x: (v.x-tx)*cos(a) - (v.y-ty)*sin(a),
y: (v.x-tx)*sin(a) + (v.y-ty)*cos(a),
a: a
};
};
return points.map(d);
},
// FIXME: I'm not satisfied with needing to turn a bbox[] into a point[],
// this needs a bezier.js solution, really, with a call curve.tightbbox()
transpose: function(points, angle, offset) {
var tx = offset.x,
ty = offset.y,
cos = Math.cos,
sin = Math.sin,
v = [points.x.min, points.y.min, points.x.max, points.y.max];
return [
{x: v[0], y: v[1] },
{x: v[2], y: v[1] },
{x: v[2], y: v[3] },
{x: v[0], y: v[3] }
].map(p => {
var x=p.x, y=p.y;
return {
x: x*cos(angle) - y*sin(angle) + tx,
y: x*sin(angle) + y*cos(angle) + ty
};
});
},
draw: function(api, curve) {
api.reset();
var pts = curve.points;
var line = {p1: pts[0], p2: pts[pts.length-1]};
var apts = this.align(pts, line);
var angle = -apts[0].a;
var aligned = new api.Bezier(apts);
var bbox = aligned.bbox();
var tpts = this.transpose(bbox, angle, pts[0]);
api.setColor("#00FF00");
api.drawLine(tpts[0], tpts[1]);
api.drawLine(tpts[1], tpts[2]);
api.drawLine(tpts[2], tpts[3]);
api.drawLine(tpts[3], tpts[0]);
api.setColor("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
}
};

View File

@ -1,89 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "tightbounds";
var TightBounds = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
},
align: function(points, line) {
var tx = line.p1.x,
ty = line.p1.y,
a = -Math.atan2(line.p2.y-ty, line.p2.x-tx),
cos = Math.cos,
sin = Math.sin,
d = function(v) {
return {
x: (v.x-tx)*cos(a) - (v.y-ty)*sin(a),
y: (v.x-tx)*sin(a) + (v.y-ty)*cos(a),
a: a
};
};
return points.map(d);
},
// FIXME: I'm not satisfied with needing to turn a bbox[] into a point[],
// this needs a bezier.js solution, really, with a call curve.tightbbox()
transpose: function(points, angle, offset) {
var tx = offset.x,
ty = offset.y,
cos = Math.cos,
sin = Math.sin,
v = [points.x.min, points.y.min, points.x.max, points.y.max];
return [
{x: v[0], y: v[1] },
{x: v[2], y: v[1] },
{x: v[2], y: v[3] },
{x: v[0], y: v[3] }
].map(p => {
var x=p.x, y=p.y;
return {
x: x*cos(angle) - y*sin(angle) + tx,
y: x*sin(angle) + y*cos(angle) + ty
};
});
},
draw: function(api, curve) {
api.reset();
var pts = curve.points;
var line = {p1: pts[0], p2: pts[pts.length-1]};
var apts = this.align(pts, line);
var angle = -apts[0].a;
var aligned = new api.Bezier(apts);
var bbox = aligned.bbox();
var tpts = this.transpose(bbox, angle, pts[0]);
api.setColor("#00FF00");
api.drawLine(tpts[0], tpts[1]);
api.drawLine(tpts[1], tpts[2]);
api.drawLine(tpts[2], tpts[3]);
api.drawLine(tpts[3], tpts[0]);
api.setColor("black");
api.drawSkeleton(curve);
api.drawCurve(curve);
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = TightBounds;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("tightbounds", handler);

View File

@ -0,0 +1,126 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
setup: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 8;
},
generate: function(api, curve, offset, pad, fwh) {
offset.x += pad;
offset.y += pad;
var len = curve.length();
var pts = [{x:0, y:0, d:0}];
for(var v=1, t, d; v<=100; v++) {
t = v/100;
d = curve.split(t).left.length();
pts.push({
x: api.utils.map(t, 0,1, 0,fwh),
y: api.utils.map(d, 0,len, 0,fwh),
d: d,
t: t
});
}
return pts;
},
draw: function(api, curve, offset) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var len = curve.length();
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var pad = 20;
var fwh = w - 2*pad;
offset.x += w;
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "d",0,len, offset);
return this.generate(api, curve, offset, pad, fwh);
},
plotOnly: function(api, curve) {
api.setPanelCount(2);
var offset = {x:0, y:0};
var pts = this.draw(api, curve, offset);
for(var i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
},
drawColoured: function(api, curve) {
api.setPanelCount(3);
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var pad = 20;
var fwh = w - 2*pad;
var offset = {x:0, y:0};
var len = curve.length();
var pts = this.draw(api, curve, offset);
var s = api.steps, i, p, ts=[];
for(i=0; i<=s; i++) {
var target = (i * len)/s;
// find the t nearest our target distance
for (p=0; p<pts.length; p++) {
if (pts[p].d > target) {
p--;
break;
}
}
if(p<0) p=0;
if(p===pts.length) p=pts.length-1;
ts.push(pts[p]);
}
for(i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
ts.forEach(p => {
var pt = { x: api.utils.map(p.t,0,1,0,fwh), y: 0 };
var pd = { x: 0, y: api.utils.map(p.d,0,len,0,fwh) };
api.setColor("black");
api.drawCircle(pt, 3, offset);
api.drawCircle(pd, 3, offset);
api.setColor("lightgrey");
api.drawLine(pt, {x:pt.x, y:pd.y}, offset);
api.drawLine(pd, {x:pt.x, y:pd.y}, offset);
});
offset = {x:2*w, y:0};
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
var idx=0, colors = ["rgb(240,0,200)", "rgb(0,40,200)"];
api.setColor(colors[idx]);
var p0 = curve.get(pts[0].t), p1;
api.drawCircle(curve.get(0), 4, offset);
for (i=1, p1; i<pts.length; i++) {
p1 = curve.get(pts[i].t);
api.drawLine(p0, p1, offset);
if (ts.indexOf(pts[i]) !== -1) {
api.setColor(colors[++idx % colors.length]);
api.drawCircle(p1, 4, offset);
}
p0 = p1;
}
}
};

View File

@ -1,146 +1,4 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "tracing";
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
var keyHandling = require("../../decorators/keyhandling-decorator.jsx");
var Tracing = React.createClass({
statics: {
keyHandlingOptions: {
propName: "steps",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
controller: function(api) {
if (api.steps < 1) {
api.steps = 1;
}
}
}
},
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
var curve = api.getDefaultCubic();
api.setCurve(curve);
api.steps = 8;
},
generate: function(api, curve, offset, pad, fwh) {
offset.x += pad;
offset.y += pad;
var len = curve.length();
var pts = [{x:0, y:0, d:0}];
for(var v=1, t, d; v<=100; v++) {
t = v/100;
d = curve.split(t).left.length();
pts.push({
x: api.utils.map(t, 0,1, 0,fwh),
y: api.utils.map(d, 0,len, 0,fwh),
d: d,
t: t
});
}
return pts;
},
draw: function(api, curve, offset) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
var len = curve.length();
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var pad = 20;
var fwh = w - 2*pad;
offset.x += w;
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
api.drawAxes(pad, "t",0,1, "d",0,len, offset);
return this.generate(api, curve, offset, pad, fwh);
},
plotOnly: function(api, curve) {
api.setPanelCount(2);
var offset = {x:0, y:0};
var pts = this.draw(api, curve, offset);
for(var i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
},
drawColoured: function(api, curve) {
api.setPanelCount(3);
var w = api.getPanelWidth();
var h = api.getPanelHeight();
var pad = 20;
var fwh = w - 2*pad;
var offset = {x:0, y:0};
var len = curve.length();
var pts = this.draw(api, curve, offset);
var s = api.steps, i, p, ts=[];
for(i=0; i<=s; i++) {
var target = (i * len)/s;
// find the t nearest our target distance
for (p=0; p<pts.length; p++) {
if (pts[p].d > target) {
p--;
break;
}
}
if(p<0) p=0;
if(p===pts.length) p=pts.length-1;
ts.push(pts[p]);
}
for(i=0; i<pts.length-1; i++) {
api.drawLine(pts[i], pts[i+1], offset);
}
ts.forEach(p => {
var pt = { x: api.utils.map(p.t,0,1,0,fwh), y: 0 };
var pd = { x: 0, y: api.utils.map(p.d,0,len,0,fwh) };
api.setColor("black");
api.drawCircle(pt, 3, offset);
api.drawCircle(pd, 3, offset);
api.setColor("lightgrey");
api.drawLine(pt, {x:pt.x, y:pd.y}, offset);
api.drawLine(pd, {x:pt.x, y:pd.y}, offset);
});
offset = {x:2*w, y:0};
api.drawLine({x:0,y:0}, {x:0,y:h}, offset);
var idx=0, colors = ["rgb(240,0,200)", "rgb(0,40,200)"];
api.setColor(colors[idx]);
var p0 = curve.get(pts[0].t), p1;
api.drawCircle(curve.get(0), 4, offset);
for (i=1, p1; i<pts.length; i++) {
p1 = curve.get(pts[i].t);
api.drawLine(p0, p1, offset);
if (ts.indexOf(pts[i]) !== -1) {
api.setColor(colors[++idx % colors.length]);
api.drawCircle(p1, 4, offset);
}
p0 = p1;
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = keyHandling(Tracing);
module.exports = keyHandling(generateBase("tracing", handler));

View File

@ -0,0 +1,105 @@
module.exports = {
setup: function(api) {
api.setPanelCount(3);
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.step = 25;
},
draw: function(api, curve) {
var dim = api.getPanelWidth(),
pts = curve.points,
p1 = pts[0],
p2=pts[1],
p3 = pts[2],
p1e, p2e, m, t, i,
offset = {x:0, y:0},
d,v,tvp;
api.reset();
api.setColor("black");
api.setFill("black");
api.drawSkeleton(curve, offset);
api.text("First linear interpolation at "+api.step+"% steps", {x:5, y:15}, offset);
offset.x += dim;
api.drawLine({x:0, y:0}, {x:0, y:this.dim}, offset);
api.drawSkeleton(curve, offset);
api.text("Second interpolation at "+api.step+"% steps", {x:5, y:15}, offset);
offset.x += dim;
api.drawLine({x:0, y:0}, {x:0, y:this.dim}, offset);
api.drawSkeleton(curve, offset);
api.text("Curve points generated this way", {x:5, y:15}, offset);
api.setColor("lightgrey");
for(t=1,d=20,v,tvp; t<d; t++) {
v = t/d;
tvp = curve.get(v);
api.drawCircle(tvp,2,offset);
}
for(i = 3*api.step; i>0; i -= api.step) {
t = i/100;
if (t>1) continue;
api.setRandomColor();
p1e = {
x: p1.x + t * (p2.x - p1.x),
y: p1.y + t * (p2.y - p1.y)
};
p2e = {
x: p2.x + t * (p3.x - p2.x),
y: p2.y + t * (p3.y - p2.y)
};
m = {
x: p1e.x + t * (p2e.x - p1e.x),
y: p1e.y + t * (p2e.y - p1e.y)
};
offset = {x:0, y:0};
api.drawCircle(p1e,3, offset);
api.drawCircle(p2e,3, offset);
api.setWeight(0.5);
api.drawLine(p1e, p2e, offset);
api.setWeight(1.5);
api.drawLine(p1, p1e, offset);
api.drawLine(p2, p2e, offset);
api.setWeight(1);
offset.x += dim;
api.drawCircle(p1e,3, offset);
api.drawCircle(p2e,3, offset);
api.setWeight(0.5);
api.drawLine(p1e, p2e, offset);
api.setWeight(1.5);
api.drawLine(p1e, m, offset);
api.setWeight(1);
api.drawCircle(m,3,offset);
offset.x += dim;
api.drawCircle(m,3,offset);
api.text(i+"%, or t = " + api.utils.round(t,2), {x: m.x + 10 + offset.x, y: m.y + 10 + offset.y});
}
},
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
onKeyDown: function(e, api) {
var v = this.values[e.keyCode];
if(v) {
e.preventDefault();
api.step += v;
if (api.step < 1) {
api.step = 1;
}
}
}
};

View File

@ -1,123 +1,3 @@
var React = require("react");
var Locale = require("../../../lib/locale");
var locale = new Locale();
var page = "whatis";
var Whatis = React.createClass({
getDefaultProps: function() {
return {
title: locale.getTitle(page)
};
},
setup: function(api) {
api.setPanelCount(3);
var curve = api.getDefaultQuadratic();
api.setCurve(curve);
api.step = 25;
},
draw: function(api, curve) {
var dim = api.getPanelWidth(),
pts = curve.points,
p1 = pts[0],
p2=pts[1],
p3 = pts[2],
p1e, p2e, m, t, i,
offset = {x:0, y:0},
d,v,tvp;
api.reset();
api.setColor("black");
api.setFill("black");
api.drawSkeleton(curve, offset);
api.text("First linear interpolation at "+api.step+"% steps", {x:5, y:15}, offset);
offset.x += dim;
api.drawLine({x:0, y:0}, {x:0, y:this.dim}, offset);
api.drawSkeleton(curve, offset);
api.text("Second interpolation at "+api.step+"% steps", {x:5, y:15}, offset);
offset.x += dim;
api.drawLine({x:0, y:0}, {x:0, y:this.dim}, offset);
api.drawSkeleton(curve, offset);
api.text("Curve points generated this way", {x:5, y:15}, offset);
api.setColor("lightgrey");
for(t=1,d=20,v,tvp; t<d; t++) {
v = t/d;
tvp = curve.get(v);
api.drawCircle(tvp,2,offset);
}
for(i = 3*api.step; i>0; i -= api.step) {
t = i/100;
if (t>1) continue;
api.setRandomColor();
p1e = {
x: p1.x + t * (p2.x - p1.x),
y: p1.y + t * (p2.y - p1.y)
};
p2e = {
x: p2.x + t * (p3.x - p2.x),
y: p2.y + t * (p3.y - p2.y)
};
m = {
x: p1e.x + t * (p2e.x - p1e.x),
y: p1e.y + t * (p2e.y - p1e.y)
};
offset = {x:0, y:0};
api.drawCircle(p1e,3, offset);
api.drawCircle(p2e,3, offset);
api.setWeight(0.5);
api.drawLine(p1e, p2e, offset);
api.setWeight(1.5);
api.drawLine(p1, p1e, offset);
api.drawLine(p2, p2e, offset);
api.setWeight(1);
offset.x += dim;
api.drawCircle(p1e,3, offset);
api.drawCircle(p2e,3, offset);
api.setWeight(0.5);
api.drawLine(p1e, p2e, offset);
api.setWeight(1.5);
api.drawLine(p1e, m, offset);
api.setWeight(1);
api.drawCircle(m,3,offset);
offset.x += dim;
api.drawCircle(m,3,offset);
api.text(i+"%, or t = " + api.utils.round(t,2), {x: m.x + 10 + offset.x, y: m.y + 10 + offset.y});
}
},
values: {
"38": 1, // up arrow
"40": -1 // down arrow
},
onKeyDown: function(e, api) {
var v = this.values[e.keyCode];
if(v) {
e.preventDefault();
api.step += v;
if (api.step < 1) {
api.step = 1;
}
}
},
render: function() {
return locale.getContent(page, this);
}
});
module.exports = Whatis;
var handler = require("./handler.js");
var generateBase = require("../../generate-base");
module.exports = generateBase("whatis", handler);

View File

@ -8,5 +8,6 @@ fs.copySync("en-GB/article.js", "article.js");
var html = fs.readFileSync("index.html").toString();
html = html.replace(' <base href="..">\n', '');
html = html.replace('className=', 'class=');
html = html.replace('<script src="en-GB/article.js', '<script src="article.js');
html = "<!-- AUTOGENERATED CONTENT, PLEASE EDIT 'index.template.html' INSTEAD! -->\n" + html;
fs.writeFileSync("index.html", html);

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -155,6 +155,6 @@
</article>
<!-- the actual article is a JS bundle -->
<script src="en-GB/article.js" async></script>
<script src="article.js" async></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -24,10 +24,10 @@ Module.prototype.require = function() {
* syntax with `)` in the links themselves.
*/
function fixMarkDownLinks(data, chunks, chunkMore) {
var p = 0,
next = chunkMore ? chunkMore[0] : false,
otherChunkers = chunkMore ? chunkMore.slice(1) : false;
var fixes = [];
var next = chunkMore ? chunkMore[0] : false,
otherChunkers = chunkMore ? chunkMore.slice(1) : false,
fixes = [];
data.replace(/\[[^\]]+\]\(/g, function(_match, pos, _fullstring) {
// this is the start of a link. Find the offset at which the next `)`
// is actually the link closer.
@ -62,7 +62,48 @@ function fixMarkDownLinks(data, chunks, chunkMore) {
}
/**
*
* ...
*/
function chunkBSplineGraphicsJSX(data, chunks, chunkMore) {
var p = 0,
next = chunkMore ? chunkMore[0] : false,
otherChunkers = chunkMore ? chunkMore.slice(1) : false,
bgfxTag = '<BSplineGraphic',
bgfxEnd = '/>',
bgfxEnd2 = '</BSplineGraphic>';
while (p !== -1) {
// Let's check a BSplineGraphic tag
let bgfx = data.indexOf(bgfxTag, p);
if (bgfx === -1) {
// No <BSplineGraphic/> block found: we're done here. Parse the remaining
// data for whatever else might be in there.
performChunking(data.substring(p), chunks, next, otherChunkers);
break;
}
// First parse the non-<BSplineGraphic/> data for whatever else might be in there.
performChunking(data.substring(p, bgfx), chunks, next, otherChunkers);
let tail = data.substring(bgfx),
noContent = !!tail.match(/^<BSplineGraphic[^>]+\/>/),
eol;
// Then capture the <BSplineGraphic>...</BSplineGraphic> or <BSplineGraphic .../> block and mark it as "don't convert".
if (noContent) {
eol = data.indexOf(bgfxEnd, bgfx) + bgfxEnd.length;
} else {
eol = data.indexOf(bgfxEnd2, bgfx) + bgfxEnd2.length;
}
chunks.push({ convert: false, type: "bgfx", s:bgfx, e:eol, data: data.substring(bgfx, eol) });
p = eol;
}
}
/**
* ...
*/
function chunkGraphicJSX(data, chunks, chunkMore) {
var p = 0,
@ -73,7 +114,7 @@ function chunkGraphicJSX(data, chunks, chunkMore) {
gfxEnd2 = '</Graphic>';
while (p !== -1) {
// Let's check a LaTeX block
// Let's check a Graphic tag
let gfx = data.indexOf(gfxTag, p);
if (gfx === -1) {
// No <Graphic/> block found: we're done here. Parse the remaining
@ -102,7 +143,7 @@ function chunkGraphicJSX(data, chunks, chunkMore) {
}
/**
*
* ...
*/
function chunkDivEnds(data, chunks, chunkMore) {
var next = chunkMore ? chunkMore[0] : false,
@ -120,7 +161,37 @@ function chunkDivEnds(data, chunks, chunkMore) {
/**
*
* ...
*/
function chunkTable(data, chunks, chunkMore) {
var p = 0,
next = chunkMore ? chunkMore[0] : false,
otherChunkers = chunkMore ? chunkMore.slice(1) : false,
tableMatch = '\n<table',
tableClosingTag = '</table>\n';
while (p !== -1) {
// Let's check for a <table> tag
let table = data.indexOf(tableMatch, p);
if (table === -1) {
// No tables found: we're done here. Parse the remaining
// data for whatever else might be in there.
performChunking(data.substring(p), chunks, next, otherChunkers);
break;
}
// First parse the non-table data for whatever else might be in there.
performChunking(data.substring(p, table), chunks, next, otherChunkers);
// then mark the table code as no-convert
let eod = data.indexOf(tableClosingTag, table) + tableClosingTag.length;
chunks.push({ convert: false, type: "table", s:table, e:eod, data: data.substring(table, eod) });
p = eod;
}
}
/**
* ...
*/
function chunkDivs(data, chunks, chunkMore) {
var p = 0,
@ -152,12 +223,11 @@ function chunkDivs(data, chunks, chunkMore) {
if (className !== null) { className = className[1]; }
let eod, type="div";
if (className === "figure") {
if (className === "figure" || className === "two-column") {
eod = data.indexOf(divClosingTag, div) + divClosingTag.length;
type += ".figure";
type += "." + className;
} else {
eod = data.indexOf(divEnd, div) + divEnd.length;
}
chunks.push({ convert: false, type: type, s:div, e:eod, data: data.substring(div, eod) });
p = eod;
@ -216,7 +286,15 @@ function performChunking(data, chunks, chunker, moreChunkers) {
*/
function chunk(data) {
var chunks = [];
performChunking(data, chunks, chunkLatex, [chunkDivs, chunkDivEnds, chunkGraphicJSX, fixMarkDownLinks]);
var chunkers = [
chunkDivs,
chunkDivEnds,
chunkTable,
chunkGraphicJSX,
chunkBSplineGraphicsJSX,
fixMarkDownLinks
];
performChunking(data, chunks, chunkLatex,chunkers);
return chunks;
}
@ -274,7 +352,19 @@ function formContentBundle(locale, content) {
.replace(/></g,'>\n<')
.replace(/\\\\/g, '\\');
var bundle = `var React = require('react');\nvar Graphic = require("../../components/Graphic.jsx");\nvar SectionHeader = require("../../components/SectionHeader.jsx");\n\nSectionHeader.locale="${locale}";\n\nmodule.exports = ${bcode};\n`;
var bundle = [
`var React = require('react');`,
`var Graphic = require("../../components/Graphic.jsx");`,
`var SectionHeader = require("../../components/SectionHeader.jsx");`,
`var BSplineGraphic = require("../../components/BSplineGraphic.jsx");`,
`var KnotController = require("../../components/KnotController.jsx");`,
`var WeightController = require("../../components/WeightController.jsx");`,
``,
`SectionHeader.locale="${locale}";`,
``,
`module.exports = ${bcode};`,
``
].join('\n');
return bundle;
}

File diff suppressed because one or more lines are too long