Speed, a chase and rivals
A racer that feels fast, a predator that is always closing in, rivals to beat, a season to win and a friend to race, scores to keep, loading like a page and running on a phone.
An underwater racing game that runs in a web page. Pick one of fifteen fish and one of eight tracks, then outswim the Mega Shark, race three rivals you pick, fight for an eight-round Championship or take on a friend in split screen, through a sunlit canyon or a neon tunnel. The ocean, the coral and the sound of the surf are all made in code.
01 — The brief
I wanted a racing game that feels like a holiday brochure for the deep sea: choose a fish, choose a route, and swim. No engine and no asset pipeline. The canyon is a tube bent along an equation or a traced circuit, the coral is instanced geometry, the light is a shader, and the surf and the seagulls are synthesised by the browser as you race.
A racer that feels fast, a predator that is always closing in, rivals to beat, a season to win and a friend to race, scores to keep, loading like a page and running on a phone.
Each track is one closed curve, from an equation or traced from a map. Caustics, fog and both looks are hand-written GLSL, and Web Audio synthesises the surf, the gulls and every sound effect.
Run & Escape, Grand Race, a two-player split screen and an eight-round Championship, with fifteen fish on eight tracks, in a realistic Classic ocean or a neon Cyber tunnel, and a log and a season for every player, from 2,200 lines of JavaScript.
02 — How it plays
Four ways to play on the same eight tracks, with the arrow keys, W A S D, touch buttons or a drag: swim in four directions to escape the shark, or steer left and right in a race. A six-step start screen sets up the mode, the players, the fish, the track (a Championship calendar during a season), the rivals and the difficulty.
Classic
Solo · High score
Collect small fish, dodge obstacles and stay ahead of the Mega Shark across four levels, a track each. Every hit costs speed and the gap closes; if the shark catches you, the run is over. Only track I is open at first: reaching a track in a run unlocks it as a start.
4 levels · 5 tools · combo up to x5 · tracks unlock as you reach them
Cyber
Solo · Versus
Three laps against three AI rivals you pick, or a fresh random field. Every racer holds the canyon’s middle depth, so it is all about your line: hold the gas, slow for the sharp bends and fire nitro on the straights. The rivals follow the same physics and grab the same nitro cells.
3 laps · 3 rivals you pick · gas, slow & nitro
Classic
Split screen · Versus
Race a friend side by side on one keyboard: Player 1 on the left with W A S D, Player 2 on the right with the arrows, each with a nitro tank and a name over their fish. Go head to head, or bring one or two AI rivals.
2 players · 0 to 2 rivals · one keyboard
Cyber
Solo · Season
A season of eight Grand Race rounds, one per track in order, against the same three rivals at the same difficulty. Only a win unlocks the next track; lose and you race that round again. Points build into standings, the rivals sharpen every round, and the season is saved under your name, so you can pick it up after closing the browser.
8 rounds · 25 · 18 · 15 · 12 pts · win to unlock
Run & Escape · Try it: earn points
Each has its own key and a cooldown: Turbo bursts ahead, Magnet pulls fish in, 2X doubles your points, Shield smashes whatever you touch and Laser clears the next 140 m. A race has none of them.
score 0
Grand Race · Try it: hold the gas
Off the gas, the water slows you to a cruise; hold Slow to flare the fins and drop hard before a bend. Nitro takes you past top speed while the tank lasts, and nitro cells on the track refill it.
Try it: pick a difficulty
The intended challenge: obstacles build up over the laps, and a new kind joins at every level.
joins at this level. Obstacles stand in slots a set gap apart; the threat, the share of slots in use, climbs to the cap by the end of lap 2. None appears within 120 m ahead of you, and each track keeps the same layout every retry.
03 — The shoal
Every fish has four stats and one ability. Each ability is passive or automatic, so there are no extra keys to learn, and the AI rivals use theirs too. Pick a fish; you can recolour yours in the game.
01 / 15
Needle-bill sprinter
Istiophorus platypterus
The fastest swimmer in the ocean. Blazing top speed, wide turning arcs. Best on smooth tracks.
Special ability
After 1.2 s on a straight it raises its sail for +6% top speed, until the next bend.
04 — The tracks
Four tracks are one line of maths each; four are traced from track maps. Either way, the game wraps a 14-metre tube around a closed curve. Pick one: the school of fish behind this page swims it.
// I · Coral Circuit:
// a circle that breathes three times a lap
const r = 300 + 55 * Math.sin(t * 3);
return [
Math.cos(t) * r,
40 * Math.sin(t * 2) + 25 * Math.cos(t * 3),
Math.sin(t) * r,
];
// II · Twin Trench: a lemniscate,
// one lobe 70 m above the other
const k = 1 + Math.sin(t) ** 2;
return [
380 * Math.cos(t) / k,
70 * Math.sin(t),
440 * Math.sin(t) * Math.cos(t) / k,
];
// III · Abyss Dive: twice a lap
// it plunges 140 m and climbs back
const r = 290 + 50 * Math.sin(t * 2);
return [
Math.cos(t) * r,
140 * Math.sin(t * 2),
Math.sin(t) * r,
];
// IV · Rapid Slalom:
// five bends in and out every lap
const r = 270 + 80 * Math.sin(t * 5);
return [
Math.cos(t) * r,
45 * Math.sin(t * 3),
Math.sin(t) * r,
];
// V · Manta Ridge: traced from a map,
// 22 points of [x, z, depth], 6 m a pixel
cp: circuit(6, [
[150, 38, 0], [162, 30, 10], [160, 20, 22],
[148, 17, 26], [125, 20, 22], [105, 24, 16],
[92, 30, 10], [86, 40, 4], [72, 52, -6],
// … 13 more, down to 34 m below the start
]),
// VI · Shipwreck Bay: traced from a map,
// 25 points, a corkscrew from +32 m to −20 m
cp: circuit(5.6, [
[96, 136, -18], [70, 140, -20], [40, 140, -20],
[22, 136, -18], [16, 122, -12], [18, 100, -6],
[24, 84, 0], [34, 72, 8], [30, 58, 18],
// … 16 more points
]),
// VII · Volcanic Vents: a traced figure-8;
// the low pass dives 80 m under the high one
cp: circuit(5.6, [
[75, 60, -40], [95, 72, -34], [118, 84, -22],
[138, 82, -8], [146, 66, 0], [140, 46, 8],
[128, 30, 16], [110, 22, 24], [92, 26, 32],
// … 11 more points
]),
// VIII · Pearl Strait: traced from a map,
// a long strait with a hairpin at each end
cp: circuit(7.5, [
[30, 26, 0], [60, 20, 4], [90, 14, 8],
[118, 10, 10], [134, 14, 12], [138, 26, 10],
[128, 36, 4], [118, 45, -2], [118, 57, -8],
// … 10 more points
]),
05 — Classic | Cyber
Same tracks, same physics, same fish. The materials, the shaders, the hazards’ costumes and the lens on top change, and you can switch at the top of the start screen. Even the browser tab’s icon follows.
Classic
Cyber
Try it here
| Part | Classic | Cyber |
|---|---|---|
| Water | Sunlit canyon | Grid tunnel |
| Fish | Shaded anatomy | Glowing particles |
| Hazards | Pufferfish, eels | Data blocks, lances |
| Nitro | Golden pearls | Cyan capsules |
| Race line | Canyon floor | Neon road |
| Lens | Soft bloom | Chromatic, scanlines |
06 — Under the hood
2,200 lines of JavaScript in thirteen plain scripts on top of three.js. No game engine, no build step for the code, and no sound or texture files.
// a track: a traced circuit's own points, or 24 samples of its equation
const pts = TK.cp ? TK.cp.map((p) => new V3(...p))
: Array.from({ length: 24 }, (_, i) => new V3(...TK.f(i / 24 * TAU)));
curve = new THREE.CatmullRomCurve3(pts, true);
// the canyon: a 14 m tube along it, 900 rings deep
const tube = new THREE.TubeGeometry(curve, 900, 14, 24, true);
// one wave of surf, from looped noise: it builds louder for 2–4 s,
// crests, then washes out over 3–5.5 s (its filter opens and closes
// with it, and foam fizzes in at the crest)
const up = 2 + Math.random() * 1.8, top = t + up + 0.2 + Math.random() * 0.5,
end = top + 3 + Math.random() * 2.5, pk = 0.5 + Math.random() * 0.5;
g.setValueAtTime(0.1, t); g.linearRampToValueAtTime(pk, t + up);
g.setValueAtTime(pk, top); g.linearRampToValueAtTime(0.1, end);
// Grand Race: the gas pulls hard at low speed and fades near the top,
// like climbing through the gears
const throttle = (ac, v, top) => ac * 0.45 * Math.max(0.2, 1 - 0.55 * v / top);
// off the gas the water drags you back towards a cruise; Slow flares
// the fins: 14 m/s² more, down to a crawl at 20% of top speed
const DRAG = (v) => 6 + v * 0.09, SLOW = 14, CRAWL = 0.2;
// nitro: how long a full tank lasts, how far past top speed it takes you
const nitroT = (sp) => 2.6 + 0.2 * sp[3], nitroMul = (sp) => 1.45 + 0.04 * sp[3];
// Slipstream (Spanish Mackerel), in a race: 3 to 25 m behind
// another racer and within 4 m of its line, +8% top speed
if (r.ab == 'draft') r.drf = FM && RAC().some((o) => {
if (o === r) return false;
const du = wrapS(o.p - r.p) * LEN;
return du > 3 && du < 25 && Math.hypot(o.x - r.x, o.y - r.y) < 4;
});
The HUD, mid-run in Cyber
Lap, time and threat, the shark’s distance, a live minimap of the route and the five tools, each with its key and cooldown. The gauge’s last line is the fish’s ability.
Position and lap, a name plate over every racer, a turn sign on Easy that says EASE OFF! when you are too fast for the bend ahead, and a speed gauge with the nitro tank in ten cells.
Moving cellular caustics light the rock, coral and fish, with exponential fog below and a rippling Snell’s window overhead.
The dolphin is a Draco-compressed .glb stored as base64 and decoded at start. Players can load their own .glb in its place.
Kelp, branching and brain coral, sponges and sea grass are instanced, and they sway in the vertex shader, not in JavaScript.
In split screen both cameras draw into one render target side by side, then share a single post-processing pass that centres its lens effects on each half.
You pick the AI rivals, or the game draws a new field each race; in a Championship the same three return every round, a little sharper each time. They use the players’ physics and abilities, ease off in time for every bend, steer round what is ahead and fire nitro only when the way is clear.
Obstacles stand a set gap apart and never on a trail of small fish, nothing comes in a lap’s first 220 m, nothing pops in within 120 m ahead, and a retry gives the same layout.
The ocean behind the game is filtered noise: two waves roll in and wash out on their own timers over a far-off swell, and now and then a gull calls, from a rasping oscillator. No music, no files.
Type a name and the game keeps that player’s Captain’s log in cookies: best score, race wins, best time on each track and the last 12 results, with a hall of fame across the device. Their Championship season and unlocked tracks are saved the same way.
Show it off
When a race ends, the top three line up on a podium past the line: the winner barrel-rolls in the middle under confetti and a banner with their name and time. The results then give every racer a time, projected from their pace if they were still swimming, and the gap to the winner.
At the end of a run or race the game paints a 1200×630 card on a canvas, in the look you played in, with your name, your fish’s own portrait and the track’s map. Switch this page’s look to see the other one.
Type your name, pick a fish and a track, then see how long you can stay ahead of the Mega Shark, beat three rivals, win an eight-round season, or race a friend on the same keyboard. Best with a keyboard; it plays on phones with touch controls too.
07 — The maker
I made Ocean Run on my own, from the first idea to the last line of code. Each contact on this sonar is one of the hats I wore: let the sweep find them, or pick one.
Pick a contact, or use the arrow keys
Contact 01 / 05BRG 038°RNG 0.74
I decided what Ocean Run is, and what it isn’t: a racer that loads like a web page and plays on a phone, four ways to play on eight tracks, a season that unlocks one track per win, fifteen fish that each do one thing best, two looks, a log for every player, and no engine or asset files to download first.
Contact 02 / 05BRG 112°RNG 0.56
I designed what you see: a six-step start screen (mode, players, fish, track, rivals, briefing) with a live preview, a Championship calendar with trophies and locked rounds, a HUD for each mode and a split one for two players, name plates over the racers, the winner’s podium, the result card you share, and both looks, sunlit Classic and neon Cyber.
Contact 03 / 05BRG 200°RNG 0.66
I shaped how it feels to play: keys, touch buttons or a drag, a race steered left and right with gas, slow and nitro, a turn sign on Easy, three difficulties whose hazards build up over the laps, a season where only a win moves you on, and abilities that need no extra keys.
Contact 04 / 05BRG 246°RNG 0.60
I built the screens around the race, the start screen with its Captain’s log, the HUDs, the help pages and the touch controls, in HTML and CSS with Bootstrap 5.3, my own Alfuix shapes and Sass, laid out to work on a phone as well as a desktop.
Contact 05 / 05BRG 312°RNG 0.80
I wrote the game in plain JavaScript on three.js r128, with no engine: 2,200 lines in thirteen scripts, eight tracks from equations and traced points, AI rivals that ease off for bends, fifteen abilities, a saved Championship season, an ocean synthesised from noise, and two players in one render pass.
Personal project, not a product. All images on this page are captures from the game. The dolphin is used under CC BY 4.0.