Back to Components
Magma Particle Field with Shockwave Interaction using Three.js
Component

Magma Particle Field with Shockwave Interaction using Three.js

CodewithLord
December 18, 2025

A high-density GPU-accelerated magma-inspired particle field built with Three.js and custom GLSL shaders. Features spiral-based particle distribution, morphing procedural motion, mouse-driven force interaction, shockwave ripples, additive glow blending, and cinematic camera movement.

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


  1. 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

  1. Shader-Based Motion

Vertex shader applies procedural trigonometric morphing

Two evolving patterns blend over time

Breathing scale creates organic expansion

  1. 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

  1. 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

Love this component?

Explore more components and build amazing UIs.

View All Components