Click Add chips to drop a batch of chips at the center, then watch the avalanche stabilize into a fractal pattern. Add more and more chips to grow it larger. The color of each cell shows how many chips it holds (0–3); no cell ever stays at 4 or above because it topples immediately.
<!-- Sandpile demo — drop chips at center and watch fractal avalanches -->
<div class="controls">
<button id="btn-add" type="button">Add chips</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="chip-count" id="chip-count"></span>
</div>
<canvas id="canvas" width="300" height="300"></canvas>
<div class="status" id="status">Press "Add chips" to start the avalanche.</div>
/* Sandpile demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; display: flex; flex-direction: column; align-items: center; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 4px; image-rendering: pixelated; max-width: 100%; }
.chip-count { font-size: .85rem; color: #555; }
.status { font-size: .9rem; font-weight: 600; margin-top: .4rem; min-height: 1.3em; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Abelian Sandpile Model — each cell holds 0..3 chips; cells with 4+ topple.
// The abelian property guarantees the same final config regardless of toppling order.
const SIZE = 60; // grid is SIZE x SIZE cells
const BATCH = 500; // chips added per click
const cx = Math.floor(SIZE / 2);
const cy = Math.floor(SIZE / 2);
// flat grid array; boundary cells lose chips to a sink (they vanish)
let grid = new Int32Array(SIZE * SIZE);
let totalChips = 0;
const COLORS = ['#f0f4f8', '#a8c5da', '#4a90d9', '#1d3557']; // colors for 0, 1, 2, 3 chips per cell
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cellPx = canvas.width / SIZE;
function idx(r, c) { return r * SIZE + c; }
// Stabilize by repeatedly toppling any cell with 4+ chips.
function stabilize() {
let changed = true;
while (changed) {
changed = false;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (grid[idx(r, c)] >= 4) {
const fires = Math.floor(grid[idx(r, c)] / 4);
grid[idx(r, c)] -= fires * 4;
if (r > 0) grid[idx(r - 1, c)] += fires;
if (r < SIZE - 1) grid[idx(r + 1, c)] += fires;
if (c > 0) grid[idx(r, c - 1)] += fires;
if (c < SIZE - 1) grid[idx(r, c + 1)] += fires;
// chips at border are lost to the sink — this keeps the grid finite
changed = true;
}
}
}
}
}
function draw() {
const img = ctx.createImageData(canvas.width, canvas.height);
const data = img.data;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const v = Math.min(grid[idx(r, c)], 3);
const hex = COLORS[v];
const R = parseInt(hex.slice(1, 3), 16);
const G = parseInt(hex.slice(3, 5), 16);
const B = parseInt(hex.slice(5, 7), 16);
const px = Math.round(r * cellPx);
const py = Math.round(c * cellPx);
const pw = Math.round((r + 1) * cellPx) - px;
const ph = Math.round((c + 1) * cellPx) - py;
for (let dy = 0; dy < ph; dy++) {
for (let dx = 0; dx < pw; dx++) {
const base = ((py + dy) * canvas.width + (px + dx)) * 4;
data[base] = R; data[base + 1] = G; data[base + 2] = B; data[base + 3] = 255;
}
}
}
}
ctx.putImageData(img, 0, 0);
}
function updateUI() {
document.getElementById('chip-count').textContent =
i18nFmt('Total chips: %n%', { n: totalChips });
}
function addChips() {
grid[idx(cx, cy)] += BATCH;
totalChips += BATCH;
stabilize();
draw();
updateUI();
document.getElementById('status').textContent =
i18nFmt('Stable fractal at %n% chips total.', { n: totalChips });
}
function reset() {
grid = new Int32Array(SIZE * SIZE);
totalChips = 0;
draw();
updateUI();
document.getElementById('status').textContent = 'Press "Add chips" to start the avalanche.';
}
document.getElementById('btn-add').addEventListener('click', addChips);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial draw (empty grid)
draw();
updateUI();
<!-- Sandpile demo — drop chips at center and watch fractal avalanches -->
<div class="controls">
<button id="btn-add" type="button">Add chips</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="chip-count" id="chip-count"></span>
</div>
<canvas id="canvas" width="300" height="300"></canvas>
<div class="status" id="status">Press "Add chips" to start the avalanche.</div>
/* Sandpile demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; display: flex; flex-direction: column; align-items: center; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 4px; image-rendering: pixelated; max-width: 100%; }
.chip-count { font-size: .85rem; color: #555; }
.status { font-size: .9rem; font-weight: 600; margin-top: .4rem; min-height: 1.3em; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Abelian Sandpile Model — each cell holds 0..3 chips; cells with 4+ topple.
// The abelian property guarantees the same final config regardless of toppling order.
const SIZE = 60; // grid is SIZE x SIZE cells
const BATCH = 500; // chips added per click
const cx = Math.floor(SIZE / 2);
const cy = Math.floor(SIZE / 2);
// flat grid array; boundary cells lose chips to a sink (they vanish)
let grid = new Int32Array(SIZE * SIZE);
let totalChips = 0;
const COLORS = ['#f0f4f8', '#a8c5da', '#4a90d9', '#1d3557']; // colors for 0, 1, 2, 3 chips per cell
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cellPx = canvas.width / SIZE;
function idx(r, c) { return r * SIZE + c; }
// Stabilize by repeatedly toppling any cell with 4+ chips.
function stabilize() {
let changed = true;
while (changed) {
changed = false;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (grid[idx(r, c)] >= 4) {
const fires = Math.floor(grid[idx(r, c)] / 4);
grid[idx(r, c)] -= fires * 4;
if (r > 0) grid[idx(r - 1, c)] += fires;
if (r < SIZE - 1) grid[idx(r + 1, c)] += fires;
if (c > 0) grid[idx(r, c - 1)] += fires;
if (c < SIZE - 1) grid[idx(r, c + 1)] += fires;
// chips at border are lost to the sink — this keeps the grid finite
changed = true;
}
}
}
}
}
function draw() {
const img = ctx.createImageData(canvas.width, canvas.height);
const data = img.data;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const v = Math.min(grid[idx(r, c)], 3);
const hex = COLORS[v];
const R = parseInt(hex.slice(1, 3), 16);
const G = parseInt(hex.slice(3, 5), 16);
const B = parseInt(hex.slice(5, 7), 16);
const px = Math.round(r * cellPx);
const py = Math.round(c * cellPx);
const pw = Math.round((r + 1) * cellPx) - px;
const ph = Math.round((c + 1) * cellPx) - py;
for (let dy = 0; dy < ph; dy++) {
for (let dx = 0; dx < pw; dx++) {
const base = ((py + dy) * canvas.width + (px + dx)) * 4;
data[base] = R; data[base + 1] = G; data[base + 2] = B; data[base + 3] = 255;
}
}
}
}
ctx.putImageData(img, 0, 0);
}
function updateUI() {
document.getElementById('chip-count').textContent =
i18nFmt('Total chips: %n%', { n: totalChips });
}
function addChips() {
grid[idx(cx, cy)] += BATCH;
totalChips += BATCH;
stabilize();
draw();
updateUI();
document.getElementById('status').textContent =
i18nFmt('Stable fractal at %n% chips total.', { n: totalChips });
}
function reset() {
grid = new Int32Array(SIZE * SIZE);
totalChips = 0;
draw();
updateUI();
document.getElementById('status').textContent = 'Press "Add chips" to start the avalanche.';
}
document.getElementById('btn-add').addEventListener('click', addChips);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial draw (empty grid)
draw();
updateUI();
Notice that the pattern has four-fold rotational symmetry — a direct consequence of the grid's symmetry and the abelian property. No matter how many chips you added or in what order internal toppling occurred, this particular shape is the unique stable configuration for that chip count. There is no other answer.
Comments
Loading comments...