| | <!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> |