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)...