
A typing test that happens inside a 3D scene.
What it is
I use monkeytype every day, so Typefall is the same thing — a real typing test — rendered in three.js instead of on a flat page. The default view is star wars, and this round turned it into the game. It's a survival run. The reading surface tilts back like a Star Wars opening title and, instead of random words, it types short Star-Wars-flavored lines — a handful of iconic catchphrases and a set of crawl lines I wrote myself, about rebellions and star systems and fallen empires, shuffled fresh each run. It sits frozen with a quiet "type to begin" until your first keystroke, then the lines start climbing toward a horizon, shrinking and fading with distance. You type each line as it travels up. The active word carries a thin caret with a slow molten-gold sheen that rides it up-plane every frame, lerping and blinking like monkeytype's. There's no miss-and-continue anymore: the moment one word reaches the top zone, the run is over. The stars behind stretch into a hyperspace jump — the whole field of dots draws out into radial light streaks and rushes past, the crawl streaks away with them, one faint gold flash — and it settles to a restrained lose card: how many words you survived, your WPM, your best at that speed. Then you're one key from going again, and the crawl is frozen and waiting. The retry has no friction on purpose; it should feel like pulling a slot machine.
A speed setting sets the climb rate — 1x, 1.4x, 1.9x, 2.5x, or auto, which rubber-bands the climb to your rolling WPM so the line always sits just behind you, chasing your true speed. Each speed keeps its own best, scored by words survived rather than WPM — the game changed, so those bests start fresh. The backdrop is a DOM/SVG starfield now, sitting behind a transparent WebGL canvas: a hundred-odd tiny dotted stars in two parallax layers, cold-white with a few faint gold, on a slow staggered twinkle — no lens flares, nothing loud. It's what stretches into the hyperspace streaks when you lose.
Underneath the crawl is a measured-flow layout engine, and it also drives a paragraph view you can switch to — monkeytype's calm readable wall of rows. The reading surface there is flat, crisp SDF text — a real monospace typeface (Space Mono) rendered with troika-three-text — so it reads like a proper typing tool rather than a row of chunky objects. Each word is measured by the font's real glyph advances and placed one normal space after the last, wrapping into a centered, constrained column exactly like prose; monospace means every row lines up. Three lines stay visible; finish the active line and the block scrolls up one row — the finished line fading out above, a fresh one fading in below. The column is derived from the camera frustum, so it keeps its size and stays centered at 1024, 1440 and 1920 and recomputes on resize without losing your place. There's also a stream view, where the words fly in from the background as chunky extruded 3D letters: the current word big and close, the next few receding into the dark; it always auto-advances, so it never needs a space, which suits a phone. Paragraph and stream carry their own faint backdrops too — a dust-and- vignette layer for paragraph, a depth haze for stream — pitched well below the text so they never compete. Click away in any view and the passage dims with a small "click to focus" line, like the real thing.
The typing is the same everywhere, and this round changed how it feels. Every correct keystroke now sends that letter out of the scene: a chunky extruded 3D copy peels off and fires the completion effect you picked — it drops and piles on the floor, bursts into particles, dissolves into ash, spirals off, rockets up, or shatters into shards — while the flat surface glyph fades out of its slot. The slot stays reserved so the passage never jumps and the caret keeps its place, but the line empties behind you as you type, which is oddly satisfying. Miss a letter and it stays red until you fix it. A word advances on space by default — even a perfect one — with any letters you skipped past marked as errors, exactly like monkeytype; there's an advance setting if you'd rather it jump the instant a word is all correct. Backspace walks back through the current word restoring each letter (they fade back in as untyped gray), and steps into the previous word too when you left it imperfect. Accuracy counts every keystroke — a corrected mistake, and every letter a star-wars word left untyped, still costs you.
The accent is a warm gold that matches the rest of my site — the caret, the live WPM, the selected settings, the results, and the new-best moment, where the score gets a single liquid-gold shimmer before it settles. Errors stay red; the scene stays dark. There's a live WPM and accuracy readout, one quiet mono line that brightens while a run is going. Paragraph and stream carry the classic modes — timed 15/30/60s, a fixed 10/25/50 words, endless zen — plus two I added this round: rush, which starts you at 15 seconds and tops the clock up 1.2s per word to a 30s cap, so the clock is the whole game; and sudden death, where a single wrong key ends it with a small red flinch. Star wars ignores the mode axis entirely — it's always survival. Every config keeps its own best in localStorage, scored by the metric that setup actually chases: WPM for the timed and fixed-word runs, words survived for survival, rush and sudden death. Beat it and the screen says so, with your last few runs underneath. The restart is instant everywhere — Tab then Enter, or on a lose screen just Enter, space, or a tap.
How it's built
three.js for rendering, troika-three-text for the flat reading surface, cannon-es for physics, plain TypeScript, no framework. The whole thing is built to hold 60fps on a laptop, which drove most of the decisions:
-
The right tool for each surface. The paragraph you read is SDF text (troika-three-text), one glyph node per character: crisp at any size, real font metrics, and cheap — three full lines plus a dozen live clones measure around 2ms a frame. Color changes are just a material uniform, so a keystroke only recolors a glyph and nudges the caret — never a geometry rebuild. The chunky 3D letters — the stream view and every clone that detaches and falls — use extruded
TextGeometry, generated once per character and cached, so those bevelled glyphs are shared by every instance and typing the same letter again costs nothing. -
Effects are pools, not allocations. All the particle effects (explode, disintegrate, launch trails, vortex) draw from a single
THREE.Pointscloud that's stepped on the CPU and drawn in one call. Shatter shards are a small recycled set of boxes integrated by hand. Nothing gets allocated on a keystroke during a fast run. -
Physics only where it earns its keep. cannon-es runs solely for the fall effect and the letters that rain behind the results screen, where real stacking is the whole point. Live bodies are capped and the oldest fade out, so a long test never drifts below frame rate. Shatter fakes its physics because it doesn't need contacts — it just needs to look right for a second.
-
The crawl is just a tilted plane. The lines live on a plane tilted back about the X axis, and I let the perspective camera do the shrinking toward the horizon for free — per frame the only work is translating a handful of line groups up-plane and fading their opacity with distance, transforms and colors, nothing rebuilt. The run ends the moment the active line reaches the top zone. The auto speed is a small proportional controller: it aims the climb rate at your rolling-WPM pace, kept a touch behind, and eases toward that aim with a time constant so it never jerks. A handful of lines plus ten falling clones measure under 2ms a frame.
-
The backdrop is DOM, not WebGL. The starfield and the paragraph/stream ambience are plain DOM/SVG layers behind a now-transparent canvas, animated in CSS. That means they keep twinkling and drifting even while the render loop is paused in a hidden tab, and the hyperspace loss is mostly CSS too: each star becomes an SVG line that draws outward from the center with
stroke-dashoffsetwhile the two parallax groups scale past the viewer, and the scene itself blurs and fades away with a canvas filter — the in-scene part (the tilted plane swelling toward the camera) is the only frame-clock piece. It costs the render loop nothing. -
A results panel that comes through the canvas, where the browser allows it. Chrome's experimental html-in-canvas API can rasterize a live DOM element with
drawElement; where it exists, the results overlay is painted into aCanvasTextureon a floating 3D panel in the scene, while the real (invisible) DOM overlay keeps the clicks and focus — pixels through the canvas, input through the DOM. It's a progressive enhancement: the detection is guarded and any failure falls back silently to the ordinary DOM overlay, which is what you get in every browser today. A small footer line says which path ran.
Pixel ratio is capped at 2 and the loop pauses when the tab is hidden. There's a
window.typefall handle that can drive a run deterministically — including the
crawl's line progress, current speed, the run kind and scoring metric, and the
rush clock — which is how I verify it.
Try it
- Live demo: aitorgallardo.github.io/typefall
- Source: github.com/AitorGallardo/typefall