Source

utils/webglUtils.js

/**
 * @fileoverview WebGL utilities for high-performance animated backgrounds
 * @module WebGLUtils
 */

/**
 * WebGL shader manager
 */
export class ShaderManager {
  constructor(gl) {
    this.gl = gl;
    this.programs = new Map();
    this.shaders = new Map();
  }

  /**
   * Create and compile shader
   * @param {string} source - Shader source code
   * @param {number} type - Shader type (VERTEX_SHADER or FRAGMENT_SHADER)
   * @returns {WebGLShader} Compiled shader
   */
  createShader(source, type) {
    const gl = this.gl;
    const shader = gl.createShader(type);
    
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      const error = gl.getShaderInfoLog(shader);
      gl.deleteShader(shader);
      throw new Error(`Shader compilation error: ${error}`);
    }
    
    return shader;
  }

  /**
   * Create shader program
   * @param {string} vertexSource - Vertex shader source
   * @param {string} fragmentSource - Fragment shader source
   * @returns {WebGLProgram} Shader program
   */
  createProgram(vertexSource, fragmentSource) {
    const gl = this.gl;
    const vertexShader = this.createShader(vertexSource, gl.VERTEX_SHADER);
    const fragmentShader = this.createShader(fragmentSource, gl.FRAGMENT_SHADER);
    
    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      const error = gl.getProgramInfoLog(program);
      gl.deleteProgram(program);
      throw new Error(`Program linking error: ${error}`);
    }
    
    // Store attribute and uniform locations
    const programInfo = {
      program,
      attributes: {},
      uniforms: {}
    };
    
    // Get all attributes
    const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
    for (let i = 0; i < numAttributes; i++) {
      const info = gl.getActiveAttrib(program, i);
      programInfo.attributes[info.name] = gl.getAttribLocation(program, info.name);
    }
    
    // Get all uniforms
    const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
    for (let i = 0; i < numUniforms; i++) {
      const info = gl.getActiveUniform(program, i);
      programInfo.uniforms[info.name] = gl.getUniformLocation(program, info.name);
    }
    
    return programInfo;
  }

  /**
   * Get or create program
   * @param {string} name - Program name
   * @param {string} vertexSource - Vertex shader source
   * @param {string} fragmentSource - Fragment shader source
   * @returns {Object} Program info
   */
  getProgram(name, vertexSource, fragmentSource) {
    if (!this.programs.has(name)) {
      this.programs.set(name, this.createProgram(vertexSource, fragmentSource));
    }
    return this.programs.get(name);
  }
}

/**
 * WebGL particle system
 */
export class WebGLParticleSystem {
  constructor(canvas, particleCount = 10000) {
    this.canvas = canvas;
    this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
    
    if (!this.gl) {
      throw new Error('WebGL not supported');
    }
    
    this.particleCount = particleCount;
    this.shaderManager = new ShaderManager(this.gl);
    this.buffers = {};
    this.textures = {};
    this.frameBuffers = {};
    
    this.time = 0;
    this.isRunning = false;
    
    this.initializeShaders();
    this.initializeBuffers();
  }

