A tiny, MIT-licensed starter for GPGPU particle scenes in the browser — the kind of interactive particle work you see on Awwwards sites. 65,536 particles simulated entirely on the GPU, drifting on divergence-free curl noise, stirred by your pointer, and springing into any word or any silhouette PNG you hand it.
No build step. No dependencies to install. Clone it, serve the folder, open the page.
git clone https://github.com/odedbahiri-arch/recstudio-particles.gitcd recstudio-particles && npx serve .Then open the URL it prints (usually http://localhost:3000).
Any static server works — python -m http.server, the VS Code Live Server extension, whatever you
already use. It does need to be a server: the demo is ES modules, so opening index.html from
the filesystem will not work.
Controls: move the pointer to stir · hold the mouse for a shove · 1 drift · 2 word · 3 shape · space autoplay on/off
The whole kit is steered by one CONFIG object at the top of index.html. That is deliberate —
it means you can point Claude Code at it and describe what you
want in a sentence. Copy any of these:
1 — install and run it
Clone https://github.com/odedbahiri-arch/recstudio-particles into this folder, start a static server in the repo root, open the page in my browser, and tell me if there are any console errors. It is a no-build ES-module project — do not add a bundler, a package.json, or npm dependencies.
2 — your word, your colour
In
index.html, setCONFIG.morph.texttoAURORAand changeCONFIG.colors.fastto my brand colour#7cf2c8, keepingcolors.slowdark and cool so the contrast still reads. Then reload and screenshot the "word" mode (press 2) so I can see it.
3 — your shape, your mood
Put
my-logo.pnginshapes/, pointCONFIG.morph.imageat it, and make the whole scene calmer: raiseCONFIG.motion.damping, lowerCONFIG.motion.noiseStrength, and softenCONFIG.pointer.strengthandCONFIG.pointer.swirl. If the shape comes out inside-out, flipCONFIG.morph.imageInvert.
For a wilder scene, invert prompt 3: lower damping, raise noise strength and pointer swirl.
Everything below lives in index.html. These are the knobs worth knowing:
| Key | Does |
|---|---|
particles.textureSize |
Particle count is this squared. 128 → 16,384, 192 → 36,864, 256 → 65,536. |
particles.size / .opacity |
Sprite size and brightness. Particles are additive, so opacity stacks into glow. |
colors.slow / .fast |
The colour ramp. Slow particles get slow, fast ones (and particles held in a shape) get fast. |
colors.accent / .accentRatio |
A sparse second colour. Past ~0.02 it reads as confetti. |
field.shellRadius / .shell |
The spheroid the drifting cloud is kept inside, and how firmly. |
field.flatten / .spin |
Squash the cloud into a disc; turn it slowly. |
motion.noiseScale / .noiseSpeed / .noiseStrength |
The curl-noise drift: eddy size, how fast the field evolves, how hard it pushes. |
motion.damping |
Drag while drifting. Low = long smooth streaks, high = calm. |
pointer.radius / .strength / .swirl |
Reach, push, and stir of the pointer. swirl is what makes it feel liquid. |
morph.text / .font |
The word. Hebrew, Arabic and CJK all work — the text is drawn to a canvas, so whatever your OS can render, the particles can form. |
morph.image / .imageInvert |
Path to a silhouette PNG. Transparent PNGs use the alpha channel; fully opaque images fall back to brightness, so a black shape on white also works. |
morph.spring / .damping |
How eagerly particles snap to their target, and how hard they are braked once there. Roughly damping ≈ 2 × √spring is crisp; much less and the letters blur. |
timeline.steps |
The autoplay loop. Reorder, retime, or set autoplay: false. |
Four ideas, and that is the whole kit:
- State lives in textures. Each particle owns one pixel of a floating-point texture:
xyzis its position, another texture holdsxyzvelocity plus a per-particle random seed. 65,536 particles is a 256×256 texture. - Ping-pong. Each frame renders a fullscreen quad that reads the current state texture and
writes the next one, then swaps the two. The CPU never touches a particle.
(
src/simulation.js) - Forces. The velocity shader adds curl drift, a spring toward the morph target, soft
containment, and the pointer force, then applies drag. (
src/shaders.js) The drift uses bitangent noise — the cross product of two noise gradients, which is divergence-free, meaning the flow has no sources or sinks and particles swirl forever instead of piling up. - Targets are just pixels. A word is drawn to an offscreen 2D canvas; a PNG is drawn to one
too. Either way the opaque pixels are scattered across the particles as target positions.
Anything you can draw to a canvas can be a shape. (
src/targets.js)
index.html the demo page + the CONFIG object (start here)
src/main.js scene, camera, input, render loop
src/simulation.js the ping-pong GPGPU simulation
src/shaders.js all the GLSL
src/targets.js text and PNG → particle target positions
src/vendor/ three.js r185 and atyuwen's bitangent noise, both MIT
shapes/ sample silhouettes — drop your own PNG here
- WebGL2 with
EXT_color_buffer_float(every current desktop and mobile browser). The page tells you plainly if the GPU cannot do it instead of failing silently. - Measured here: 65,536 particles at 170 fps, 1440×900, Chrome, RTX 4070 Super. The simulation
cost is per-particle, so if a weaker machine struggles, drop
textureSizeto192or128— 16,384 particles still looks good. - Zero console errors, zero network requests after load — three.js is vendored in the repo.
Built with Claude Code by RecStudio.
- three.js — MIT
- Bitangent noise by Yuwen Wu (atyuwen) — MIT
- Full notices: THIRD-PARTY-NOTICES.md
MIT licensed. Use it in client work, ship it, sell it — no attribution required (though it is always nice).

