Description
This project creates a cinematic magma-like particle field using Three.js and custom vertex and fragment shaders.
The visual is driven entirely on the GPU and combines procedural geometry, shader-based morphing, and interactive force feedback.
Key characteristics:
- Golden-angle spiral particle distribution
- Procedural morphing patterns for organic motion
- Mouse-driven velocity-based particle displacement
- Click-triggered radial shockwave ripples
- Additive blending for fiery glow aesthetics
- High particle count (130k+) with stable performance
- Subtle camera orbit for depth and immersion
This effect is ideal for hero backgrounds, creative coding demos, sci-fi visuals, and portfolio showcases.
HTML
1<!DOCTYPE html>
2<html lang="en">
3
4 <head>
5 <meta charset="UTF-8">
6 <title>Magma Particle Field</title>
7
8
9 </head>
10
11 <body>
12 <meta charset="UTF-8">
13<meta name="viewport" content="width=device-width, initial-scale=1.0">
14<title>Magma Particle Field</title>
15<style>
16 * {
17 margin: 0;
18 padding: 0;
19 box-sizing: border-box;
20 }
21
22 body {
23 background-color: #000;
24 overflow: hidden;
25 cursor: pointer;
26 }
27
28 #container {
29 position: fixed;
30 width: 100%;
31 height: 100%;
32 background: linear-gradient(180deg,
33 #050000 0%,
34 #1a0001 50%,
35 #2a0100 100%
36 );
37 overflow: hidden;
38 }
39
40 .glow {
41 position: fixed;
42 top: 0;
43 left: 0;
44 width: 100%;
45 height: 100%;
46 pointer-events: none;
47 background: radial-gradient(circle at 50% 50%,
48 rgba(255, 60, 0, 0.05) 0%,
49 rgba(255, 0, 40, 0.08) 40%,
50 transparent 75%
51 );
52 mix-blend-mode: screen;
53 opacity: 0.9;
54 }
55
56 .ui-hint {
57 position: fixed;
58 bottom: 30px;
59 left: 50%;
60 transform: translateX(-50%);
61 color: rgba(255, 100, 50, 0.5);
62 font-family: 'Arial', sans-serif;
63 font-size: 14px;
64 letter-spacing: 2px;
65 pointer-events: none;
66 text-transform: uppercase;
67 }
68</style>
69
70
71<script type="importmap">
72{
73 "imports": {
74 "three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js"
75 }
76}
77</script>
78
79<div id="container"></div>
80<div class="glow"></div>
81<div class="ui-hint">Click center for shockwave</div>
82
83<script type="module">
84import * as THREE from 'three';
85
86let scene, camera, renderer, particles;
87let time = 0;
88const screenMouse = new THREE.Vector2(10000, 10000);
89const worldMouse = new THREE.Vector3();
90const lastWorldMouse = new THREE.Vector3();
91const mouseVelocity = new THREE.Vector3();
92const smoothedMouseVelocity = new THREE.Vector3();
93
94function generateShape(t, angle, phase) {
95 const baseRadius = 65;
96 const spiralTwist = 6;
97
98 const pulse = Math.sin(t * Math.PI * 2 + phase);
99
100 const radius = baseRadius * Math.sqrt(t) * (1 + 0.3 * pulse);
101
102 let x = radius * Math.cos(angle + t * spiralTwist);
103 let y = radius * Math.sin(angle + t * spiralTwist);
104 let z = radius * Math.sin(t * 8 + phase) * 0.8;
105
106 return { x, y, z };
107}
108
109const particleCount = 130000;
110
111function createParticleSystem() {
112 const geometry = new THREE.BufferGeometry();
113 const positions = new Float32Array(particleCount * 3);
114 const colors = new Float32Array(particleCount * 3);
115 const sizes = new Float32Array(particleCount);
116 const angles = new Float32Array(particleCount);
117 const originalPos = new Float32Array(particleCount * 3);
118 const randomFactors = new Float32Array(particleCount);
119
120 const colorPalette = [
121 new THREE.Color('#ff9a00'),
122 new THREE.Color('#ff004d'),
123 new THREE.Color('#ffec00'),
124 new THREE.Color('#ff3300'),
125 new THREE.Color('#ff5500')
126 ];
127
128 const goldenAngle = Math.PI * (3 - Math.sqrt(5));
129
130 for(let i = 0; i < particleCount; i++) {
131 const i3 = i * 3;
132 const t = i / particleCount;
133 const angle = i * goldenAngle;
134 const pos = generateShape(t, angle, 0.8);
135
136 positions[i3] = pos.x;
137 positions[i3 + 1] = pos.y;
138 positions[i3 + 2] = pos.z;
139 originalPos[i3] = pos.x;
140 originalPos[i3 + 1] = pos.y;
141 originalPos[i3 + 2] = pos.z;
142 angles[i] = angle;
143
144 const colorT = t * (colorPalette.length - 1);
145 const idx = Math.floor(colorT);
146 const mix = colorT - idx;
147 const c1 = colorPalette[idx];
148 const c2 = colorPalette[Math.min(idx + 1, colorPalette.length - 1)];
149
150 const color = new THREE.Color().lerpColors(c1, c2, mix);
151 color.multiplyScalar(0.8 + Math.random() * 0.4);
152
153 colors[i3] = color.r;
154 colors[i3 + 1] = color.g;
155 colors[i3 + 2] = color.b;
156
157 sizes[i] = 1.0 * (1.1 - t * 0.3) * (0.6 + Math.random() * 0.7);
158 randomFactors[i] = Math.random();
159 }
160
161 geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
162 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
163 geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
164 geometry.setAttribute('angle', new THREE.BufferAttribute(angles, 1));
165 geometry.setAttribute('originalPos', new THREE.BufferAttribute(originalPos, 3));
166 geometry.setAttribute('randomFactor', new THREE.BufferAttribute(randomFactors, 1));
167
168 const material = new THREE.ShaderMaterial({
169 uniforms: {
170 time: { value: 0 },
171 shockwaveTime: { value: -100.0 },
172 mousePos: { value: new THREE.Vector3(10000, 10000, 0) },
173 mouseVel: { value: new THREE.Vector3() },
174 pixelRatio: { value: window.devicePixelRatio }
175 },
176 vertexShader: `
177 uniform float time;
178 uniform float shockwaveTime;
179 uniform vec3 mousePos;
180 uniform vec3 mouseVel;
181 uniform float pixelRatio;
182
183 attribute vec3 originalPos;
184 attribute float size;
185 attribute float angle;
186 attribute float randomFactor;
187
188 varying vec3 vColor;
189 varying float vIntensity;
190 varying float vRandomFactor;
191
192 void main() {
193 vColor = color;
194 vRandomFactor = randomFactor;
195 vec3 basePos = originalPos;
196
197 float morphTime = time * 0.5;
198 float pattern1 = sin(angle * 5.0 + morphTime) * cos(angle * 2.0);
199 float pattern2 = cos(angle * 4.0 - morphTime) * sin(angle * 3.0);
200 float blend = sin(morphTime * 0.6) * 0.5 + 0.5;
201 float displacement = mix(pattern1, pattern2, blend);
202
203 vec3 normOrig = normalize(originalPos + vec3(0.001));
204 vec3 animatedPos = originalPos + normOrig * displacement * 6.0;
205
206 vec3 pos = animatedPos;
207
208 float waveAge = time - shockwaveTime;
209 float waveSpeed = 60.0;
210
211 if (waveAge > 0.0 && waveAge < 3.0) {
212 float distFromCenter = length(originalPos.xy);
213 float currentRadius = waveAge * waveSpeed;
214 float waveWidth = 20.0;
215
216 float diff = distFromCenter - currentRadius;
217
218 if (abs(diff) < waveWidth) {
219 float rippleShape = cos(diff * 0.25) * 0.5 + 0.5;
220
221 float decay = 1.0 - smoothstep(0.0, 3.0, waveAge);
222
223 float zForce = rippleShape * 20.0 * decay;
224 pos.z += zForce;
225
226 pos.xy += normalize(pos.xy) * rippleShape * 5.0 * decay;
227
228 vColor += vec3(0.5, 0.4, 0.2) * rippleShape * decay;
229 }
230 }
231
232 vec3 toMouse = mousePos - pos;
233 float dist = length(toMouse);
234 float influence = pow(smoothstep(45.0, 5.0, dist), 2.0);
235 vIntensity = 0.0;
236
237 if (influence > 0.001) {
238 float velMag = length(mouseVel);
239 float velInfluence = smoothstep(0.1, 2.0, velMag) * influence;
240 vec3 forceDir = normalize(mouseVel + vec3(0.001));
241 vec3 pushDir = normalize(pos - mousePos);
242 vec3 dir = normalize(mix(forceDir, pushDir, 0.3));
243
244 float dispMag = velInfluence * (5.0 + randomFactor * 6.0);
245 pos += dir * dispMag;
246 vIntensity = clamp(influence * 0.6 + velInfluence * 0.9, 0.0, 1.0);
247 }
248
249 pos += (animatedPos - pos) * 0.01;
250
251 float breath = sin(time * 1.3 + angle) * 0.08;
252 pos *= 1.0 + breath * (1.0 - influence * 0.7) * (0.6 + vRandomFactor * 0.7);
253
254 vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
255 gl_Position = projectionMatrix * mvPos;
256
257 float sizeFactor = 1.0 + vIntensity * 0.3;
258 float perspective = 500.0 / -mvPos.z;
259 gl_PointSize = size * sizeFactor * perspective * pixelRatio;
260 }
261 `,
262 fragmentShader: `
263 uniform float time;
264 varying vec3 vColor;
265 varying float vIntensity;
266 varying float vRandomFactor;
267
268 void main() {
269 vec2 pc = gl_PointCoord * 2.0 - 1.0;
270 float dist = length(pc);
271 if (dist > 1.0) discard;
272
273 float core = smoothstep(0.2, 0.0, dist) * 0.6;
274 float glow = exp(-dist * 2.4) * 0.7;
275
276 vec3 highlight = vec3(1.0, 0.8, 0.4);
277 vec3 finalColor = mix(vColor, highlight, vIntensity * 0.6);
278
279 float shimmer = 1.0 + sin((time * 60.0 + vRandomFactor * 150.0) * (0.7 + vIntensity * 0.3)) * 0.15 * (1.0 - dist * 0.5);
280
281 float baseAlpha = (core + glow) * clamp(0.3 + vIntensity * 0.6, 0.0, 1.0);
282 float finalAlpha = baseAlpha * shimmer;
283
284 gl_FragColor = vec4(finalColor, finalAlpha);
285 }
286 `,
287 transparent: true,
288 depthWrite: false,
289 blending: THREE.AdditiveBlending,
290 vertexColors: true
291 });
292
293 material.uniforms.time = { value: 0 };
294 return new THREE.Points(geometry, material);
295}
296
297function init() {
298 scene = new THREE.Scene();
299 camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
300 camera.position.z = 110;
301
302 renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
303 renderer.setSize(window.innerWidth, window.innerHeight);
304 renderer.setClearColor(0x000000, 0);
305 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
306 document.getElementById('container').appendChild(renderer.domElement);
307
308 particles = createParticleSystem();
309 scene.add(particles);
310
311 document.addEventListener('click', () => {
312 if(particles) {
313 particles.material.uniforms.shockwaveTime.value = time;
314 }
315 });
316
317 document.addEventListener('mousemove', e => {
318 screenMouse.x = (e.clientX / window.innerWidth) * 2 - 1;
319 screenMouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
320 }, { passive: true });
321
322 document.addEventListener('mouseleave', () => {
323 screenMouse.x = 10000;
324 screenMouse.y = 10000;
325 });
326
327 window.addEventListener('resize', onWindowResize);
328}
329
330const raycaster = new THREE.Raycaster();
331const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
332
333function updateMouseAndUniforms() {
334 lastWorldMouse.copy(worldMouse);
335 raycaster.setFromCamera(screenMouse, camera);
336 const intersect = new THREE.Vector3();
337 if (screenMouse.x < 9999 && raycaster.ray.intersectPlane(plane, intersect)) {
338 worldMouse.copy(intersect);
339 if (lastWorldMouse.x < 9000) {
340 mouseVelocity.subVectors(worldMouse, lastWorldMouse);
341 } else {
342 mouseVelocity.set(0, 0, 0);
343 }
344 } else {
345 worldMouse.set(10000, 10000, 0);
346 mouseVelocity.set(0, 0, 0);
347 }
348 smoothedMouseVelocity.lerp(mouseVelocity, 0.15);
349 mouseVelocity.multiplyScalar(0.92);
350
351 if (particles) {
352 particles.material.uniforms.mousePos.value.copy(worldMouse);
353 particles.material.uniforms.mouseVel.value.copy(smoothedMouseVelocity);
354 particles.material.uniforms.time.value = time;
355 }
356}
357
358function animate() {
359 requestAnimationFrame(animate);
360 time = performance.now() * 0.0007;
361
362 updateMouseAndUniforms();
363
364 camera.position.x = Math.sin(time * 0.3) * 12;
365 camera.position.y = Math.cos(time * 0.4) * 12;
366 camera.lookAt(0, 0, 0);
367
368 renderer.render(scene, camera);
369}
370
371function onWindowResize() {
372 camera.aspect = window.innerWidth / window.innerHeight;
373 camera.updateProjectionMatrix();
374
375 if (window.innerWidth < 800) {
376 camera.position.z = 140;
377 } else {
378 camera.position.z = 110;
379 }
380
381 renderer.setSize(window.innerWidth, window.innerHeight);
382 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
383 if(particles) particles.material.uniforms.pixelRatio.value = renderer.getPixelRatio();
384}
385
386init();
387animate();
388</script>
389
390 </body>
391
392</html>
How It Works
- Particle Distribution
Uses golden-angle spiral math for even radial spacing
Particles form a volumetric, magma-like structure
Original positions are stored for smooth reversion
- Shader-Based Motion
Vertex shader applies procedural trigonometric morphing
Two evolving patterns blend over time
Breathing scale creates organic expansion
- Interaction System
Mouse position is projected into world space
Velocity-based force pushes nearby particles
Click triggers a radial shockwave ripple
Shockwave affects Z-depth, radial offset, and color
- Visual Styling
Additive blending for fiery glow
Fragment shader creates soft circular sprites
Time-based shimmer for heat distortion feel
Key Features
🔥 Magma-inspired particle aesthetics
🌀 Golden-angle spiral geometry
💥 Click-based shockwave ripples
🖱️ Velocity-sensitive mouse interaction
✨ Additive glow & shimmer effects
🎥 Cinematic camera motion
⚡ GPU-accelerated rendering
Use Cases
Creative coding showcases
Interactive hero backgrounds
Sci-fi / molten visuals
WebGL demos
Portfolio highlight projects
