Genuary 2026.19: 16x16

I’m… not really sure where I was going with this one. 😄

Basically, 16x16 tiny sketches that slide around like a sliding block puzzle. Although with how chaotic some of the simulations are, it’s not always easy to see.


const DEBUG_PRINT = false;
const NOISE_SIZE = 100;

let gui;
let params = {
  // How many frames each move takes
  slideFrames: 10,
  slideFramesMin: 1,
  slideFramesMax: 100,

  // Variation on speed
  slideSpeedVariation: 0.1,
  slideSpeedVariationMin: 0,
  slideSpeedVariationMax: 1,
  slideSpeedVariationStep: 0.01,
  
  // How many blocks should be removed
  missingBlocks: 3,
  missingBlocksMin: 1,
  missingBlocksMax: 16,

  // Fade mode will fade instead of clearing 
  fadeMode: true,

  // Draw borders
  drawBorders: true,
};

let lastParams;
let grid = [];
let slidingBlocks = [];
let noiseTexture;

const BLOCK_VARIANTS = [
  {
    // Animated noise texture
    name: 'noise',
    init: (self) => {
      self.offsetX = floor(random(NOISE_SIZE - width / 16));
      self.offsetY = floor(random(NOISE_SIZE - height / 16));
      self.tintHue = random(360);
    },
    update: (self) => {},
    draw: (self, {screenX, screenY}) => {
      tint(self.tintHue, 100, 100);
      image(noiseTexture, 0, 0, width / 16, height / 16, self.offsetX, self.offsetY, width / 16, height / 16);
      noTint();
    },
  },
  {
    // Conway's Game of Life
    name: 'gameoflife',
    init: (self) => {
      self.cols = width / 16;
      self.rows = height / 16;
      self.grid = [];
      for (let i = 0; i < self.cols; i++) {
        self.grid.push([]);
        for (let j = 0; j < self.rows; j++) {
          self.grid[i].push(random() < 0.5 ? 1 : 0);
        }
      }
      self.hue = random(360);
    },
    update: (self) => {
      let newGrid = [];
      let updates = 0;

      for (let i = 0; i < self.cols; i++) {
        newGrid.push([]);
        for (let j = 0; j < self.rows; j++) {
          let sum = 0;
          for (let xOff = -1; xOff <= 1; xOff++) {
            for (let yOff = -1; yOff <= 1; yOff++) {
              if (xOff === 0 && yOff === 0) continue;
              let x = (i + xOff + self.cols) % self.cols;
              let y = (j + yOff + self.rows) % self.rows;
              sum += self.grid[x][y];
            }
          }
          if (self.grid[i][j] === 1 && (sum < 2 || sum > 3)) {
            newGrid[i].push(0);
          } else if (self.grid[i][j] === 0 && sum === 3) {
            newGrid[i].push(1);
          } else {
            newGrid[i].push(self.grid[i][j]);
          }
        }
      }

      for (let i = 0; i < self.cols; i++) {
        for (let j = 0; j < self.rows; j++) {
          if (self.grid[i][j] !== newGrid[i][j]) {
            updates++;
          }
        }
      }

      if (updates < 16 || random() < 0.001) {
        for (let i = 0; i < self.cols; i++) {
          for (let j = 0; j < self.rows; j++) {
            newGrid[i][j] = random() < 0.5 ? 1 : 0;
          }
        }
      }

      self.grid = newGrid;
    },
    draw: (self) => {
      let w = width / 16 / self.cols;
      let h = height / 16 / self.rows;
      for (let i = 0; i < self.cols; i++) {
        for (let j = 0; j < self.rows; j++) {
          if (self.grid[i][j] === 1) {
            noStroke();
            fill(self.hue, 80, 100);
            rect(i * w, j * h, w, h);
          }
        }
      }
    },
  },
  {
    // Bouncing balls, no collision
    name: 'bouncing-balls',
    init: (self) => {
      self.balls = [];
      let numBalls = floor(random(3, 7));
      for (let i = 0; i < numBalls; i++) {
        self.balls.push({
          x: random(width / 16),
          y: random(height / 16),
          vx: random([-1, 1]) * random(0.5, 2),
          vy: random([-1, 1]) * random(0.5, 2),
          radius: random(1, 7),
          hue: random(360),
        });
      }
    },
    update: (self) => {
      for (let ball of self.balls) {
        ball.x += ball.vx;
        ball.y += ball.vy;

        if (ball.x < 0 || ball.x > width / 16) {
          ball.vx *= -1;
        }
        if (ball.y < 0 || ball.y > height / 16) {
          ball.vy *= -1;
        }
      }
    },
    draw: (self) => {
      for (let ball of self.balls) {
        noStroke();
        fill(ball.hue, 80, 100);
        ellipse(ball.x, ball.y, ball.radius);
      }
    },
  },
  {
    // Falling rain particles, matrix style
    name: 'rain',
    init: (self) => {
      self.particles = [];
      let numParticles = floor(random(20, 50));
      for (let i = 0; i < numParticles; i++) {
        self.particles.push({
          x: random(width / 16),
          y: random(height / 16),
          hue: random(360),
        });
      }
    },
    update: (self) => {
      for (let particle of self.particles) {
        particle.y += random(0.5, 1.5);
        if (particle.y > height / 16) {
          particle.y = 0;
          particle.x = random(width / 16);
        }
      }
    },
    draw: (self) => {
      for (let particle of self.particles) {
        noStroke();
        fill(particle.hue, 80, 100);
        ellipse(particle.x, particle.y, 1);
      }
    },  
  },
  {
    // A pulsing circle
    name: 'pulse',
    init: (self) => {
      self.hue = random(360);
      self.phase = random(TWO_PI);
    },
    update: (self) => {
      self.phase += 0.1;
    },
    draw: (self) => {
      let size = (sin(self.phase) + 1) / 2 * (width / 16);
      noStroke();
      fill(self.hue, 80, 100);
      ellipse((width / 16) / 2, (height / 16) / 2, size);
    },
  },
  {
    // A worm that moves around like the snake game
    name: 'snake',
    init: (self) => {
      self.length = floor(random(5, 15));
      self.segments = [];
      self.direction = random([[1,0],[-1,0],[0,1],[0,-1]]);
      self.hue = random(360);

      let startX = floor(random(16));
      let startY = floor(random(16));
      for (let i = 0; i < self.length; i++) {
        self.segments.push({ x: startX, y: startY });
      }
    },
    update: (self) => {
      let head = self.segments[0];
      let newX = (head.x + self.direction[0] + 16) % 16;
      let newY = (head.y + self.direction[1] + 16) % 16;

      if (self.segments.some(seg => seg.x === newX && seg.y === newY)) {
        let startX = floor(random(16));
        let startY = floor(random(16));
        self.segments = [];
        for (let i = 0; i < self.length; i++) {
          self.segments.push({ x: startX, y: startY });
        }
      } else {
        self.segments.pop();
        self.segments.unshift({ x: newX, y: newY });

        if (random() < 0.3) {
          self.direction = random([[1,0],[-1,0],[0,1],[0,-1]]);
        }
      }
    },
    draw: (self) => {
      let w = width / 16;
      let h = height / 16;
      for (let seg of self.segments) {
        noStroke();
        fill(self.hue, 80, 100);
        rect(seg.x, seg.y, 1, 1);
      }
    },
  },
  {
    // N random walking particles
    name: 'bugs',
    init: (self) => {
      let blockWidth = width / 16;
      let blockHeight = height / 16;

      self.particles = [];
      let numParticles = floor(random(5, 15));
      for (let i = 0; i < numParticles; i++) {
        self.particles.push({
          x: floor(random(blockWidth)),
          y: floor(random(blockHeight)),
          hue: random(360),
        });
      }
    },
    update: (self) => {
      let blockWidth = width / 16;
      let blockHeight = height / 16;

      for (let particle of self.particles) {
        let dir = random([[1,0],[-1,0],[0,1],[0,-1]]);
        particle.x = (particle.x + dir[0] + blockWidth) % blockWidth;
        particle.y = (particle.y + dir[1] + blockHeight) % blockHeight;
      }
    },
    draw: (self) => {
      let w = width / 16;
      let h = height / 16;
      for (let particle of self.particles) {
        noStroke();
        fill(particle.hue, 80, 100);
        rect(particle.x, particle.y, 1, 1);
      }
    },  
  },
  {
    // A random chinese character that changes periodically
    name: 'char',
    init: (self) => {
      self.char = String.fromCharCode(floor(random(0x4E00, 0x9FFF)));
      self.hue = random(360);
      self.frameCounter = 0;
      self.changeInterval = floor(random(100, 200));
    },
    update: (self) => {
      self.frameCounter++;
      if (self.frameCounter >= self.changeInterval) {
        self.char = String.fromCharCode(floor(random(0x4E00, 0x9FFF)));
        self.hue = random(360);
        self.frameCounter = 0;
        self.changeInterval = floor(random(30, 120));
      }
    },
    draw: (self) => {
      const bw = width / 16;
      const bh = height / 16;
      const pad = bw * 0.1;

      textAlign(CENTER, CENTER);
      textSize(1);
      const w = textWidth(self.char);
      const h = textAscent() + textDescent();
      const s = Math.min((bw - 2 * pad) / w, (bh - 2 * pad) / h);
      textSize(s * 4);

      fill(self.hue, 80, 100);
      noStroke();
      text(self.char, bw / 2, bh / 2);
    },  
  }
];

