A patience-as-virtue fishing minigame. Hold the pond to charge a cast — the longer you hold, the farther the lure flies. Release. Wait. When the bobber jerks down, click to set the hook. 14 swamp curiosities can come up: minnows, boots, messages, a coin that is heads if you wanted heads. Sometimes the line returns empty — the swamp kept it.
hold anywhere to charge a cast ↑
An idle ritual. Great as a daily "I cast a line" button on a homepage, a 404, an empty-state ("nothing here yet — go fish?"), or a meditation widget that doesn't pretend to be one. Each cast takes 5–10 seconds. Each result is a tiny gift.
Skip it when the page is task-focused. Lure Cast is the antithesis of speed.
A pond, a fisher silhouette, a lure, an SVG line, and a catch card. JS animates them.
<div class="pond" id="pond"> <div class="charge-meter"> <div class="cm-bar"><div class="cm-fill" id="chargeFill"></div></div> </div> <div class="fisher"> ...silhouette SVG... </div> <svg class="line" viewBox="0 0 100 100" preserveAspectRatio="none"> <path id="linePath" stroke="#f3eedd"/> </svg> <div class="lure" id="lure"></div> <div class="catch-card" id="catchCard"> ...filled by JS... </div> </div>
Four states: idle → charging → flying → waiting → biting → resolved.
Pointer down starts charging; pointer up triggers flying with
power = clamped charge. After splash, schedule a random bite. On bite, the next click
must arrive inside BITE_MS or the fish gets away.
// Swamp UI — Lure Cast (core flow) const ROD = { x: 12, y: 42 }; // rod tip in % of pond const MAX_CHARGE = 1200; const BITE_MS = 560; let state = 'idle', charge = 0, chargeStart = 0; pond.addEventListener('pointerdown', e => { if (state !== 'idle') return; state = 'charging'; chargeStart = performance.now(); }); pond.addEventListener('pointerup', () => { if (state === 'charging') { charge = Math.min(1, (performance.now() - chargeStart) / MAX_CHARGE); cast(charge); } else if (state === 'biting') { resolve('caught'); } else if (state === 'waiting') { resolve('spooked'); // clicked too early } }); function cast(p){ state = 'flying'; const dist = 25 + p * 55; // % across pond const tx = ROD.x + dist; const ty = 48 + (Math.random() - .5) * 14; animateArc(ROD.x, ROD.y, tx, ty, 700, () => { splash(tx, ty); state = 'waiting'; setTimeout(() => { if (state === 'waiting') bite(); }, 1800 + Math.random() * 3500); }); }
Each catch has a name, a one-line description, and a small emoji (used in the log). Edit the array to make it yours.
const CATCHES = [ { e:'🐟', n:'A STARTLED MINNOW', d:'silver, brief, gone.' }, { e:'👢', n:'AN OLD BOOT', d:'mud, and a story you can finish for it.' }, { e:'🍃', n:'A LILY ROOT', d:'it remembers everything. you do not.' }, { e:'📜', n:'A MESSAGE', d:'"you have already decided."' }, { e:'🪙', n:'A COIN', d:'face down. it is heads if you wanted heads.' }, { e:'✨', n:'A GLOWING PEBBLE', d:'it hums for about an hour.' }, { e:'🐸', n:'A FROG', d:'one croak. then it leaves.' }, { e:'🪶', n:"A HERON'S FEATHER", d:'it weighs nothing. you carry it anyway.' }, { e:'🪞', n:'YOUR REFLECTION', d:'unexpected. closer than you think.' }, { e:'🔑', n:'A SMALL KEY', d:"you don't know what to. yet." }, { e:'🎵', n:'A PIECE OF MUSIC', d:'in the original key.' }, { e:'🪨', n:'A ROUND STONE', d:'ten thousand years of being held.' }, { e:'🍶', n:'A BOTTLE CAP', d:'someone else was here before you.' }, { e:'💨', n:'NOTHING', d:'the swamp kept it. that is allowed.' } ];
| Where | Default | Effect |
|---|---|---|
JS MAX_CHARGE | 1200ms | How long to hold for max cast distance. |
| JS bite delay | 1.8–5.3s | How long you wait before a bite. Lower = less patient game. |
JS BITE_MS | 560ms | Strike window. Shorter = harder. Longer = forgiving. |
JS CATCHES | 14 items | Your loot table. Add your own and they show up automatically. |