A rebuild of gestomeme.com — the Windows 98 desktop where you teach a webcam
your own hand gestures, mint them into reaction cards and duel a friend. The premise is kept and the
thing that was broken is gone: no model, no WASM and no font is fetched from a CDN. Every number
below was measured off the shipped index.html and its 21 MB of assets, not planned.
gestomeme.com loads its MediaPipe bundles and .task models from
storage.googleapis.com. That host is blocked in mainland China — which is exactly
where the original's Simplified Chinese interface was written for. On that network the page does
not fail loudly; it sits on 「正在加載模型…」 forever. The fix is not a mirror: it is shipping the
bytes with the app.
There is no built-in gesture library. You pick an emoji, hold a pose for 1.5 s and the app keeps only the frames where the detector agrees on the hand count — a frame with a second hand wandering in is dropped rather than averaged into the template.
The template is the per-dimension median of those frames, with the median absolute deviation beside it. One blink of a bad landmark cannot drag a median the way it drags a mean.
thr = clamp(0.075 + 3.6 × MAD, 0.09, 0.30). A still recording earns a tight
threshold; a shaky one earns a forgiving one. This is the fix for the single global slider that
made every gesture either trigger-happy or deaf.
L1 distance over 42 hand dimensions, also scored against the mirrored template, so a left-handed and right-handed attempt at the same pose are the same gesture. Faces are matched on 52 blendshape scores — expression, not geometry.
The best match has to win four consecutive frames, then a 1.6 s cooldown holds. Without that handshake a single stray frame mints a reaction card, and the gallery fills with garbage.
first paint
└─ GET index.html 140,498 B (inline CSS + JS, no framework)
└─ GET ./vendor/vision_bundle.mjs 136,870 B MediaPipe tasks-vision
└─ GET ./wasm/vision_wasm_internal.wasm 9.4 MB SIMD runtime
└─ GET ./models/hand_landmarker.task 7.8 MB 21 hand landmarks
└─ GET ./models/face_landmarker.task 3.8 MB 478 landmarks + 52 blendshapes
│
└── all five from THIS origin. Same host as the page.
No storage.googleapis.com, no jsDelivr, no fonts.
jawOpen, browDown, mouthSmile…Two browsers need one SDP blob each way. There is no signalling server holding it: the offer is
JSON → CompressionStream('gzip') → base64, which is short enough to paste into a chat
window, and the answer comes back the same way. After that the connection is peer-to-peer and the
site is not in the path. It is a deliberate trade — a manual code exchange instead of an account and
a signalling box — and it keeps the whole app deployable as static files.
| Symptom | Cause | Fix |
|---|---|---|
| Skeleton floated off the hand, labels read backwards | The video and the overlay canvas were both CSS-mirrored with
scaleX(-1). Mirrored pixels are fine; mirrored text is not. |
Mirror only the video, and flip the x axis inside the canvas draw — labels stay legible. |
| Every window opened at 0,0 in the top-left corner | clampIntoStage() ran over closed windows on resize, writing
left:0px into their inline style. show() then read a truthy
"0px" and skipped parking entirely. |
Bail out unless data-open="1" — a hidden window keeps its saved slot. |
| Preview dropped to 2 fps once a hand appeared | Face landmarker + blendshapes on every frame, on a software rasteriser. | Face detection throttled to ~11 Hz with the previous result reused in between. |
| Camera window cut its own empty state in half | A fixed 520 px window against a 4:3 video plus a 110 px note block. | Height derived from the real stack (570 px) instead of guessed once. |
| Duel codes forced a horizontal scrollbar | Base64 in a textarea with default width set the fieldset's min-content width. |
width:100% + box-sizing:border-box + word-break:break-all. |
Headless Chrome, a synthetic camera fed from a still image of a hand
(--use-file-for-fake-video-capture), and the app's own test hooks:
RTCPeerConnection datachannel loopback, minesweeper placement/cascade/win, the window
manager and card rendering| gestomeme.com | GESTO 2000 | |
|---|---|---|
| Models | fetched from storage.googleapis.com | served from this origin, first paint included |
| Training | one pass, one global threshold | median template + per-gesture tolerance from your own jitter |
| Hand features | raw landmark distance | 42 dims, wrist-origin + palm-normalised, mirror-invariant |
| Face features | landmarks | 52 blendshapes |
| Duel | needs a signalling path | one gzip+base64 SDP code each way, no server |
| Language | 简体 only | 繁體 / 简体 / EN, every string a triple |
| Layout | fixed pixels | window manager (drag, resize, minimise, cascade) + real mobile tabs |
localStorage and cards in IndexedDB, so they are per browser
and per device — clearing site data deletes them. The duel requires both people to paste a code
each way; there is no contact list and no discovery. And it is a rebuild of an idea, not a fork:
none of the original's code or art is reused.