DEV Community

Cover image for I built a one-button game in vanilla JS Canvas — single file, no engine, plays in your browser
EmaadS
EmaadS

Posted on

I built a one-button game in vanilla JS Canvas — single file, no engine, plays in your browser

▶️ Play it first (10 seconds): https://emaadshamsi.github.io/paper-hands/

It's called PAPER HANDS. One button. The line goes up while you hold — your
multiplier climbs, and so do the odds it all rugs. Let go to bank it. Hold too
long and you lose the whole run. Pure greed, distilled.

No engine, no build step, no dependencies — one index.html, ~250 lines of Canvas.
Here's how it works.

The whole game is one loop: greed vs. risk

The mechanic is a single tension: every moment you don't sell, you earn more — and
get closer to losing everything.

if (held) {
  const rate = 1.1 + mult * 0.16;   // climbs faster the higher it goes
  mult += rate * dt;
  // near-safe early, risk ramps steeply as you get greedy:
  const pct = (0.0028 + Math.pow(Math.max(mult - 1, 0), 1.6) * 0.0015) * (dt * 60);
  if (t > 0.6 && Math.random() < pct) gameOver();   // 0.6s grace so you never insta-rug
}
Enter fullscreen mode Exit fullscreen mode

That Math.pow(mult-1, 1.6) curve is the entire feel of the game. My first version
used a flat crash chance and players rugged in the first second
— brutal, not fun.
Swapping to a curve that's almost-safe at low multipliers and punishing only when you
get greedy (plus a 0.6s grace per pump) turned it from frustrating into "one more run."
Balance is a one-line change you only find by playing.

Juice with zero assets

No sprites, no audio files. Everything is procedural:

  • Sound = WebAudio oscillators — a rising blip while you pump, a noise burst + a detuned saw on the rug.
  • Feel = screen shake (ctx.translate(rand, rand) scaled by a decaying shake), particle bursts on bank/crash, a glowing price marker, CRT scanlines via a CSS repeating-linear-gradient overlay.
function tone(freq, dur, type='square', vol=.16){
  const o=ac().createOscillator(), g=ac().createGain();
  o.type=type; o.frequency.value=freq; g.gain.value=vol;
  o.connect(g); g.connect(ac().destination);
  const t=ac().currentTime;
  g.gain.exponentialRampToValueAtTime(.0001, t+dur);
  o.start(t); o.stop(t+dur);
}
Enter fullscreen mode Exit fullscreen mode

A little juice on a trivial mechanic does more for "fun" than a complex mechanic with
none.

The viral hook is one URL param

On game over you can copy a brag link — ?s=<score> — and whoever opens it sees
"a friend banked $4,200 — beat them" on the menu. No backend, no accounts:

const beatTarget = +(new URLSearchParams(location.search).get('s') || 0);
Enter fullscreen mode Exit fullscreen mode

Why single-file?

It deploys anywhere static — I dropped it on GitHub Pages and it was live in a minute.
Whole thing (HTML + CSS + JS) is one file you can read top to bottom.

Play: https://emaadshamsi.github.io/paper-hands/
Code: https://github.com/emaadshamsi/paper-hands

Curious what scores people get — drop yours in the comments. 📈

Top comments (1)

Collapse
 
zacharydurland profile image
Zach

Around 2k, hehe. Very fun!