  /**
   * Initialize shaders
   */
  initializeShaders() {
    // Particle vertex shader
    const particleVertexShader = `
      attribute vec2 a_position;
      attribute vec2 a_velocity;
      attribute float a_size;
      attribute vec4 a_color;
      attribute float a_life;
      
      uniform mat3 u_transform;
      uniform float u_time;
      uniform vec2 u_resolution;
      
      varying vec4 v_color;
      varying float v_life;
      
      void main() {
        vec2 position = a_position + a_velocity * u_time;
        vec3 transformed = u_transform * vec3(position, 1.0);
        
        gl_Position = vec4(
          (transformed.xy / u_resolution) * 2.0 - 1.0,
          0.0,
          1.0
        );
        
        gl_PointSize = a_size * (1.0 + sin(u_time * 0.01) * 0.2);
        v_color = a_color;
        v_life = a_life;
      }
    `;

    // Particle fragment shader
    const particleFragmentShader = `
      precision mediump float;
      
      varying vec4 v_color;
      varying float v_life;
      
      void main() {
        vec2 center = gl_PointCoord - 0.5;
        float distance = length(center);
        
        if (distance > 0.5) {
          discard;
        }
        
        float alpha = (1.0 - distance * 2.0) * v_color.a * v_life;
        gl_FragColor = vec4(v_color.rgb, alpha);
      }
    `;

    // Background vertex shader
    const backgroundVertexShader = `
      attribute vec2 a_position;
      varying vec2 v_uv;
      
      void main() {
        gl_Position = vec4(a_position, 0.0, 1.0);
        v_uv = (a_position + 1.0) * 0.5;
      }
    `;

    // Animated background fragment shader
    const backgroundFragmentShader = `
      precision mediump float;
      
      varying vec2 v_uv;
      uniform float u_time;
      uniform vec2 u_resolution;
      uniform vec3 u_color1;
      uniform vec3 u_color2;
      uniform vec3 u_color3;
      
      // Noise function
      float noise(vec2 p) {
        return sin(p.x * 10.0) * sin(p.y * 10.0);
      }
      
      void main() {
        vec2 uv = v_uv;
        vec2 p = uv * 2.0 - 1.0;
        p.x *= u_resolution.x / u_resolution.y;
        
        // Animated patterns
        float wave1 = sin(p.x * 3.0 + u_time * 0.002) * 0.5;
        float wave2 = cos(p.y * 2.0 + u_time * 0.003) * 0.5;
        float ripple = sin(length(p) * 5.0 - u_time * 0.01) * 0.3;
        
        float pattern = wave1 + wave2 + ripple;
        
        // Color mixing
        vec3 color = mix(u_color1, u_color2, uv.x);
        color = mix(color, u_color3, uv.y);
        color += pattern * 0.1;
        
        gl_FragColor = vec4(color, 0.8);
      }
    `;

    this.particleProgram = this.shaderManager.getProgram(
      'particles',
      particleVertexShader,
      particleFragmentShader
    );

    this.backgroundProgram = this.shaderManager.getProgram(
      'background',
      backgroundVertexShader,
      backgroundFragmentShader
    );
  }

  /**
   * Initialize buffers
   */
  initializeBuffers() {
    const gl = this.gl;
    
    // Particle data
    const positions = new Float32Array(this.particleCount * 2);
    const velocities = new Float32Array(this.particleCount * 2);
    const sizes = new Float32Array(this.particleCount);
    const colors = new Float32Array(this.particleCount * 4);
    const lives = new Float32Array(this.particleCount);
    
    // Initialize particle data
    for (let i = 0; i < this.particleCount; i++) {
      const i2 = i * 2;
      const i4 = i * 4;
      
      // Random positions
      positions[i2] = Math.random() * this.canvas.width;
      positions[i2 + 1] = Math.random() * this.canvas.height;
      
      // Random velocities
      velocities[i2] = (Math.random() - 0.5) * 2;
      velocities[i2 + 1] = (Math.random() - 0.5) * 2;
      
      // Random sizes
      sizes[i] = Math.random() * 8 + 2;
      
      // Random colors
      colors[i4] = Math.random();
      colors[i4 + 1] = Math.random();
      colors[i4 + 2] = Math.random();
      colors[i4 + 3] = Math.random() * 0.8 + 0.2;
      
      // Random life
      lives[i] = Math.random();
    }
    
    // Create buffers
    this.buffers.position = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.position);
    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
    
