| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover" /> |
| <title>Space-X Debris Explosion Shader</title> |
| <style> |
| :root { |
| color-scheme: dark; |
| } |
| |
| html, |
| body { |
| margin: 0; |
| height: 100%; |
| background: #000; |
| overflow: hidden; |
| font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; |
| } |
| |
| #wrap { |
| position: relative; |
| width: 100vw; |
| height: 100vh; |
| background: radial-gradient(1200px 800px at 50% 60%, #04060a 0%, #020307 55%, #010205 75%, #000 100%); |
| } |
| |
| canvas { |
| position: absolute; |
| inset: 0; |
| width: 100%; |
| height: 100%; |
| display: block; |
| } |
| |
| #overlay { |
| position: absolute; |
| top: 12px; |
| left: 12px; |
| right: 12px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| gap: 12px; |
| z-index: 10; |
| pointer-events: none; |
| } |
| |
| #title { |
| font-size: clamp(12px, 2vw, 16px); |
| letter-spacing: .08em; |
| text-transform: uppercase; |
| opacity: .8; |
| color: #a9b7d1; |
| pointer-events: auto; |
| } |
| |
| #title a { |
| color: #9ad4ff; |
| text-decoration: none; |
| border-bottom: 1px dashed rgba(154, 212, 255, .35); |
| } |
| |
| #controls { |
| pointer-events: auto; |
| display: flex; |
| gap: 8px; |
| } |
| |
| button { |
| background: rgba(255, 255, 255, .06); |
| color: #d9e3ff; |
| border: 1px solid rgba(255, 255, 255, .12); |
| border-radius: 10px; |
| padding: 8px 12px; |
| font-size: 12px; |
| cursor: pointer; |
| backdrop-filter: blur(6px); |
| transition: all .2s ease; |
| } |
| |
| button:hover { |
| background: rgba(255, 255, 255, .1); |
| transform: translateY(-1px); |
| } |
| |
| #hint { |
| position: absolute; |
| bottom: 12px; |
| right: 12px; |
| color: #b9c6e6; |
| opacity: .65; |
| font-size: 12px; |
| padding: 6px 10px; |
| border: 1px solid rgba(255, 255, 255, .08); |
| border-radius: 8px; |
| background: rgba(0, 0, 0, .25); |
| backdrop-filter: blur(6px); |
| pointer-events: none; |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div id="wrap"> |
| <canvas id="gl"></canvas> |
| <div id="overlay"> |
| <div id="title"> |
| Debris After a Space-X Rocket Explosion |
| | Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" |
| rel="noopener noreferrer">anycoder</a> |
| </div> |
| <div id="controls"> |
| <button id="btn-explode" title="Spawn a new explosion (E)">Explode</button> |
| <button id="btn-toggle" title="Pause/Resume (Space)">Pause</button> |
| <button id="btn-reset" title="Reset particles (R)">Reset</button> |
| </div> |
| </div> |
| <div id="hint">Click or press E to explode • Space to pause • R to reset</div> |
| </div> |
|
|
| <script> |
| (function(){ |
| const canvas = document.getElementById('gl'); |
| const gl = canvas.getContext('webgl', { antialias: false, alpha: false, preserveDrawingBuffer: false }); |
| if (!gl) { |
| alert('WebGL not supported on this device/browser.'); |
| return; |
| } |
| |
| |
| const DPR = Math.min(2, window.devicePixelRatio || 1); |
| let W = 0, H = 0; |
| let time = 0, lastT = 0, paused = false; |
| |
| |
| const G = 0.10; |
| const DRAG = 0.995; |
| const MAX_PARTICLES = 14000; |
| const MIN_PARTICLES = 4000; |
| let PARTICLE_COUNT = MAX_PARTICLES; |
| |
| |
| let origin = { x: 0, y: 0 }; |
| |
| |
| let pLife, pPos, pVel, pSize, pSeed; |
| |
| |
| let STAR_COUNT = 600; |
| let starPos, starPhase, starSize; |
| |
| |
| let shockActive = false; |
| let shockStart = 0; |
| let shockDuration = 6.0; |
| let shockMaxRadius = 0.0; |
| let shockThickness = 0.06; |
| |
| |
| let trailFbo = null; |
| let trailTex = null; |
| |
| |
| const rand = (a=0,b=1)=>a+Math.random()*(b-a); |
| const clamp = (x, a, b)=>Math.max(a, Math.min(b, x)); |
| const mix = (a,b,t)=>a*(1-t)+b*t; |
| |
| |
| const particleVS = ` |
| attribute vec2 a_pos; |
| attribute vec2 a_vel; |
| attribute float a_size; |
| attribute float a_seed; |
| |
| uniform vec2 u_origin; |
| uniform vec2 u_scale; |
| uniform float u_aspect; |
| uniform float u_time; |
| |
| varying float v_speed; |
| varying float v_seed; |
| varying float v_size; |
| |
| void main() { |
| vec2 dir = normalize(a_vel + vec2(1e-6)); |
| float speed = length(a_vel); |
| v_speed = speed; |
| v_seed = a_seed; |
| v_size = a_size; |
| |
| vec2 world = a_pos; |
| vec2 clip = (world - u_origin) * u_scale; |
| gl_Position = vec4(clip, 0.0, 1.0); |
| gl_PointSize = a_size; |
| } |
| `; |
| |
| const particleFS = ` |
| precision mediump float; |
| |
| uniform float u_time; |
| |
| varying float v_speed; |
| varying float v_seed; |
| varying float v_size; |
| |
| // 2D rotation |
| mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); } |
| |
| float hash21(vec2 p){ |
| p = fract(p*vec2(123.34, 345.45)); |
| p += dot(p, p+34.345); |
| return fract(p.x*p.y); |
| } |
| |
| void main() { |
| // Point sprite coord |
| vec2 uv = gl_PointCoord*2.0 - 1.0; |
| |
| // Flicker rotation for anisotropy |
| float ang = v_seed*6.2831 + u_time*2.0; |
| uv = rot(ang) * uv; |
| |
| // Elliptical falloff: elongated along x |
| float e = 0.42; |
| float d = dot(vec2(uv.x/e, uv.y), vec2(uv.x/e, uv.y)); |
| float alpha = smoothstep(1.0, 0.0, d); |
| |
| // Spark core |
| float core = smoothstep(0.20, 0.0, d); |
| alpha = max(alpha, core*0.75); |
| |
| // Color by speed: white->yellow->orange->red |
| float t = clamp(v_speed*0.6, 0.0, 1.0); |
| vec3 col = mix(vec3(1.0, 0.95, 0.90), vec3(1.0, 0.75, 0.25), smoothstep(0.0, 0.5, t)); |
| col = mix(col, vec3(1.0, 0.45, 0.10), smoothstep(0.5, 1.0, t)); |
| col = mix(col, vec3(0.9, 0.1, 0.05), smoothstep(0.9, 1.6, t)); |
| |
| // Slight color jitter |
| float n = hash21(vec2(v_seed*19.31, v_seed*91.7)); |
| col *= mix(0.9, 1.1, n); |
| |
| // Flicker |
| float flick = 0.85 + 0.25*sin(u_time*40.0 + v_seed*123.4); |
| alpha *= flick; |
| |
| gl_FragColor = vec4(col, alpha); |
| // Premultiplied-like glow via additive blend |
| } |
| `; |
| |
| const compositeVS = ` |
| attribute vec2 a_pos; |
| varying vec2 v_uv; |
| void main(){ |
| v_uv = a_pos*0.5 + 0.5; |
| gl_Position = vec4(a_pos, 0.0, 1.0); |
| } |
| `; |
| |
| const compositeFS = ` |
| precision mediump float; |
| varying vec2 v_uv; |
| |
| uniform sampler2D u_prevTrail; |
| uniform sampler2D u_curr; |
| uniform float u_decay; |
| |
| uniform vec2 u_resolution; |
| uniform float u_time; |
| uniform float u_shockStart; |
| uniform float u_shockDuration; |
| |
| uniform int u_starCount; |
| uniform vec2 u_starPos[1200]; |
| uniform float u_starPhase[1200]; |
| uniform float u_starSize[1200]; |
| |
| // Starfield function |
| float hash(float x){ return fract(sin(x)*43758.5453123); } |
| vec3 starfield(vec2 uv, float aspect){ |
| // Tile to reduce cost |
| vec2 grid = uv*vec2(aspect, 1.0)*400.0; |
| vec2 id = floor(grid); |
| vec2 gv = fract(grid) - 0.5; |
| |
| float n = hash(id.x + id.y*57.0); |
| vec3 col = vec3(0.0); |
| |
| // Sparse stars |
| if (n > 0.997) { |
| float tw = 0.6 + 0.4*sin(u_time*2.0 + n*123.0); |
| float size = mix(0.5, 1.5, hash(n*13.7)); |
| float d = length(gv); |
| float s = smoothstep(size, 0.0, d); |
| col += vec3(1.0, 1.0, 1.0) * s * tw; |
| } |
| return col; |
| } |
| |
| // Shockwave ring |
| float ring(vec2 uv, vec2 center, float radius, float thickness){ |
| float d = length(uv - center); |
| float inner = smoothstep(radius - thickness, radius, d); |
| float outer = 1.0 - smoothstep(radius, radius + thickness, d); |
| float ringv = inner * outer; |
| // fade ends |
| return ringv; |
| } |
| |
| // Background nebula (cheap multi-noise) |
| float noise(vec2 p){ |
| vec2 i = floor(p); |
| vec2 f = fract(p); |
| float a = hash(i.x + i.y*57.0); |
| float b = hash(i.x+1.0 + i.y*57.0); |
| float c = hash(i.x + (i.y+1.0)*57.0); |
| float d = hash(i.x+1.0 + (i.y+1.0)*57.0); |
| vec2 u = f*f*(3.0-2.0*f); |
| return mix(mix(a,b,u.x), mix(c,d,u.x), u.y); |
| } |
| vec3 nebula(vec2 uv, float t){ |
| uv *= 2.5; |
| float n = 0.0; |
| n += noise(uv + vec2(t*0.03, t*0.01)); |
| n += 0.5*noise(uv*2.0 - vec2(t*0.02, -t*0.015)); |
| n += 0.25*noise(uv*4.0 + vec2(t*0.01, t*0.02)); |
| n = pow(n, 1.2); |
| vec3 c = mix(vec3(0.05, 0.07, 0.10), vec3(0.10, 0.05, 0.15), n); |
| c += vec3(0.03, 0.02, 0.06)*n*n; |
| return c; |
| } |
| |
| void main(){ |
| // UV to NDC space (0..1) |
| vec2 uv = v_uv; |
| vec2 ndc = uv*2.0 - 1.0; |
| |
| // Background base |
| vec3 col = nebula(ndc, u_time); |
| |
| // Stars (procedural sparse) |
| float aspect = u_resolution.x / u_resolution.y; |
| vec3 stars = starfield(uv, aspect); |
| col += stars * 0.7; |
| |
| // Shockwave ring |
| if (u_time < u_shockDuration) { |
| float radius = (u_time - u_shockStart) * 0.9; // world units to NDC |
| // convert center |
| vec2 c = vec2(0.0); // origin in NDC |
| float ringv = ring(ndc, c, radius, 0.06); |
| vec3 ringCol = mix(vec3(0.5,0.8,1.0), vec3(1.0,0.9,0.6), smoothstep(0.0, 1.0, radius)); |
| col += ringCol * ringv * 1.2; |
| } |
| |
| // Trails: fade previous, add current |
| vec3 prev = texture2D(u_prevTrail, uv).rgb; |
| vec3 curr = texture2D(u_curr, uv).rgb; |
| |
| vec3 combined = prev * u_decay + curr; |
| |
| // Vignette |
| float vign = smoothstep(1.3, 0.2, length(ndc)); |
| col *= vign; |
| |
| // Mix trails over background |
| col += combined; |
| |
| // Tone map and gamma |
| col = col / (1.0 + col); // simple Reinhard |
| col = pow(col, vec3(1.0/2.2)); |
| |
| gl_FragColor = vec4(col, 1.0); |
| } |
| `; |
| |
| |
| function createShader(type, src){ |
| const sh = gl.createShader(type); |
| gl.shaderSource(sh, src); |
| gl.compileShader(sh); |
| if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { |
| console.error(gl.getShaderInfoLog(sh)); |
| throw new Error('Shader compile error'); |
| } |
| return sh; |
| } |
| function createProgram(vsSrc, fsSrc){ |
| const vs = createShader(gl.VERTEX_SHADER, vsSrc); |
| const fs = createShader(gl.FRAGMENT_SHADER, fsSrc); |
| const pr = gl.createProgram(); |
| gl.attachShader(pr, vs); |
| gl.attachShader(pr, fs); |
| gl.linkProgram(pr); |
| if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) { |
| console.error(gl.getProgramInfoLog(pr)); |
| throw new Error('Program link error'); |
| } |
| return pr; |
| } |
| |
| |
| const particleProg = createProgram(particleVS, particleFS); |
| const particleLoc = { |
| a_pos: gl.getAttribLocation(particleProg, 'a_pos'), |
| a_vel: gl.getAttribLocation(particleProg, 'a_vel'), |
| a_size: gl.getAttribLocation(particleProg, 'a_size'), |
| a_seed: gl.getAttribLocation(particleProg, 'a_seed'), |
| u_origin: gl.getUniformLocation(particleProg, 'u_origin'), |
| u_scale: gl.getUniformLocation(particleProg, 'u_scale'), |
| u_aspect: gl.getUniformLocation(particleProg, 'u_aspect'), |
| u_time: gl.getUniformLocation(particleProg, 'u_time'), |
| }; |
| |
| const compositeProg = createProgram(compositeVS, compositeFS); |
| const compositeLoc = { |
| a_pos: gl.getAttribLocation(compositeProg, 'a_pos'), |
| u_prevTrail: gl.getUniformLocation(compositeProg, 'u_prevTrail'), |
| u_curr: gl.getUniformLocation(compositeProg, 'u_curr'), |
| u_decay: gl.getUniformLocation(compositeProg, 'u_decay'), |
| u_resolution: gl.getUniformLocation(compositeProg, 'u_resolution'), |
| u_time: gl.getUniformLocation(compositeProg, 'u_time'), |
| u_shockStart: gl.getUniformLocation(compositeProg, 'u_shockStart'), |
| u_shockDuration: gl.getUniformLocation(compositeProg, 'u_shockDuration'), |
| u_starCount: gl.getUniformLocation(compositeProg, 'u_starCount'), |
| u_starPos: gl.getUniformLocation(compositeProg, 'u_starPos'), |
| u_starPhase: gl.getUniformLocation(compositeProg, 'u_starPhase'), |
| u_starSize: gl.getUniformLocation(compositeProg, 'u_starSize'), |
| }; |
| |
| |
| const fsQuad = gl.createBuffer(); |
| gl.bindBuffer(gl.ARRAY_BUFFER, fsQuad); |
| |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ |
| -1, -1, |
| 3, -1, |
| -1, 3 |
| ]), gl.STATIC_DRAW); |
| |
| |
| const bufPos = gl.createBuffer(); |
| const bufVel = gl.createBuffer(); |
| const bufSize = gl.createBuffer(); |
| const bufSeed = gl.createBuffer(); |
| |
| |
| function createTrailTarget(w, h){ |
| const tex = gl.createTexture(); |
| gl.bindTexture(gl.TEXTURE_2D, tex); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
| gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); |
| |
| const fbo = gl.createFramebuffer(); |
| gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); |
| gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); |
| |
| const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); |
| if (status !== gl.FRAMEBUFFER_COMPLETE) { |
| console.error('FBO incomplete', status.toString(16)); |
| } |
| gl.bindFramebuffer(gl.FRAMEBUFFER, null); |
| |
| return { fbo, tex }; |
| } |
| |
| function resize(){ |
| const cssW = canvas.clientWidth || window.innerWidth; |
| const cssH = canvas.clientHeight || window.innerHeight; |
| const w = Math.floor(cssW * DPR); |
| const h = Math.floor(cssH * DPR); |
| if (w === W && h === H) return; |
| |
| W = w; H = h; |
| canvas.width = W; |
| canvas.height = H; |
| gl.viewport(0, 0, W, H); |
| |
| |
| if (trailTex) { |
| gl.deleteTexture(trailTex); |
| gl.deleteFramebuffer(trailFbo); |
| } |
| const t = createTrailTarget(W, H); |
| trailFbo = t.fbo; |
| trailTex = t.tex; |
| |
| |
| origin.x = 0; |
| origin.y = 0; |
| |
| |
| worldScale.x = 1.0 / (WORLD_RADIUS); |
| worldScale.y = 1.0 / (WORLD_RADIUS); |
| } |
| |
| |
| const WORLD_RADIUS = 2.2; |
| const worldScale = { x: 1/WORLD_RADIUS, y: 1/WORLD_RADIUS }; |
| |
| |
| function allocParticles(count){ |
| PARTICLE_COUNT = clamp(count|0, MIN_PARTICLES, MAX_PARTICLES); |
| pLife = new Float32Array(PARTICLE_COUNT); |
| pPos = new Float32Array(PARTICLE_COUNT * 2); |
| pVel = new Float32Array(PARTICLE_COUNT * 2); |
| pSize = new Float32Array(PARTICLE_COUNT); |
| pSeed = new Float32Array(PARTICLE_COUNT); |
| } |
| |
| function randomInCircle(radius){ |
| const t = Math.random()*Math.PI*2; |
| const r = Math.sqrt(Math.random())*radius; |
| return { x: Math.cos(t)*r, y: Math.sin(t)*r }; |
| } |
| |
| function hslToRgb(h, s, l) { |
| let r, g, b; |
| if (s === 0) { |
| r = g = b = l; |
| } else { |
| const hue2rgb = function hue2rgb(p, q, t){ |
| if (t < 0) t += 1; |
| if (t > 1) t -= 1; |
| if (t < 1/6) return p + (q - p) * 6 * t; |
| if (t < 1/2) return q; |
| if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; |
| return p; |
| }; |
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; |
| const p = 2 * l - q; |
| r = hue2rgb(p, q, h + 1/3); |
| g = hue2rgb(p, q, h); |
| b = hue2rgb(p, q, h - 1/3); |
| } |
| return [r, g, b]; |
| } |
| |
| function resetParticles(count){ |
| allocParticles(count); |
| for (let i = 0; i < PARTICLE_COUNT; i++) { |
| pSeed[i] = Math.random(); |
| const r = randomInCircle(0.15 + Math.random()*0.25); |
| pPos[i*2+0] = origin.x + r.x; |
| pPos[i*2+1] = origin.y + r.y; |
| |
| |
| const angle = Math.atan2(r.y, r.x); |
| const speed = 0.7 + Math.random()*2.6; |
| pVel[i*2+0] = Math.cos(angle) * speed; |
| pVel[i*2+1] = Math.sin(angle) * speed; |
| |
| |
| pSize[i] = (1.0 + Math.random()*3.5) * DPR * 1.2; |
| |
| |
| pLife[i] = 1.5 + Math.random()*5.0; |
| } |
| |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufSeed); |
| gl.bufferData(gl.ARRAY_BUFFER, pSeed, gl.STATIC_DRAW); |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufSize); |
| gl.bufferData(gl.ARRAY_BUFFER, pSize, gl.DYNAMIC_DRAW); |
| |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufPos); |
| gl.bufferData(gl.ARRAY_BUFFER, pPos, gl.DYNAMIC_DRAW); |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufVel); |
| gl.bufferData(gl.ARRAY_BUFFER, pVel, gl.DYNAMIC_DRAW); |
| } |
| |
| function respawn(i){ |
| pSeed[i] = Math.random(); |
| const r = randomInCircle(0.05 + Math.random()*0.18); |
| pPos[i*2+0] = origin.x + r.x; |
| pPos[i*2+1] = origin.y + r.y; |
| |
| const angle = Math.atan2(r.y, r.x); |
| const speed = 0.9 + Math.random()*2.8; |
| pVel[i*2+0] = Math.cos(angle) * speed; |
| pVel[i*2+1] = Math.sin(angle) * speed; |
| |
| pSize[i] = (1.0 + Math.random()*3.5) * DPR * 1.2; |
| pLife[i] = 1.8 + Math.random()*6.0; |
| } |
| |
| function initStars(count){ |
| STAR_COUNT = Math.min(1200, count|0); |
| starPos = new Float32Array(STAR_COUNT*2); |
| starPhase = new Float32Array(STAR_COUNT); |
| starSize = new Float32Array(STAR_COUNT); |
| for (let i = 0; i < STAR_COUNT; i++){ |
| |
| starPos[i*2+0] = rand(-1, 1); |
| starPos[i*2+1] = rand(-1, 1); |
| starPhase[i] = rand(0, Math.PI*2); |
| starSize[i] = rand(0.5, 1.5); |
| } |
| } |
| |
| function explode(){ |
| |
| shockActive = true; |
| shockStart = time; |
| |
| for (let i = 0; i < PARTICLE_COUNT; i++){ |
| const angle = Math.random()*Math.PI*2; |
| const speed = 1.2 + Math.random()*3.2; |
| pVel[i*2+0] = Math.cos(angle) * speed; |
| pVel[i*2+1] = Math.sin(angle) * speed; |
| |
| pSize[i] = (1.2 + Math.random()*4.2) * DPR * 1.2; |
| pLife[i] = 2.0 + Math.random()*6.0; |
| } |
| } |
| |
| |
| resetParticles(MAX_PARTICLES); |
| initStars(STAR_COUNT); |
| |
| |
| gl.disable(gl.DEPTH_TEST); |
| gl.enable(gl.BLEND); |
| gl.blendFunc(gl.ONE, gl.ONE); |
| gl.clearColor(0,0,0,1); |
| |
| |
| const btnExplode = document.getElementById('btn-explode'); |
| const btnToggle = document.getElementById('btn-toggle'); |
| const btnReset = document.getElementById('btn-reset'); |
| btnExplode.addEventListener('click', explode); |
| btnToggle.addEventListener('click', ()=>{ |
| paused = !paused; |
| btnToggle.textContent = paused ? 'Resume' : 'Pause'; |
| }); |
| btnReset.addEventListener('click', ()=>resetParticles(PARTICLE_COUNT)); |
| |
| window.addEventListener('keydown', (e)=>{ |
| if (e.code === 'Space') { paused = !paused; btnToggle.textContent = paused ? 'Resume' : 'Pause'; } |
| if (e.key.toLowerCase() === 'e') explode(); |
| if (e.key.toLowerCase() === 'r') resetParticles(PARTICLE_COUNT); |
| }); |
| window.addEventListener('resize', resize); |
| window.addEventListener('pointerdown', explode); |
| resize(); |
| |
| |
| function update(dt){ |
| |
| const decay = Math.pow(DRAG, dt*60); |
| for (let i = 0; i < PARTICLE_COUNT; i++){ |
| |
| pLife[i] -= dt; |
| if (pLife[i] <= 0) { |
| respawn(i); |
| continue; |
| } |
| |
| pVel[i*2+0] *= decay; |
| pVel[i*2+1] = pVel[i*2+1]*decay - G*dt; |
| |
| |
| pPos[i*2+0] += pVel[i*2+0] * dt; |
| pPos[i*2+1] += pVel[i*2+1] * dt; |
| |
| |
| const x = pPos[i*2+0], y = pPos[i*2+1]; |
| if (Math.abs(x) > WORLD_RADIUS*1.8 || Math.abs(y) > WORLD_RADIUS*1.8) { |
| respawn(i); |
| } |
| } |
| } |
| |
| function drawParticlesToFBO(){ |
| |
| gl.bindFramebuffer(gl.FRAMEBUFFER, trailFbo); |
| gl.viewport(0, 0, W, H); |
| gl.clearColor(0,0,0,0); |
| gl.clear(gl.COLOR_BUFFER_BIT); |
| |
| gl.useProgram(particleProg); |
| |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufPos); |
| gl.enableVertexAttribArray(particleLoc.a_pos); |
| gl.vertexAttribPointer(particleLoc.a_pos, 2, gl.FLOAT, false, 0, 0); |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufVel); |
| gl.enableVertexAttribArray(particleLoc.a_vel); |
| gl.vertexAttribPointer(particleLoc.a_vel, 2, gl.FLOAT, false, 0, 0); |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufSize); |
| gl.enableVertexAttribArray(particleLoc.a_size); |
| gl.vertexAttribPointer(particleLoc.a_size, 1, gl.FLOAT, false, 0, 0); |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufSeed); |
| gl.enableVertexAttribArray(particleLoc.a_seed); |
| gl.vertexAttribPointer(particleLoc.a_seed, 1, gl.FLOAT, false, 0, 0); |
| |
| |
| gl.uniform2f(particleLoc.u_origin, origin.x, origin.y); |
| gl.uniform2f(particleLoc.u_scale, worldScale.x, worldScale.y); |
| gl.uniform1f(particleLoc.u_aspect, W / H); |
| gl.uniform1f(particleLoc.u_time, time); |
| |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufPos); |
| gl.bufferSubData(gl.ARRAY_BUFFER, 0, pPos); |
| gl.bindBuffer(gl.ARRAY_BUFFER, bufVel); |
| gl.bufferSubData(gl.ARRAY_BUFFER, 0, pVel); |
| |
| gl.drawArrays(gl.POINTS, 0, PARTICLE_COUNT); |
| |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, null); |
| gl.useProgram(null); |
| gl.bindFramebuffer(gl.FRAMEBUFFER, null); |
| } |
| |
| function compositeToScreen(){ |
| gl.useProgram(compositeProg); |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, fsQuad); |
| gl.enableVertexAttribArray(compositeLoc.a_pos); |
| gl.vertexAttribPointer(compositeLoc.a_pos, 2, gl.FLOAT, false, 0, 0); |
| |
| gl.activeTexture(gl.TEXTURE0); |
| gl.bindTexture(gl.TEXTURE_2D, trailTex); |
| gl.uniform1i(compositeLoc.u_prevTrail, 0); |
| |
| gl.activeTexture(gl.TEXTURE1); |
| gl.bindTexture(gl.TEXTURE_2D, trailTex); |
| gl.uniform1i(compositeLoc.u_curr, 1); |
| |
| gl.uniform1f(compositeLoc.u_decay, 0.965); |
| gl.uniform2f(compositeLoc.u_resolution, W, H); |
| gl.uniform1f(compositeLoc.u_time, time); |
| gl.uniform1f(compositeLoc.u_shockStart, shockStart); |
| gl.uniform1f(compositeLoc.u_shockDuration, shockDuration); |
| |
| gl.uniform1i(compositeLoc.u_starCount, STAR_COUNT); |
| gl.uniform2fv(compositeLoc.u_starPos, starPos); |
| gl.uniform1fv(compositeLoc.u_starPhase, starPhase); |
| gl.uniform1fv(compositeLoc.u_starSize, starSize); |
| |
| gl.drawArrays(gl.TRIANGLES, 0, 3); |
| |
| gl.bindBuffer(gl.ARRAY_BUFFER, null); |
| gl.useProgram(null); |
| } |
| |
| function frame(t){ |
| const now = t * 0.001; |
| const dt = Math.min(0.033, now - lastT || 0.016); |
| lastT = now; |
| if (!paused) { |
| time += dt; |
| update(dt); |
| } |
| |
| |
| drawParticlesToFBO(); |
| |
| |
| gl.viewport(0, 0, W, H); |
| gl.clearColor(0,0,0,1); |
| gl.clear(gl.COLOR_BUFFER_BIT); |
| compositeToScreen(); |
| |
| requestAnimationFrame(frame); |
| } |
| requestAnimationFrame(frame); |
| |
| |
| setTimeout(()=>explode(), 900); |
| })(); |
| </script> |
| </body> |
|
|
| </html> |