anycoder-3e798e34 / index.html
terminallylazy's picture
Upload folder using huggingface_hub
7904f92 verified
<!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
&nbsp;|&nbsp; 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;
}
// Globals
const DPR = Math.min(2, window.devicePixelRatio || 1);
let W = 0, H = 0;
let time = 0, lastT = 0, paused = false;
// Explosion and physics
const G = 0.10; // "gravity" in world units per second^2 (scaled)
const DRAG = 0.995; // velocity damping per frame
const MAX_PARTICLES = 14000;
const MIN_PARTICLES = 4000;
let PARTICLE_COUNT = MAX_PARTICLES;
// Explosion origin in world coordinates (will be recentered on resize)
let origin = { x: 0, y: 0 };
// Particles data
let pLife, pPos, pVel, pSize, pSeed;
// Stars
let STAR_COUNT = 600;
let starPos, starPhase, starSize;
// Shockwave
let shockActive = false;
let shockStart = 0;
let shockDuration = 6.0;
let shockMaxRadius = 0.0;
let shockThickness = 0.06;
// Trail FBO
let trailFbo = null;
let trailTex = null;
// Utility
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;
// Shaders
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);
}
`;
// GL helpers
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;
}
// Programs
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'),
};
// Buffers
const fsQuad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, fsQuad);
// Full-screen triangle
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
3, -1,
-1, 3
]), gl.STATIC_DRAW);
// Particle buffers
const bufPos = gl.createBuffer();
const bufVel = gl.createBuffer();
const bufSize = gl.createBuffer();
const bufSeed = gl.createBuffer();
// Trail target
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);
// Recreate trail target
if (trailTex) {
gl.deleteTexture(trailTex);
gl.deleteFramebuffer(trailFbo);
}
const t = createTrailTarget(W, H);
trailFbo = t.fbo;
trailTex = t.tex;
// Center explosion
origin.x = 0;
origin.y = 0;
// Recompute scale (world -> NDC)
worldScale.x = 1.0 / (WORLD_RADIUS);
worldScale.y = 1.0 / (WORLD_RADIUS);
}
// World settings
const WORLD_RADIUS = 2.2; // world half-extent in NDC
const worldScale = { x: 1/WORLD_RADIUS, y: 1/WORLD_RADIUS };
// Particles init
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; // achromatic
} 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;
// Velocity: radial burst
const angle = Math.atan2(r.y, r.x);
const speed = 0.7 + Math.random()*2.6; // world units per second
pVel[i*2+0] = Math.cos(angle) * speed;
pVel[i*2+1] = Math.sin(angle) * speed;
// Size in pixels (scaled with DPR)
pSize[i] = (1.0 + Math.random()*3.5) * DPR * 1.2;
// Life in seconds
pLife[i] = 1.5 + Math.random()*5.0;
}
// Upload static buffers
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);
// Initial positions
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++){
// Distribute roughly uniformly in NDC
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(){
// Shockwave start
shockActive = true;
shockStart = time;
// Reset velocities with a powerful burst and add some turbulence
for (let i = 0; i < PARTICLE_COUNT; i++){
const angle = Math.random()*Math.PI*2;
const speed = 1.2 + Math.random()*3.2; // stronger impulse
pVel[i*2+0] = Math.cos(angle) * speed;
pVel[i*2+1] = Math.sin(angle) * speed;
// Add slight size jitter for flair
pSize[i] = (1.2 + Math.random()*4.2) * DPR * 1.2;
pLife[i] = 2.0 + Math.random()*6.0;
}
}
// Init
resetParticles(MAX_PARTICLES);
initStars(STAR_COUNT);
// GL State
gl.disable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE); // additive
gl.clearColor(0,0,0,1);
// Controls
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();
// Animation
function update(dt){
// Update particles on CPU
const decay = Math.pow(DRAG, dt*60);
for (let i = 0; i < PARTICLE_COUNT; i++){
// Life
pLife[i] -= dt;
if (pLife[i] <= 0) {
respawn(i);
continue;
}
// Velocity
pVel[i*2+0] *= decay;
pVel[i*2+1] = pVel[i*2+1]*decay - G*dt;
// Position
pPos[i*2+0] += pVel[i*2+0] * dt;
pPos[i*2+1] += pVel[i*2+1] * dt;
// If out of bounds (with margin), respawn
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(){
// Render current particles into trailTex FBO
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);
// Attributes
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);
// Uniforms
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);
// Update dynamic buffers
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);
// Cleanup
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); // same target this frame
gl.uniform1i(compositeLoc.u_curr, 1);
gl.uniform1f(compositeLoc.u_decay, 0.965); // trail persistence
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);
}
// Draw particles to trail buffer
drawParticlesToFBO();
// Composite to screen
gl.viewport(0, 0, W, H);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
compositeToScreen();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// Auto explode after a moment for dramatic entrance
setTimeout(()=>explode(), 900);
})();
</script>
</body>
</html>