Skip to content

Abelian sandpile

An exploration of animating Abelian sandpiles. Turned more fun by adding "negative sandpiles" and ensuring the total sum on-screen is always 0.

Click to drop a square grain of sand. Right click to drop a circle grain of sand. Enjoy the chain reactions.

A gray square waiting to be filled with orange and blue grains of sand, following the Abelian sandpile model

let N = 35
let counts = Array(N * N).fill(0)
let nextCounts = counts.slice()
let t = 1

function setup() {
  let s = min(windowWidth, windowHeight)
  createCanvas(s, s);
  rectMode(CENTER)
  document.querySelector('canvas').addEventListener('contextmenu', e => e.preventDefault()) // https://github.com/processing/p5.js/issues/7098
}
function windowResized() {
  let s = min(windowWidth, windowHeight)
  resizeCanvas(s, s);
}

function draw() {
  background(220);
  
  t += keyIsDown(32) ? 1 : deltaTime / 100
  if (t > 1) {
    ;[counts, nextCounts] = [nextCounts, counts]
    
    for (let i = 0; i < nextCounts.length; i++) {
      nextCounts[i] = counts[i] % 4 +
        Math.trunc(counts[(i + 1) % counts.length] / 4) + 
        Math.trunc(counts[(i + counts.length - 1) % counts.length] / 4) + 
        Math.trunc(counts[(i + N) % counts.length] / 4) + 
        Math.trunc(counts[(i + counts.length - N) % counts.length] / 4)
    }
    
    if (mouseIsPressed) {
      let unitSize = width / N
      let amount = (mouseButton.right ? -1 : 1)
      let mousePos = (floor(mouseX / unitSize) + floor(mouseY / unitSize) * N) % nextCounts.length
      nextCounts[mousePos] += amount
      nextCounts[floor(random(nextCounts.length))] -= amount
    }
    
    t = 0
  }
  
  let interp = t
  for (let i = 0; i < counts.length; i++) {
    drawGrain(i % N, floor(i / N), lerp(counts[i], nextCounts[i], interp))
  }
}

function drawGrain(i, j, size) {
  let unitSize = width / N
  let sizeOnScreen = unitSize * (abs(size) / 4) ** 0.8
  push()
  fill(paletteLerp([
    [color(100, 255, 255), -8],
    [color(0, 255, 255), -4],
    [color(0, 0, 255), -0],
    [color(255, 255, 0), 0],
    [color(255, 100, 0), 4],
    [color(255, 0, 100), 8]
  ], size))
  translate((i + 0.5) * unitSize, (j + 0.5) * unitSize)
  if (size < -3) {
    rotate(PI / 4 * min(-3 - size, 1) ** 3)
    let fracture = sizeOnScreen * sqrt(-3 - size) * 0.1
    arc(+fracture, +fracture, sizeOnScreen, sizeOnScreen, 0, PI / 2, PIE)
    arc(-fracture, +fracture, sizeOnScreen, sizeOnScreen, PI / 2, PI, PIE)
    arc(-fracture, -fracture, sizeOnScreen, sizeOnScreen, PI, PI * 3 / 2, PIE)
    arc(+fracture, -fracture, sizeOnScreen, sizeOnScreen, PI * 3 / 2, PI * 2, PIE)
  } else if (size < 0) {
    circle(0, 0, sizeOnScreen)
  } else if (size <= 3) {
    rect(0, 0, sizeOnScreen)
  } else {
    rotate(PI / 4 * min(size - 3, 1) ** 3)
    let fracture = sizeOnScreen * (0.25 + sqrt(size - 3) * 0.1)
    rect(-fracture, -fracture, sizeOnScreen/2)
    rect(+fracture, -fracture, sizeOnScreen/2)
    rect(+fracture, +fracture, sizeOnScreen/2)
    rect(-fracture, +fracture, sizeOnScreen/2)
  }
  pop()
}

(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/CqOEm4Zpr)

Experiments tagged p5 (45/85)

Experiments tagged interactive (15/26)

Experiments on this site (45/85)