# Source: FloatColorRenderer.js

    /*
     * Copyright 2024 The Ray Optics Simulation authors and contributors
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /**
     * An experimental canvas renderer for the light layer with the same API as `CanvasRenderer`, but uses the WebGL floating point texture and properly calculates color mixtures. This largely solves the issue of brightness and color inconsistency issue in this simulator, especially whe "Simulate Colors" is enabled (where the performace issue is also solved). Additionally, this renderer allows for different color modes (as defined in `Scene`, but the 'legacy' mode with `simulateColors` enabled is split into 'legacy_color').
     *
     * This renderer is currently used only in the web app when the "Float Color Renderer" experimental feature is enabled, and is not used in generation of Gallery images and the automatic tests.
     * @class
     */
    class FloatColorRenderer {

      /**
       * Maximum number of elements in the cache before flushing.
       * @type {number}
       */
      static MAX_CACHE_SIZE = 500;

      constructor(gl, origin, scale, lengthScale, backgroundImage, ctxVirtual, colorMode) {
        this.gl = gl;
        if (!this.gl) {
          throw new Error('Unable to initialize WebGL. Your browser may not support it.');
        }

        // Enable floating point texture
        const ext = this.gl.getExtension('OES_texture_float');
        if (!ext) {
          throw new Error('OES_texture_float not supported');
        }

        this.canvas = gl.canvas;
        this.width = this.canvas.width;
        this.height = this.canvas.height;
        this.origin = origin;
        this.scale = scale;
        this.lengthScale = lengthScale;
        this.colorMode = colorMode;
        this.rayCache = [];
        this.segmentCache = [];
        this.pointCache = [];
        this.arrowCache = [];
        this.hasFirstFlush = false;

        switch (this.colorMode) {
          case 'legacy':
          case 'legacy_color':
          case 'colorizedIntensity':
            this.msaaCount = 1; // Colorized intensity does not work well with MSAA since the color is not additive
            break;
          default:
            this.msaaCount = 4;
        }

        // Create reusable buffers
        this.lineBuffer = this.gl.createBuffer();
        this.pointBuffer = this.gl.createBuffer();
        this.arrowBuffer = this.gl.createBuffer();

        this.initializeShaders();
        this.initializeFramebuffer();
      }

      /**
       * Initialize the framebuffer and textures.
       */
      initializeFramebuffer() {
        const gl = this.gl;

        // Create floating point texture
        this.floatTexture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, this.floatTexture);

        // Use RGBA format for the texture
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.FLOAT, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        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);

        // Create and set up framebuffer
        this.framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.floatTexture, 0);

        // Check framebuffer status
        const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
        if (status !== gl.FRAMEBUFFER_COMPLETE) {
          throw new Error('Framebuffer is not complete: ' + status);
        }

        // Create quad buffer for final render
        this.quadBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
          -1, -1,
           1, -1,
          -1,  1,
           1,  1
        ]), gl.STATIC_DRAW);

        // Reset bindings
        gl.bindTexture(gl.TEXTURE_2D, null);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
      }

      /**
       * Initialize shaders.
       */
      initializeShaders() {
        // Vertex shader for rays
        const rayVertexSource = `
          attribute vec2 position;
          uniform vec2 u_resolution;
          uniform vec2 u_origin;
          uniform float u_scale;
          uniform bool u_isScreenSpace;

          varying vec2 v_position;
          varying vec2 v_resolution;

          void main() {
            vec2 pos;
            if (u_isScreenSpace) {
              pos = position;
            } else {
              pos = position * u_scale + u_origin;
            }

            // Convert to clip space
            vec2 clipSpace = (pos / u_resolution) * 2.0 - 1.0;
            clipSpace.y = -clipSpace.y;
            gl_Position = vec4(clipSpace, 0.0, 1.0);

            // Pass values to fragment shader
            v_position = pos;
            v_resolution = u_resolution;
          }
        `;

        // Fragment shader for rays - just output color for additive blending
        const rayFragmentSource = `
          precision highp float;
          uniform vec4 u_color;
          uniform bool u_isScreenSpace;

          varying vec2 v_position;
          varying vec2 v_resolution;

          void main() {
            gl_FragColor = u_color;
          }
        `;

        // Vertex shader for points
        const pointVertexSource = `
          attribute vec2 position;
          uniform vec2 u_resolution;
          uniform vec2 u_origin;
          uniform float u_scale;
          uniform vec2 u_point;
          uniform float u_size;

          varying vec2 v_position;
          varying vec2 v_resolution;

          void main() {
            // Calculate point position
            vec2 pointPos = u_point * u_scale + u_origin;
            vec2 pos = pointPos + position * u_size * u_scale;

            // Convert to clip space
            vec2 clipSpace = (pos / u_resolution) * 2.0 - 1.0;
            clipSpace.y = -clipSpace.y;
            gl_Position = vec4(clipSpace, 0.0, 1.0);

            // Pass values to fragment shader
            v_position = pos;
            v_resolution = u_resolution;
          }
        `;

        // Fragment shader for points - same as rays
        const pointFragmentSource = rayFragmentSource;

        // Vertex shader for final pass
        const quadVertexSource = `
          attribute vec2 position;
          varying vec2 v_texCoord;

          void main() {
            gl_Position = vec4(position, 0.0, 1.0);
            v_texCoord = position * 0.5 + 0.5;
          }
        `;

        // Fragment shader for final pass
        let quadFragmentSource;
        switch (this.colorMode) {
          case 'legacy':
            quadFragmentSource = `
              precision highp float;
              uniform sampler2D u_texture;
              varying vec2 v_texCoord;

              void main() {
                vec4 color = texture2D(u_texture, v_texCoord);
                float maxComponent = max(max(color.r, color.g), color.b);
                float factor = 1.0 - exp(-maxComponent);
                gl_FragColor = vec4(color.rgb / maxComponent * factor, factor);
              }
            `;
            break;
          case 'legacy_color':
            quadFragmentSource = `
              precision highp float;
              uniform sampler2D u_texture;
              varying vec2 v_texCoord;

              void main() {
                vec4 color = texture2D(u_texture, v_texCoord);
                float maxComponent = max(max(color.r, color.g), color.b);
                if (maxComponent > 1.0) {
                  color.rgb /= maxComponent;
                }
                gl_FragColor = vec4(color.rgb, min(maxComponent, 1.0));
              }
            `;
            break;
          case 'linear':
            quadFragmentSource = `
              precision highp float;
              uniform sampler2D u_texture;
              varying vec2 v_texCoord;

              void main() {
                vec4 color = texture2D(u_texture, v_texCoord);
                float maxComponent = max(max(color.r, color.g), color.b);
                if (maxComponent > 1.0) {
                  color.rgb /= maxComponent;
                }
                gl_FragColor = vec4(pow(color.rgb, vec3(1.0 / 2.2)), pow(min(maxComponent, 1.0), 1.0 / 2.2));
              }
            `;
            break;
          case 'linearRGB':
            quadFragmentSource = `
              precision highp float;
              uniform sampler2D u_texture;
              varying vec2 v_texCoord;

              void main() {
                vec4 color = texture2D(u_texture, v_texCoord);
                float maxComponent = max(max(color.r, color.g), color.b);
                gl_FragColor = vec4(pow(color.rgb, vec3(1.0 / 2.2)), pow(maxComponent, 1.0 / 2.2));
              }
            `;
            break;
          case 'reinhard':
            quadFragmentSource = `
              precision highp float;

              uniform sampler2D u_texture;
              varying vec2 v_texCoord;

              // Convert RGB to luminance using perceptual weights
              float getLuminance(vec3 color) {
                return dot(color, vec3(0.2126, 0.7152, 0.0722));
              }

              void main() {
                // Sample the texture
                vec4 color = texture2D(u_texture, v_texCoord);

                // Calculate luminance
                float Lold = getLuminance(color.rgb);

                // Apply Reinhard tone mapping to luminance
                float Lnew = Lold / (1.0 + Lold);

                // Scale RGB by ratio of new to old luminance
                vec3 toneMapped = color.rgb * (Lnew / Lold);

                // Apply gamma correction
                vec3 gammaCorrected = pow(toneMapped, vec3(1.0 / 2.2));

                // Store the maximum component of the original color
                float maxComponent = max(max(color.r, color.g), color.b);

                gl_FragColor = vec4(gammaCorrected, pow(maxComponent, 1.0 / 2.2));
              }
            `;
            break;
          case 'colorizedIntensity':
            quadFragmentSource = `
              precision highp float;
              uniform sampler2D u_texture;
              varying vec2 v_texCoord;

              vec4 brightnessToColor(float brightness) {
                  if (brightness > 100.0) {
                      return vec4(1.0, 0.0, 0.0, 1.0); // Red
                  } else if (brightness > 10.0) {
                      return vec4(mix(vec3(1.0, 0.5, 0.0), vec3(1.0, 0.0, 0.0), (log2(brightness) - log2(10.0)) / (log2(100.0) - log2(10.0))), 1.0); // Smooth transition from orange to red
                  } else if (brightness > 1.0) {
                      return vec4(mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.5, 0.0), (log2(brightness) - log2(1.0)) / (log2(10.0) - log2(1.0))), 1.0); // Smooth transition from yellow to orange
                  } else if (brightness > 0.1) {
                      return vec4(mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (log2(brightness) - log2(0.1)) / (log2(1.0) - log2(0.1))), 1.0); // Smooth transition from green to yellow
                  } else if (brightness > 0.01) {
                      return vec4(mix(vec3(0.0, 1.0, 1.0), vec3(0.0, 1.0, 0.0), (log2(brightness) - log2(0.01)) / (log2(0.1) - log2(0.01))), 1.0); // Smooth transition from cyan to green
                  } else if (brightness > 0.001) {
                      return vec4(mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 1.0), (log2(brightness) - log2(0.001)) / (log2(0.01) - log2(0.001))), 1.0); // Smooth transition from blue to cyan
                  } else if (brightness > 0.0001) {
                      return vec4(mix(vec3(0.3, 0.0, 0.3), vec3(0.0, 0.0, 1.0), (log2(brightness) - log2(0.0001)) / (log2(0.001) - log2(0.0001))), 1.0); // Smooth transition from purple to blue
                  } else {
                      return vec4(mix(vec3(0.0, 0.0, 0.0), vec3(0.3, 0.0, 0.3), (log2(max(brightness, 1e-7)) - log2(1e-7)) / (log2(0.0001) - log2(1e-7))), (log2(max(brightness, 1e-7)) - log2(1e-7)) / (log2(0.0001) - log2(1e-7))); // Smooth transition from purple to transparent
                  }
              }

              void main() {
                  vec4 color = texture2D(u_texture, v_texCoord);
                  float maxComponent = max(max(color.r, color.g), color.b);

                  vec4 mappedColor = brightnessToColor(maxComponent);
                  gl_FragColor = vec4(mappedColor.rgb * 0.8, 0.0);
              }
            `
            break;
        }

        // Compile ray shaders
        const rayVertexShader = this.compileShader(this.gl, rayVertexSource, this.gl.VERTEX_SHADER);
        const rayFragmentShader = this.compileShader(this.gl, rayFragmentSource, this.gl.FRAGMENT_SHADER);

        // Create ray program
        this.rayProgram = this.gl.createProgram();
        this.gl.attachShader(this.rayProgram, rayVertexShader);
        this.gl.attachShader(this.rayProgram, rayFragmentShader);
        this.gl.linkProgram(this.rayProgram);

        if (!this.gl.getProgramParameter(this.rayProgram, this.gl.LINK_STATUS)) {
          this.gl.deleteProgram(this.rayProgram);
          return;
        }

        // Get ray program locations
        this.positionAttributeLocation = this.gl.getAttribLocation(this.rayProgram, 'position');
        this.resolutionUniformLocation = this.gl.getUniformLocation(this.rayProgram, 'u_resolution');
        this.originUniformLocation = this.gl.getUniformLocation(this.rayProgram, 'u_origin');
        this.scaleUniformLocation = this.gl.getUniformLocation(this.rayProgram, 'u_scale');
        this.isScreenSpaceLocation = this.gl.getUniformLocation(this.rayProgram, 'u_isScreenSpace');
        this.colorUniformLocation = this.gl.getUniformLocation(this.rayProgram, 'u_color');

        // Compile point shaders
        const pointVertexShader = this.compileShader(this.gl, pointVertexSource, this.gl.VERTEX_SHADER);
        const pointFragmentShader = this.compileShader(this.gl, pointFragmentSource, this.gl.FRAGMENT_SHADER);

        // Create point program
        this.pointProgram = this.gl.createProgram();
        this.gl.attachShader(this.pointProgram, pointVertexShader);
        this.gl.attachShader(this.pointProgram, pointFragmentShader);
        this.gl.linkProgram(this.pointProgram);

        if (!this.gl.getProgramParameter(this.pointProgram, this.gl.LINK_STATUS)) {
          this.gl.deleteProgram(this.pointProgram);
          return;
        }

        // Get point program locations
        this.pointPositionAttributeLocation = this.gl.getAttribLocation(this.pointProgram, 'position');
        this.pointResolutionUniformLocation = this.gl.getUniformLocation(this.pointProgram, 'u_resolution');
        this.pointOriginUniformLocation = this.gl.getUniformLocation(this.pointProgram, 'u_origin');
        this.pointScaleUniformLocation = this.gl.getUniformLocation(this.pointProgram, 'u_scale');
        this.pointPositionUniformLocation = this.gl.getUniformLocation(this.pointProgram, 'u_point');
        this.pointSizeUniformLocation = this.gl.getUniformLocation(this.pointProgram, 'u_size');
        this.pointColorUniformLocation = this.gl.getUniformLocation(this.pointProgram, 'u_color');

        // Compile quad shaders
        const quadVertexShader = this.compileShader(this.gl, quadVertexSource, this.gl.VERTEX_SHADER);
        const quadFragmentShader = this.compileShader(this.gl, quadFragmentSource, this.gl.FRAGMENT_SHADER);

        // Create quad program
        this.quadProgram = this.gl.createProgram();
        this.gl.attachShader(this.quadProgram, quadVertexShader);
        this.gl.attachShader(this.quadProgram, quadFragmentShader);
        this.gl.linkProgram(this.quadProgram);

        if (!this.gl.getProgramParameter(this.quadProgram, this.gl.LINK_STATUS)) {
          this.gl.deleteProgram(this.quadProgram);
          return;
        }

        // Get quad program locations
        this.quadPositionLocation = this.gl.getAttribLocation(this.quadProgram, 'position');
        this.textureLocation = this.gl.getUniformLocation(this.quadProgram, 'u_texture');

        // Create point vertices (unit square)
        const pointVertices = new Float32Array([
          -0.5, -0.5,
           0.5, -0.5,
          -0.5,  0.5,
           0.5,  0.5
        ]);
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.pointBuffer);
        this.gl.bufferData(this.gl.ARRAY_BUFFER, pointVertices, this.gl.STATIC_DRAW);
      }

      /**
       * Compile a shader.
       * @param {WebGLRenderingContext} gl
       * @param {string} source
       * @param {number} type
       * @returns {WebGLShader}
       */
      compileShader(gl, source, type) {
        const shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);

        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          gl.deleteShader(shader);
          return null;
        }

        return shader;
      }

      /**
       * Begin the drawing process.
       * This method is called at the beginning of each frame.
       */
      begin() {
        this.rayCache = [];
        this.segmentCache = [];
        this.pointCache = [];
        this.arrowCache = [];
        this.hasFirstFlush = false;
      }

      /**
       * Draw a point.
       * @param {Point} p
       * @param {number[]} [color=[0, 0, 0, 1]]
       * @param {number} [size=5]
       */
      drawPoint(p, color = [0, 0, 0, 1], size = 5) {
        color = this.preprocessColor(color);
        this.pointCache.push({ p, color, size });

        // If cache is too large, flush immediately
        if (this.pointCache.length >= FloatColorRenderer.MAX_CACHE_SIZE) {
          this.flush();
        }
      }

      /**
       * Draw a ray.
       * @param {Line} r
       * @param {number[]} [color=[0, 0, 0, 1]]
       * @param {boolean} [showArrow=false]
       * @param {number[]} [lineDash=[]]
       */
      drawRay(r, color = [0, 0, 0, 1], showArrow = false, lineDash = []) {
        // Check if ray has a valid direction
        const dx = r.p2.x - r.p1.x;
        const dy = r.p2.y - r.p1.y;
        const length = Math.sqrt(dx * dx + dy * dy);

        if (length < 1e-5 * this.lengthScale) {
          return;
        }

        color = this.preprocessColor(color);

        const unitX = dx / length;
        const unitY = dy / length;

        // Calculate canvas limit for ray length
        const cvsLimit = (Math.abs(r.p1.x + this.origin.x) + Math.abs(r.p1.y + this.origin.y) + this.canvas.height + this.canvas.width) / Math.min(1, this.scale);

        // Calculate arrow parameters
        const arrowSize = 5 * this.lengthScale;
        const arrowDistance = 150 * this.lengthScale;

        if (showArrow && arrowSize >= this.lengthScale * 1.2) {
          // Draw first part (from start to arrow)
          const firstSegment = {
            p1: r.p1,
            p2: {
              x: r.p1.x + unitX * arrowDistance,
              y: r.p1.y + unitY * arrowDistance
            }
          };

          if (lineDash && lineDash.length > 0) {
            this.drawDashedSegment(firstSegment, color, lineDash);
          } else {
            this.segmentCache.push({ s: firstSegment, color });
          }

          // Add arrow
          const arrowX = r.p1.x + unitX * arrowDistance;
          const arrowY = r.p1.y + unitY * arrowDistance;
          const perpX = -unitY;
          const perpY = unitX;
          const baseWidth = this.lengthScale;
          const tipWidth = arrowSize;

          this.arrowCache.push({
            points: [
              // Front points (wide part)
              arrowX - (tipWidth/2) * perpX,
              arrowY - (tipWidth/2) * perpY,
              arrowX + (tipWidth/2) * perpX,
              arrowY + (tipWidth/2) * perpY,
              // Back points (narrow part)
              arrowX + arrowSize * unitX + (baseWidth/2) * perpX,
              arrowY + arrowSize * unitY + (baseWidth/2) * perpY,
              arrowX + arrowSize * unitX - (baseWidth/2) * perpX,
              arrowY + arrowSize * unitY - (baseWidth/2) * perpY
            ],
            color
          });

          // Draw second part (from arrow to infinity)
          const secondRay = {
            p1: {
              x: arrowX + arrowSize * unitX,
              y: arrowY + arrowSize * unitY
            },
            p2: {
              x: r.p1.x + unitX * cvsLimit,
              y: r.p1.y + unitY * cvsLimit
            }
          };

          if (lineDash && lineDash.length > 0) {
            this.drawDashedSegment(secondRay, color, lineDash);
          } else {
            this.rayCache.push({ r: secondRay, color });
          }
        } else {
          // Draw without arrow
          if (lineDash && lineDash.length > 0) {
            // For dashed lines, create segments
            let dashPos = 0;
            let isDraw = true;
            let currentPos = 0;

            while (currentPos < cvsLimit) {
              const dashLength = lineDash[dashPos] * this.lengthScale;

              if (isDraw) {
                const segStart = {
                  x: r.p1.x + currentPos * unitX,
                  y: r.p1.y + currentPos * unitY
                };
                const segEnd = {
                  x: r.p1.x + Math.min(currentPos + dashLength, cvsLimit) * unitX,
                  y: r.p1.y + Math.min(currentPos + dashLength, cvsLimit) * unitY
                };
                this.segmentCache.push({
                  s: { p1: segStart, p2: segEnd },
                  color
                });
              }

              currentPos += dashLength;
              dashPos = (dashPos + 1) % lineDash.length;
              isDraw = !isDraw;
            }
          } else if (this.msaaCount > 1) {
            // For MSAA, create rays with subpixel offsets along the direction perpendicular to the ray with color averaging
            const subpixelOffset = 1 / this.msaaCount;
            const perpX = -unitY;
            const perpY = unitX;
            const dividedColor = [color[0], color[1], color[2], color[3] / this.msaaCount];

            for (let i = 0; i < this.msaaCount; i++) {
              const offset = (((i % 2 === 0 ? 1 : -1) * (Math.floor(i / 2) + 0.5)) * subpixelOffset) / this.scale;
              const subpixelRay = {
                p1: {
                  x: r.p1.x + offset * perpX,
                  y: r.p1.y + offset * perpY
                },
                p2: {
                  x: r.p1.x + unitX * cvsLimit + offset * perpX,
                  y: r.p1.y + unitY * cvsLimit + offset * perpY
                }
              };
              this.rayCache.push({ r: subpixelRay, color: dividedColor });
            }
          } else {
            this.rayCache.push({ r, color });
          }
        }

        // If cache is too large, flush immediately
        if (this.rayCache.length >= FloatColorRenderer.MAX_CACHE_SIZE ||
            this.segmentCache.length >= FloatColorRenderer.MAX_CACHE_SIZE ||
            this.arrowCache.length >= FloatColorRenderer.MAX_CACHE_SIZE) {
          this.flush();
        }
      }

      /**
       * Draw a dashed segment.
       * @param {Line} s
       * @param {number[]} [color=[0, 0, 0, 1]]
       * @param {number[]} [lineDash=[]]
       */
      drawDashedSegment(s, color, lineDash) {
        const dx = s.p2.x - s.p1.x;
        const dy = s.p2.y - s.p1.y;
        const length = Math.sqrt(dx * dx + dy * dy);

        if (length < 1e-5 * this.lengthScale) {
          return;
        }

        const unitX = dx / length;
        const unitY = dy / length;

        let dashPos = 0;
        let isDraw = true;
        let currentPos = 0;

        while (currentPos < length) {
          const dashLength = lineDash[dashPos] * this.lengthScale;
          const remainingLength = length - currentPos;

          if (isDraw) {
            const segStart = {
              x: s.p1.x + currentPos * unitX,
              y: s.p1.y + currentPos * unitY
            };
            const segEnd = {
              x: s.p1.x + Math.min(currentPos + dashLength, length) * unitX,
              y: s.p1.y + Math.min(currentPos + dashLength, length) * unitY
            };
            this.segmentCache.push({
              s: { p1: segStart, p2: segEnd },
              color
            });
          }

          currentPos += dashLength;
          dashPos = (dashPos + 1) % lineDash.length;
          isDraw = !isDraw;

          if (currentPos >= length) break;
        }
      }

      /**
       * Draw a segment.
       * @param {Line} s
       * @param {number[]} [color=[0, 0, 0, 1]]
       * @param {boolean} [showArrow=false]
       * @param {number[]} [lineDash=[]]
       */
      drawSegment(s, color = [0, 0, 0, 1], showArrow = false, lineDash = []) {
        const dx = s.p2.x - s.p1.x;
        const dy = s.p2.y - s.p1.y;
        const length = Math.sqrt(dx * dx + dy * dy);
        const unitX = dx / length;
        const unitY = dy / length;

        if (length < 1e-5 * this.lengthScale) {
          return;
        }

        color = this.preprocessColor(color);

        const arrowSize = Math.min(length * 0.15, 5 * this.lengthScale);
        const arrowPosition = 0.67;

        if (showArrow && arrowSize >= this.lengthScale * 1.2) {
          // Calculate arrow position
          const arrowX = s.p1.x + dx * arrowPosition;
          const arrowY = s.p1.y + dy * arrowPosition;

          // Draw first part (from start to arrow)
          const firstSegment = {
            p1: s.p1,
            p2: {
              x: arrowX - arrowSize/2 * unitX,
              y: arrowY - arrowSize/2 * unitY
            }
          };

          if (lineDash && lineDash.length > 0) {
            this.drawDashedSegment(firstSegment, color, lineDash);
          } else {
            this.segmentCache.push({ s: firstSegment, color });
          }

          // Add arrow
          const perpX = -unitY;
          const perpY = unitX;
          const baseWidth = this.lengthScale;
          const tipWidth = arrowSize;

          this.arrowCache.push({
            points: [
              // Front points (wide part)
              arrowX - arrowSize/2 * unitX - (tipWidth/2) * perpX,
              arrowY - arrowSize/2 * unitY - (tipWidth/2) * perpY,
              arrowX - arrowSize/2 * unitX + (tipWidth/2) * perpX,
              arrowY - arrowSize/2 * unitY + (tipWidth/2) * perpY,
              // Back points (narrow part)
              arrowX + arrowSize/2 * unitX + (baseWidth/2) * perpX,
              arrowY + arrowSize/2 * unitY + (baseWidth/2) * perpY,
              arrowX + arrowSize/2 * unitX - (baseWidth/2) * perpX,
              arrowY + arrowSize/2 * unitY - (baseWidth/2) * perpY
            ],
            color
          });

          // Draw second part (from arrow to end)
          const secondSegment = {
            p1: {
              x: arrowX + arrowSize/2 * unitX,
              y: arrowY + arrowSize/2 * unitY
            },
            p2: s.p2
          };

          if (lineDash && lineDash.length > 0) {
            this.drawDashedSegment(secondSegment, color, lineDash);
          } else {
            this.segmentCache.push({ s: secondSegment, color });
          }
        } else {
          // Draw without arrow
          if (lineDash && lineDash.length > 0) {
            this.drawDashedSegment(s, color, lineDash);
          } else if (this.msaaCount > 1) {
            // For MSAA, create segments with subpixel offsets along the direction perpendicular to the ray with color averaging
            const subpixelOffset = 1 / this.msaaCount;
            const perpX = -unitY;
            const perpY = unitX;
            const dividedColor = [color[0], color[1], color[2], color[3] / this.msaaCount];

            for (let i = 0; i < this.msaaCount; i++) {
              const offset = (((i % 2 === 0 ? 1 : -1) * (Math.floor(i / 2) + 0.5)) * subpixelOffset) / this.scale;
              const subpixelSegment = {
                p1: {
                  x: s.p1.x + offset * perpX,
                  y: s.p1.y + offset * perpY
                },
                p2: {
                  x: s.p2.x + offset * perpX,
                  y: s.p2.y + offset * perpY
                }
              };
              this.segmentCache.push({ s: subpixelSegment, color: dividedColor });
            }
          } else {
            this.segmentCache.push({ s, color });
          }
        }

        // If cache is too large, flush immediately
        if (this.segmentCache.length >= FloatColorRenderer.MAX_CACHE_SIZE ||
            this.arrowCache.length >= FloatColorRenderer.MAX_CACHE_SIZE) {
          this.flush();
        }
      }

      /**
       * Flush the caches and render the accumulated rays, segments, and points.
       * This method is called when the cache size exceeds the maximum cache size, when the simulation is paused, or when the simulation is completed.
       */
      flush() {
        if (!this.rayProgram || !this.quadProgram || !this.pointProgram) {
          return;
        }

        const gl = this.gl;
        const canvas = gl.canvas;

        // First pass: render rays to floating point texture
        gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
        gl.viewport(0, 0, canvas.width, canvas.height);

        // Only clear on first use
        if (!this.hasFirstFlush) {
          gl.clearColor(0.0, 0.0, 0.0, 0.0);
          gl.clear(gl.COLOR_BUFFER_BIT);
          this.hasFirstFlush = true;
        }

        // Enable additive blending
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.ONE, gl.ONE);

        // Draw points
        if (this.pointCache.length > 0) {
          gl.useProgram(this.pointProgram);

          // Set shared uniforms
          gl.uniform2f(this.pointResolutionUniformLocation, canvas.width, canvas.height);
          gl.uniform2f(this.pointOriginUniformLocation, this.origin.x, this.origin.y);
          gl.uniform1f(this.pointScaleUniformLocation, this.scale);

          // Bind point vertices
          gl.bindBuffer(gl.ARRAY_BUFFER, this.pointBuffer);
          gl.enableVertexAttribArray(this.pointPositionAttributeLocation);
          gl.vertexAttribPointer(this.pointPositionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

          // Draw each point
          this.pointCache.forEach(({ p, color, size }) => {
            const [r, g, b, a] = color;
            gl.uniform2f(this.pointPositionUniformLocation, p.x, p.y);
            gl.uniform1f(this.pointSizeUniformLocation, size * this.lengthScale);
            gl.uniform4f(this.pointColorUniformLocation, r * a, g * a, b * a, 1.0);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
          });
        }

        // Draw rays and segments
        gl.useProgram(this.rayProgram);

        // Set shared uniforms
        gl.uniform2f(this.resolutionUniformLocation, canvas.width, canvas.height);
        gl.uniform2f(this.originUniformLocation, this.origin.x, this.origin.y);
        gl.uniform1f(this.scaleUniformLocation, this.scale);

        // Use the reusable line buffer
        gl.bindBuffer(gl.ARRAY_BUFFER, this.lineBuffer);
        gl.enableVertexAttribArray(this.positionAttributeLocation);
        gl.vertexAttribPointer(this.positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

        // Switch to scene space for rays and segments
        gl.uniform1i(this.isScreenSpaceLocation, false);

        // Draw rays
        this.rayCache.forEach(({ r, color }) => {
          const [r_, g, b, a] = color;
          gl.uniform4f(this.colorUniformLocation, r_ * a, g * a, b * a, 1.0);

          // Calculate canvas limit for ray length
          const cvsLimit = (Math.abs(r.p1.x + this.origin.x) + Math.abs(r.p1.y + this.origin.y) + this.canvas.height + this.canvas.width) / Math.min(1, this.scale);
          const rayEnd = {
            x: r.p1.x + (r.p2.x - r.p1.x) * cvsLimit,
            y: r.p1.y + (r.p2.y - r.p1.y) * cvsLimit
          };

          const vertices = this.createRectangleFromLine(r.p1, rayEnd);
          gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
          gl.drawArrays(gl.TRIANGLES, 0, 6);
        });

        // Draw segments
        this.segmentCache.forEach(({ s, color }) => {
          const [r_, g, b, a] = color;
          gl.uniform4f(this.colorUniformLocation, r_ * a, g * a, b * a, 1.0);

          const vertices = this.createRectangleFromLine(s.p1, s.p2);
          gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
          gl.drawArrays(gl.TRIANGLES, 0, 6);
        });

        // Draw arrows
        if (this.arrowCache.length > 0) {
          // Use the same program as rays and points
          gl.useProgram(this.rayProgram);
          gl.bindBuffer(gl.ARRAY_BUFFER, this.arrowBuffer);
          gl.enableVertexAttribArray(this.positionAttributeLocation);
          gl.vertexAttribPointer(this.positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

          this.arrowCache.forEach(({ points, color }) => {
            const [r, g, b, a] = color;
            gl.uniform4f(this.colorUniformLocation, r * a, g * a, b * a, 1.0);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.DYNAMIC_DRAW);
            gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
          });
        }

        // Second pass: render floating point texture to screen with normalization
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.viewport(0, 0, canvas.width, canvas.height);
        gl.clearColor(0.0, 0.0, 0.0, 0.0);
        gl.clear(gl.COLOR_BUFFER_BIT);

        // Disable blending for final pass
        gl.disable(gl.BLEND);

        gl.useProgram(this.quadProgram);

        // Bind the floating point texture
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, this.floatTexture);
        gl.uniform1i(this.textureLocation, 0);

        // Draw fullscreen quad
        gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
        gl.enableVertexAttribArray(this.quadPositionLocation);
        gl.vertexAttribPointer(this.quadPositionLocation, 2, gl.FLOAT, false, 0, 0);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        // Clear caches
        this.rayCache.length = 0;
        this.segmentCache.length = 0;
        this.pointCache.length = 0;
        this.arrowCache.length = 0;
      }

      /**
       * Create a rectangle from a line segment. Since WebGL lineWidth is not widely supported, this method creates a rectangle with the given width to render the line segment or ray.
       * @param {Point} p1
       * @param {Point} p2
       * @param {number} [width=1]
       */
      createRectangleFromLine(p1, p2, width = 1) {
        const dx = p2.x - p1.x;
        const dy = p2.y - p1.y;
        const length = Math.sqrt(dx * dx + dy * dy);

        if (length < 1e-5 * this.lengthScale) {
          return new Float32Array(0);
        }

        // Calculate unit vectors
        const unitX = dx / length;
        const unitY = dy / length;
        const perpX = -unitY * (width * this.lengthScale / 2);
        const perpY = unitX * (width * this.lengthScale / 2);

        // Create rectangle vertices
        return new Float32Array([
          // First triangle
          p1.x + perpX, p1.y + perpY,
          p1.x - perpX, p1.y - perpY,
          p2.x + perpX, p2.y + perpY,
          // Second triangle
          p2.x + perpX, p2.y + perpY,
          p1.x - perpX, p1.y - perpY,
          p2.x - perpX, p2.y - perpY
        ]);
      }

      /**
       * Preprocess color for gamma correction and normalization.
       * @param {number[]} color
       * @returns {number[]}
       */
      preprocessColor(color) {
        const r = color[0] * color[3];
        const g = color[1] * color[3];
        const b = color[2] * color[3];

        if (r + g + b == 0) {
          return [0, 0, 0, 0];
        }

        const m = Math.max(r, g, b);

        switch (this.colorMode) {
          case 'legacy':
            const factor = Math.min(-Math.log(1 - m) / m, 100);
            return [r * factor, g * factor, b * factor, 1.0];
          case 'legacy_color':
            return [r, g, b, 1.0];
          case 'colorizedIntensity':
            return [m, m, m, 1.0];
          default:
            // Correct gamma
            const rr = Math.pow(r, 2.2);
            const gg = Math.pow(g, 2.2);
            const bb = Math.pow(b, 2.2);

            const ratio = m / Math.max(rr, gg, bb);

            return [rr * ratio, gg * ratio, bb * ratio, 1.0];
        }
      }

      /**
       * Destroy the renderer and release resources.
       */
      destroy() {
        const gl = this.gl;

        // Delete buffers
        gl.deleteBuffer(this.lineBuffer);
        gl.deleteBuffer(this.pointBuffer);
        gl.deleteBuffer(this.arrowBuffer);
        gl.deleteBuffer(this.quadBuffer);

        // Delete textures
        gl.deleteTexture(this.floatTexture);

        // Delete framebuffer
        gl.deleteFramebuffer(this.framebuffer);

        // Delete shader programs
        gl.deleteProgram(this.rayProgram);
        gl.deleteProgram(this.pointProgram);
        gl.deleteProgram(this.quadProgram);
      }

      applyColorTransformation() {
        // This is just to maintain the same API as `CanvasRenderer`. Color transformation is already done in the fragment shader in this renderer.
      }
    }

    export default FloatColorRenderer;