pickuma.
Infrastructure

I Shipped Two Web Games This Weekend — Here's the Stack

Stop at 7.77 and Eagle Run are live at play.pickuma.com. A 250-line vanilla canvas game and a one-button time-sense test, both shipped in a day. The stack, the tradeoffs, the things that worked.

5 min read

Two new games live at play.pickuma.com: a single-button time-sense test called Stop at 7.77, and a third-person 3D-ish flight survival called Eagle Run. Both were built in a weekend on the same stack pickuma.com runs on. This is a quick writeup of why, what’s in the box, and what surprised me.

The stack

Astro 6 (static output), Cloudflare Workers + Static Assets, Tailwind v4, vanilla JS for the game logic. No game engine. No state management library. No framework. Each game is one Astro page plus a single .js file in public/.

pickuma-play/
├── src/pages/
│ ├── index.astro # game hub
│ ├── seven.astro # Stop at 7.77 (45 lines)
│ └── eagle.astro # Eagle Run (50 lines)
└── public/
├── seven.js # 7.77 logic (~180 lines)
└── eagle.js # Eagle Run logic (~290 lines)

The hosting story is the same as pickuma.com: build to static, deploy to a Cloudflare Worker bound to a subdomain. play.pickuma.com is one wrangler deploy away from any change. SSL is free and automatic.

The thing I underestimated: the Cloudflare Custom Domain API for Workers. One PUT request and the subdomain was live with HTTPS in under a minute. No DNS records to fiddle with, no certificate dance.

Stop at 7.77

The whole game is: press start, then press stop when you think exactly 7.77 seconds have passed. Hard mode (default) hides the timer entirely — five bouncing dots tell you the game is running, but never how long you’ve been running. Easy mode shows a live counter.

The animation took longer than the game logic. The dots needed to feel alive without giving you a timing cue. A wave animation at 1.0 seconds per cycle would let you count it: tap on the eighth wave. So the dots bounce on an 830ms period with a 130ms stagger — irregular enough that you can’t count them, smooth enough that they feel intentional.

Timing precision uses performance.now() directly. Sub-millisecond on modern browsers. The score is |elapsed - 7770| in milliseconds. World-record territory is under 30ms.

Eagle Run

Third-person view. You’re an eagle in the middle of the screen, sky around you, ground grid sliding past below, obstacles flying toward the camera in 3D. Mouse steers, click adds permanent +0.15× speed. Survive as long as you can.

The rendering is pinhole projection on a 2D canvas. Each obstacle has world coordinates (x, y, z). To draw it, project:

const screenX = canvas.width / 2 + (worldX - eagleX) * (FOCAL / z);
const screenY = horizonY - (worldY - eagleY) * (FOCAL / z);
const drawSize = baseSize * (FOCAL / z);

That’s the whole 3D engine. As z shrinks (obstacle approaches camera), the projected size grows; the same constant divisor produces both perspective and parallax. No matrix math, no shaders.

Four obstacle types: cubes (random Y), spheres (random Y), spikes (rise from the ground), rings (you can fly through the center; only the rim hurts you). Each spins on its own axis. Painter’s algorithm sorts them back-to-front per frame.

The bald-eagle silhouette is bezier-curved paths with a 4.5Hz wing-flap oscillation. It looks more deliberate than the geometric placeholder it replaced.

What surprised me

Canvas 2D is fast enough for this. I was prepared to reach for WebGL or PixiJS. Didn’t need to. A few dozen obstacles per frame with painter’s-algorithm sort and gradient fills hold 60fps on a five-year-old MacBook. Pre-rendering star sprites to an offscreen canvas (instead of drawing a radial gradient per star per frame) was the only optimization that mattered.

performance.now() makes click-to-stop games feasible. Older browsers throttled it to 1ms or 100µs precision for security reasons. Current Chrome and Safari give you sub-millisecond on the main thread. That’s the entire premise of Stop at 7.77.

SVG → PNG at build time is the right OG image strategy when you’re static-hosted. No dynamic OG, no edge function gymnastics. @resvg/resvg-js renders a 1200×630 in a few hundred milliseconds. Wire it into the build script, ship it as a static asset, done.

What’s missing

No global leaderboards yet — both games use localStorage for personal bests. Adding Supabase-backed leaderboards is a v1.1 task. No sound. No mobile-specific tuning beyond touch-as-mouse. No Poki/CrazyGames SDK integration; that’s a separate build target for whenever the games are accepted.

Try them

  • Stop at 7.77 — press space (or tap) to start, again to stop. World record: under 0.030s off.
  • Eagle Run — move mouse to steer, click to accelerate. How long can you survive?

If you build something interesting on the same stack, send it. I’m always looking for what people are shipping on the small end of the indie scale.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.