1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-12 19:54:31 +02:00
This commit is contained in:
Pomax
2016-09-14 14:55:46 -07:00
parent d7d1df1119
commit b9e1d711fd
15 changed files with 538 additions and 74 deletions

File diff suppressed because one or more lines are too long

View File

@@ -18,6 +18,7 @@ var BSplineGraphic = React.createClass({
this.activeDistance = 9;
this.points = [];
this.knots = [];
this.weights = [];
this.nodes = [];
this.cp = undefined;
this.dx = undefined;
@@ -37,33 +38,55 @@ var BSplineGraphic = React.createClass({
return <canvas className="bspline-graphic" ref="sketch" />;
},
keydownlisten(e) { this.setKeyboardValues(e); this.keyDown(); },
keyuplisten(e) { this.setKeyboardValues(e); this.keyUp(); },
keypresslisten(e) { this.setKeyboardValues(e); this.keyPressed(); },
mousedownlisten(e) { this.setMouseValues(e); this.mouseDown(); },
mouseuplisten(e) { this.setMouseValues(e); this.mouseUp(); },
mousemovelisten(e) { this.setMouseValues(e); this.mouseMove(); if(this.isMouseDown && this.mouseDrag) { this.mouseDrag(); }},
wheellissten(e) { e.preventDefault(); this.scrolled(e.deltaY < 0 ? 1 : -1); },
componentDidMount() {
var cvs = this.cvs = this.refs.sketch;
// Keyboard event handling
cvs.addEventListener("keydown", (e) => { this.setKeyboardValues(e); if (typeof this.keyDown !== "undefined") { this.keyDown(); }});
cvs.addEventListener("keyup", (e) => { this.setKeyboardValues(e); if (typeof this.keyUp !== "undefined") { this.keyUp(); }});
cvs.addEventListener("keypress", (e) => { this.setKeyboardValues(e); if (typeof this.keyPressed !== "undefined") { this.keyPressed(); }});
cvs.addEventListener("keydown", this.keydownlisten);
cvs.addEventListener("keyup", this.keyuplisten);
cvs.addEventListener("keypress", this.keypresslisten);
// Mouse event handling
cvs.addEventListener("mousedown", (e) => { this.setMouseValues(e); if (typeof this.mouseDown !== "undefined") { this.mouseDown(); }});
cvs.addEventListener("mouseup", (e) => { this.setMouseValues(e); if (typeof this.mouseUp !== "undefined") { this.mouseUp(); }});
cvs.addEventListener("mousemove", (e) => { this.setMouseValues(e); if (typeof this.mouseMove !== "undefined") { this.mouseMove(); } if(this.isMouseDown && this.mouseDrag) { this.mouseDrag(); }});
cvs.addEventListener("mousedown", this.mousedownlisten);
cvs.addEventListener("mouseup", this.mouseuplisten);
cvs.addEventListener("mousemove", this.mousemovelisten);
// Scroll event handling
if (this.props.scrolling) { cvs.addEventListener("wheel", this.wheellissten); }
// Boom let's go
this.setup();
},
componentWillUnmount() {
var cvs = this.cvs = this.refs.sketch;
cvs.removeEventListener("keydown", this.keydownlisten);
cvs.removeEventListener("keyup", this.keyuplisten);
cvs.removeEventListener("keypress", this.keypresslisten);
cvs.removeEventListener("mousedown", this.mousedownlisten);
cvs.removeEventListener("mouseup", this.mouseuplisten);
cvs.removeEventListener("mousemove", this.mousemovelisten);
if (this.props.scrolling) { cvs.removeEventListener("wheel", this.wheellissten); }
},
// base API
drawCurve(points) {
points = points || this.points;
var ctx = this.ctx;
var weights = this.weights.length>0 ? this.weights : false;
ctx.beginPath();
var p = interpolate(0, this.degree, points, this.knots);
var p = interpolate(0, this.degree, points, this.knots, weights);
ctx.moveTo(p[0], p[1]);
for(let t=0.01; t<1; t+=0.01) {
p = interpolate(t, this.degree, points, this.knots);
p = interpolate(t, this.degree, points, this.knots, weights);
ctx.lineTo(p[0], p[1]);
}
p = interpolate(1, this.degree, points, this.knots);
p = interpolate(1, this.degree, points, this.knots, weights);
ctx.lineTo(p[0], p[1]);
ctx.stroke();
ctx.closePath();
@@ -71,11 +94,12 @@ var BSplineGraphic = React.createClass({
drawKnots(points) {
var knots = this.knots;
var weights = this.weights.length>0 ? this.weights : false;
knots.forEach((knot,i) => {
if (i < this.degree) return;
if (i > knots.length - 1 - this.degree) return;
var p = interpolate(knot, this.degree, points, knots, false, false, true);
this.ellipse(p[0], p[1], 3);
var p = interpolate(knot, this.degree, points, knots, weights, false, true);
this.circle(p[0], p[1], 3);
});
},
@@ -102,11 +126,11 @@ var BSplineGraphic = React.createClass({
i;
// form the open-uniform knot vector
for (i=1; i < l - this.degree; i++) { knots.push(i); }
for (i=1; i < l - this.degree; i++) { knots.push(i + this.degree); }
// add [degree] zeroes at the front
for (i=0; i <= this.degree; i++) { knots = [0].concat(knots); }
for (i=0; i <= this.degree; i++) { knots = [this.degree].concat(knots); }
// add [degree] max-values to the back
for (i=0; i <= this.degree; i++) { knots.push(m); }
for (i=0; i <= this.degree; i++) { knots.push(m + this.degree); }
return knots;
},
@@ -138,6 +162,12 @@ var BSplineGraphic = React.createClass({
return nodes;
},
formWeights(points) {
var weights = [];
points.forEach(p => weights.push(1));
return weights;
},
setDegree(d) {
this.degree += d;
this.knots = this.formKnots(this.points);
@@ -211,6 +241,28 @@ var BSplineGraphic = React.createClass({
// ... do nothing?
},
scrolled(direction) {
this.cp = this.getCurrentPoint(this.mouseX, this.mouseY);
if (!this.cp) return;
// base case
var pos = this.points.indexOf(this.cp);
if (this.weights.length>pos) {
this.weights[pos] += direction * 0.1;
if (this.weights[pos] < 0) {
this.weights[pos] = 0;
}
}
// possible multiplicity due to "closed" curves
pos = this.points.indexOf(this.cp, pos+1);
if (pos!==-1 && this.weights.length>pos) {
this.weights[pos] += direction * 0.1;
if (this.weights[pos] < 0) {
this.weights[pos] = 0;
}
}
this.redraw();
},
// keyboard events
setKeyboardValues(e) {
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
@@ -250,7 +302,7 @@ var BSplineGraphic = React.createClass({
for(let y=spacing; y<this.height-1; y+=spacing) { this.line(0,y,this.width,y); }
},
ellipse(x,y,r) {
circle(x,y,r) {
let hr = r/2;
var ctx = this.ctx;
ctx.beginPath();
@@ -270,6 +322,9 @@ var BSplineGraphic = React.createClass({
},
stroke(r,g,b,a) {
if (typeof r === "string") {
return (this.ctx.strokeStyle = r);
}
if (g===undefined) { g=r; b=r; }
if (a===undefined) { a = 1; }
this.ctx.strokeStyle = this.rgba(r,g,b,a);
@@ -278,6 +333,9 @@ var BSplineGraphic = React.createClass({
noStroke() { this.ctx.strokeStyle = "none"; },
fill(r,g,b,a) {
if (typeof r === "string") {
return (this.ctx.fillStyle = r);
}
if (g===undefined) { g=r; b=r; }
if (a===undefined) { a = 1; }
this.ctx.fillStyle = this.rgba(r,g,b,a);

View File

@@ -0,0 +1,39 @@
var React = require('react');
var KnotController = React.createClass({
getInitialState() {
return {
owner: false,
knots: []
};
},
bindKnots(owner, knots) {
this.setState({owner, knots});
},
render() {
var type = 'range';
var min = 0;
var max = this.state.knots.length;
var step = 1;
return (
<section className='knot-controls'><h2>knot values</h2>{
this.state.knots.map((value,position) => {
var props = {
type, min, max, step,
value,
onChange: e => {
var k = this.state.knots;
k[position] = e.target.value;
this.setState({ knots: k }, () => {
this.state.owner.redraw();
});
}
};
return <div key={'knot'+position}>{min}<input {...props}/>{max} (= {value})</div>;
})
}</section>
);
}
});
module.exports = KnotController;

View File

@@ -0,0 +1,53 @@
var React = require('react');
var WeightController = React.createClass({
getInitialState() {
return {
owner: false,
weights: [],
closed: false
};
},
bindWeights(owner, weights, closed) {
this.setState({owner, weights, closed});
},
render() {
var type = 'range';
var min = 0;
var max = this.state.weights.length;
var step = 1;
var overlap = this.state.closed;
var baselength = this.state.weights.length;
if (overlap !== false) {
baselength -= overlap;
}
return (
<section className='knot-controls'><h2>weight values</h2>{
this.state.weights.map((value,position) => {
if (overlap && position >= baselength) {
return null;
}
var props = {
type, min, max, step,
value,
onChange: e => {
var k = this.state.weights;
k[position] = e.target.value;
if (overlap && position < overlap) {
k[position+baselength] = e.target.value;
}
this.setState({ weights: k }, () => {
this.state.owner.redraw();
});
}
};
return <div key={'knot'+position}>{min}<input {...props}/>{max} (= {value})</div>;
})
}</section>
);
}
});
module.exports = WeightController;

View File

@@ -16,7 +16,7 @@ module.exports = {
this.line(n.x, n.y, p.x, p.y);
p = n;
this.stroke(0);
this.ellipse(p.x, p.y, 4);
this.circle(p.x, p.y, 4);
});
this.drawSplineData();
},

View File

@@ -0,0 +1,52 @@
module.exports = {
degree: 3,
activeDistance: 9,
setup() {
this.size(400, 400);
var TAU = Math.PI*2;
for (let i=0; i<TAU; i+=TAU/9) {
this.points.push({
x: this.width/2 + 100 * Math.cos(i),
y: this.height/2 + 100 * Math.sin(i)
});
}
this.knots = this.formKnots(this.points);
var m = Math.round(this.points.length/2)|0;
this.knots[m+0] = this.knots[m];
this.knots[m+1] = this.knots[m];
this.knots[m+2] = this.knots[m];
for (let i=m+3; i<this.knots.length; i++) {
this.knots[i] = this.knots[i-1] + 1;
}
if(this.props.controller) {
this.props.controller(this, this.knots);
}
this.draw();
},
draw() {
this.clear();
this.grid(25);
var p = this.points[0];
this.points.forEach(n => {
this.stroke(200);
this.line(n.x, n.y, p.x, p.y);
p = n;
this.stroke(0);
this.circle(p.x, p.y, 4);
});
this.drawSplineData();
},
drawSplineData() {
if (this.points.length <= this.degree) return;
var mapped = this.points.map(p => [p.x, p.y]);
this.drawCurve(mapped);
this.drawKnots(mapped);
}
};

View File

@@ -1,6 +1,8 @@
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() {
@@ -9,6 +11,14 @@ var BoundingBox = React.createClass({
};
},
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>
@@ -254,6 +264,18 @@ var BoundingBox = React.createClass({
value for <code>i</code> such that those <code>v[i-1]</code> don't have an array index that doesn't exist)
</p>
<h2>
Open vs. closed paths
</h2>
<p>
Much like poly-Beziers, 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>
<h2>
Manipulating the curve through the knot vector
</h2>
@@ -284,7 +306,10 @@ var BoundingBox = React.createClass({
which becomes normalised during interpolation and so does not contribute to the curvature.
</p>
<p>SHOW A UNIFORM SPLINE HERE</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
@@ -308,7 +333,10 @@ var BoundingBox = React.createClass({
collapsing <code>order</code> knots creates a situation where all continuity is lost and the curve "kinks".
</p>
<p>SHOW A CENTER-CUT SPLINE HERE</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>
@@ -325,7 +353,10 @@ var BoundingBox = React.createClass({
"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>
<p>SHOW AN OPEN-UNIFORM SPLINE HERE</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>
@@ -343,7 +374,16 @@ var BoundingBox = React.createClass({
point carries, the close to that point the spline curve will lie, a bit like turning up the gravity
of a control point.</p>
<p>SHOW A WEIGHT-MANIPULABLE RATIONAL B-SPLINE HERE</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 (NUBRS is not a plural, the capital S actually just stands
@@ -357,8 +397,6 @@ var BoundingBox = React.createClass({
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>
<p>SHOW A NURBS HERE</p>
<h2>Extending our implementation to cover rational splines</h2>
<p>

View File

@@ -24,8 +24,9 @@ module.exports = {
var nt = n+k+1;
var w2 = this.width/2;
var h1 = this.height;
var step = 0.1;
var ti = [0,1,2,3,4,5,6];
var step = 0.1, t = ti[0];
var t = ti[0];
var N = [[],[],[],[],[],[],[],[]];
var i1 = 0;
@@ -52,10 +53,20 @@ module.exports = {
t += step;
}
var stw = this.width/6;
var colors = [
'#C00',
'#CC0',
'#0C0',
'#0CC',
'#00C',
'#C0C'
];
var stw = this.width/8;
for (let j = 0; j < n1; j++) {
t = ti[0];
let to = t;
this.stroke(colors[j]);
for (let l = 1; l < w2; l++) {
t += step;
let t1 = t;
@@ -68,5 +79,11 @@ module.exports = {
to = t1;
}
}
this.stroke(0);
this.fill(0);
for(let j=0; j<n+k+1; j++) {
this.circle(pad + j*stw, h1 - pad, 3);
}
}
};

View File

@@ -0,0 +1,45 @@
module.exports = {
degree: 3,
activeDistance: 9,
setup() {
this.size(400, 400);
var TAU = Math.PI*2;
for (let i=0; i<TAU; i+=TAU/10) {
this.points.push({
x: this.width/2 + 100 * Math.cos(i),
y: this.height/2 + 100 * Math.sin(i)
});
}
this.knots = this.formKnots(this.points, true);
if(this.props.controller) {
this.props.controller(this, this.knots);
}
this.draw();
},
draw() {
this.clear();
this.grid(25);
var p = this.points[0];
this.points.forEach(n => {
this.stroke(200);
this.line(n.x, n.y, p.x, p.y);
p = n;
this.stroke(0);
this.circle(p.x, p.y, 4);
});
this.drawSplineData();
},
drawSplineData() {
if (this.points.length <= this.degree) return;
var mapped = this.points.map(p => [p.x, p.y]);
this.drawCurve(mapped);
this.drawKnots(mapped);
}
};

View File

@@ -0,0 +1,42 @@
module.exports = {
degree: 3,
activeDistance: 9,
weights: [],
setup() {
this.size(400, 400);
var TAU = Math.PI*2;
for (let i=0; i<TAU; i+=TAU/10) {
this.points.push({
x: this.width/2 + 100 * Math.cos(i),
y: this.height/2 + 100 * Math.sin(i)
});
}
this.knots = this.formKnots(this.points, true);
this.weights = this.formWeights(this.points);
this.draw();
},
draw() {
this.clear();
this.grid(25);
var p = this.points[0];
this.points.forEach(n => {
this.stroke(200);
this.line(n.x, n.y, p.x, p.y);
p = n;
this.stroke(0);
this.circle(p.x, p.y, 4);
});
this.drawSplineData();
},
drawSplineData() {
if (this.points.length <= this.degree) return;
var mapped = this.points.map(p => [p.x, p.y]);
this.drawCurve(mapped);
this.drawKnots(mapped);
}
};

View File

@@ -0,0 +1,50 @@
module.exports = {
degree: 3,
activeDistance: 9,
weights: [],
setup() {
this.size(400, 400);
var TAU = Math.PI*2;
var r = this.width/3;
for (let i=0; i<6; i++) {
this.points.push({
x: this.width/2 + r * Math.cos(i/6 * TAU),
y: this.height/2 + r * Math.sin(i/6 * TAU)
});
}
this.points = this.points.concat(this.points.slice(0,3));
this.closed = this.degree;
this.knots = this.formKnots(this.points);
this.weights = this.formWeights(this.points);
if(this.props.controller) {
this.props.controller(this, this.knots, this.weights, this.closed);
}
this.draw();
},
draw() {
this.clear();
this.grid(25);
var p = this.points[0];
this.points.forEach(n => {
this.stroke(200);
this.line(n.x, n.y, p.x, p.y);
p = n;
this.stroke(0);
this.circle(p.x, p.y, 4);
});
this.drawSplineData();
},
drawSplineData() {
if (this.points.length <= this.degree) return;
var mapped = this.points.map(p => [p.x, p.y]);
this.drawCurve(mapped);
this.drawKnots(mapped);
}
};

View File

@@ -0,0 +1,43 @@
module.exports = {
degree: 3,
activeDistance: 9,
setup() {
this.size(400, 400);
var TAU = Math.PI*2;
for (let i=0; i<TAU; i+=TAU/10) {
this.points.push({
x: this.width/2 + 100 * Math.cos(i),
y: this.height/2 + 100 * Math.sin(i)
});
}
this.knots = this.formKnots(this.points);
if(this.props.controller) {
this.props.controller(this, this.knots);
}
this.draw();
},
draw() {
this.clear();
this.grid(25);
var p = this.points[0];
this.points.forEach(n => {
this.stroke(200);
this.line(n.x, n.y, p.x, p.y);
p = n;
this.stroke(0);
this.circle(p.x, p.y, 4);
});
this.drawSplineData();
},
drawSplineData() {
if (this.points.length <= this.degree) return;
var mapped = this.points.map(p => [p.x, p.y]);
this.drawCurve(mapped);
this.drawKnots(mapped);
}
};

View File

@@ -7,7 +7,7 @@
"build:singles": "npm run less && webpack --prod --singles",
"dev": "npm run less && webpack-dev-server --progress --colors --hot --inline",
"latex": "node tools/mathjax",
"less": "node ./node_modules/less/bin/lessc stylesheets/style.less > stylesheets/style.css",
"less": "lessc stylesheets/style.less > stylesheets/style.css",
"singles": "npm run dev -- --singles",
"start": "npm run dev",
"style": "webpack --jscs",

View File

@@ -1,3 +1,18 @@
.bspline-graphic {
border: 1px solid black;
background: white;
}
.two-column {
display: flex;
flex-direction: row-reverse;
canvas {
flex: none;
}
section {
margin-top: 0;
padding-left: 2em;
}
}

View File

@@ -320,6 +320,18 @@ footer {
}
.bspline-graphic {
border: 1px solid black;
background: white;
}
.two-column {
display: flex;
flex-direction: row-reverse;
}
.two-column canvas {
flex: none;
}
.two-column section {
margin-top: 0;
padding-left: 2em;
}
code {
background: #DDE;