← Back to blog

magnet poetry board

July 22, 2025

wip


I wanted:

01 — board shape

centered, 3:4, soft depth. positions live in percentages, not pixels.

what to notice

  • 3:4 frame stays centered
  • quiet shadow, no chrome
  • percent coords = stable layout on resize
core idea (tiny code)
type WordItem = {
  id: string; text: string;
  xPercent: number; // 0..100
  yPercent: number; // 0..100
};

02 — under-finger drag

measure from the magnet’s center → convert to board % → clamp to 0..100.

what to notice

  • the word stays under your finger
  • no “snap” to top-left
  • mouse and touch feel the same
core idea (tiny code)
const clamp = (v:number, lo=0, hi=100) => Math.min(Math.max(v, lo), hi);
const posFromClient = (cx:number, cy:number, board:DOMRect, off={x:0,y:0}) => ({
  xPercent: clamp(((cx - board.left - off.x) / board.width)  * 100),
  yPercent: clamp(((cy - board.top  - off.y) / board.height) * 100),
});

03 — delete (only on drop)

the dock glows while you hover with a drag. nothing happens until you drop.

what to notice

  • feedback ≠ action (glow is only a hint)
  • drop to confirm deletion
  • forgiving target area
core idea (tiny code)
const overlaps = (a:DOMRect, b:DOMRect) =>
  !(a.right<b.left || a.left>b.right || a.bottom<b.top || a.top>b.bottom);

// while dragging:
setInDelete(overlaps(new DOMRect(cx, cy, 1, 1), dockRect));

// on drop:
if (inDelete) removeWord(dragId);

04 — export that stays crisp

dom snapshot via html2canvas; scale by devicePixelRatio for hidpi.

what to notice

  • text edges stay sharp on retina
  • one click → png
  • works with html magnets
core idea (tiny code)
import html2canvas from 'html2canvas';
async function exportPNG(boardEl: HTMLElement) {
  const scale = window.devicePixelRatio || 1;
  const canvas = await html2canvas(boardEl, { scale });
  const a = document.createElement('a');
  a.href = canvas.toDataURL('image/png'); a.download = 'poem.png'; a.click();
}

05 — spawning (near center)

words come from public/words.txt. spawn near the middle so i’m not chasing them.

what to notice

  • spawn zone is centered
  • small random offset for variety
  • no initial overlaps (future pass)
core idea (tiny code)
const spawn = () => ({
  xPercent: 50 + (Math.random() - 0.5) * 30,
  yPercent: 50 + (Math.random() - 0.5) * 30,
});

small choices that help

  • select-none on the board + magnets → no accidental highlights
  • clamp to 0..100% → magnets don’t drift off-board
  • delete only on drop → glow is feedback, not action
  • tap size ~28–32px, rounded + soft shadow → low fatigue

next tiny passes: arrow-key nudge (1% / 5% with Shift), json export/import, and a lightweight overlap guard on spawn.