Back to Components
Interactive Pyramid FX – Three.js + GLSL
Component

Interactive Pyramid FX – Three.js + GLSL

CodewithLord
October 4, 2025

This project showcases a fully interactive VFX-based pyramid built using Three.js, GLSL shaders, and post-processing effects such as UnrealBloomPass.

Three.js Effects Component Documentation


This document provides a complete Markdown-formatted reference for the Three.js visual effects implementation including Glow Effect, Flare Effect, Spiral Effect, and Neutron Effect.

Overview


This script creates multiple particle-based visual effects triggered by different UI buttons. It uses Three.js for 3D rendering and HTML Canvas for display. Each effect is handled independently with dynamic creation, animation, and cleanup.

File Structure


- index.html
- style.css
- script.js

JavaScript Breakdown


🎬 Initialization


  • Creates the scene, camera, and renderer.
  • Camera uses PerspectiveCamera with depth for 3D-like parallax movement.
  • Renderer attaches to <canvas> and fills the screen.

✨ Particles System


Particles are created using:

  • THREE.BufferGeometry()
  • THREE.PointsMaterial()
  • Stored in arrays for dynamic animation.

Each effect (glow, flare, spiral, neutron) manages:

  • Creation of geometry + positions
  • Material settings
  • Adding to scene
  • Animation loop
  • Automatic cleanup

🌟 Glow Effect


Creation


  • Produces 500 particles
  • Randomly positioned
  • Material: low-opacity glow color

Animation


  • Slight upward floating
  • Reset to bottom when going out of view

🔥 Flare Effect


Creation


  • Burst of randomly positioned bright particles
  • Material: stronger opacity

Animation


  • Particles move outward from center
  • Gradually fade
  • Removed after 3 seconds

🌀 Spiral Effect


Creation


  • Creates thousands of particles arranged in a 3D spiral
  • Uses trigonometry: sin(), cos() for circular motion

Animation


  • Spiral slowly rotates
  • Creates hypnotic vortex effect

⚛️ Neutron Effect


Creation


  • Center “core” and moving orbit particles
  • Orbit uses circular path around origin

Animation


  • Orbit speed decreases over time
  • Auto-disappears when speed becomes low

🎥 Global Animation Loop


Handles:

  • Updating all active effects
  • Rotations
  • Movements
  • Fade-outs

Uses requestAnimationFrame() for smooth 60 FPS rendering.



📱 Responsive Behavior


Resizes renderer & camera on window size change:

window.addEventListener("resize", ...)

Cleanup


Every effect uses:

  • scene.remove()
  • dispose() for materials & geometry
  • Timed removal using setTimeout

This prevents memory leaks.


Usage


Clicking a button triggers:

triggerGlow()
triggerFlare(randomColor)
triggerSpiral(colorArray)
triggerNeutron()

Each effect is independent and can run simultaneously.



Notes


  • Uses pure Three.js (no React, no frameworks)
  • All particles rendered via GPU shaders
  • Highly efficient despite thousands of points

Full Code


