CrunchBits

HTML

<div class="stage">
    <canvas id="canvas" width="820" height="180" role="img" aria-label="Shattering text"></canvas>

    <div class="row controls">
      <button id="shatterBtn">Shatter</button>
      <button id="autoBtn">Auto: off</button>
      <label class="small">Tile size</label>
      <input id="tileRange" type="range" min="4" max="32" value="10">
      <label class="small">Force</label>
      <input id="forceRange" type="range" min="30" max="300" value="120">
    </div>

    <div class="row">
      <label for="textInput">Text</label>
      <input id="textInput" value="CRUNCHBITS" style="padding:8px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);background:#0b1220;color:var(--fg)">
    </div>
  </div>

CSS

:root{
    --bg: #0f1720;
    --fg: #e6eef8;
    --ui: #111827;
  }
  html,body{height:100%;margin:0}
  body{
    display:flex;
    align-items:center;
    justify-content:center;
    background:var(--bg);
    color:var(--fg);
    font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial;
    padding:24px;
  }

  .stage {
    display:flex;
    flex-direction:column;
    gap:12px;
    align-items:center;
  }

  #canvas {
    background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);
    border-radius:10px;
    box-shadow: 0 6px 30px rgba(0,0,0,0.6);
  }

  .controls {
    display:flex;
    gap:8px;
  }
  button{
    background:var(--ui);
    color:var(--fg);
    border:1px solid rgba(255,255,255,0.04);
    padding:8px 12px;
    border-radius:8px;
    cursor:pointer;
    font-size:14px;
  }
  input[type="range"]{width:160px;}
  label{font-size:13px; color: #cbd5e1;}
  .row{display:flex;gap:12px;align-items:center}
  .small{font-size:12px;color:#9aa8bd}

JavaScript

(() => {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const shatterBtn = document.getElementById('shatterBtn');
  const autoBtn = document.getElementById('autoBtn');
  const tileRange = document.getElementById('tileRange');
  const forceRange = document.getElementById('forceRange');
  const textInput = document.getElementById('textInput');

  let auto = false;
  let autoTimer = null;

  // draw text onto canvas, centered
  function drawTextOn(ctx, text, opts = {}) {
    const {fontSize = 96, fontFamily = 'Inter, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial', color = '#861d30'} = opts;
    ctx.clearRect(0,0,canvas.width, canvas.height);
    ctx.save();
    ctx.fillStyle = color;
    ctx.font = `700 ${fontSize}px ${fontFamily}`;
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'center';
    // measure and adjust font size to fit if very long
    const metrics = ctx.measureText(text);
    const textWidth = metrics.width;
    const maxWidth = canvas.width * 0.9;
    let size = fontSize;
    if (textWidth > maxWidth) {
      size = Math.floor(fontSize * maxWidth / textWidth);
      ctx.font = `700 ${size}px ${fontFamily}`;
    }
    ctx.fillText(text, canvas.width/2, canvas.height/2);
    ctx.restore();
    return {fontSize: size};
  }

  function makePieces(tileSize = 8) {
    // rasterize current canvas into imageData and split into tiles
    const w = canvas.width, h = canvas.height;
    const img = ctx.getImageData(0,0,w,h);
    const pieces = [];
    for (let y = 0; y < h; y += tileSize) {
      for (let x = 0; x < w; x += tileSize) {
        // check if tile contains any non-transparent pixels (alpha > 0)
        let has = false;
        const tw = Math.min(tileSize, w - x);
        const th = Math.min(tileSize, h - y);
        for (let yy = 0; yy < th && !has; yy++) {
          for (let xx = 0; xx < tw; xx++) {
            const idx = ((y + yy)...