1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-29 01:29:03 +02:00
Files
BezierInfo-2/components/sections/polybezier/index.js
2016-01-10 15:17:56 -08:00

343 lines
13 KiB
JavaScript

var React = require("react");
var Graphic = require("../../Graphic.jsx");
var SectionHeader = require("../../SectionHeader.jsx");
var atan2 = Math.atan2, sqrt = Math.sqrt, sin = Math.sin, cos = Math.cos;
var PolyBezier = React.createClass({
getDefaultProps: function() {
return {
title: "Forming poly-Bézier curves"
};
},
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);
}
},
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 && 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 && 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 (
<section>
<SectionHeader {...this.props} />
<p>Much like lines can be chained together to form polygons, Bézier curves can be chained together
to form poly-Béziers, and the only trick required is to make sure that:</p>
<ol>
<li>the end point of each section is the starting point of the following section, and</li>
<li>the derivatives across that dual point line up.</li>
</ol>
<p>Unless, of course, you want discontinuities; then you don't even need 2.</p>
<p>We'll cover three forms of poly-Bézier curves in this section. First, we'll look at the kind
that just follows point 1. where the end point of a segment is the same point as the start point
of the next segment. This leads to poly-Béziers that are pretty hard to work with, but they're
the easiest to implement:</p>
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}/>
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}/>
<p>Dragging the control points around only affects the curve segments that the control point belongs
to, and moving an on-curve point leaves the control points where they are, which is not the most useful
for practical modelling purposes. So, let's add in the logic we need to make things a little better.
We'll start by linking up control points by ensuring that the "incoming" derivative at an on-curve
point is the same as it's "outgoing" derivative:</p>
<p>\[
B'(1)_n = B'(0)_{n+1}
\]</p>
<p>We can effect this quite easily, because we know that the vector from a curve's last control point
to its last on-curve point is equal to the derivative vector. If we want to ensure that the first control
point of the next curve matches that, all we have to do is mirror that last control point through the
last on-curve point. And mirroring any point A through any point B is really simple:</p>
<p>\[
Mirrored = \left [
\begin{matrix} B_x + (B_x - A_x) \\ B_y + (B_y - A_y) \end{matrix}
\right ] = \left [
\begin{matrix} 2B_x - A_x \\ 2B_y - A_y \end{matrix}
\right ]
\]</p>
<p>So let's implement that and see what it gets us. The following two graphics show a quadratic
and a cubic poly-Bézier curve again, but this time moving the control points around moves others,
too. However, you might see something unexpected going on for quadratic curves...</p>
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}
onMouseMove={this.linkDerivatives}/>
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}
onMouseMove={this.linkDerivatives}/>
<p>As you can see, quadratic curves are particularly ill-suited for poly-Bézier curves, as all
the control points are effectively linked. Move one of them, and you move all of them. This means
that we cannot use quadratic poly-Béziers for anything other than really, really simple shapes.
And even then, they're probably the wrong choice. Cubic curves are pretty decent, but the fact
that the derivatives are linked means we can't manipulate curves as well as we might if we
relaxed the constraints a little.</p>
<p>So: let's relax the requirement a little.</p>
<p>We can change the constraint so that we still preserve the <em>angle</em> of the derivatives across
sections (so transitions from one section to the next will still look natural), but give up the
requirement that they should also have the same <em>vector length</em>. Doing so will give us a much
more useful kind of poly-Bézier curve:</p>
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw} onMouseMove={this.linkDirection}/>
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw} onMouseMove={this.linkDirection}/>
<p>Cubic curves are now better behaved when it comes to dragging control points around, but the
quadratic poly-Bézier has a problem: in the example shape, moving one control points will move
the control points around it properly, but they in turn define "the next" control point and they
do so in incompatible ways! This is one of the many reasons why quadratic curves are not really
useful to work with.</p>
<p>Finally, we also want to make sure that moving the on-curve coordinates preserves the relative
positions of the associated control points. With that, we get to the kind of curve control that you
might be familiar with from applications like Photoshop, Inkscape, Blender, etc.</p>
<Graphic preset="poly" title="Loosely connected quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}
onMouseDown={this.bufferPoints} onMouseMove={this.modelCurve}/>
<Graphic preset="poly" title="Loosely connected cubic poly-Bézier" setup={this.setupCubic} draw={this.draw}
onMouseDown={this.bufferPoints} onMouseMove={this.modelCurve}/>
<p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a
serious problem: we can move an on-curve point in such a way that we can't compute what needs to
"happen next". Move the top point down, below the left and right points, for instance. There
is no way to preserve correct control points without a kink at the bottom point. Quadratic curves:
just not that good...</p>
<p>A final improvement is to offer fine-level control over which points behave which, so that you can
have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves
for the rest of the path. Implementing that, is left as an excercise for the reader.</p>
</section>
);
}
});
module.exports = PolyBezier;