Published
Edited
May 1, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Click handler
d3.select("#main-canvas").on("click", (e) => {
mutable clickPosition = d3.pointer(e);
const [x, y] = d3.pointer(e);

// Check if this grid position is already scored (occupied by bar or scored space)
let gridPositionFilled = false;
const gridX = roundToGrid(x) / gridSize;
const gridY = roundToGrid(y) / gridSize;
const cellId = gridX*gridHeight + gridY;
// If so, return without spawning new bars
if (simulation.grid[cellId].scored) {
return;
}

// Otherwise, spawn two new bar segments to the left/right or up/down, depending on selected orientation
if (barDirection === "Horizontal") {
addBar({ x, y, direction: "left" });
addBar({ x, y, direction: "right" });
} else {
addBar({ x, y, direction: "up" });
addBar({ x, y, direction: "down" });
}
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gridSet = {
const gridCells = [];
let id = 0;

for (let x = 0; x < width; x += gridSize) {
for (let y = 0; y < height; y += gridSize) {
const neighbors = [];
// up (if not at top)
if (id % gridHeight !== 0) {
neighbors.push(id-1);
}
// down (if not at bottom)
if ((id + 1) % gridHeight !== 0) {
neighbors.push(id+1);
}
// left (if not at left edge)
if (id - gridHeight >= 0) {
neighbors.push(id - gridHeight);
}
// right (if not at right edge)
if (id + gridHeight < gridHeight*gridWidth) {
neighbors.push(id + gridHeight);
}
gridCells.push(
new GridCell({
id,
x,
y,
width: (gridSize - 1),
height: (gridSize - 1), ctx,
neighbors
})
);
id += 1;
}
}

return gridCells;
}
Insert cell
Insert cell
class Simulation {
constructor({ width, height, ctx, balls, bars, grid }) {
this.width = width || 800;
this.height = height || 800;
this.center = [this.width / 2, this.height / 2];

this.ctx = ctx || null;

this.maxRadius = d3.max(balls, (a) => a.radius);
this.quadtree = null;

this.balls = balls || [];
this.bars = bars || [];
this.grid = grid;
this.grid.forEach((cell) => {
cell.scored = false;
});

this.fillQueue = [];

// Collision methods
this.fastDetectBallCollision = fastDetectBallCollision;
this.detectBallWallCollision = detectBallWallCollision;
this.detectBarWallCollision = detectBarWallCollision;
this.detectBallBarCollision = detectBallBarCollision;
this.detectBarToBarCollision = detectBarToBarCollision;
this.evaluateGrid = evaluateGrid;
}

clearCanvas() {
this.ctx.fillStyle = "#ffffff";
this.ctx.clearRect(0, 0, width, height);
}

tick() {
this.clearCanvas();

this.grid.forEach((cell) => {
cell.drawCell();
});

this.quadtree = d3
.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.extent([-1, -1], [this.width + 1, this.height + 1])
.addAll(this.balls);

this.balls.forEach((ball) => {
this.detectBallBarCollision(ball);
this.detectBallWallCollision(ball);
this.fastDetectBallCollision(ball);

ball.tick();
ball.drawCircle();
});

this.bars = this.bars.filter((bar) => bar.remove === false);

this.bars.forEach((bar) => {
if (bar.active) {
this.detectBarWallCollision(bar);
this.detectBarToBarCollision(bar);
bar.tick();
}

bar.drawRect();
});

if (this.bars.filter((bar) => bar.active === true).length === 0) {
this.fillQueue.forEach((bar) => {
this.evaluateGrid(bar);
});

this.fillQueue = [];
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
evaluateGrid = function(bar) {
const startingCells =
bar.orientation === "vertical" ?
this.grid.filter(cell => {
return (
((cell.x === bar.left - gridSize) || (cell.x === bar.right + 1)) &&
cell.y >= bar.top &&
cell.y < bar.bottom &&
cell.visited === false
)
}) :
this.grid.filter(cell => {
return (
((cell.y === bar.top - gridSize) || (cell.y === bar.bottom + 1)) &&
cell.x >= bar.left &&
cell.x < bar.right &&
cell.visited === false
)
})
// startingCells.forEach(cell => { cell.scored = true });
const ballCells = this.balls.map(ball => {
const x = roundToGrid(ball.x) / gridSize;
const y = roundToGrid(ball.y) / gridSize;
const cellId = x*gridHeight + y;
return cellId;
}) // .forEach(id => { this.grid.find(cell => cell.id === id).scored = true; });
let visited = [];
startingCells.forEach(startCell => {
if (!visited.includes(startCell.id)) {
const section = floodFill([startCell], this.grid);
if (!section.find(id => ballCells.includes(id))) {
section.forEach(id => { this.grid[id].scored = true });
}
visited = [...visited, ...section];
}
});
}
Insert cell
floodFill = (queue, grid) => {
const section = [queue[0].id];
while (queue.length) {
const cell = queue.shift();

cell.neighbors.forEach(neighbor => {
const neighborCell = grid[neighbor];
if (neighborCell.scored === false && neighborCell.visited === false) {
neighborCell.visited = true;
section.push(neighbor);
queue.push(neighborCell);
}
});
}

return section;
}
Insert cell
Insert cell
Insert cell
class GridCell {
constructor({ id, x, y, width, height, ctx, neighbors }) {
this.id = id;

this.x = x;
this.y = y;

this.width = width || (gridSize - 1);
this.height = height || (gridSize - 1);

this.ctx = ctx;

this.neighbors = neighbors || [];

this.scored = false;
this.visited = false;
}

drawCell() {
this.visited = false;
this.ctx.beginPath();
this.ctx.rect(this.x, this.y, this.width, this.height);

this.ctx.fillStyle = this.scored ? darkGray : backgroundColor;
this.ctx.strokeStyle = this.scored ? darkGray : lightGray;

this.ctx.fill();
this.ctx.stroke();
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more