1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-02-23 09:03:47 +01:00
BezierInfo-2/components/Graphic.jsx
Markus Stange 8e8d09e825 Make bezier canvases HiDPI-aware. (#47)
* Make bezier canvases HiDPI-aware.

* Fix HiDPI bugs and linting issues.

* Address review comments.
2017-02-11 15:36:12 -08:00

661 lines
17 KiB
JavaScript

var React = require("react");
var chroma = require("chroma-js");
var Bezier = require("bezier-js");
// event coordinate fix
var fix = function(e) {
e = e || window.event;
var target = e.target || e.srcElement,
rect = target.getBoundingClientRect();
e.offsetX = e.clientX - rect.left;
e.offsetY = e.clientY - rect.top;
};
var Graphic = React.createClass({
Paper: false,
defaultWidth: 275,
defaultHeight: 275,
panelCount: 1,
Bezier: Bezier,
utils: Bezier.getUtils(),
curve: false,
mx:0,
my:0,
cx:0,
cy:0,
mp: false,
offset: { x: 0, y: 0},
lpts: [],
colorSeed: 0,
playing: false,
frame: 0,
playinterval: 33,
animate: function animate() {
if (this.playing) {
this.frame++;
// requestAnimationFrame is spectacularly too fast
setTimeout(this.animate, this.playinterval);
this.props.draw(this,this.curve);
}
},
getFrame: function() { return this.frame; },
getPlayInterval: function() { return this.playinterval; },
play: function() { this.playing = true; this.animate(); },
pause: function() { this.playing = false; },
redraw: function() { if (this.props.draw) { this.props.draw(this, this.curve); }},
render: function() {
//var href = "data:text/plain," + this.props.code;
return (
<figure className={this.props.inline ? "inline": false}>
<canvas ref="canvas"
tabIndex="0"
style={{
width: this.panelCount * this.defaultWidth + "px",
height: this.defaultHeight + "px"
}}
onMouseDown={this.mouseDown}
onMouseMove={this.mouseMove}
onMouseUp={this.mouseUp}
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onKeyDown={this.onKeyDown}
onKeyPress={this.onKeyPress}
/>
<figcaption>{this.props.title} {this.props.children}</figcaption>
</figure>
);
},
componentDidMount: function() {
var cvs = this.refs.canvas;
var dpr = this.getPixelRatio();
cvs.width = this.defaultWidth * dpr;
cvs.height = this.defaultHeight * dpr;
this.cvs = cvs;
var ctx = cvs.getContext("2d");
this.ctx = ctx;
this.ctx.scale(dpr, dpr);
if (this.props.paperjs) {
var Paper = this.Paper = require("../lib/vendor/paperjs/paper-core");
Paper.setup(cvs);
}
if (this.props.setup) {
this.props.setup(this);
}
if (this.props.draw) {
this.props.draw(this,this.curve);
}
if (this.props.autoplay) {
this.play();
}
},
mouseDown: function(evt) {
fix(evt);
this.mx = evt.offsetX;
this.my = evt.offsetY;
this.movingPoint = false;
this.dragging = false;
this.down = true;
this.lpts.forEach((p,idx) => {
if(Math.abs(this.mx - p.x)<10 && Math.abs(this.my - p.y)<10) {
this.movingPoint = true;
this.mp = p;
this.mp_idx = idx;
this.cx = p.x;
this.cy = p.y;
}
});
if (this.props.onMouseDown) {
this.props.onMouseDown(evt, this);
}
if ('setCapture' in evt.target) {
evt.target.setCapture();
}
},
mouseMove: function(evt) {
fix(evt);
if(!this.props.static) {
if (this.down) {
this.dragging = true;
}
var found = false;
this.lpts.forEach(p => {
var mx = evt.offsetX;
var my = evt.offsetY;
if(Math.abs(mx-p.x)<10 && Math.abs(my-p.y)<10) {
found = found || true;
}
});
this.cvs.style.cursor = found ? "pointer" : "default";
this.hover = {
x: evt.offsetX,
y: evt.offsetY
};
if(this.movingPoint) {
this.ox = evt.offsetX - this.mx;
this.oy = evt.offsetY - this.my;
this.mp.x = Math.max(0, Math.min(this.defaultWidth, this.cx + this.ox));
this.mp.y = Math.max(0, Math.min(this.defaultHeight, this.cy + this.oy));
if (this.curve.forEach) {
for (var i=0, c, _pts; i<this.curve.length; i++) {
c = this.curve[i];
_pts = c.points;
if (_pts.indexOf(this.mp)>-1) {
c.update();
break;
}
}
} else if (this.curve && this.curve.update) { this.curve.update(); }
}
}
if (this.props.onMouseMove) {
this.props.onMouseMove(evt, this);
}
if (this.dragging && this.props.onMouseDrag) {
this.props.onMouseDrag(evt, this);
}
if (!this.props.static && !this.playing && this.props.draw) {
this.props.draw(this, this.curve);
}
},
mouseUp: function(evt) {
this.down = false;
if(!this.movingPoint) {
if (this.props.onMouseUp) {
this.props.onMouseUp(evt, this);
}
return;
}
this.movingPoint = false;
this.mp = false;
if (this.props.onMouseUp) {
this.props.onMouseUp(evt, this);
}
},
onClick: function(evt) {
fix(evt);
this.mx = evt.offsetX;
this.my = evt.offsetY;
if (!this.dragging && this.props.onClick) {
this.props.onClick(evt, this);
}
},
onKeyUp: function(evt) {
if (this.props.onKeyUp) {
this.props.onKeyUp(evt, this);
if (!this.playing && this.props.draw) {
this.props.draw(this, this.curve);
}
}
},
onKeyDown: function(evt) {
if (this.props.onKeyDown) {
this.props.onKeyDown(evt, this);
if (!this.playing && this.props.draw) {
this.props.draw(this, this.curve);
}
}
},
onKeyPress: function(evt) {
if (this.props.onKeyPress) {
this.props.onKeyPress(evt, this);
if (!this.playing && this.props.draw) {
this.props.draw(this, this.curve);
}
}
},
/**
* API for curve drawing.
*/
reset: function() {
this.refs.canvas.width = this.refs.canvas.width;
this.ctx.strokeStyle = "black";
this.ctx.lineWidth = 1;
this.ctx.fillStyle = "none";
var dpr = this.getPixelRatio();
this.ctx.scale(dpr, dpr);
this.offset = {x:0, y:0};
this.colorSeed = 0;
},
setSize: function(w,h) {
this.defaultWidth = w;
this.defaultHeight = h;
var cvs = this.refs.canvas;
cvs.style.width = this.panelCount * w + "px";
cvs.style.height = h + "px";
var dpr = this.getPixelRatio();
cvs.width = this.panelCount * w * dpr;
cvs.height = h * dpr;
this.ctx.scale(dpr, dpr);
},
setCurves: function(c) {
this.setCurve(c);
},
setCurve: function(c) {
var pts = [];
c = (c instanceof Array) ? c : Array.prototype.slice.call(arguments);
c.forEach(nc => {
pts = pts.concat(nc.points);
});
this.curve = (c.length === 1) ? c[0] : c;
this.lpts = pts;
},
getPanelWidth: function() {
return this.defaultWidth;
},
getPanelHeight: function() {
return this.defaultHeight;
},
getDefaultQuadratic: function() {
return new this.Bezier(70,250,20,110,250,60);
},
getDefaultCubic: function() {
return new this.Bezier(120,160,35,200,220,260,220,40);
},
getPixelRatio: function () {
return window.devicePixelRatio || 1;
},
toImage: function() {
var dataURL = this.refs.canvas.toDataURL();
var img = new Image();
img.src = dataURL;
img.devicePixelRatio = this.getPixelRatio();
return img;
},
setPanelCount: function(c) {
this.panelCount = c;
var cvs = this.refs.canvas;
cvs.width = c * this.defaultWidth * this.getPixelRatio();
cvs.style.width = c * this.defaultWidth + "px";
},
setOffset: function(f) {
this.offset = f;
},
setColor: function(c) {
this.ctx.strokeStyle = c;
},
getColor: function() {
return this.ctx.strokeStyle || "black";
},
setWeight: function(c) {
this.ctx.lineWidth = c;
},
noColor: function(c) {
this.ctx.strokeStyle = "transparent";
},
setRandomColor: function(a) {
a = (typeof a === "undefined") ? 1 : a;
var h = this.colorSeed % 360,
s = 1.0,
l = 0.34;
this.colorSeed += 87;
this.ctx.strokeStyle = chroma.hsl(h,s,l).alpha(a).css();
},
setRandomFill: function(a) {
a = (typeof a === "undefined") ? 1 : a;
var h = this.colorSeed % 360,
s = 1.0,
l = 0.34;
this.colorSeed += 87;
this.ctx.fillStyle = chroma.hsl(h,s,l).alpha(a).css();
},
setFill: function(c) {
this.ctx.fillStyle = c;
},
getFill: function() {
return this.ctx.fillStyle || "transparent";
},
noFill: function() {
this.ctx.fillStyle = "transparent";
},
drawSkeleton: function(curve, offset, nocoords) {
offset = offset || { x:0, y:0 };
var pts = curve.points;
if(pts.length>2) {
this.ctx.strokeStyle = "lightgrey";
this.drawLine(pts[0], pts[1], offset);
var last = pts.length-2;
for (var i=1; i<last; i++) {
this.drawLine(pts[i], pts[i+1], offset);
}
this.drawLine(pts[last], pts[last+1], offset);
}
this.ctx.strokeStyle = "black";
this.drawPoints(pts, offset);
if (!nocoords) {
this.drawCoordinates(curve, offset);
}
},
drawGrid: function(xcount, ycount, offset) {
var w = this.defaultWidth,
h = this.defaultHeight,
xstep = w/xcount,
ox = xstep/2,
ystep = h/ycount,
oy = ystep/2,
x, xv, y, yv, p1, p2;
for (x=0; x<xcount; x++) {
xv = ox + (x*xstep);
p1 = {x:xv, y:0};
p2 = {x:xv, y:h};
this.drawLine(p1, p2, offset);
}
for (y=0; y<ycount; y++) {
yv = oy + (y*ystep);
p1 = {x:0, y:yv};
p2 = {x:w, y:yv};
this.drawLine(p1, p2, offset);
}
},
drawHull: function(curve, t, offset) {
var hull = (curve instanceof Array) ? curve : curve.hull(t);
if(hull.length === 6) {
this.drawLine(hull[0], hull[1], offset);
this.drawLine(hull[1], hull[2], offset);
this.drawLine(hull[3], hull[4], offset);
} else {
this.drawLine(hull[0], hull[1], offset);
this.drawLine(hull[1], hull[2], offset);
this.drawLine(hull[2], hull[3], offset);
this.drawLine(hull[4], hull[5], offset);
this.drawLine(hull[5], hull[6], offset);
this.drawLine(hull[7], hull[8], offset);
}
return hull;
},
drawCoordinates: function(curve, offset) {
offset = offset || { x:0, y:0 };
var pts = curve.points;
this.setFill("black");
pts.forEach(p => {
this.text(`(${p.x},${p.y})`, {x:p.x + offset.x + 5, y:p.y + offset.y + 10});
});
},
drawFunction: function(generator, offset) {
var start = generator.start || 0,
p0 = generator(start),
end = generator.end || 1,
plast = generator(end),
step = generator.step || 0.01,
scale = generator.scale || 1,
p, t;
for (t=step; t<end; t+=step) {
p = generator(t, scale);
this.drawLine(p0, p, offset);
p0 = p;
}
this.drawLine(p, plast, offset);
},
drawCurve: function(curve, offset) {
offset = offset || { x:0, y:0 };
var p = curve.points;
if (p.length <= 3 || 5 <= p.length) {
var points = curve.getLUT(100);
var p0 = points[0];
points.forEach((p1,i) => {
if(!i) return;
this.drawLine(p0, p1, offset);
p0 = p1;
});
return;
}
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(p[0].x + ox, p[0].y + oy);
if(p.length === 3) {
this.ctx.quadraticCurveTo(
p[1].x + ox, p[1].y + oy,
p[2].x + ox, p[2].y + oy
);
}
else if(p.length === 4) {
this.ctx.bezierCurveTo(
p[1].x + ox, p[1].y + oy,
p[2].x + ox, p[2].y + oy,
p[3].x + ox, p[3].y + oy
);
}
this.ctx.stroke();
this.ctx.closePath();
},
drawLine: function(p1, p2, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(p1.x + ox,p1.y + oy);
this.ctx.lineTo(p2.x + ox,p2.y + oy);
this.ctx.stroke();
},
drawPoint: function(p, offset) {
this.drawCircle(p, 1, offset);
},
drawPoints: function(points, offset) {
offset = offset || { x:0, y:0 };
points.forEach(function(p) {
this.drawCircle(p, (offset.x!==0||offset.y!==0)? 1.5: 3, offset);
}.bind(this));
},
drawArc: function(p, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(p.x + ox, p.y + oy);
this.ctx.arc(p.x + ox, p.y + oy, p.r, p.s, p.e);
this.ctx.lineTo(p.x + ox, p.y + oy);
this.ctx.fill();
this.ctx.stroke();
},
drawCircle: function(p, r, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.arc(p.x + ox, p.y + oy, r, 0, 2*Math.PI);
this.ctx.stroke();
},
drawbbox: function(bbox, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
this.ctx.moveTo(bbox.x.min + ox, bbox.y.min + oy);
this.ctx.lineTo(bbox.x.min + ox, bbox.y.max + oy);
this.ctx.lineTo(bbox.x.max + ox, bbox.y.max + oy);
this.ctx.lineTo(bbox.x.max + ox, bbox.y.min + oy);
this.ctx.closePath();
this.ctx.stroke();
},
drawRect: function(p1, p2, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
var x = p1.x + ox,
y = p1.y + oy,
w = p2.x-p1.x,
h = p2.y-p1.y;
this.ctx.beginPath();
this.ctx.moveTo(x,y);
this.ctx.lineTo(x+w, y);
this.ctx.lineTo(x+w, y+h);
this.ctx.lineTo(x,y+h);
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
},
drawPath: function(path, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
this.ctx.beginPath();
path.forEach((p,idx) => {
if (idx === 0) {
return this.ctx.moveTo(p.x + ox, p.y + oy);
}
this.ctx.lineTo(p.x + ox, p.y + oy);
});
if (closed) this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
},
drawShape: function(shape, offset) {
offset = offset || { x:0, y:0 };
var ox = offset.x + this.offset.x;
var oy = offset.y + this.offset.y;
var order = shape.forward.points.length - 1;
this.ctx.beginPath();
this.ctx.moveTo(ox + shape.startcap.points[0].x, oy + shape.startcap.points[0].y);
this.ctx.lineTo(ox + shape.startcap.points[3].x, oy + shape.startcap.points[3].y);
if(order === 3) {
this.ctx.bezierCurveTo(
ox + shape.forward.points[1].x, oy + shape.forward.points[1].y,
ox + shape.forward.points[2].x, oy + shape.forward.points[2].y,
ox + shape.forward.points[3].x, oy + shape.forward.points[3].y
);
} else {
this.ctx.quadraticCurveTo(
ox + shape.forward.points[1].x, oy + shape.forward.points[1].y,
ox + shape.forward.points[2].x, oy + shape.forward.points[2].y
);
}
this.ctx.lineTo(ox + shape.endcap.points[3].x, oy + shape.endcap.points[3].y);
if(order === 3) {
this.ctx.bezierCurveTo(
ox + shape.back.points[1].x, oy + shape.back.points[1].y,
ox + shape.back.points[2].x, oy + shape.back.points[2].y,
ox + shape.back.points[3].x, oy + shape.back.points[3].y
);
} else {
this.ctx.quadraticCurveTo(
ox + shape.back.points[1].x, oy + shape.back.points[1].y,
ox + shape.back.points[2].x, oy + shape.back.points[2].y
);
}
this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
},
text: function(text, coord, offset) {
offset = offset || { x:0, y:0 };
if (this.offset) {
offset.x += this.offset.x;
offset.y += this.offset.y;
}
this.ctx.fillText(text, coord.x + offset.x, coord.y + offset.y);
},
image: function(image, offset) {
offset = offset || { x:0, y:0 };
if (this.offset) {
offset.x += this.offset.x;
offset.y += this.offset.y;
}
var dpr = image.devicePixelRatio || 1;
if (image.loaded) {
this.ctx.drawImage(image,offset.x,offset.y, image.width/dpr, image.height/dpr);
} else {
image.onload = () => {
image.loaded = true;
this.ctx.drawImage(image,offset.x,offset.y, image.width/dpr, image.height/dpr);
};
}
},
drawAxes: function(pad, xlabel, xs, xe, ylabel, ys, ye, offset) {
offset = offset || { x:0, y:0 };
var dim = this.getPanelWidth();
this.drawLine({x:pad, y:pad}, {x:dim-pad, y:pad}, offset);
this.drawLine({x:pad, y:pad}, {x:pad, y:dim-pad}, offset);
this.setFill("black");
this.text(xlabel + " →", {x: offset.x + dim/2, y: offset.y + 15});
this.text(xs, {x: offset.x + pad, y: offset.y + 15});
this.text(xe, {x: offset.x + dim - pad, y: offset.y + 15});
this.text(ylabel, {x: offset.x + 5, y: offset.y + dim/2 - pad});
this.text("↓", {x: offset.x + 5, y: offset.y + dim/2});
this.text(ys, {x: offset.x + 4, y: offset.y + pad + 5});
this.text(ye, {x: offset.x + 2, y: offset.y + dim - pad + 10});
}
});
module.exports = Graphic;