| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Clear Outward Explosion - Low Lag</title> |
| <style> |
| body, html { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; font-family:system-ui,sans-serif; } |
| #canvas-container { position:absolute; inset:0; z-index:1; } |
| #flash { |
| position:absolute; inset:0; background:linear-gradient(#ff450033, #8b000022); opacity:0; |
| z-index:5; pointer-events:none; transition:opacity 0.18s ease-out; |
| } |
| #flash.active { opacity:0.65; } |
| #flash.fade-out { opacity:0; transition:opacity 2s ease-out; } |
| #title-container { |
| position:absolute; inset:0; display:flex; align-items:center; justify-content:center; z-index:10; pointer-events:none; |
| } |
| #title { |
| color:#ffcc00; font-size:12vw; font-weight:900; text-transform:uppercase; letter-spacing:0.25em; |
| text-shadow:0 0 25px #ff6600aa, 0 0 60px #ff3300aa; |
| opacity:0; transform:scale(2.8); filter:blur(30px); |
| transition:all 0.8s cubic-bezier(0.15,1.4,0.3,1.1); |
| } |
| #title.revealed { opacity:0.92; transform:scale(1); filter:blur(0); } |
| #replay { |
| position:absolute; bottom:90px; left:50%; transform:translateX(-50%); |
| padding:18px 55px; background:transparent; color:#ffeb3b; border:1px solid #ff980055; |
| border-radius:50px; font-size:18px; letter-spacing:4px; text-transform:uppercase; cursor:pointer; |
| z-index:20; opacity:0; pointer-events:none; transition:all 0.9s; |
| } |
| #replay.visible { opacity:1; pointer-events:auto; } |
| #replay:hover { color:#fff; border-color:#ff9800; background:rgba(255,152,0,0.2); } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="canvas-container"></div> |
| <div id="flash"></div> |
| <div id="title-container"><h1 id="title">BOOM</h1></div> |
| <button id="replay">RETRY</button> |
|
|
| <script type="importmap"> |
| { |
| "imports": { |
| "three": "https://cdn.jsdelivr.net/npm/three@0.168.0/build/three.module.js", |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.168.0/examples/jsm/" |
| } |
| } |
| </script> |
|
|
| <script type="module"> |
| import * as THREE from 'three'; |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; |
| |
| const container = document.getElementById('canvas-container'); |
| const flashEl = document.getElementById('flash'); |
| const titleEl = document.getElementById('title'); |
| const replayBtn = document.getElementById('replay'); |
| |
| const scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x0a0014); |
| |
| const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.5, 4000); |
| camera.position.set(0, 0, 14); |
| |
| const renderer = new THREE.WebGLRenderer({antialias:false, powerPreference:"high-performance"}); |
| renderer.setSize(innerWidth, innerHeight); |
| renderer.setPixelRatio(Math.min(devicePixelRatio, 1.3)); |
| container.appendChild(renderer.domElement); |
| |
| const composer = new EffectComposer(renderer); |
| composer.addPass(new RenderPass(scene, camera)); |
| |
| const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.7, 0.2, 0.85); |
| bloom.threshold = 0.7; |
| composer.addPass(bloom); |
| |
| |
| const COUNT = 2000; |
| const geo = new THREE.BufferGeometry(); |
| const pos = new Float32Array(COUNT*3); |
| const vel = new Float32Array(COUNT*3); |
| const col = new Float32Array(COUNT*3); |
| const siz = new Float32Array(COUNT); |
| const birth = new Float32Array(COUNT); |
| |
| const palette = [ |
| new THREE.Color(0xff6600), |
| new THREE.Color(0xff3300), |
| new THREE.Color(0xff9900), |
| new THREE.Color(0xffcc00), |
| new THREE.Color(0xff4422) |
| ]; |
| |
| for(let i = 0; i < COUNT; i++){ |
| |
| pos[i*3 ] = (Math.random() - 0.5) * 0.6; |
| pos[i*3+1] = (Math.random() - 0.5) * 0.6; |
| pos[i*3+2] = (Math.random() - 0.5) * 0.6; |
| |
| const dist = Math.hypot(pos[i*3], pos[i*3+1], pos[i*3+2]) || 0.01; |
| |
| |
| let dx = pos[i*3 ] / dist; |
| let dy = pos[i*3+1] / dist; |
| let dz = pos[i*3+2] / dist; |
| const speed = 18 + dist * 45 + Math.random() * 15; |
| vel[i*3 ] = dx * speed; |
| vel[i*3+1] = dy * speed; |
| vel[i*3+2] = dz * speed; |
| |
| const c = palette[Math.floor(Math.random() * palette.length)]; |
| const bright = 0.65 + Math.random() * 0.35; |
| col[i*3] = c.r * bright; |
| col[i*3+1] = c.g * bright; |
| col[i*3+2] = c.b * bright; |
| |
| siz[i] = 9 + Math.random() * 14; |
| |
| |
| birth[i] = - (dist * 1.2 + Math.random() * 0.5); |
| } |
| |
| geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); |
| geo.setAttribute('velocity', new THREE.BufferAttribute(vel, 3)); |
| geo.setAttribute('color', new THREE.BufferAttribute(col, 3)); |
| geo.setAttribute('size', new THREE.BufferAttribute(siz, 1)); |
| geo.setAttribute('birthTime', new THREE.BufferAttribute(birth, 1)); |
| |
| const mat = new THREE.ShaderMaterial({ |
| uniforms: { uTime: {value:0} }, |
| vertexShader: ` |
| attribute vec3 velocity; |
| attribute vec3 color; |
| attribute float size; |
| attribute float birthTime; |
| varying vec3 vColor; |
| varying float vAlpha; |
| uniform float uTime; |
| void main(){ |
| vColor = color; |
| float age = uTime - birthTime; |
| if(age < 0.0){ gl_Position=vec4(10000.0); vAlpha=0.0; return; } |
| |
| float t = clamp(age * 1.3, 0.0, 1.0); |
| float eased = 1.0 - pow(1.0 - t, 2.5); // quick start, then decelerate |
| |
| vec3 p = position + velocity * eased * 0.9; |
| |
| float scale = 1.0 + (1.0 - eased) * 10.0; // bigger at birth |
| |
| vec4 mv = modelViewMatrix * vec4(p, 1.0); |
| gl_PointSize = size * scale * (280.0 / -mv.z); |
| gl_Position = projectionMatrix * mv; |
| |
| vAlpha = 1.0 - eased * eased; // smooth fade |
| } |
| `, |
| fragmentShader: ` |
| varying vec3 vColor; |
| varying float vAlpha; |
| void main(){ |
| vec2 c = gl_PointCoord - 0.5; |
| float d = length(c); |
| if(d > 0.5) discard; |
| gl_FragColor = vec4(vColor, (1.0 - d*d*1.8) * vAlpha * 0.95); |
| } |
| `, |
| transparent: true, |
| blending: THREE.AdditiveBlending, |
| depthWrite: false |
| }); |
| |
| scene.add(new THREE.Points(geo, mat)); |
| |
| |
| const clock = new THREE.Clock(); |
| let time = 0; |
| |
| function resetExplosion() { |
| time = 0; |
| titleEl.classList.remove('revealed'); |
| flashEl.classList.remove('active','fade-out'); |
| replayBtn.classList.remove('visible'); |
| const bt = geo.attributes.birthTime.array; |
| for(let i = 0; i < COUNT; i++) { |
| const dist = Math.hypot(pos[i*3], pos[i*3+1], pos[i*3+2]) || 0.01; |
| bt[i] = - (dist * 1.2 + Math.random() * 0.5); |
| } |
| geo.attributes.birthTime.needsUpdate = true; |
| } |
| |
| function explode() { |
| resetExplosion(); |
| |
| setTimeout(() => { |
| flashEl.classList.add('active'); |
| bloom.strength = 1.3; |
| |
| setTimeout(() => flashEl.classList.add('fade-out'), 500); |
| |
| setTimeout(() => { |
| bloom.strength = 0.55; |
| titleEl.classList.add('revealed'); |
| setTimeout(() => replayBtn.classList.add('visible'), 200); |
| }, 70); |
| }, 30); |
| } |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| time += clock.getDelta(); |
| mat.uniforms.uTime.value = time; |
| |
| if(time > 3.5) bloom.strength = 0.55 + Math.sin(time * 1.1) * 0.1; |
| |
| composer.render(); |
| } |
| |
| animate(); |
| explode(); |
| |
| replayBtn.addEventListener('click', explode); |
| |
| window.addEventListener('resize', () => { |
| camera.aspect = innerWidth / innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(innerWidth, innerHeight); |
| composer.setSize(innerWidth, innerHeight); |
| }); |
| </script> |
| </body> |
| </html> |