Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
A calm, colourful slime playground for children. Stretch, poke, squish and bounce soft-body slime, collect stars and make your own slimes.
GamesTypeScript · JavaScriptGitHub @fawadaslam1OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
A calm, colourful sensory slime playground for children aged 3–10.
There is one thing to do and it feels good every time: pull a wobbling blob of slime
around with your fingers. Stretch it into a long arm and let it snap back. Tap it and
watch ripples run across its skin. Squeeze it flat with two fingers. Every touch earns
stars, and stars unlock new slimes, new places to play, new sparkles and new sounds —
or you can mix your own slime from scratch in the Slime Factory.Read the full README ↗A calm, colourful sensory slime playground for children aged 3–10.
There is one thing to do and it feels good every time: pull a wobbling blob of slime around with your fingers. Stretch it into a long arm and let it snap back. Tap it and watch ripples run across its skin. Squeeze it flat with two fingers. Every touch earns stars, and stars unlock new slimes, new places to play, new sparkles and new sounds — or you can mix your own slime from scratch in the Slime Factory.
No enemies. No timers. No score to beat. No way to lose. Nothing in the game can be failed, missed or taken away, so a child can put it down mid-stretch and come back to exactly the same slime.
Built with Next.js (App Router) · TypeScript · Tailwind CSS · Matter.js · Framer Motion · Howler.js, and installable as an offline PWA.
The slime is a real soft body, not an animated sprite. It is 26 physical points ringed around a heavy core, held together by three families of springs and simulated by Matter.js every frame. Pull one edge and the mass genuinely lags behind, so the skin stretches into an arm. Let go and the springs — not a tween — snap it back. Two children pulling in opposite directions get a shape neither of them could have drawn.
It ships no audio files. Every squish, pop and bounce is synthesised into a WAV buffer in the browser at runtime. That keeps the download tiny and means the sound still works with no network at all.
It repairs itself. A ring of springs is perfectly stable in a knot, so a hard enough whip could once turn the blob inside out permanently. The game now watches its own geometry and quietly inflates it back into shape — a child never has to find a menu.
npm install
npm run dev
Then open http://localhost:3000.
| Script | What it does |
|---|---|
npm run dev | Development server |
npm run build | Generates the PWA icons, then builds for production |
npm start | Serves the production build |
npm run typecheck | tsc --noEmit |
npm run icons | Regenerates public/icons/*.png |
Requires Node 18+. There is no configuration, no environment file, no database and no network call — the whole game runs in the browser.
Everything happens with one finger, or two. The same gestures work with a mouse.
| Gesture | What happens |
|---|---|
| Drag the slime | Stretches it. The grabbed edge follows your finger while the body lags behind, pulling out a rounded arm. |
| Let go | It springs back with a bounce sound and a burst of particles, both scaled to how far you pulled. |
| Tap quickly | A poke. A radial impulse dents the surface and an expanding ripple ring runs outwards from your fingertip. |
| Pinch with two fingers | A squish. Every point is drawn towards the midpoint between your fingers, squeezing the blob flat. |
| Drag fast | Leaves a trail of the current theme's particles behind your finger. |
| Tap the background | A small sparkle and a soft tap sound, so nothing feels dead. |
The play screen shows a rotating hint ("Drag the slime to stretch it!", "Tap it quickly to poke a ripple", …) until the first touch, then gets out of the way.
Poke and drag are told apart by intent, not by event type: a press that travels under 14 px and lasts under 320 ms is a poke; anything longer or further is a stretch.
/The current slime floats in the middle, animated with CSS rather than physics so the menu stays cheap. Four big rounded buttons — Play, Slime Factory, Collection, Settings — spring in one after another. The star counter sits top right, and the daily present pops up here on the first visit of each day.
/playFull-screen canvas with the slime, and as little else as possible:
The pause menu holds Keep playing, Photo, Reshape (rebuilds the blob fresh if you have squashed it into something silly), quick toggles for sound, screen shake, vibration and night mode, and a way home. The game also pauses itself when the tab is hidden, so nothing runs in the background.
/factoryMake your own slime and play with it immediately. See The Slime Factory.
/collectionFive tabs — Slimes, Scenes, Sparkles, Sounds, Awards — with a progress bar showing how much of the game has been unlocked. Locked items stay recognisable rather than hidden: they are drained of colour and show their price, so there is always something visible to save up for. The Awards tab shows lifetime stats (stretches, pokes, squishes, play time) and every achievement with its progress bar.
/settingsSound on/off, volume, sound pack, night mode, screen shake, calm mode, vibration, the PWA install button when the browser offers one, and a two-step Reset progress.
24 unlockables in total, costing 1,365 stars to complete.
Each one has its own canvas painting routine and its own feel — softness scales the
spring stiffness, so bubblegum really is floppier than ice.
| Slime | Cost | Softness | Surface | Particles |
|---|---|---|---|---|
| 🌈 Rainbow | free | 1.00 | Six-hue gradient cycling around the blob | ✨ Sparkles |
| 🍬 Bubblegum | ⭐15 | 1.25 | Soft pink, the stretchiest of the set | 💖 Hearts |
| 🌌 Galaxy | ⭐30 | 0.95 | Nebula gradient with 34 twinkling inner stars | ⭐ Stars |
| 🪙 Gold | ⭐60 | 0.80 | Metallic bands with a shine that slides across | 🌟 Glitter |
| 🧊 Ice | ⭐90 | 0.70 | Translucent, with crystal facets | ❄️ Snowflakes |
| 🌋 Lava | ⭐130 | 0.90 | Pulsing glowing cracks and a heat haze | 🔥 Fire |
| 🍀 Forest | ⭐170 | 1.10 | Calm green jelly | 🫧 Bubbles |
Backgrounds are painted into the canvas as well as the page, so they show up in photos.
| Scene | Cost |
|---|---|
| 🍭 Candy Sky | free |
| 🌻 Sunny Meadow | ⭐20 |
| 🐳 Deep Ocean | ⭐45 |
| 🚀 Night Sky | ⭐75 |
| 🌅 Sunset Beach | ⭐110 |
Eight particle kinds, each with its own drawing routine — four-point sparkles, five-point stars, glinting glitter squares, snowflake crosses, additive fire glows, rimmed bubbles, béziered hearts and tumbling confetti.
| Pack | Cost | Pack | Cost | |
|---|---|---|---|---|
| ✨ Sparkles | free | 🌟 Glitter | ⭐55 | |
| 🫧 Bubbles | free | ❄️ Snowflakes | ⭐70 | |
| 💖 Hearts | ⭐25 | 🔥 Fire | ⭐95 | |
| ⭐ Stars | ⭐40 | 🎉 Confetti | ⭐120 |
A sound pack is just a pitch multiplier and a timbre, because every sound is generated rather than loaded.
| Pack | Cost | Character |
|---|---|---|
| 🫧 Classic Squish | free | The cosy default. Pitch ×1, soft timbre |
| 🎈 Bubble Pop | ⭐35 | Poppy and a little bit silly. Pitch ×1.25 |
| 🔔 Crystal Chimes | ⭐80 | Glassy bells for extra calm play. Pitch ×1.5 |
| 🥁 Deep Goo | ⭐100 | Low, slow and extra gloopy. Pitch ×0.7 |
Stars come from the slime's own wobble energy — the average speed of its skin, accumulated every frame. There is no scoring rule to understand: the more you play with it, the more you earn. 260 units of energy makes one star, and the counter's progress bar fills as you go.
Fourteen of them, worth 263 stars in total. They claim themselves the moment their goal is met and announce it with a toast — there is no menu to visit.
| Achievement | Goal | Reward |
|---|---|---|
| 🤏 First Pull | Stretch the slime for the very first time | +3 |
| 🖐️ Stretchy Fingers | Stretch 50 times | +10 |
| 💪 Super Stretcher | Stretch 250 times | +25 |
| 👉 Poke Master | Poke 25 times | +8 |
| 🌊 Ripple Maker | Poke 200 times | +20 |
| 🫳 Squish Squad | Squish with two fingers 20 times | +12 |
| ⭐ Star Collector | Earn 100 stars | +15 |
| 🌟 Star Explorer | Earn 500 stars | +40 |
| 🎨 Slime Collector | Unlock 3 slimes | +10 |
| 🏆 Slime Museum | Unlock every slime | +50 |
| 🧪 Slime Chef | Make your own slime | +10 |
| 🔬 Slime Inventor | Make 5 of your own | +25 |
| 📅 Slime Buddy | Play on 3 different days | +15 |
| ☁️ Calm Cloud | Play for 10 relaxing minutes | +20 |
A present waits on the home screen once per calendar day. The streak pays 5 → 8 → 12 → 16 → 20 → 25 → 40 stars, 126 over a full week, then 40 a day for as long as the streak holds. Missing a day resets the streak but never takes anything away.
A live preview of the slime you are building sits at the top and updates as you change anything.
Saving takes you straight to the play screen with your creation loaded. Saved slimes appear in the factory's My slimes grid and in the play screen's theme strip, and can be edited or deleted at any time.
src/lib/softbody.ts builds a ring of 26 small circular Matter.js bodies around a single
heavy nucleus, wired with three families of springs:
| Constraint | Stiffness | Job |
|---|---|---|
Neighbour (i → i+1) | 0.26 | Holds the skin together |
Skip (i → i+2) | 0.07 | Stops the ring folding in on itself |
Spoke (i → nucleus) | 0.022 | Pulls the blob back into a round shape |
The ring shares a negative collision group so its own parts never collide with each other, but they still collide with the walls that keep the slime on screen. The nucleus is roughly three times as dense as the whole ring — that inertia is exactly what makes a pulled edge stretch into an arm rather than dragging the entire blob along.
Blob radius scales with the viewport (28% of the smaller side, clamped to 60–190 px),
and the floor sits above the theme strip so the slime never hides behind the UI.
src/hooks/useSlimeEngine.ts owns the world, the pointer handling and the render loop.
React never re-renders during play — only the HUD does.
[0.04, 0.13, 0.4, 0.13, 0.04]). Grabbing
a single point pulls out an ugly needle; spreading the pull gives a rounded arm.Pointer capture is wrapped in a try/catch, so browsers that reject capture still drag
normally. Multi-touch is fully supported: each pointer keeps its own set of constraints.
Physics advances once per frame on a lightly smoothed delta, never exceeding Matter's recommended 16.67 ms step, with extra sub-steps on slower devices.
The obvious alternative — an accumulator with a hard 1/60 step — aliases badly against rAF's own ~16.7 ms cadence: the accumulator alternates between holding 0 sub-steps and 2, so the blob visibly stutters even at a rock solid 60 fps. Measured over 900 frames of a perfectly smooth circular drag, frame-to-frame step-size variation:
| Scheme | mean | p95 | worst |
|---|---|---|---|
| Accumulator, hard 1/60 step | 0.366 px | 2.378 px | 25.26 px |
| Smoothed delta (current) | 0.109 px | 0.140 px | 8.78 px |
Matter applies time correction to its Verlet integration, so the slightly variable step is stable: 20 simulated seconds of thrashing at 30, 60 and 120 Hz survives with no runaway velocities. At 30 Hz it takes 2–3 sub-steps to keep real time; at 120 Hz it simply runs the physics at 120 Hz.
The other half of smooth play is not touching React. Star energy is batched on a 250 ms clock rather than a size threshold — flushing per-frame re-rendered every context consumer, animated theme strip included, which was by itself enough to stutter the simulation.
A ring of springs is perfectly stable in a knot: if the skin is whipped hard enough to pass through itself, the blob turns inside out and never comes back. In a game with no fail state, that would leave a child stuck.
So every frame the loop measures the ring's shoelace area against its rest area
(getShapeHealth). Ordinary play never comes close — 20 seconds of simulated thrashing,
poking and squeezing bottoms out at 0.80, while a full inversion reads as -1.0.
When the value stays below 0.28 for 18 frames with no finger down, or goes negative
for 9 frames, the game lets go of any active grab and eases the points back onto a circle
over 360 ms (relaxSlime), finishing with a particle burst and a bounce so it reads as
part of the play rather than a repair. Stars are not credited during the recovery.
Squeezing the slime flat on purpose is left alone — the collapse rule only applies when no finger is down. A true inversion always triggers, even mid-drag.
src/lib/render.ts paints everything by hand on a single 2D canvas, sized to the device
pixel ratio (capped at 2):
The outline is a Catmull-Rom spline converted to cubic béziers, so 26 simulated points still read as a perfectly smooth blob.
Every slime has a face: it blinks about every four seconds, squints as the stretch value rises, and its smile widens the more the slime is played with. The eye colour comes from each theme's shadow tone so it stays readable on gold and ice as well as galaxy.
src/lib/particles.ts is a pooled system capped at 260 live particles, with per-kind
gravity — bubbles and fire drift upwards, confetti falls hard — and per-kind drawing.
Bursts fire on poke, release, squish, screenshots and the untangle recovery, and a light
trail follows fast-moving skin unless calm mode is on.
The game ships no binary sound assets. src/lib/synth.ts renders every sound into a
16-bit PCM mono WAV buffer at 22.05 kHz, base64-encodes it and hands it to Howler as a
data URI:
| Sound | Length | How it is made |
|---|---|---|
stretch | 1600 ms, looped | Filtered noise with a slow pulling wobble and a low creak |
squish | 420 ms | Noise burst under a downward filter sweep, plus a falling tone |
pop | 200 ms | Fast pitch-dropping tone with a noise click |
bounce | 340 ms | Rising tone with a springy secondary bump |
tap | 130 ms | Short soft blip |
star | 700 ms | Two-tone bell |
unlock | 1140 ms | Rising C–E–G–C arpeggio |
Sound packs become a pure data change — pitch and timbre — the bundle stays small, and
every sound still works offline. src/lib/audio.ts builds the Howls lazily on the first
pointer down, because browsers block audio before a user gesture, and continuously
modulates the looping stretch texture's volume and playback rate from the live stretch
amount, so pulling harder audibly strains the slime.
src/hooks/useGameState.tsx holds the entire game in one reducer behind a React context:
stars, unlocks, custom slimes, lifetime stats, settings, streaks. It exposes small
intent-shaped actions (unlock, selectTheme, saveCustomSlime, claimDailyReward)
rather than raw dispatch.
Everything is written to localStorage under slime-stretch:save, debounced by
250 ms. Loading merges the saved data over a fresh state, so a partial, older or corrupt
save degrades into defaults instead of throwing. Settings are read field by field rather
than spread, so a setting that gets removed does not linger in the save file forever —
and adding one to the Settings type fails the build until it is handled there too — a bad write can never lock a child out
of their game. Writes are wrapped in try/catch too, so private-mode browsers with
storage disabled still play perfectly; they just do not remember.
src/
├── app/ App Router
│ ├── layout.tsx Metadata, viewport, manifest
│ ├── providers.tsx GameProvider, toasts, service worker
│ ├── page.tsx Home
│ ├── play/ factory/ collection/ settings/
│ ├── not-found.tsx
│ └── globals.css
├── components/
│ ├── ui/ Button, IconButton, Card, StarCounter,
│ │ ProgressBar, Toasts, SlimePreview, ScreenShell
│ ├── game/ ThemeSelector, PauseMenu, DailyRewardModal
│ └── screens/ Home, Play, Factory, Collection, Settings
├── hooks/
│ ├── useGameState.tsx Reducer, persistence, unlocks, achievements
│ └── useSlimeEngine.ts Matter.js world, pointers, render loop
├── lib/
│ ├── softbody.ts Soft-body construction, forces, self-untangle
│ ├── render.ts Canvas painting for every theme
│ ├── particles.ts Pooled particle system (8 kinds)
│ ├── synth.ts WAV synthesiser
│ ├── audio.ts Howler manager
│ ├── content.ts Themes, backgrounds, packs, achievements
│ ├── storage.ts localStorage save/load
│ └── utils.ts Colour and maths helpers
├── types/index.ts Shared domain types
public/
├── manifest.webmanifest
├── sw.js Network-first pages, cache-first assets
└── icons/ Generated by scripts/gen-icons.mjs
The catalogue lives entirely in src/lib/content.ts. Adding a slime, a scene, a sparkle
pack or a sound pack is a data edit — the collection screen, theme strip, factory and
progression all read from it.
aria-labels, aria-pressed/role="switch"
state and visible focus rings.prefers-reduced-motion
setting is honoured in CSS independently.public/sw.js serves pages network-first (so a deployment is picked up quickly) and
falls back to the cached shell offline; everything else is cache-first and refreshed in
the background. Because the audio is synthesised rather than downloaded, the game is
genuinely complete offline.
Settings surfaces the install prompt when the browser fires beforeinstallprompt, and
the manifest declares standalone display plus shortcuts straight to Play and the Slime
Factory.
Icons are generated at build time by scripts/gen-icons.mjs, which writes the PNGs by
hand with zlib and a CRC32 table — so the repository carries no binary assets at all,
and the icon is defined by the same palette as the game.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 15, App Router | File-based screens, static export of every route |
| Language | TypeScript, strict | The whole domain is typed in src/types |
| Styling | Tailwind CSS 3, class dark mode | Chunky rounded toy UI with almost no custom CSS |
| Physics | Matter.js 0.20 | Mature constraint solver with time-corrected Verlet |
| Animation | Framer Motion 12 | Springy menus and toasts, outside the game loop |
| Audio | Howler.js 2.2 | Pooled playback with per-sound rate and volume |
| Rendering | Canvas 2D, hand-written | Full control over the blob, and cheap on old devices |
| Storage | localStorage | No account, no server, no data leaves the device |
Every route is statically prerendered; the play screen is ~39 kB on top of a ~103 kB shared bundle.