1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-30 03:30:34 +02:00

polybezier

This commit is contained in:
Pomax
2016-01-10 13:46:59 -08:00
parent d6a335006e
commit 3c190e630c
7 changed files with 416 additions and 99 deletions

View File

@@ -39,10 +39,7 @@ module.exports = {
catmullconv: require("./catmullconv"),
catmullmoulding: require("./catmullmoulding"),
/*
// This requires bezier.js to have a proper poly implementation
polybezier: require("./polybezier"),
*/
polybezier: require("./polybezier"),
/*
// This section is way too much work to port, and not worth implementing given paper.js etc.

View File

@@ -2,6 +2,8 @@ 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 {
@@ -27,27 +29,213 @@ var PolyBezier = React.createClass({
},
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), anchor, toMove, fixed;
// then move the other control points
[-1,1].forEach(v => {
anchor = i - v + api.lpts.length;
anchor = api.lpts[anchor % api.lpts.length];
fixed = i - 2*v + api.lpts.length;
fixed = api.lpts[fixed % api.lpts.length];
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 = new api.Bezier(pts[0],pts[1],pts[2]);
api.drawSkeleton(c1);
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 = new api.Bezier(pts[2],pts[3],pts[4]);
api.drawSkeleton(c2);
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 = new api.Bezier(pts[4],pts[5],pts[6]);
api.drawSkeleton(c3);
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 = new api.Bezier(pts[6],pts[7],pts[0]);
api.drawSkeleton(c4);
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() {
@@ -56,22 +244,37 @@ var PolyBezier = React.createClass({
<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: A) the end point of each
section is the starting point of the following section, and B) the derivatives across that
dual point line up. Unless, of course, you want discontinuities; then you don't even need (B).</p>
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 enforces "the outgoing derivative is the same as the incoming derivative" across sections:</p>
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 actually guarantee this really easily, because we know that the vector from a curve's
last control point to its last on-curve point is equal to the derivative vector. If we want to
ensure that the first control point of the next curve matches that, all we have to do is mirror
that last control point through the last on-curve point. And mirroring any point A through any
point B is really simple:</p>
<p>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 [
@@ -82,20 +285,13 @@ var PolyBezier = React.createClass({
\]</p>
<p>So let's implement that and see what it gets us. The following two graphics show a quadratic
and a cubic poly-Bézier curve; both consist of multiple sub-curves, but because of our constraint,
not all points on the curves can be moved around freely. Some points, when moved, will move other
points by virtue of changing the curve across sections.</p>
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="Forming a quadratic poly-Bézier" setup={this.setupQuadratic} draw={this.draw}/>
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="Forming a cubic poly-Bézier">
void setupCurve() {
setupDefaultCubicPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointConstrained(pt, mx, my);
}</textarea>
<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
@@ -104,66 +300,40 @@ var PolyBezier = React.createClass({
that the derivatives are linked means we can't manipulate curves as well as we might if we
relaxed the constraints a little.</p>
<p>So: let's relax them!</p>
<p>So: let's relax the requirement a little.</p>
<p>We can change the constraint so that we still preserve the angle of the derivatives across
sections (so transitions from one section to the next will still look natural), but give up
the requirement that they should also have the same vector length. Doing so will give us
a much more a useful kind of poly-Bézier curve:</p>
<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>
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="A half-constrained quadratic poly-Bézier">
void setupCurve() {
setupDefaultQuadraticPoly();
}
<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}/>
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointHalfConstrained(pt, mx, my);
}</textarea>
<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>
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="A half-constrained cubic poly-Bézier">
void setupCurve() {
setupDefaultCubicPoly();
}
<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>
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePointHalfConstrained(pt, mx, my);
}</textarea>
<p>Quadratic curves are still silly, but cubic curves are now much more controllable.</p>
<p>If we want even more control, we could just abandon the derivative constraints entirely,
and simply assure that the end point of one section is the same as the start point of the next section,
and then keep it at that. This gives us the greatest degree of freedom when it comes to modelling
shapes, but also means that our poly-Bézier constructs are no longer continuous curves. Sometimes
this is exactly what you want (because it lets you add corners to a shape, while still only using
Bézier curves).</p>
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="An unconstrained quadratic poly-Bézier">
void setupCurve() {
setupDefaultQuadraticPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePoint(pvt, mx, my);
}</textarea>
<textarea className="sketch-code" data-sketch-preset="poly" data-sketch-title="An unconstrained cubic poly-Bézier">
void setupCurve() {
setupDefaultCubicPoly();
}
void movePoint(PolyBezierCurve p, int pt, int mx, int my) {
p.movePoint(pvt, mx, my);
}</textarea>
<p>When doing any kind of modelling, you generally don't want a poly-Bézier that will only let you
pick one of the three forms for all your points; most graphics applications that deal with Bézier
curves will actually let you pick, per on-curve point, how to deal with the control points around it:
fully constrained, loosely constrained, or completely unconstrained. The best shape modelling comes
from having a curve that will let you pick what you need, when you need it, without having to start
a new poly-Bézier curve.</p>
<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>
);
}

View File

@@ -46,6 +46,7 @@ var Projections = React.createClass({
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);
@@ -53,6 +54,7 @@ var Projections = React.createClass({
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});
}
},