1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-02-23 17:14:49 +01:00
BezierInfo-2/components/BSplineGraphic.jsx
Pomax 782c509850
Rational bezier (#208)
* .

* meh

* rational beziers
2019-08-18 17:13:27 -07:00

368 lines
9.5 KiB
JavaScript

var React = require('react');
var interpolate = require("../lib/spline");
var BSplineGraphic = React.createClass({
componentWillMount() {
// lots of instance binding, because we want instance binding, not proto/class binding.
this.cvs = undefined;
this.ctx = undefined;
this.key = undefined;
this.keyCode = undefined;
this.mouseX = undefined;
this.mouseY = undefined;
this.isMouseDown = undefined;
this.width = 0;
this.height = 0;
this.activeDistance = 9;
this.points = [];
this.knots = [];
this.weights = [];
this.nodes = [];
this.cp = undefined;
this.dx = undefined;
this.dy = undefined;
// and sketch value binding.
var sketch = this.props.sketch;
Object.keys(sketch).forEach(fn => {
this[fn] = sketch[fn];
// rebind "this" if we're dealing with a function
if (typeof this[fn] === "function") {
this[fn] = this[fn].bind(this);
}
});
},
render: function() {
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", this.keydownlisten);
cvs.addEventListener("keyup", this.keyuplisten);
cvs.addEventListener("keypress", this.keypresslisten);
// Mouse event handling
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, 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, weights);
ctx.lineTo(p[0], p[1]);
}
p = interpolate(1, this.degree, points, this.knots, weights);
ctx.lineTo(p[0], p[1]);
ctx.stroke();
ctx.closePath();
},
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, weights, false, true);
this.circle(p[0], p[1], 3);
});
},
// FIXME: TODO: these do not seem correct using uniform knots
drawNodes(points) {
var i = 0, p;
this.stroke(150);
this.nodes.forEach((node,i) => {
try {
p = interpolate(node, this.degree, points, this.knots, false, false, true);
this.line(p[0],p[1], points[i][0], points[i++][1]);
} catch (e) { console.error(e); }
});
},
// FIXME: this doesn't work with a degree change
formKnots(points, open) {
open = (open===true) ? true: false;
if(!open) return this.formUniformKnots(points);
var l = points.length,
knots = [],
m = l - this.degree,
i;
// form the open-uniform knot vector
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 = [this.degree].concat(knots); }
// add [degree] max-values to the back
for (i=0; i <= this.degree; i++) { knots.push(m + this.degree); }
return knots;
},
formUniformKnots(points) {
var knots = [];
for(var i = this.points.length + this.degree; i >= 0; i--) {
knots.push(i);
}
return knots.reverse();
},
formNodes(knots, points) {
var domain = [
this.degree,
knots.length - 1 - this.degree
];
var nodes = [], node, k, offset;
for (k=0; k < this.points.length; k++) {
node = 0;
for (offset=1; offset <= this.degree; offset++) {
node += knots[k+offset];
}
node /= this.degree;
if (node < knots[domain[0]]) continue;
if (node > knots[domain[1]]) continue;
nodes.push(node);
}
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);
this.nodes = this.formNodes(this.knots, this.points);
},
near(p, x, y) {
var dx = p.x-x,
dy = p.y-y,
d = Math.sqrt(dx*dx + dy*dy);
return d < this.activeDistance;
},
getCurrentPoint(x,y) {
for(let i=0; i < this.points.length; i++) {
if (this.near(this.points[i],x,y)) {
return this.points[i];
}
}
},
// Interaction stuffs
keyDown() {
if (this.key === 'ArrowUp') {
this.setDegree(1);
}
if (this.key === 'ArrowDown') {
if(this.degree>1) {
this.setDegree(-1);
}
}
this.redraw();
},
keyUp() {
// ... do nothing?
},
keyPressed() {
// ... do nothing?
},
mouseDown() {
this.isMouseDown = true;
this.cp = this.getCurrentPoint(this.mouseX, this.mouseY);
if (!this.cp) {
this.points.push({ x: this.mouseX, y: this.mouseY});
this.knots = this.formKnots(this.points);
this.nodes = this.formNodes(this.knots, this.points);
}
this.redraw();
},
mouseUp() {
this.isMouseDown = false;
this.cp = false;
this.redraw();
},
mouseDrag() {
if (this.cp) {
this.cp.x = this.mouseX;
this.cp.y = this.mouseY;
this.redraw();
}
},
mouseMove() {
// ... 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) {
e.preventDefault();
}
this.key = e.key;
this.keyCode = e.code;
},
// mouse events
setMouseValues(e) {
var brect = this.cvs.getBoundingClientRect();
this.mouseX = e.clientX - brect.left;
this.mouseY = e.clientY - brect.top;
},
// API stuffs
size(w,h) {
this.width = w|0;
this.height = (h||w)|0;
this.cvs.width = this.width;
this.cvs.height = this.height;
this.cvs.style.width = this.width + "px";
this.cvs.style.height = this.height + "px";
this.ctx = this.cvs.getContext("2d");
},
redraw() { this.draw(); },
clear() {
this.ctx.clearRect(0,0,this.width,this.height);
},
grid(spacing) {
spacing = ((spacing || 10)|0) + 0.5;
this.stroke(200,200,220);
for(let x=spacing; x<this.width-1; x+=spacing) { this.line(x,0,x,this.height); }
for(let y=spacing; y<this.height-1; y+=spacing) { this.line(0,y,this.width,y); }
},
circle(x,y,r) {
let hr = r/2;
var ctx = this.ctx;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.arc(x,y,r,0,Math.PI*2);
ctx.stroke();
ctx.closePath();
},
line(a,b,c,d) {
var ctx = this.ctx;
ctx.beginPath();
ctx.moveTo(a,b);
ctx.lineTo(c,d);
ctx.stroke();
ctx.closePath();
},
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);
},
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);
},
noFill() { this.ctx.fillStyle = "none"; },
rgba(r,g,b,a) { return "rgba("+r+","+g+","+b+","+a+")"; },
beginPath() {
this.ctx.beginPath(); this.bp = true;
},
vertex(x,y) {
if (!this.bp) {
return this.ctx.lineTo(x,y);
}
this.ctx.moveTo(x,y);
this.bp = false;
},
endPath() {
this.ctx.stroke();
this.ctx.closePath();
}
});
module.exports = BSplineGraphic;