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