canvas.width = document.body.clientWidth;
const min = [0, 0],
max = [canvas.width, canvas.height],
points = Array.from(Array(10000), i => {
let x = Math.floor(Math.random() * (max[0]-min[0]) + min[0]);
let y = Math.floor(Math.random() * (max[1]-min[1]) + min[1]);
return [x, y];
}),
polygon = [],
boxSize = Math.ceil((max[0] - min[0]) / 50),
boxes = (function (xBoxes, yBoxes) {
return Array.from(Array(yBoxes), _ =>
Array.from(Array(xBoxes), _ => []));
})(toBox(0, max[0])+1, toBox(1, max[1])+1),
insidePoints = new Set,
ctx = canvas.getContext('2d');
function drawPoint(p) {
ctx.fillRect(p[0], p[1], 1, 1);
}
function drawPolygon(pol) {
ctx.beginPath();
ctx.moveTo(pol[0][0], pol[0][1]);
for (const p of pol) {
ctx.lineTo(p[0], p[1]);
}
ctx.stroke();
}
function segmentMap(a, b, dim, coord) {
// Find the coordinate where ab is intersected by a coaxial line at
// the given coord.
// First some boundary conditions:
const dim2 = 1 - dim;
if (a[dim] === coord) {
if (b[dim] === coord) return [a[dim2], b[dim2]];
return [a[dim2]];
}
if (b[dim] === coord) return [b[dim2]];
// See if there is no intersection:
if ((coord > a[dim]) === (coord > b[dim])) return [];
// There is an intersection point:
const res = (coord - a[dim]) * (b[dim2] - a[dim2]) / (b[dim] - a[dim]) + a[dim2];
return [res];
}
function isLeft(a, b, c) {
// Return true if c lies at the left of ab:
return (b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) > 0;
}
function inTriangle(a, b, c, p) {
// First do a bounding box check:
if (p[0] < Math.min(a[0], b[0], c[0]) ||
p[0] > Math.max(a[0], b[0], c[0]) ||
p[1] < Math.min(a[1], b[1], c[1]) ||
p[1] > Math.max(a[1], b[1], c[1])) return false;
// Then check that the point is on the same side of each of the
// three edges:
const x = isLeft(a, b, p),
y = isLeft(b, c, p),
z = isLeft(c, a, p);
return x ? y && z : !y && !z;
}
function toBox(dim, coord) {
return Math.floor((coord - min[dim]) / boxSize);
}
function toWorld(dim, box) {
return box * boxSize + min[dim];
}
function drawBox(boxX, boxY) {
let x = toWorld(0, boxX);
let y = toWorld(1, boxY);
drawPolygon([[x, y], [x + boxSize, y], [x + boxSize, y + boxSize], [x, y + boxSize], [x, y]]);
}
function triangleTest(a, b, c, points, insidePoints) {
const markedBoxes = new Set(), // collection of boxes that overlap with triangle
box = [];
for (let dim = 0; dim < 2; dim++) {
const dim2 = 1-dim,
// Order triangle points by coordinate
[d, e, f] = [a, b, c].sort( (p, q) => p[dim] - q[dim] ),
lastBox = toBox(dim, f[dim]);
for (box[dim] = toBox(dim, d[dim]); box[dim] <= lastBox; box[dim]++) {
// Calculate intersections of the triangle edges with the row/column of boxes
const coord = toWorld(dim, box[dim]),
intersections =
[...new Set([...segmentMap(a, b, dim, coord),
...segmentMap(b, c, dim, coord),
...segmentMap(a, c, dim, coord)])];
if (!intersections.length) continue;
intersections.sort( (a,b) => a - b );
const lastBox2 = toBox(dim2, intersections.slice(-1)[0]);
// Mark all boxes between the two intersection points
for (box[dim2] = toBox(dim2, intersections[0]); box[dim2] <= lastBox2; box[dim2]++) {
markedBoxes.add(boxes[box[1]][box[0]]);
if (box[dim]) {
markedBoxes.add(boxes[box[1]-dim][box[0]-(dim2)]);
}
}
}
}
// Perform the triangle test for each individual point in the marked boxes
for (const box of markedBoxes) {
for (const p of box) {
if (inTriangle(a, b, c, p)) {
// Toggle in/out state of this point
if (insidePoints.delete(p)) {
ctx.fillStyle = '#000000';
} else {
ctx.fillStyle = '#e0e0e0';
insidePoints.add(p);
}
drawPoint(p);
}
}
}
}
// Draw points
points.forEach(drawPoint);
// Distribute points into boxes
for (const p of points) {
let hor = Math.floor((p[0] - min[0]) / boxSize);
let ver = Math.floor((p[1] - min[1]) / boxSize);
boxes[ver][hor].push(p);
}
canvas.addEventListener('mousemove', (e) => {
if (e.buttons !== 1) return;
polygon.push([Math.max(e.offsetX,0), Math.max(e.offsetY,0)]);
ctx.strokeStyle = '#000000';
drawPolygon(polygon);
const len = polygon.length;
if (len > 2) {
triangleTest(polygon[0], polygon[len-2+len%2], polygon[len-1-len%2], points, insidePoints);
}
});
canvas.addEventListener('mousedown', (e) => {
// Start a new polygon
polygon.length = 0;
});
Drag mouse to draw a shape:
<canvas id="canvas"></canvas>