    this.buffers.velocity = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.velocity);
    gl.bufferData(gl.ARRAY_BUFFER, velocities, gl.DYNAMIC_DRAW);
    
    this.buffers.size = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.size);
    gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.DYNAMIC_DRAW);
    
    this.buffers.color = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.color);
    gl.bufferData(gl.ARRAY_BUFFER, colors, gl.DYNAMIC_DRAW);
    
    this.buffers.life = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.life);
    gl.bufferData(gl.ARRAY_BUFFER, lives, gl.DYNAMIC_DRAW);
    
    // Background quad
    const quadVertices = new Float32Array([
      -1, -1,
       1, -1,
      -1,  1,
       1, -1,
       1,  1,
      -1,  1
    ]);
    
    this.buffers.quad = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.quad);
    gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);
  }

  /**
   * Render frame
   */
  render() {
    const gl = this.gl;
    
    // Set viewport
    gl.viewport(0, 0, this.canvas.width, this.canvas.height);
    
    // Clear
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    // Enable blending
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    
    // Render background
    this.renderBackground();
    
    // Render particles
    this.renderParticles();
    
    this.time++;
  }

  /**
   * Render background
   */
  renderBackground() {
    const gl = this.gl;
    const program = this.backgroundProgram;
    
    gl.useProgram(program.program);
    
    // Bind quad vertices
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.quad);
    gl.enableVertexAttribArray(program.attributes.a_position);
    gl.vertexAttribPointer(program.attributes.a_position, 2, gl.FLOAT, false, 0, 0);
    
    // Set uniforms
    gl.uniform1f(program.uniforms.u_time, this.time);
    gl.uniform2f(program.uniforms.u_resolution, this.canvas.width, this.canvas.height);
    gl.uniform3f(program.uniforms.u_color1, 0.1, 0.2, 0.4);
    gl.uniform3f(program.uniforms.u_color2, 0.4, 0.1, 0.6);
    gl.uniform3f(program.uniforms.u_color3, 0.2, 0.5, 0.3);
    
    // Draw
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }

  /**
   * Render particles
   */
  renderParticles() {
    const gl = this.gl;
    const program = this.particleProgram;
    
    gl.useProgram(program.program);
    
    // Bind attributes
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.position);
    gl.enableVertexAttribArray(program.attributes.a_position);
    gl.vertexAttribPointer(program.attributes.a_position, 2, gl.FLOAT, false, 0, 0);
    
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.velocity);
    gl.enableVertexAttribArray(program.attributes.a_velocity);
    gl.vertexAttribPointer(program.attributes.a_velocity, 2, gl.FLOAT, false, 0, 0);
    
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.size);
    gl.enableVertexAttribArray(program.attributes.a_size);
    gl.vertexAttribPointer(program.attributes.a_size, 1, gl.FLOAT, false, 0, 0);
    
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.color);
    gl.enableVertexAttribArray(program.attributes.a_color);
    gl.vertexAttribPointer(program.attributes.a_color, 4, gl.FLOAT, false, 0, 0);
    
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.life);
    gl.enableVertexAttribArray(program.attributes.a_life);
    gl.vertexAttribPointer(program.attributes.a_life, 1, gl.FLOAT, false, 0, 0);
    
    // Set uniforms
    gl.uniform1f(program.uniforms.u_time, this.time);
    gl.uniform2f(program.uniforms.u_resolution, this.canvas.width, this.canvas.height);
    
    // Identity transform for now
    const transform = [1, 0, 0, 0, 1, 0, 0, 0, 1];
    gl.uniformMatrix3fv(program.uniforms.u_transform, false, transform);
    
    // Draw particles
    gl.drawArrays(gl.POINTS, 0, this.particleCount);
  }

  /**
   * Start animation loop
   */
  start() {
    if (this.isRunning) return;
    
    this.isRunning = true;
    
    const animate = () => {
      if (!this.isRunning) return;
      
      this.render();
      requestAnimationFrame(animate);
    };
    
    animate();
  }

  /**
   * Stop animation
   */
  stop() {
    this.isRunning = false;
  }

  /**
   * Update particle colors
   * @param {Array} colors - Array of RGB colors
   */
  updateColors(colors) {
    const gl = this.gl;
    const colorData = new Float32Array(this.particleCount * 4);
    
    for (let i = 0; i < this.particleCount; i++) {
      const colorIndex = i % colors.length;
      const color = this.hexToRgb(colors[colorIndex]);
      const i4 = i * 4;
      
      colorData[i4] = color.r / 255;
      colorData[i4 + 1] = color.g / 255;
      colorData[i4 + 2] = color.b / 255;
      colorData[i4 + 3] = Math.random() * 0.8 + 0.2;
    }
    
    gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.color);
    gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
  }

  /**
   * Convert hex color to RGB
   * @param {string} hex - Hex color string
   * @returns {Object} RGB object
   */
  hexToRgb(hex) {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16)
    } : { r: 255, g: 255, b: 255 };
  }

  /**
   * Resize canvas
   * @param {number} width - New width
   * @param {number} height - New height
   */
  resize(width, height) {
    this.canvas.width = width;
    this.canvas.height = height;
  }

  /**
   * Cleanup resources
   */
  dispose() {
    this.stop();
    
    const gl = this.gl;
    
    // Delete buffers
    Object.values(this.buffers).forEach(buffer => {
      if (buffer) gl.deleteBuffer(buffer);
    });
    
    // Delete programs
    this.shaderManager.programs.forEach(program => {
      if (program.program) gl.deleteProgram(program.program);
    });
  }
}

