Show code
<think>
Okay, let me tackle this. The user wants a self-contained HTML file that uses Bridson's Poisson-disk sampling to fill a canvas. The requirements are pretty specific, so I need to make sure I cover all the points.
First, I'll start by setting up the basic HTML structure with a canvas element. Since it needs to be self-contained, all the code has to be in the <script> tags. The canvas needs to be responsive, so maybe set width and height with a resize listener. Wait, the user mentioned a smooth animation with requestAnimationFrame and no user input, so maybe the canvas size should be fixed. Hmm, perhaps just set a default size and have it scale with the viewport? Or maybe just a fixed size for simplicity. Let's go with a fixed size for now to avoid complications.
Next, implementing Bridson's algorithm. The steps are:
1. Create a background grid to track neighbors quickly. The grid size should be half the minimum distance (since any point in an adjacent cell is within the min distance). Let's say the min distance is 50, so grid cell size is 25.
2. The active list holds all the points we're currently considering neighbors for new points.
3. Start with a random point, add it to the grid and active list.
4. While the active list isn't empty, pick a random point from it (maybe the last one for efficiency?), and generate candidate points in the ring between min/2 and min around it. The ring is a circle with inner radius min/2 and outer radius min.
5. For each candidate, check all neighboring cells in the grid (up to 3x3 around the cell the candidate is in) to see if any existing points are too close.
6. If a candidate has no close neighbors, accept it by adding to grid and active list, and draw it on the canvas.
7. When the active list is empty, start over with a new random pattern.
I need to draw each point as soon as it's accepted, so the canvas should update each time a new point is added. Using requestAnimationFrame seems good here because it allows the animation to be smooth and responsive.
Wait, but how to structure the loop. Since Bridson's can take a while depending on parameters, we need to run it step by step each frame. Otherwise, if we run it all in one go, it might block the UI. So each frame, we try to process one iteration of the algorithm: pick a random active point, generate candidates, check, add new points if possible, then draw.
So the main loop will be inside the animation frame callback. Each frame, we process the active points, check for candidates, add new ones, etc. Also, when done, reset the grid and active list to restart.
Variables needed:
- canvas and context
- width and height of the canvas
- grid (array of arrays, or a 1D array with indices calculated)
- active list (array of points)
- minDistance (maybe 50 initially, but could be adjusted)
- cellSize = minDistance / 2
- sampleCount (to display)
- isDone (flag to know when to reset)
Drawing each point as soon as it's accepted. The draw function can be called every time a new point is added.
Now, the grid. Since we need to quickly find neighbors of a point, the grid can be an array where each cell is a pointer to a point, or null if empty. The cell indices are calculated based on x and y divided by cellSize. The grid dimensions would be ceil(canvasWidth/cellSize) x ceil(canvasHeight/cellSize). So maybe store them as a 1D array where index is (y/cellSize)*gridWidth + (x/cellSize). Or maybe a 2D array for easier access.
Wait, in JavaScript, a 2D array might be more manageable. Let's say grid is an array where grid[y][x] holds the point or null. Wait, no, actually, the grid will need to be a grid of cells, each cell can have a point or not. So maybe grid[y][x] is the point, or null. But for efficiency, perhaps a 1D array is better. Let me think.
Alternatively, the grid can be an object where the keys are computed from the coordinates. Hmm, but that's more complex. Let me go with a 2D array.
The grid will be an array of rows, each row is an array of cells. So grid[y][x] = point? Or just a flag? Actually, each cell can store a reference to the point that is located in that cell. Wait, but a point could be in multiple cells? No, each point is in exactly one cell. So the grid's purpose is to allow us to quickly check neighboring cells.
So to create the grid, when adding a point (x,y), compute its grid cell:
cellX = Math.floor(x / cellSize);
cellY = Math.floor(y / cellSize);
Then, the grid at cellY and cellX will store the point. Wait, but maybe multiple points can be in adjacent cells? Actually, since the cell size is half the minimum distance, any two points within the same cell are at least min/2 apart? Or maybe the cell size is chosen so that any two points in adjacent cells are within the minimum distance? Hmm, Bridson's algorithm uses cell size equal to the minimum distance divided by sqrt(2) or something? Wait, I might need to recall the proper setup for Bridson's grid.
I think in Bridson's paper, the grid is used to quickly find neighbors within the minimum distance. To do that, the grid cell size should be no more than half the minimum distance. That way, any point in the cell can only have neighbors in adjacent cells (up to 3x3 grid cells around the current cell). So the cell size is set to minDistance / 2. That way, any two points within minDistance must be in adjacent grid cells or the same cell.
Therefore, each time a new candidate point is generated, we can check all the adjacent grid cells (including the current cell) to see if any points are within radius minDistance.
So the grid will need to be a 2D array where each cell can store a point (or just a flag indicating presence), but perhaps storing the point reference is better for distance checks.
Wait, but we'll have to check all points in the neighboring cells. So maybe the grid stores pointers to the points in each cell, so that for a candidate point, we can check all the points in the neighboring grid cells.
Alternatively, since the grid is a background grid, each cell can store the point (or null), and to check neighbors, we look at the surrounding cells (up to 3x3) and check if any of those cells have a point, then compute the distance between the candidate and each such point.
So the steps for adding a new point:
1. Select a random active point (maybe from the end of the list for efficiency, but Bridson's algorithm usually picks randomly from the active list).
2. Generate 10 or so random candidate points in the annulus between minDistance/2 and minDistance from the current point.
3. For each candidate, compute which grid cell it's in.
4. Then check all neighboring grid cells (up to 3x3 cells around the candidate's cell), and for each non-null cell, check the distance between the candidate and that point.
5. If any of those points are within minDistance, discard the candidate.
6. If none, then accept the candidate: add it to the grid, add to active list, increment sampleCount, draw it on canvas.
7. If after generating 10 candidates and none are accepted, remove the current point from the active list, since no more points can be added from it.
Okay, so the grid is crucial here. The grid's cell size is minDistance / 2.
Now, implementing this in JS:
First, the canvas setup:
<canvas id="canvas"></canvas>
Then in script:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
Set the canvas dimensions. Maybe set to full viewport for responsiveness, but since it must be self-contained, perhaps hardcode a size like 600x600? Or maybe set it to 400x400 for simplicity. Let's go with 600x600 initially. The user didn't specify size, so maybe it's okay. Also, using style to make it visible, maybe 100% width and height, but the actual canvas is fixed size. Alternatively, use the CSS to set it, but the user wants it without comments, so proceed.
Wait, the user says "Draw each point the moment it is accepted, so the blue-noise pattern visibly fills the area." So the drawing is done as points are added. So each time a point is accepted, it should be drawn.
The drawing can be done by filling a circle at the point's coordinates with a certain radius. Maybe 1px? Or 2px? The user didn't specify, but for visibility, perhaps 2px.
Now, variables:
Let me outline variables:
const minDistance = 50; // adjust as needed
const cellSize = minDistance / 2;
let width = 600;
let height = 600;
canvas.width = width;
canvas.height = height;
Wait, but in code, need to set the canvas' attributes:
canvas.width = width;
canvas.height = height;
Then the grid:
const gridWidth = Math.ceil(width / cellSize);
const gridHeight = Math.ceil(height / cellSize);
// Create a 2D grid array with nulls
let grid = [];
for (let y = 0; y < gridHeight; y++) {
grid[y] = [];
for (let x = 0; x < gridWidth; x++) {
grid[y][x] = null;
}
}
Wait, but in JS, 2D arrays can be created with Array.from or nested loops. Alternatively, using a 1D array with x + y * gridWidth. Maybe that's more efficient. Let me see:
Alternatively:
const grid = new Array(gridHeight);
for (let y = 0; y < gridHeight; y++) {
grid[y] = new Array(gridWidth).fill(null);
}
Hmm, but in strict code, it might be better to initialize properly.
The active list:
let activeList = [];
let sampleCount = 0;
Then, the function to add a point:
function addPoint(x, y) {
const cellX = Math.floor(x / cellSize);
const cellY = Math.floor(y / cellSize);
grid[cellY][cellX] = {x, y}; // or store as an object
activeList.push({x, y});
sampleCount++;
ctx.fillRect(x-1, y-1, 2, 2); // draw a small square
}
Wait, but maybe using ctx.fillRect with 2x2 pixels centered at (x,y), so subtract 1 from each coordinate.
Alternatively, using ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI*2); ctx.fill(); but that's more code. For efficiency, maybe rectangles.
Wait, the drawing code needs to be efficient as there might be many points. Using rectangles is faster? Not sure, but for the purpose here, maybe rectangles are better.
Wait, but if the points are small, maybe a single pixel. Hmm. Alternatively, just draw a point with a 2px radius, but the user might want it to be visible.
Perhaps using ctx.fillRect(x, y, 1, 1) but that's just a single pixel. To make it visible, maybe 2px. So subtract 1 from x and y to center.
Wait, but the coordinates might not be integers. So maybe better to use ctx.fillRect with the position as is, but with a size of 2, so the center is (x+1, y+1) if using 2x2.
Wait, perhaps better to use:
ctx.fillStyle = 'hsl(' + Math.random() * 360 + ',50%,50%)';
Wait, but the user didn't mention colors, so maybe just a simple color like black. Let's set ctx.fillStyle to 'black' and use:
ctx.fillRect(x, y, 1, 1);
But that's a single pixel. To make it visible, maybe 2px. Let me adjust:
ctx.fillRect(x - 0.5, y - 0.5, 1, 1); // centered 1x1 pixel?
Alternatively:
ctx.fillStyle = 'black';
ctx.fillRect(Math.floor(x), Math.floor(y), 1, 1);
But that might not be precise. Hmm. Well, for the purpose of the visualization, maybe it's okay. Let's proceed without worrying too much about the exact drawing method as long as points are visible.
Now, the main algorithm loop.
The function that runs each frame: let's call it animate() which is called via requestAnimationFrame.
Inside animate, while activeList is not empty, process a point:
Wait, but frame-based processing can't just loop all the time, else it blocks. So in each frame, we process one or a few steps, then request another frame.
Let me structure it like this:
let isRunning = true;
function animate() {
if (!isRunning) return;
let stepCount = 0;
while (activeList.length > 0 && stepCount < 10) { // process up to 10 steps per frame
// pick a random active point
const index = Math.floor(Math.random() * activeList.length);
const [currentX, currentY] = activeList[index]; // Wait, need to store the points correctly in activeList. Maybe as objects.
Wait, in the addPoint function earlier, we pushed an object {x, y} to activeList. So:
const current = activeList[index];
const currentX = current.x;
const currentY = current.y;
// Generate 10 candidate points around the current point
let found = false;
for (let i = 0; i < 10; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = minDistance * (0.5 + Math.random() * 0.5); // between min/2 and min
const dx = Math.cos(angle) * radius;
const dy = Math.sin(angle) * radius;
const candidateX = currentX + dx;
const candidateY = currentY + dy;
// Check if candidate is within canvas boundaries
if (candidateX < 0 || candidateX >= width || candidateY < 0 || candidateY >= height) {
continue;
}
// Determine the candidate's grid cell
const cellX = Math.floor(candidateX / cellSize);
const cellY = Math.floor(candidateY / cellSize);
// Check neighboring cells for conflicts
let conflict = false;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const neighborCellY = cellY + ky;
const neighborCellX = cellX + kx;
if (neighborCellY < 0 || neighborCellY >= gridHeight || neighborCellX < 0 || neighborCellX >= gridWidth) {
continue;
}
const neighborPoint = grid[neighborCellY][neighborCellX];
if (neighborPoint) {
const dx = candidateX - neighborPoint.x;
const dy = candidateY - neighborPoint.y;
if (dx*dx + dy*dy < minDistance*minDistance) {
conflict = true;
break;
}
}
}
if (conflict) break;
}
if (!conflict) {
// Accept candidate
addPoint(candidateX, candidateY);
found = true;
break;
}
}
if (!found) {
// No candidates found, remove this point from active list
activeList.splice(index, 1);
}
stepCount++;
}
if (activeList.length === 0) {
// Done, reset
reset();
}
// Update the counter
ctx.fillText('Samples: ' + sampleCount, 10, 20);
// Request next frame
requestAnimationFrame(animate);
}
Wait, but the addPoint function also needs to draw the point. Let me adjust that.
Wait, in the addPoint function:
function addPoint(x, y) {
const cellX = Math.floor(x / cellSize);
const cellY = Math.floor(y / cellSize);
grid[cellY][cellX] = {x, y}; // storing the point in grid
activeList.push({x, y});
sampleCount++;
// Draw the point
ctx.fillRect(x - 0.5, y - 0.5, 1, 1); // assuming 1x1 pixel centered
}
Alternatively, using ctx.arc:
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI*2);
ctx.fill();
But that's a bit more code. Maybe the rectangle is better for brevity.
Also, the canvas needs to clear before starting? Or not, since each new pattern should overwrite. Wait, when resetting, the grid and active list are cleared, but the canvas must be cleared as well. So in the reset function:
function reset() {
grid.forEach(row => row.fill(null));
activeList.length = 0;
sampleCount = 0;
// Clear the canvas
ctx.clearRect(0, 0, width, height);
// Start again
// Pick a random initial point
const firstX = Math.random() * width;
const firstY = Math.random() * height;
addPoint(firstX, firstY);
}
Wait, but initially, when the page loads, the animate function and reset need to be called. So in the setup:
// Initialize on start
reset();
requestAnimationFrame(animate);
Putting all together, need to structure the code properly in the script tags.
Wait, also, in the grid initialization, since grid is a 2D array, maybe the code for grid needs to be:
grid = [];
for (let y = 0; y < gridHeight; y++) {
grid[y] = [];
for (let x = 0; x < gridWidth; x++) {
grid[y][x] = null;
}
}
Alternatively, using Array.from:
grid = Array.from({length: gridHeight}, () =>
Array.from({length: gridWidth}, () => null)
);
Which is shorter.
Additionally, the initial point must be added in reset().
Testing possible issues:
- When calculating cellX and cellY, need to ensure that even if the coordinate is exactly on the grid boundary (e.g., x = cellSize * gridWidth), it doesn't go out of bounds.
Wait, gridWidth is Math.ceil(width / cellSize). For example, if width is exactly divisible by cellSize, then gridWidth is exactly width/cellSize, so cellSize * gridWidth = width. But in that case, candidateX could be exactly width, which would give cellX = (width / cellSize) - 1, since floor would drop the decimal.
Wait, for example:
width = 600, cellSize = 50 → gridWidth = 12 (600/50=12). So x=600 would have cellX = 600/50 = 12, floor is 12, but gridWidth is 12, so indexes go up to 11.
Thus, candidateX = 600 is invalid because it's >= width (600). So in that case, the condition candidateX >= width would exclude it.
Therefore, the boundary conditions should be okay.
Another possible issue: when the candidate is inside the canvas, but the grid cell might be at the edge.
Another point: in the loop over neighboring cells (ky and kx), we need to ensure that neighborCellY and neighborCellX are within the grid dimensions.
Also, in the code for checking neighborPoint, grid[neighborCellY][neighborCellX] is an object with x and y. So neighborPoint.x is accessible.
Now, putting all together in the script.
Wait, also need to define the reset function properly.
Let me try to outline the full code:
<script>
const minDistance = 50;
const cellSize = minDistance / 2;
const width = 600;
const height = 600;
const canvas = document.querySelector('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
let gridWidth = Math.ceil(width / cellSize);
let gridHeight = Math.ceil(height / cellSize);
let grid = Array.from({length: gridHeight}, () =>
Array.from({length: gridWidth}, () => null)
);
let activeList = [];
let sampleCount = 0;
function addPoint(x, y) {
const cellX = Math.floor(x / cellSize);
const cellY = Math.floor(y / cellSize);
grid[cellY][cellX] = {x, y};
activeList.push({x, y});
sampleCount++;
ctx.fillRect(x - 0.5, y - 0.5, 1, 1); // draw a 1x1 pixel square, centered
}
function reset() {
// Clear the grid
grid.forEach(row => row.fill(null));
activeList = [];
sampleCount = 0;
ctx.clearRect(0, 0, width, height);
// Start with a random point
const firstX = Math.random() * width;
const firstY = Math.random() * height;
addPoint(firstX, firstY);
}
function animate() {
let stepCount = 0;
while (activeList.length > 0 && stepCount < 10) {
const index = Math.floor(Math.random() * activeList.length);
const current = activeList[index];
const currentX = current.x;
const currentY = current.y;
let found = false;
for (let i = 0; i < 10; i++) {
const angle = Math.random() * 2 * Math.PI;
const radius = minDistance * (0.5 + Math.random() * 0.5);
const dx = Math.cos(angle) * radius;
const dy = Math.sin(angle) * radius;
const candidateX = currentX + dx;
const candidateY = currentY + dy;
if (candidateX < 0 || candidateX >= width || candidateY < 0 || candidateY >= height) continue;
const cellX = Math.floor(candidateX / cellSize);
const cellY = Math.floor(candidateY / cellSize);
let conflict = false;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const neighborCellY = cellY + ky;
const neighborCellX = cellX + kx;
if (neighborCellY < 0 || neighborCellY >= gridHeight) continue;
if (neighborCellX < 0 || neighborCellX >= gridWidth) continue;
const neighborPoint = grid[neighborCellY][neighborCellX];
if (neighborPoint) {
const dx = candidateX - neighborPoint.x;
const dy = candidateY - neighborPoint.y;
if (dx*dx + dy*dy < minDistance * minDistance) {
conflict = true;
break;
}
}
}
if (conflict) break;
}
if (!conflict) {
addPoint(candidateX, candidateY);
found = true;
break;
}
}
if (!found) activeList.splice(index, 1);
stepCount++;
}
if (activeList.length === 0) {
reset();
}
ctx.fillText('Samples: ' + sampleCount, 10, 20);
requestAnimationFrame(animate);
}
// Initialize and start
reset();
requestAnimationFrame(animate);
</script>
Wait, but in the addPoint function, after adding to the grid, the position is stored as an object {x, y}. So when checking neighborPoint, grid[neighborCellY][neighborCellX] is that object, so accessing neighborPoint.x and neighborPoint.y is okay.
Also, in the reset function, grid.forEach(...).fill(null) might not work as expected because Array.fill modifies the array in place. Wait, when using Array.from, each row is an array. So:
grid.forEach(row => row.fill(null));
This should reset each row to have all nulls.
Another possible issue: when adding the first point, the grid is initialized properly.
Also, in the animate function, after resetting, the next frame will start processing the new initial point.
Now, testing possible errors:
- The canvas element must be present in the HTML. So, the user must have a canvas tag in their HTML. Since the problem says to create the code, we can assume that the HTML includes a canvas element. But in the code above, the script is selecting it via document.querySelector('canvas'). So the user should have:
<canvas></canvas>
In their HTML.
Putting all together, the full HTML:
<!DOCTYPE html>
<html>
<body>
<canvas></canvas>
<script>
// The script code above
</script>
</body>
</html>
Now, in the code, the counter is drawn at (10,20), but initially, that might be covered by the first point, but since it's text, it should be on top.
Potential optimization: To prevent the text from being cleared, but since the canvas is cleared on reset, it's okay to redraw it each frame.
Another possible issue: when the grid is reset, the next time addPoint is called, the grid is properly set to nulls.
Testing the code:
The initial point is added via reset(). Then the animate loop processes it. Each step adds up to 10 points per frame (since stepCount <10).
The loop continues until the activeList is empty, then resets.
This should create a random pattern of points with at least minDistance between them, restarting periodically.
Now, possible edge cases:
- The first point is added, but no more points can be added (e.g., in a very small canvas). But with width=600 and minDistance=50, it should have enough room.
Testing on small canvas might cause it to reset quickly, but for 600 it should work.
Now, the problem says "Create a program that...", so the user expects the code to be in the form of a script, likely in HTML with canvas.
Thus, the final code would be as outlined.