Spaces:
Running
Running
| // Galaxia de partículas WebGL para el splash — sin librerías, autocontenida. | |
| // Técnica: GL_POINTS + blending aditivo + rotación diferencial calculada por | |
| // completo en el vertex shader desde una semilla (la CPU no toca nada por | |
| // frame). ~24k partículas en espiral rosa→violeta con motas doradas, parallax | |
| // de ratón y explosión al pulsar Entrar. | |
| export function startGalaxy(container) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.className = 'gl'; | |
| container.prepend(canvas); | |
| const gl = canvas.getContext('webgl', { alpha: true, antialias: false, depth: false }); | |
| if (!gl) { canvas.remove(); return null; } // fallback: se queda el gradiente CSS | |
| const N = 24000; | |
| const seeds = new Float32Array(N * 2); | |
| for (let i = 0; i < seeds.length; i++) seeds[i] = Math.random(); | |
| const VS = ` | |
| attribute vec2 seed; | |
| uniform float t, aspect, burst, dpr; | |
| uniform vec2 par; | |
| varying vec3 vColor; | |
| varying float vGlow; | |
| float hash(float n){ return fract(sin(n) * 43758.5453); } | |
| void main(){ | |
| float r = 0.08 + 1.35 * pow(seed.y, 0.65); | |
| float arm = floor(hash(seed.x * 7.0 + seed.y) * 3.0); | |
| float ang = seed.x * 0.9 + arm * 2.0944 + r * 2.6 + t * (0.12 + 0.25 / (r + 0.2)); | |
| float wob = (hash(seed.x * 13.1) - 0.5) * 0.22 * r; | |
| vec2 p = vec2(cos(ang), sin(ang)) * (r + wob); | |
| p.y *= 0.62; | |
| p.y += (hash(seed.y * 17.7) - 0.5) * 0.14; | |
| p += par * (0.03 + r * 0.06); | |
| p *= 1.0 + burst * (0.4 + seed.y * 2.4); | |
| gl_Position = vec4(p.x / aspect, p.y, 0.0, 1.0); | |
| float tw = 0.55 + 0.45 * sin(t * (1.5 + seed.x * 3.0) + seed.y * 40.0); | |
| vGlow = tw * (1.0 - burst * 0.55); | |
| float gold = step(0.93, hash(seed.x * 31.3 + seed.y * 7.7)); | |
| vec3 pink = vec3(1.0, 0.30, 0.55); | |
| vec3 violet = vec3(0.49, 0.36, 1.0); | |
| vec3 goldc = vec3(1.0, 0.84, 0.42); | |
| vColor = mix(mix(pink, violet, smoothstep(0.1, 1.3, r)), goldc, gold); | |
| gl_PointSize = (1.3 + 2.8 * tw * max(1.2 - r * 0.5, 0.2)) * dpr; | |
| }`; | |
| const FS = ` | |
| precision mediump float; | |
| varying vec3 vColor; | |
| varying float vGlow; | |
| void main(){ | |
| float m = smoothstep(0.5, 0.0, length(gl_PointCoord - 0.5)); | |
| gl_FragColor = vec4(vColor * vGlow, m * vGlow); | |
| }`; | |
| const sh = (type, src) => { | |
| const s = gl.createShader(type); | |
| gl.shaderSource(s, src); | |
| gl.compileShader(s); | |
| if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); | |
| return s; | |
| }; | |
| const prog = gl.createProgram(); | |
| gl.attachShader(prog, sh(gl.VERTEX_SHADER, VS)); | |
| gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FS)); | |
| gl.linkProgram(prog); | |
| gl.useProgram(prog); | |
| const buf = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, buf); | |
| gl.bufferData(gl.ARRAY_BUFFER, seeds, gl.STATIC_DRAW); // una vez; la GPU manda | |
| const loc = gl.getAttribLocation(prog, 'seed'); | |
| gl.enableVertexAttribArray(loc); | |
| gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); | |
| const U = n => gl.getUniformLocation(prog, n); | |
| const uT = U('t'), uAspect = U('aspect'), uBurst = U('burst'), uPar = U('par'), uDpr = U('dpr'); | |
| gl.enable(gl.BLEND); | |
| gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // aditivo = brillo | |
| gl.clearColor(0, 0, 0, 0); | |
| let burstAt = 0, alive = true; | |
| const mouse = { x: 0, y: 0, tx: 0, ty: 0 }; | |
| const onMove = e => { | |
| mouse.tx = (e.clientX / innerWidth - 0.5) * 2; | |
| mouse.ty = -(e.clientY / innerHeight - 0.5) * 2; | |
| }; | |
| addEventListener('pointermove', onMove); | |
| const dpr = Math.min(devicePixelRatio || 1, 2); | |
| function frame(now) { | |
| if (!alive) return; | |
| const w = container.clientWidth, h = container.clientHeight; | |
| if (canvas.width !== w * dpr || canvas.height !== h * dpr) { | |
| canvas.width = w * dpr; | |
| canvas.height = h * dpr; | |
| gl.viewport(0, 0, canvas.width, canvas.height); | |
| } | |
| mouse.x += (mouse.tx - mouse.x) * 0.05; | |
| mouse.y += (mouse.ty - mouse.y) * 0.05; | |
| const t = now / 1000; | |
| const burst = burstAt ? Math.min((now - burstAt) / 900, 1) : 0; | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| gl.uniform1f(uT, t); | |
| gl.uniform1f(uAspect, w / h); | |
| gl.uniform1f(uBurst, burst); | |
| gl.uniform1f(uDpr, dpr); | |
| gl.uniform2f(uPar, mouse.x, mouse.y); | |
| gl.drawArrays(gl.POINTS, 0, N); | |
| requestAnimationFrame(frame); | |
| } | |
| requestAnimationFrame(frame); | |
| return { | |
| burst() { burstAt = performance.now(); }, | |
| stop() { alive = false; removeEventListener('pointermove', onMove); canvas.remove(); }, | |
| }; | |
| } | |