/**
 * WebGL effect composer
 */
export class WebGLEffectComposer {
  constructor(canvas) {
    this.canvas = canvas;
    this.gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
    this.effects = [];
    this.frameBuffers = [];
    this.currentFrameBuffer = 0;
  }

  /**
   * Add post-processing effect
   * @param {Object} effect - Effect configuration
   */
  addEffect(effect) {
    this.effects.push(effect);
  }

  /**
   * Render with effects
   * @param {Function} renderCallback - Main render function
   */
  render(renderCallback) {
    // Render to first framebuffer
    this.bindFrameBuffer(0);
    renderCallback();
    
    // Apply effects
    this.effects.forEach((effect, index) => {
      const inputBuffer = index;
      const outputBuffer = (index + 1) % 2;
      
      this.bindFrameBuffer(outputBuffer);
      this.applyEffect(effect, this.frameBuffers[inputBuffer].texture);
    });
    
    // Final render to screen
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
    this.renderToScreen(this.frameBuffers[this.effects.length % 2].texture);
  }

  /**
   * Apply single effect
   * @param {Object} effect - Effect to apply
   * @param {WebGLTexture} inputTexture - Input texture
   */
  applyEffect(effect, inputTexture) {
    // Implementation depends on specific effect
    // This is a placeholder for effect processing
  }

  /**
   * Render texture to screen
   * @param {WebGLTexture} texture - Texture to render
   */
  renderToScreen(texture) {
    // Render final result to canvas
  }

  /**
   * Bind framebuffer
   * @param {number} index - Framebuffer index
   */
  bindFrameBuffer(index) {
    if (!this.frameBuffers[index]) {
      this.createFrameBuffer(index);
    }
    
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.frameBuffers[index].framebuffer);
  }

  /**
   * Create framebuffer
   * @param {number} index - Framebuffer index
   */
  createFrameBuffer(index) {
    const gl = this.gl;
    
    const framebuffer = gl.createFramebuffer();
    const texture = gl.createTexture();
    
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.canvas.width, this.canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
    
    this.frameBuffers[index] = { framebuffer, texture };
  }
}

// Pre-built WebGL effects
export const webglEffects = {
  /**
   * Bloom effect shader
   */
  bloom: {
    fragment: `
      precision mediump float;
      uniform sampler2D u_texture;
      uniform vec2 u_resolution;
      varying vec2 v_uv;
      
      void main() {
        vec4 color = texture2D(u_texture, v_uv);
        float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
        
        if (brightness > 0.8) {
          gl_FragColor = color * 1.5;
        } else {
          gl_FragColor = color;
        }
      }
    `
  },

  /**
   * Blur effect shader
   */
  blur: {
    fragment: `
      precision mediump float;
      uniform sampler2D u_texture;
      uniform vec2 u_resolution;
      uniform float u_blurSize;
      varying vec2 v_uv;
      
      void main() {
        vec4 color = vec4(0.0);
        vec2 texelSize = 1.0 / u_resolution;
        
        for (int x = -2; x <= 2; x++) {
          for (int y = -2; y <= 2; y++) {
            vec2 offset = vec2(float(x), float(y)) * texelSize * u_blurSize;
            color += texture2D(u_texture, v_uv + offset);
          }
        }
        
        gl_FragColor = color / 25.0;
      }
    `
  }
};