1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>Interactive Pyramid FX | Three.js + GLSL</title> 7 8 9 </head> 10 11 <body> 12 <style> 13 body { 14 margin: 0; 15 overflow: hidden; 16 background-color: #000; 17 font-family: 'Inter', sans-serif; 18 } 19 canvas { 20 width: 100%; 21 height: 100vh; 22 display: block; 23 position: fixed; 24 top: 50%; 25 left: 50%; 26 transform: translate(-50%, -50%); 27 } 28 #ui-container { 29 position: fixed; 30 bottom: 30px; 31 left: 50%; 32 transform: translateX(-50%); 33 z-index: 10; 34 text-align: center; 35 } 36 #effect-trigger { 37 padding: 12px 24px; 38 font-size: 16px; 39 font-weight: 600; 40 color: #fff; 41 background: rgba(255, 255, 255, 0.1); 42 border: 2px solid rgba(255, 255, 255, 0.3); 43 border-radius: 12px; 44 cursor: pointer; 45 backdrop-filter: blur(10px); 46 -webkit-backdrop-filter: blur(10px); 47 transition: background 0.3s, color 0.3s, transform 0.2s; 48 } 49 #effect-trigger:hover { 50 background: rgba(255, 255, 255, 0.2); 51 border-color: rgba(255, 255, 255, 0.5); 52 } 53 #effect-trigger:active { 54 transform: scale(0.95); 55 } 56 #effect-info { 57 margin-top: 10px; 58 color: rgba(255, 255, 255, 0.8); 59 font-size: 14px; 60 } 61</style> 62 63<div id="ui-container"> 64 <button id="effect-trigger">Trigger Effect</button> 65 <div id="effect-info">Lightning Storm</div> 66</div> 67 68<script type="importmap"> 69{ 70 "imports": { 71 "three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js", 72 "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/" 73 } 74} 75</script> 76 77<script type="module"> 78import * as THREE from 'three'; 79import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; 80import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; 81import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; 82import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; 83 84const scene = new THREE.Scene(); 85const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 86const renderer = new THREE.WebGLRenderer({ antialias: true }); 87renderer.setSize(window.innerWidth, window.innerHeight); 88renderer.setClearColor(0x000000); 89document.body.appendChild(renderer.domElement); 90 91const controls = new OrbitControls(camera, renderer.domElement); 92controls.enableDamping = true; 93controls.dampingFactor = 0.05; 94camera.position.set(0, 2, 6); 95controls.target.set(0, 1, 0); 96controls.update(); 97 98const composer = new EffectComposer(renderer); 99const renderPass = new RenderPass(scene, camera); 100composer.addPass(renderPass); 101const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); 102bloomPass.threshold = 0.1; 103bloomPass.strength = 2.5; 104bloomPass.radius = 0.8; 105composer.addPass(bloomPass); 106 107let effectState = 0; 108const clock = new THREE.Clock(); 109let time = 0; 110 111const effectThemes = [ 112 { 113 name: "Lightning Storm", 114 effect: "lightning", 115 outer: [new THREE.Color(0x00ffff), new THREE.Color(0x4169e1), new THREE.Color(0x9400d3)], 116 outerEdge: new THREE.Color(0x87cefa), 117 inner: [new THREE.Color(0xff1493), new THREE.Color(0xff4500), new THREE.Color(0xffd700)], 118 innerEdge: new THREE.Color(0xffd700) 119 }, 120 { 121 name: "Volcanic Shards", 122 effect: "shards", 123 outer: [new THREE.Color(0xffd700), new THREE.Color(0xff4500), new THREE.Color(0x8b0000)], 124 outerEdge: new THREE.Color(0xff8c00), 125 inner: [new THREE.Color(0xffff00), new THREE.Color(0xff6347), new THREE.Color(0xdc143c)], 126 innerEdge: new THREE.Color(0xffa500), 127 shardColors: [new THREE.Color(0xff8c00), new THREE.Color(0xffa500), new THREE.Color(0xffff00)] 128 }, 129 { 130 name: "Arctic Rings", 131 effect: "rings", 132 outer: [new THREE.Color(0x00ffff), new THREE.Color(0x87ceeb), new THREE.Color(0xb0e0e6)], 133 outerEdge: new THREE.Color(0x00ffff), 134 inner: [new THREE.Color(0xffffff), new THREE.Color(0xe0ffff), new THREE.Color(0xf0f8ff)], 135 innerEdge: new THREE.Color(0xffffff), 136 ringColors: [new THREE.Color(0x00ffff), new THREE.Color(0x87ceeb), new THREE.Color(0xffffff)] 137 }, 138 { 139 name: "Emerald Spiral", 140 effect: "spiral", 141 outer: [new THREE.Color(0x00ff00), new THREE.Color(0x32cd32), new THREE.Color(0x228b22)], 142 outerEdge: new THREE.Color(0x98fb98), 143 inner: [new THREE.Color(0xadff2f), new THREE.Color(0x9acd32), new THREE.Color(0x6b8e23)], 144 innerEdge: new THREE.Color(0xadff2f), 145 spiralColors: [new THREE.Color(0x00ff00), new THREE.Color(0x32cd32), new THREE.Color(0xadff2f)] 146 }, 147 { 148 name: "Solar Flare", 149 effect: "flare", 150 outer: [new THREE.Color(0xffa500), new THREE.Color(0xff8c00), new THREE.Color(0xff7f50)], 151 outerEdge: new THREE.Color(0xffd700), 152 inner: [new THREE.Color(0xffff00), new THREE.Color(0xffd700), new THREE.Color(0xffa500)], 153 innerEdge: new THREE.Color(0xffff00), 154 flareColors: [new THREE.Color(0xff4500), new THREE.Color(0xff6600), new THREE.Color(0xffa500), new THREE.Color(0xffff00), new THREE.Color(0xffd700)] 155 } 156]; 157 158const effectGroups = { 159 lightning: null, 160 shards: new THREE.Group(), 161 rings: new THREE.Group(), 162 spiral: new THREE.Group(), 163 flare: new THREE.Group() 164}; 165 166scene.add(effectGroups.shards, effectGroups.rings, effectGroups.spiral, effectGroups.flare); 167 168const effectData = { 169 lightning: { isActive: false, startTime: 0, duration: 1.0 }, 170 shards: { isActive: false }, 171 rings: { isActive: false }, 172 spiral: { isActive: false }, 173 flare: { isActive: false } 174}; 175 176function createParticlePyramid(height, baseSize, particleCount, innerPyramid = false) { 177 const geometry = new THREE.BufferGeometry(); 178 const positions = []; 179 const particleColors = []; 180 const originalColors = []; 181 const twinkleFactors = []; 182 const initialSizes = []; 183 const baseParticleSize = innerPyramid ? 0.02 : 0.03; 184 185 for (let i = 0; i < particleCount; i++) { 186 const t = Math.random(); 187 const u = Math.random(); 188 const apex = { x: 0, y: height, z: 0 }; 189 const base = [ 190 { x: -baseSize, y: 0, z: -baseSize }, 191 { x: baseSize, y: 0, z: -baseSize }, 192 { x: baseSize, y: 0, z: baseSize }, 193 { x: -baseSize, y: 0, z: baseSize } 194 ]; 195 const face = Math.floor(Math.random() * 4); 196 const basePoint1 = base[face]; 197 const basePoint2 = base[(face + 1) % 4]; 198 const x = (1 - t) * ((1 - u) * basePoint1.x + u * basePoint2.x) + t * apex.x; 199 const y = (1 - t) * 0 + t * height; 200 const z = (1 - t) * ((1 - u) * basePoint1.z + u * basePoint2.z) + t * apex.z; 201 positions.push(x, y, z); 202 particleColors.push(0, 0, 0); 203 originalColors.push(new THREE.Color()); 204 initialSizes.push(baseParticleSize); 205 if (Math.random() < 0.2) { 206 twinkleFactors.push(Math.random() * 2 + 1.0); 207 } else { 208 twinkleFactors.push(0); 209 } 210 } 211 212 geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); 213 geometry.setAttribute('color', new THREE.Float32BufferAttribute(particleColors, 3)); 214 geometry.setAttribute('size', new THREE.Float32BufferAttribute(initialSizes, 1)); 215 216 const material = new THREE.PointsMaterial({ 217 vertexColors: true, 218 size: baseParticleSize, 219 sizeAttenuation: true, 220 transparent: true, 221 opacity: 0.9, 222 blending: THREE.AdditiveBlending, 223 depthWrite: false 224 }); 225 226 const particles = new THREE.Points(geometry, material); 227 return { particles, originalColors, twinkleFactors, baseParticleSize }; 228} 229 230function updatePyramidColors(pyramidData, newColors) { 231 const { particles, originalColors } = pyramidData; 232 const positions = particles.geometry.attributes.position; 233 const colors = particles.geometry.attributes.color; 234 const height = particles.parent.userData.height; 235 236 for (let i = 0; i < positions.count; i++) { 237 const y = positions.getY(i); 238 const colorPos = y / height; 239 const idx = Math.min(newColors.length - 2, Math.floor(colorPos * (newColors.length - 1))); 240 const c1 = newColors[idx]; 241 const c2 = newColors[idx + 1]; 242 const mix = (colorPos * (newColors.length - 1)) % 1; 243 const finalColor = new THREE.Color().lerpColors(c1, c2, mix); 244 colors.setXYZ(i, finalColor.r, finalColor.g, finalColor.b); 245 originalColors[i].copy(finalColor); 246 } 247 colors.needsUpdate = true; 248} 249 250function setPalette(theme) { 251 updatePyramidColors(outerPyramidData, theme.outer); 252 outerEdges.material.color.set(theme.outerEdge); 253 updatePyramidColors(innerPyramidData, theme.inner); 254 innerEdges.material.color.set(theme.innerEdge); 255} 256 257function createPyramidEdges(height, baseSize, color) { 258 const points = []; 259 const apex = new THREE.Vector3(0, height, 0); 260 const verts = [ 261 new THREE.Vector3(-baseSize, 0, -baseSize), 262 new THREE.Vector3(baseSize, 0, -baseSize), 263 new THREE.Vector3(baseSize, 0, baseSize), 264 new THREE.Vector3(-baseSize, 0, baseSize) 265 ]; 266 for (let i = 0; i < 4; i++) { 267 points.push(apex.x, apex.y, apex.z, verts[i].x, verts[i].y, verts[i].z); 268 points.push(verts[i].x, verts[i].y, verts[i].z, verts[(i + 1) % 4].x, verts[(i + 1) % 4].y, verts[(i + 1) % 4].z); 269 } 270 const geo = new THREE.BufferGeometry(); 271 geo.setAttribute('position', new THREE.Float32BufferAttribute(points, 3)); 272 const mat = new THREE.LineBasicMaterial({ 273 color: color, 274 linewidth: 2, 275 transparent: true, 276 opacity: 0.9, 277 blending: THREE.AdditiveBlending, 278 depthWrite: false 279 }); 280 return new THREE.LineSegments(geo, mat); 281} 282 283const pyramidGroup = new THREE.Group(); 284scene.add(pyramidGroup); 285 286const outerPyramidData = createParticlePyramid(2.5, 1.8, 5000); 287const outerEdges = createPyramidEdges(2.5, 1.8, new THREE.Color()); 288const outerGroup = new THREE.Group(); 289outerGroup.userData.height = 2.5; 290outerGroup.add(outerPyramidData.particles); 291 292const innerPyramidData = createParticlePyramid(1.5, 1.0, 3000, true); 293const innerEdges = createPyramidEdges(1.5, 1.0, new THREE.Color()); 294const innerGroup = new THREE.Group(); 295innerGroup.userData.height = 1.5; 296innerGroup.add(innerPyramidData.particles); 297 298pyramidGroup.add(outerGroup, outerEdges, innerGroup, innerEdges); 299 300const lightningMaterial = new THREE.ShaderMaterial({ 301 uniforms: { uTime: { value: 0 }, uLife: { value: 0 }, uFlicker: { value: 1.0 } }, 302 vertexShader: ` 303 varying vec2 vUv; 304 void main() { 305 vUv = uv; 306 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 307 } 308 `, 309 fragmentShader: ` 310 uniform float uTime, uLife, uFlicker; 311 varying vec2 vUv; 312 float random(vec2 st) { 313 return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453); 314 } 315 float noise(vec2 st) { 316 vec2 i = floor(st), f = fract(st); 317 float a = random(i), b = random(i + vec2(1,0)), c = random(i + vec2(0,1)), d = random(i + vec2(1,1)); 318 vec2 u = f*f*(3.0-2.0*f); 319 return mix(a,b,u.x) + (c-a)*u.y*(1.0-u.x) + (d-b)*u.x*u.y; 320 } 321 void main() { 322 float core = smoothstep(0.4, 0.0, abs(vUv.x - 0.5)); 323 core += noise(vec2(vUv.y*40.0, uTime*2.0)) * noise(vec2(vUv.y*25.0, uTime*1.5)) * 0.8; 324 vec3 color = mix(vec3(0.1,0.5,1.0), vec3(0.6,0.2,1.0), core*0.7); 325 color = mix(color, vec3(1.0), pow(core, 2.0)*0.9); 326 float lifeAlpha = smoothstep(0.0, 0.2, uLife) * (1.0 - smoothstep(0.6, 1.0, uLife)); 327 float intense = sin(uLife * 3.14159 * 3.0) * 0.5 + 0.5; 328 float alpha = pow(1.0 - abs(vUv.x - 0.5)*2.0, 2.0) * lifeAlpha * uFlicker * intense; 329 gl_FragColor = vec4(color, alpha); 330 } 331 `, 332 transparent: true, 333 blending: THREE.AdditiveBlending, 334 depthWrite: false 335}); 336 337function triggerShardBurst(colors) { 338 if (effectGroups.shards.children.length) return; 339 effectData.shards.isActive = true; 340 for (let i = 0; i < 300; i++) { 341 const geo = new THREE.ConeGeometry(0.015, 0.5, 4); 342 const col = colors[Math.floor(Math.random() * colors.length)]; 343 const mat = new THREE.MeshBasicMaterial({ color: col, blending: THREE.AdditiveBlending, transparent: true, opacity: 1.0 }); 344 const shard = new THREE.Mesh(geo, mat); 345 const origin = new THREE.Vector3(0, 1.5, 0); 346 shard.position.copy(origin); 347 const dir = new THREE.Vector3((Math.random()-0.5),(Math.random()-0.5),(Math.random()-0.5)).normalize(); 348 shard.lookAt(dir.clone().add(origin)); 349 shard.rotateX(Math.PI/2); 350 shard.userData.velocity = dir.multiplyScalar(0.08 + Math.random() * 0.12); 351 shard.userData.life = 1.0; 352 effectGroups.shards.add(shard); 353 } 354} 355 356function triggerRings(colors) { 357 if (effectGroups.rings.children.length) return; 358 effectData.rings.isActive = true; 359 for (let r = 0; r < 5; r++) { 360 const ringGeo = new THREE.RingGeometry(0.5 + r*0.3, 0.6 + r*0.3, 32); 361 const col = colors[r % colors.length]; 362 const mat = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 1.0, blending: THREE.AdditiveBlending, side: THREE.DoubleSide }); 363 const ring = new THREE.Mesh(ringGeo, mat); 364 ring.position.set(0,1.5,0); 365 ring.rotation.x = Math.PI/2; 366 ring.userData.speed = 0.03 + r*0.02; 367 ring.userData.life = 1.0; 368 effectGroups.rings.add(ring); 369 } 370} 371 372function triggerSpiral(colors) { 373 if (effectGroups.spiral.children.length) return; 374 effectData.spiral.isActive = true; 375 for (let i = 0; i < 200; i++) { 376 const geo = new THREE.SphereGeometry(0.02, 8, 6); 377 const col = colors[i % colors.length]; 378 const mat = new THREE.MeshBasicMaterial({ color: col, blending: THREE.AdditiveBlending, transparent: true, opacity: 1.0 }); 379 const p = new THREE.Mesh(geo, mat); 380 const angle = (i/200)*Math.PI*8; 381 const radius = 0.1 + (i/200)*2; 382 p.position.set(Math.cos(angle)*radius, 1.5 + (i/200)*2, Math.sin(angle)*radius); 383 p.userData.angle = angle; 384 p.userData.radius = radius; 385 p.userData.life = 1.0; 386 p.userData.speed = 0.05; 387 effectGroups.spiral.add(p); 388 } 389} 390 391function triggerFlare(colors) { 392 if (effectGroups.flare.children.length) return; 393 effectData.flare.isActive = true; 394 for (let i = 0; i < 200; i++) { 395 const geo = new THREE.PlaneGeometry(0.08 + Math.random()*0.04, 0.4 + Math.random()*0.3); 396 const col = colors[Math.floor(Math.random()*colors.length)]; 397 const mat = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 1.0, blending: THREE.AdditiveBlending }); 398 const f = new THREE.Mesh(geo, mat); 399 f.position.set(0,2.5,0); 400 const dir = new THREE.Vector3((Math.random()-0.5)*2, -Math.random()*0.8, (Math.random()-0.5)*2).normalize(); 401 f.lookAt(dir.clone().add(f.position)); 402 f.userData.velocity = dir.multiplyScalar(0.05 + Math.random()*0.10); 403 f.userData.life = 1.0; 404 f.userData.initialVelocity = f.userData.velocity.clone(); 405 effectGroups.flare.add(f); 406 } 407} 408 409document.getElementById('effect-trigger').addEventListener('click', () => { 410 if (Object.values(effectData).some(d => d.isActive)) return; 411 const theme = effectThemes[effectState]; 412 setPalette(theme); 413 document.getElementById('effect-info').textContent = theme.name; 414 switch (theme.effect) { 415 case 'lightning': 416 if (effectGroups.lightning) { 417 effectGroups.lightning.traverse(c => { if (c.isMesh) c.geometry.dispose(); }); 418 pyramidGroup.remove(effectGroups.lightning); 419 } 420 effectGroups.lightning = createEnhancedLightningBolt(); 421 pyramidGroup.add(effectGroups.lightning); 422 effectData.lightning.isActive = true; 423 effectData.lightning.startTime = clock.getElapsedTime(); 424 bloomPass.strength = 4.5; 425 setTimeout(() => bloomPass.strength = 2.5, 400); 426 break; 427 case 'shards': 428 triggerShardBurst(theme.shardColors); 429 bloomPass.strength = 5.5; 430 setTimeout(() => bloomPass.strength = 2.5, 600); 431 break; 432 case 'rings': 433 triggerRings(theme.ringColors); 434 bloomPass.strength = 4.0; 435 setTimeout(() => bloomPass.strength = 2.5, 500); 436 break; 437 case 'spiral': 438 triggerSpiral(theme.spiralColors); 439 bloomPass.strength = 3.5; 440 setTimeout(() => bloomPass.strength = 2.5, 700); 441 break; 442 case 'flare': 443 triggerFlare(theme.flareColors); 444 bloomPass.strength = 5.5; 445 setTimeout(() => bloomPass.strength = 2.5, 600); 446 break; 447 } 448 effectState = (effectState + 1) % effectThemes.length; 449}); 450 451function applySparkle(pData, t) { 452 const { particles, originalColors, twinkleFactors, baseParticleSize } = pData; 453 const colors = particles.geometry.attributes.color; 454 const sizes = particles.geometry.attributes.size; 455 for (let i = 0; i < colors.count; i++) { 456 if (twinkleFactors[i] > 0) { 457 const pulse = Math.pow(Math.abs(Math.sin(twinkleFactors[i]*t + i*0.1)), 10); 458 const brightness = 1.0 + 2.0*pulse; 459 const sizePulse = 1.0 + 3.0*pulse; 460 const oc = originalColors[i]; 461 colors.setXYZ(i, oc.r*brightness, oc.g*brightness, oc.b*brightness); 462 sizes.setX(i, baseParticleSize*sizePulse); 463 } 464 } 465 colors.needsUpdate = true; 466 sizes.needsUpdate = true; 467} 468 469function animateLightning(totalTime) { 470 const lt = totalTime - effectData.lightning.startTime; 471 const life = lt / effectData.lightning.duration; 472 effectGroups.lightning.traverse(c => { 473 if (c.isMesh && c.material.uniforms) { 474 c.material.uniforms.uTime.value = totalTime; 475 c.material.uniforms.uLife.value = life; 476 c.material.uniforms.uFlicker.value = Math.random()>0.05?1.0:0.0; 477 } 478 }); 479 if (life >= 1.0) { 480 effectData.lightning.isActive = false; 481 effectGroups.lightning.visible = false; 482 } 483} 484 485function animateShards() { 486 if (!effectGroups.shards.children.length) { effectData.shards.isActive = false; return; } 487 for (let i = effectGroups.shards.children.length - 1; i >= 0; i--) { 488 const s = effectGroups.shards.children[i]; 489 s.position.add(s.userData.velocity); 490 s.userData.life -= 0.015; 491 s.material.opacity = s.userData.life; 492 if (s.userData.life <= 0) { 493 s.geometry.dispose(); 494 s.material.dispose(); 495 effectGroups.shards.remove(s); 496 } 497 } 498} 499 500function animateRings() { 501 if (!effectGroups.rings.children.length) { effectData.rings.isActive = false; return; } 502 for (let i = effectGroups.rings.children.length - 1; i >= 0; i--) { 503 const r = effectGroups.rings.children[i]; 504 r.scale.x += r.userData.speed; 505 r.scale.y += r.userData.speed; 506 r.userData.life -= 0.01; 507 r.material.opacity = r.userData.life; 508 if (r.userData.life <= 0) { 509 r.geometry.dispose(); 510 r.material.dispose(); 511 effectGroups.rings.remove(r); 512 } 513 } 514} 515 516function animateSpiral() { 517 if (!effectGroups.spiral.children.length) { effectData.spiral.isActive = false; return; } 518 for (let i = effectGroups.spiral.children.length - 1; i >= 0; i--) { 519 const p = effectGroups.spiral.children[i]; 520 p.userData.angle += p.userData.speed; 521 p.userData.radius += 0.02; 522 p.position.x = Math.cos(p.userData.angle)*p.userData.radius; 523 p.position.z = Math.sin(p.userData.angle)*p.userData.radius; 524 p.position.y += 0.03; 525 p.userData.life -= 0.008; 526 p.material.opacity = p.userData.life; 527 if (p.userData.life <= 0) { 528 p.geometry.dispose(); 529 p.material.dispose(); 530 effectGroups.spiral.remove(p); 531 } 532 } 533} 534 535function animateFlare() { 536 if (!effectGroups.flare.children.length) { effectData.flare.isActive = false; return; } 537 for (let i = effectGroups.flare.children.length - 1; i >= 0; i--) { 538 const f = effectGroups.flare.children[i]; 539 f.position.add(f.userData.velocity); 540 f.userData.velocity.y -= 0.0008; 541 f.userData.velocity.x += (Math.random()-0.5)*0.001; 542 f.userData.life -= 0.010; 543 const flick = 0.85 + Math.sin(time*15 + i*0.5)*0.15; 544 f.material.opacity = f.userData.life * flick; 545 if (f.userData.life <= 0) { 546 f.geometry.dispose(); 547 f.material.dispose(); 548 effectGroups.flare.remove(f); 549 } 550 } 551} 552 553window.addEventListener('resize', () => { 554 const w = window.innerWidth; 555 const h = window.innerHeight; 556 camera.aspect = w / h; 557 camera.updateProjectionMatrix(); 558 renderer.setSize(w, h); 559 composer.setSize(w, h); 560}); 561 562setPalette(effectThemes[0]); 563 564function createCylinder(start, end, radius) { 565 const dir = new THREE.Vector3().subVectors(end, start); 566 const orient = new THREE.Matrix4(); 567 const rot = new THREE.Matrix4(); 568 orient.lookAt(start, end, new THREE.Object3D().up); 569 rot.makeRotationX(Math.PI*0.5); 570 orient.multiply(rot); 571 const geo = new THREE.CylinderGeometry(radius, radius, dir.length(), 8, 1, true); 572 geo.applyMatrix4(orient); 573 geo.translate((start.x+end.x)/2, (start.y+end.y)/2, (start.z+end.z)/2); 574 return geo; 575} 576 577function createEnhancedLightningBolt() { 578 const group = new THREE.Group(); 579 const origin = new THREE.Vector3(0, 2.8, 0); 580 581 function branch(start, dir, energy, depth) { 582 if (energy < 0.3 || depth > 15) return; 583 const len = (Math.random()*0.7+0.3)*energy*0.6; 584 const end = start.clone().add(dir.clone().multiplyScalar(len)); 585 const rad = 0.005 + (energy/120) + Math.random()*0.005; 586 const seg = createCylinder(start, end, rad); 587 const mesh = new THREE.Mesh(seg, lightningMaterial.clone()); 588 group.add(mesh); 589 const nextE = energy*(0.85+Math.random()*0.1); 590 const nextDir = dir.clone().add(new THREE.Vector3((Math.random()-0.5)*4.5,(Math.random()-0.5)*2.5,(Math.random()-0.5)*4.5)).normalize(); 591 branch(end, nextDir, nextE, depth+1); 592 if (Math.random()<0.6 && depth>0) { 593 const bDir = new THREE.Vector3((Math.random()-0.5)*6.0,(Math.random()-0.5)*4.0,(Math.random()-0.5)*6.0).normalize(); 594 branch(end, bDir, nextE*0.7, depth+1); 595 } 596 } 597 598 const n = Math.floor(Math.random()*4)+7; 599 for (let i = 0; i < n; i++) { 600 const d = new THREE.Vector3((Math.random()-0.5)*3.5, Math.random()*0.7+0.3, (Math.random()-0.5)*3.5).normalize(); 601 branch(origin, d, 7, 0); 602 } 603 return group; 604} 605 606function animate() { 607 requestAnimationFrame(animate); 608 const et = clock.getElapsedTime(); 609 const dt = clock.getDelta(); 610 time += dt; 611 pyramidGroup.rotation.y += 0.005; 612 innerGroup.rotation.y -= 0.015; 613 applySparkle(outerPyramidData, time); 614 applySparkle(innerPyramidData, time); 615 if (effectData.lightning.isActive) animateLightning(et); 616 if (effectData.shards.isActive) animateShards(); 617 if (effectData.rings.isActive) animateRings(); 618 if (effectData.spiral.isActive) animateSpiral(); 619 if (effectData.flare.isActive) animateFlare(); 620 controls.update(); 621 composer.render(); 622} 623 624animate(); 625</script> 626 627 </body> 628 629</html>

Love this component?

Explore more components and build amazing UIs.

View All Components