Where typography meets light.
A real-time particle typography experience built with Three.js. Text dissolves into thousands of luminous sparkles that react to your cursor, explode on click, and reform into whatever you type.
- Overview
- Features
- Tech Stack
- Getting Started
- How It Works
- Controls
- Color Themes
- Architecture
- Shader System
- Performance
- Future Improvements
- License
Particle Text is an interactive WebGL experiment that transforms text into a field of glowing particles. Each character is rendered to an off-screen canvas, sampled at the pixel level, and mapped into a Three.js particle system β creating a living, breathing typographic sculpture.
The particles don't just sit there. They flee from your cursor, explode on click, and smoothly reform into new text as you type. All in real time, all in the browser.
| Feature | Description |
|---|---|
| Live Text Input | Type anything β particles reform instantly into your text |
| Sparkle Shader | Custom GLSL fragment shader creating 4-pointed star shapes |
| Mouse Repulsion | Particles scatter away from the cursor within a defined radius |
| Attraction Mode | Toggle to pull particles toward the cursor instead |
| Click Explosion | Click anywhere to blast particles outward; they recover after 2 seconds |
| 4 Color Themes | Neon, Pastel, Monochromatic, and Rainbow palettes |
| Bloom Post-Processing | UnrealBloomPass adds cinematic glow to every particle |
| Background Particles | 600 ambient particles float with sinusoidal motion for depth |
| Dynamic Count | Slider adjusts particle count from 2,000 to 20,000 in real time |
| OrbitControls | Drag to orbit, scroll to zoom, explore the 3D space |
| Screenshot Export | One-click PNG download of the current scene |
| Custom Font | Google Sans Flex loaded via Google Fonts |
- Three.js β 3D rendering engine
- GLSL β Custom vertex and fragment shaders
- EffectComposer β Post-processing pipeline
- Google Sans Flex β Typography
- Vanilla JS β No frameworks, no build tools, no dependencies
A modern browser with WebGL 2 support. No server required β but some browsers restrict importmap over file://, so a local server is recommended.
# Option 1: Python
python -m http.server 8000
# Option 2: Node.js
npx serve .
# Option 3: VS Code
# Install "Live Server" extension, then right-click index.html β Open with Live ServerOpen http://localhost:8000 in your browser.
particles/
βββ index.html # Entry point, UI controls, import map
βββ style.css # Glassmorphism UI, slider, responsive layout
βββ main.js # Three.js scene, shaders, particle logic
- Each character is rendered to an off-screen
<canvas>using the 2D API - Pixel data is read with
getImageData() - White pixels (alpha > 128) become 3D coordinates, mapped from screen space to world space
- A small random offset is added per particle for organic spread
Particles start at random positions across a 25Γ15Γ12 unit volume. On each frame, every particle interpolates toward its target position using linear interpolation:
positions[i] += (target - positions[i]) * 0.06;This creates the fluid "formation" effect β particles drift into place like a swarm finding its shape.
The mouse position is projected onto a plane perpendicular to the camera using Raycaster. For each particle, the distance to the mouse is computed. Within the interaction radius, a force vector is applied:
- Repulsion: Pushes particles away from the cursor
- Attraction: Pulls particles toward the cursor
On click, every particle receives a radial impulse away from the click point. The force is randomized (8β14 units) for a natural scatter. After 2 seconds, isExploding resets and particles reform.
| Input | Action |
|---|---|
| Drag | Orbit the camera around the scene |
| Scroll | Zoom in / out |
| Move mouse | Repel or attract particles (depending on mode) |
| Click | Explode particles outward |
| Type in input | Particles reform into the new text |
| Element | Function |
|---|---|
| Text Input | Enter custom text (max 20 characters) |
| Theme Selector | Switch between Neon, Pastel, Monochromatic, Rainbow |
| Particle Slider | Adjust count from 2K to 20K |
| Mode Button | Toggle between Repulsion and Attraction |
| Screenshot Button | Download current view as PNG |
| Theme | Palette | Style |
|---|---|---|
| Neon | Cyan, Magenta, Lime, Yellow, Purple, Blue | High saturation, electric feel |
| Pastel | Soft Red, Green, Blue, Yellow, Orange, Lavender | Muted, warm, gentle |
| Monochromatic | 6 variations of blue | Cohesive, minimal, elegant |
| Rainbow | Full HSL spectrum mapped across particles | Every particle gets a unique hue |
Colors are assigned per-particle with slight random variation (Β±0.075) to avoid flat, uniform appearance.
Scene
βββ AmbientLight (0x334466, intensity 1.0)
βββ DirectionalLight (white, intensity 1.2)
βββ PointLight (blue, 0x4488ff)
βββ PointLight (orange, 0xff5533)
βββ Points (text particles) β ShaderMaterial
βββ Points (background particles) β ShaderMaterial
Renderer
βββ EffectComposer
βββ RenderPass (scene β camera)
βββ UnrealBloomPass
βββ strength: 0.8
βββ radius: 0.4
βββ threshold: 0.85
Input Text
β Canvas 2D (render text)
β Pixel Sampling (getImageData)
β Target Positions (Float32Array)
β Lerp Animation (each frame)
β BufferGeometry Update
β ShaderMaterial Render
β Bloom Post-Processing
β Screen
// Per-particle size scaling with distance
gl_PointSize = aSize * uPixelRatio * (60.0 / -mvPosition.z);
// Alpha fade based on distance from center
vAlpha = smoothstep(14.0, 6.0, dist) * 0.6 + 0.4;The sparkle effect is composed of three overlapping shapes:
- Core β A soft circle at the center (
smoothstep(0.35, 0.0, d)) - Horizontal arm β Thin horizontal line through center
- Vertical arm β Thin vertical line through center
float sparkle = max(core, max(armX, armY));
sparkle = pow(sparkle, 1.5); // Sharpen the glowBackground particles use sinusoidal displacement for organic floating motion:
pos.y += sin(uTime * aSpeed + position.x * 2.0) * 0.3;
pos.x += cos(uTime * aSpeed * 0.5 + position.y) * 0.2;| Metric | Value |
|---|---|
| Default particles | 8,000 text + 600 background |
| Max particles | 20,000 text + 600 background |
| Geometry | BufferGeometry with DynamicDrawUsage |
| Rendering | Points with AdditiveBlending |
| Pixel ratio | Capped at 2Γ to prevent GPU overload |
| Bloom | Single-pass UnrealBloomPass |
preserveDrawingBuffer: trueonly enabled for screenshot supportdepthWrite: falseprevents Z-fighting with transparent particles- Particle positions updated via direct
Float32Arraymutation (no object allocation in loop) - Mouse distance computed once per frame with cached
Vector3
- Audio reactivity β Microphone input drives particle size or color
- Gravity mode β Particles fall and bounce
- Trail effect β Particles leave fading traces when moving
- Texture particles β Sample from a user-uploaded image
- VR support β WebXR integration for immersive viewing
- Physics engine β Verlet integration for more natural motion
- Multi-language β CJK character support with denser sampling
- PWA β Installable as a standalone app
Built with Three.js and a fascination for the space between code and light.
Made with β€οΈ by SebastiΓ‘n V
