File size: 4,365 Bytes
b30b7c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// 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(); },
  };
}