class Block {
  constructor() {
    this.sliding = false;
    
    let i = floor(random(BLOCK_VARIANTS.length));
    let variant = BLOCK_VARIANTS[i];

    variant.init(this);
    this.update = () => variant.update(this);
    this.draw = (args) => variant.draw(this, args);
  }
}

function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100, 100);

  gui = createGuiPanel("params");
  gui.addObject(params);
  gui.setPosition(420, 0);

  reset();
}

function reset() {
  grid = [];
  slidingBlocks = [];

  for (let i = 0; i < 16; i++) {
    grid.push([]);
    for (let j = 0; j < 16; j++) {
      grid[i].push(null);
    }
  }

  for (let i = 0; i < 16; i++) {
    for (let j = 0; j < 16; j++) {
      grid[i][j] = new Block();
    }
  }
  
  // Remove missing blocks
  for (let i = 0; i < params.missingBlocks; i++) {
    while(true) {
      let x = floor(random() * 16);
      let y = floor(random() * 16);
      
      if (grid[x][y] != null) {
        if (DEBUG_PRINT) console.log("Removing block at", x, y);
        grid[x][y] = null;
        break;
      }
    }
  }
}

function draw() {
  if (lastParams == undefined || Object.keys(params).some((k) => params[k] !== lastParams[k])) {
    reset();
    lastParams = { ...params };
  }

  // Initialize/update noise texture
  if (!noiseTexture) {
    noiseTexture = createGraphics(NOISE_SIZE, NOISE_SIZE);
    noiseTexture.colorMode(HSB, 360, 100, 100, 100);
  }
  noiseTexture.loadPixels();
  for (let x = 0; x < NOISE_SIZE; x++) {
    for (let y = 0; y < NOISE_SIZE; y++) {
      let n = noise(x * 0.05, y * 0.05, frameCount * 0.01);
      n = n > 0.5 ? 1 : 0;
      let brightness = n * 100;
      noiseTexture.set(x, y, color(0, 0, brightness));
    }
  }
  noiseTexture.updatePixels();

  if (params.fadeMode) {
    noStroke();
    fill(0, 0, 0, 10);
    rect(0, 0, width, height);
  } else {
    background("black");
  }

  // Update all block's internal states
  for (let x = 0; x < 16; x++) {
    for (let y = 0; y < 16; y++) {
      if (grid[x][y] !== null) {
        grid[x][y].update();
      }
    }
  }

  // If we have any empty spaces, start a slide into that position
  for (let x = 0; x < 16; x++) {
    for (let y = 0; y < 16; y++) {
      // Skip cells that are already occupied
      if (grid[x][y] !== null) continue;

      // Skip cells something is already sliding into
      if (slidingBlocks.some(sb => sb.dst.x === x && sb.dst.y === y)) continue;

      // Find neighbors that can slide into this cell
      let neighbors = [];
      for (let [xd, yd] of [[-1,0],[1,0],[0,-1],[0,1]]) {
        let nx = x + xd;
        let ny = y + yd;
        if (nx >= 0 && nx < 16 && ny >= 0 && ny < 16) {
          // Only consider non-empty, non-sliding neighbors
          if (grid[nx][ny] !== null && !grid[nx][ny].sliding) {
            neighbors.push({ x: nx, y: ny });
          }
        }
      }

      // If we have neighbors, pick one at random to slide into that position
      if (neighbors.length > 0) {
        let choice = random(neighbors);

        if (DEBUG_PRINT) {
          console.log(`Starting slide from (${choice.x}, ${choice.y}) to (${x}, ${y})`);
        }

        slidingBlocks.push({
          src: { x: choice.x, y: choice.y },
          dst: { x: x, y: y },
          progress: 0,
        });
        grid[choice.x][choice.y].sliding = true;
      }
    }
  }
  
  if (DEBUG_PRINT) console.log("Currently sliding count:", slidingBlocks.length);

  // Update sliding blocks
  for (let i = slidingBlocks.length - 1; i >= 0; i--) {
    let sb = slidingBlocks[i];
    sb.progress++;

    if (random() < params.slideSpeedVariation) {
      sb.progress++;
    }

    // If the slide is complete, finalize the move
    if (sb.progress >= params.slideFrames) {
      if (DEBUG_PRINT) {
        console.log(`Completing slide from (${sb.src.x}, ${sb.src.y}) to (${sb.dst.x}, ${sb.dst.y})`);
      }

      grid[sb.dst.x][sb.dst.y] = grid[sb.src.x][sb.src.y];
      grid[sb.dst.x][sb.dst.y].sliding = false;
      grid[sb.src.x][sb.src.y] = null;
      slidingBlocks.splice(i, 1);
    }
  }

  let blockWidth = width / 16;
  let blockHeight = height / 16;

  for (let x = 0; x < 16; x++) {
    for (let y = 0; y < 16; y++) {
      let block = grid[x][y];
      if (!block) continue;
        
      push();
      translate(blockWidth * x, blockHeight * y);

      // If this block is sliding, compute its position
      let slidingBlock = slidingBlocks.find(sb => sb.src.x === x && sb.src.y === y);
      let screenX = x * blockWidth;
      let screenY = y * blockHeight;

      if (slidingBlock) {
        let progress = slidingBlock.progress / params.slideFrames;
        screenX = lerp(slidingBlock.src.x, slidingBlock.dst.x, progress) * blockWidth;
        screenY = lerp(slidingBlock.src.y, slidingBlock.dst.y, progress) * blockHeight;
        translate(screenX - x * blockWidth, screenY - y * blockHeight);
      }

      block.draw({x, y, screenX, screenY});
      
      if (params.drawBorders) {
        stroke("darkgray");
        strokeWeight(0.1);
        noFill();
        rect(0, 0, blockWidth, blockHeight);
      }

      pop();
    }
  }
}