Source code of plot #054 back to plot
Download full working sketch as 054.tar.gz.
Unzip, then start a local web server and load the page in a browser.
Unless otherwise noted, code published here is © Gábor L Ugray, shared under the Creative Commons
BY-NC-SA license (Attribution, Non-Commercial, Share-Alike). Files in lib/thirdparty
, and additional
libraries in the downloadable archive, are shared under their respective open-source licenses, attributed
to their authors.
class CanvasMasker {
/**
* Initializes canvas-based masker.
* @param w Pixel width of modeled area
* @param h Pixel height of modeled area
* @param rf Raster factor: Canvas width and height is w * rf and h * rf
* @param elmHost Host element to append canvas to. If missing, canvas will not be part of DOM.
* @param id ID of canvas element. Can be omitted.
*/
constructor(w, h, rf = 5, elmHost, id) {
this.reqPosCount = 1;
this.w = w;
this.h = h;
this.rf = rf;
this.canvasW = w * rf;
this.elm = document.createElement("canvas");
this.elm.style.backgroundColor = "black";
this.elm.width = w * rf;
this.elm.height = h * rf;
if (id) this.elm.id = "canvas-calc";
if (elmHost) elmHost.appendChild(this.elm);
this.ctx = this.elm.getContext("2d");
// Full imageData from canvas: created when a snapshot is requested
this.imgData = null;
}
clear() {
this.imgData = null;
this.ctx.globalCompositeOperation = "source-over";
this.ctx.fillStyle = "#000000";
this.ctx.fillRect(0, 0, this.w * this.rf, this.h * this.rf);
}
/**
* Marks a margin of specified breadth as a blocked area.
* @param margin Breadth of margin
*/
blockMargin(margin) {
this.imgData = null;
this.ctx.globalCompositeOperation = "source-over";
this.ctx.fillStyle = "#400000";
this.ctx.fillRect(0, 0, this.elm.width, margin * this.rf);
this.ctx.fillRect(0, 0, margin * this.rf, this.elm.height);
this.ctx.fillRect(0, (this.h - margin) * this.rf, this.elm.width, margin * this.rf);
this.ctx.fillRect((this.w - margin) * this.rf, 0, margin * this.rf, this.elm.height);
}
blockMarginXY(mx, my) {
this.imgData = null;
this.ctx.globalCompositeOperation = "source-over";
this.ctx.fillStyle = "#400000";
this.ctx.fillRect(0, 0, this.elm.width, my * this.rf);
this.ctx.fillRect(0, 0, mx * this.rf, this.elm.height);
this.ctx.fillRect(0, (this.h - my) * this.rf, this.elm.width, my * this.rf);
this.ctx.fillRect((this.w - mx) * this.rf, 0, mx * this.rf, this.elm.height);
}
includePoly(vertices) {
markPoly(this, vertices, false);
}
blockPoly(vertices) {
markPoly(this, vertices, true);
}
blockPath(vertices, lineWidth) {
markPath(this, vertices, lineWidth, true);
}
inlcudeCircle(center, radius) {
markCircle(this, center, radius, false);
}
blockCircle(center, radius) {
markCircle(this, center, radius, true);
}
includeRect(x, y, w, h) {
markRect(this, x, y, w, h, false);
}
blockRect(x, y, w, h) {
markRect(this, x, y, w, h, true);
}
/**
* Retrieves and caches full image data. Cache gets invalidated when you next block or include something.
* Do this when you know you'll be checking a lot of points before drawing again.
*/
takeSnapshot() {
this.imgData = this.ctx.getImageData(0, 0, this.elm.width, this.elm.height);
}
extendLine(midPt, angleDeg, mustBeMarkedVisible = false, segLength = 2) {
let rgb = [0, 0, 0];
const isVisible = pt => {
this.getPixel(pt.x, pt.y, rgb);
let isBlue = rgb[2] >= this.reqPosCount * 0x40;
let isRed = rgb[0] >= 0x40;
return (isBlue || !mustBeMarkedVisible) && !isRed;
}
if (!isVisible(midPt)) return null;
let step = new Point(0, -segLength);
step = step.rotate(angleDeg);
// Walk one way
const currPt = midPt.clone();
const endPt = midPt.clone();
while (true) {
currPt.x += step.x;
currPt.y += step.y;
if (!isVisible(currPt)) break;
endPt.x = currPt.x;
endPt.y = currPt.y;
}
if (endPt.equals(midPt)) return null;
// Walk the other way
const startPt = midPt.clone();
while (true) {
currPt.x -= step.x;
currPt.y -= step.y;
if (!isVisible(currPt)) break;
startPt.x = currPt.x;
startPt.y = currPt.y;
}
return [startPt, endPt];
}
genSeq(midPt, next, maxPts, mustBeMarkedVisible = false) {
let rgb = [0, 0, 0];
const isVisible = pt => {
this.getPixel(pt.x, pt.y, rgb);
let isBlue = rgb[2] >= this.reqPosCount * 0x40;
let isRed = rgb[0] >= 0x40;
return (isBlue || !mustBeMarkedVisible) && !isRed;
}
if (!isVisible(midPt)) return null;
// Walk one way
const pts = [midPt.clone()];
let currPt = midPt.clone();
while (pts.length < maxPts) {
if (!next(currPt, 1)) break;
if (!isVisible(currPt)) break;
pts.push(currPt.clone());
}
// Walk the other way
currPt = midPt.clone();
while (pts.length < maxPts) {
if (!next(currPt, -1)) break;
if (!isVisible(currPt)) break;
pts.unshift(currPt.clone())
}
if (pts.length < 3) return null;
else return pts;
}
getMaskedPoly(pts, mustBeMarkedVisible = false) {
// Result: array of array of points.
const polys = [];
let currPts = [];
let rgb = [0, 0, 0];
for (const pt of pts) {
this.getPixel(pt.x, pt.y, rgb);
let isBlue = rgb[2] >= this.reqPosCount * 0x40;
let isRed = rgb[0] >= 0x40;
let visible = (isBlue || !mustBeMarkedVisible) && !isRed;
if (!visible) addCurrentPoints();
else currPts.push(pt);
}
addCurrentPoints();
return polys;
function addCurrentPoints() {
if (currPts.length == 0) return;
if (currPts.length >= 2) {
polys.push(currPts);
}
currPts = [];
}
}
getMaskedLine(pt1, pt2, mustBeMarkedVisible = false, segLength = 2) {
// Build points: short segments of the requested length
const lineVect = pt2.subtract(pt1);
const lineLength = lineVect.length;
const nSegs = Math.max(2, Math.round(lineLength / segLength));
const segVect = lineVect.divide(nSegs);
const pts = [];
for (let i = 0; i <= nSegs; ++i) {
pts.push(pt1.add(segVect.multiply(i)));
}
// Get polylines
const polys = this.getMaskedPoly(pts, mustBeMarkedVisible);
const res = [];
// Simplify: just keep first and last point of each polyline.
// These are all straight lines.
polys.forEach(poly => {
const pta = poly[0];
const ptb = poly[poly.length - 1];
res.push([pta, ptb]);
});
return res;
}
getPixel(x, y, rgb) {
rgb[0] = rgb[1] = rgb[2] = 0;
if (x < 0 || y < 0) return;
if (x >= this.w || y >= this.h) return;
x = Math.round(x * this.rf);
y = Math.round(y * this.rf);
// If we have an image data snapshot of full canvas, query that
if (this.imgData) {
let pxOfs = ((y * (this.canvasW * 4)) + (x * 4));
rgb[0] = this.imgData.data[pxOfs];
rgb[1] = this.imgData.data[pxOfs + 1];
rgb[2] = this.imgData.data[pxOfs + 2];
return;
}
// Retrieve image data for a single pixel
let spid = this.ctx.getImageData(x, y, 1, 1);
rgb[0] = spid.data[0];
rgb[1] = spid.data[1];
rgb[2] = spid.data[2];
}
}
function markPath(cm, vertices, lineWidth, blocked) {
cm.imgData = null;
let clr = "#000040";
if (blocked) clr = "#400000";
cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
cm.ctx.lineWidth = lineWidth;
cm.ctx.lineCap = 'round';
cm.ctx.globalCompositeOperation = "lighter";
cm.ctx.beginPath();
vertices.forEach((pt, ix) => {
if (ix == 0) cm.ctx.moveTo(pt.x * cm.rf, pt.y * cm.rf);
else cm.ctx.lineTo(pt.x * cm.rf, pt.y * cm.rf);
});
cm.ctx.stroke();
}
function markPoly(cm, vertices, blocked) {
cm.imgData = null;
let clr = "#000040";
if (blocked) clr = "#400000";
cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
cm.ctx.lineWidth = 1;
cm.ctx.globalCompositeOperation = "lighter";
cm.ctx.beginPath();
vertices.forEach((pt, ix) => {
if (ix == 0) cm.ctx.moveTo(pt.x * cm.rf, pt.y * cm.rf);
else cm.ctx.lineTo(pt.x * cm.rf, pt.y * cm.rf);
});
cm.ctx.fill();
}
function markCircle(cm, center, radius, blocked) {
cm.imgData = null;
let clr = "#000040";
if (blocked) clr = "#400000";
cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
cm.ctx.lineWidth = 1;
cm.ctx.globalCompositeOperation = "lighter";
cm.ctx.beginPath();
// Arc: Top is -Math.PI/2; false means clockwise
cm.ctx.arc(center.x * cm.rf, center.y * cm.rf, radius * cm.rf, 0, 2 * Math.PI, false);
cm.ctx.arc(center.x * cm.rf, center.y * cm.rf, 0, 0, 2 * Math.PI, true);
cm.ctx.fill();
}
function markRect(cm, x, y, w, h, blocked) {
cm.imgData = null;
let clr = "#000040";
if (blocked) clr = "#400000";
cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
cm.ctx.lineWidth = 1;
cm.ctx.globalCompositeOperation = "lighter";
cm.ctx.fillRect(x * cm.rf, y * cm.rf, w * cm.rf, h * cm.rf);
}
export {CanvasMasker}