Source: ShaderFilterVector.js

import { Color } from './Colormap.js';
import { ShaderFilter } from './ShaderFilter.js'

// vector field https://www.shadertoy.com/view/4s23DG
// isolines https://www.shadertoy.com/view/Ms2XWc

/**
 * @typedef {Object} ShaderFilterVector~Options
 * Configuration options for vector field visualization
 * @property {number[]} [inDomain=[]] - Input value range [min, max] for magnitude mapping
 * @property {number} [maxSteps=256] - Number of discrete steps in the colormap texture
 * @property {number[]} [arrowColor=[0.0, 0.0, 0.0, 1.0]] - RGBA color for arrows when using 'col' mode
 */

/**
 * @typedef {Object} ShaderFilterVector~Modes
 * Available visualization modes
 * @property {string} normalize - Arrow normalization ('on'|'off')
 * @property {string} arrow - Arrow coloring mode ('mag'|'col')
 * @property {string} field - Background field visualization ('none'|'mag')
 */

/**
 * 
 * ShaderFilterVector implements 2D vector field visualization techniques.
 * Based on techniques from "2D vector field visualization by Morgan McGuire"
 * and enhanced by Matthias Reitinger.
 * 
 * Features:
 * - Arrow-based vector field visualization
 * - Magnitude-based or custom arrow coloring
 * - Optional vector normalization
 * - Background field visualization
 * - Customizable arrow appearance
 * - Smooth interpolation
 * 
 * Technical Implementation:
 * - Tile-based arrow rendering
 * - Signed distance field for arrow shapes
 * - Dynamic magnitude scaling
 * - Colormap-based magnitude visualization
 * - WebGL 1.0 and 2.0 compatibility
 *
 * Example usage:
 * ```javascript
 * // Basic usage with default options
 * const vectorField = new ShaderFilterVector(myColorScale);
 * shader.addFilter(vectorField);
 * 
 * // Configure visualization modes
 * vectorField.setMode('normalize', 'on');  // Normalize arrow lengths
 * vectorField.setMode('arrow', 'col');     // Use custom arrow color
 * vectorField.setMode('field', 'mag');     // Show magnitude field
 * ```
 * 
 * Advanced usage with custom configuration:
 * ```javascript
 * const vectorField = new ShaderFilterVector(colorscale, {
 *     inDomain: [-10, 10],         // Vector magnitude range
 *     maxSteps: 512,               // Higher colormap resolution
 *     arrowColor: [1, 0, 0, 1]     // Red arrows
 * });
 * 
 * // Add to shader pipeline
 * shader.addFilter(vectorField);
 * ```
 *
 * GLSL Implementation Details
 * 
 * Key Components:
 * 1. Arrow Generation:
 *    - Tile-based positioning
 *    - Shaft and head construction
 *    - Size and direction control
 * 
 * 2. Distance Functions:
 *    - line3(): Distance to line segment
 *    - line(): Signed distance to line
 *    - arrow(): Complete arrow shape
 * 
 * 3. Color Processing:
 *    - Vector magnitude computation
 *    - Colormap lookup
 *    - Mode-based blending
 * 
 * Constants:
 * - ARROW_TILE_SIZE: Spacing between arrows (16.0)
 * - ISQRT2: 1/sqrt(2) for magnitude normalization
 * 
 * Uniforms:
 * - {vec4} arrow_color - Custom arrow color
 * - {vec4} low_color - Color for values below range
 * - {vec4} high_color - Color for values above range
 * - {float} scale - Magnitude scaling factor
 * - {float} bias - Magnitude offset
 * - {sampler2D} colormap - Magnitude colormap texture
 *
 * @extends ShaderFilter
 */
class ShaderFilterVector extends ShaderFilter {
    /**
     * Creates a new vector field visualization filter
     * @param {ColorScale} colorscale - Colorscale for magnitude mapping
     * @param {ShaderFilterVector~Options} [options] - Configuration options
     * @throws {Error} If inDomain is invalid (length !== 2 or min >= max)
     * 
     * @example
     * ```javascript
     * // Create with default options
     * const filter = new ShaderFilterVector(colorscale, {
     *     inDomain: [0, 1],
     *     maxSteps: 256,
     *     arrowColor: [0, 0, 0, 1]
     * });
     * ```
     */
    constructor(colorscale, options) {
        super(options);
        options = Object.assign({
            inDomain: [],
            maxSteps: 256,
            arrowColor: [0.0, 0.0, 0.0, 1.0],

        }, options);
        Object.assign(this, options);

        if (this.inDomain.length != 2 && this.inDomain[1] <= this.inDomain[0]) {
            throw Error("inDomain bad format");
        }

        this.colorscale = colorscale;
        if (this.inDomain.length == 0) this.inDomain = this.colorscale.rangeDomain();

        const cscaleDomain = this.colorscale.rangeDomain();

        const scale = Math.sqrt((this.inDomain[1] * this.inDomain[1] + this.inDomain[0] * this.inDomain[0]) / (cscaleDomain[1] * cscaleDomain[1] + cscaleDomain[0] * cscaleDomain[0]));
        const bias = 0.0;

        this.modes = {
            normalize: [
                { id: 'off', enable: true, src: `const bool ${this.modeName('arrowNormalize')} = false;` },
                { id: 'on', enable: false, src: `const bool ${this.modeName('arrowNormalize')} = true;` }
            ],
            arrow: [
                { id: 'mag', enable: true, src: `const int ${this.modeName('arrowColor')} = 0;` },
                { id: 'col', enable: false, src: `const int ${this.modeName('arrowColor')} = 1;` }
            ],
            field: [
                { id: 'none', enable: true, src: `const int ${this.modeName('fieldColor')} = 0;` },
                { id: 'mag', enable: false, src: `const int ${this.modeName('fieldColor')} = 1;` }
            ]
        };

        this.samplers = [{ name: `${this.samplerName('colormap')}` }];

        this.uniforms[this.uniformName('arrow_color')] = { type: 'vec4', needsUpdate: true, size: 4, value: this.arrowColor };
        this.uniforms[this.uniformName('low_color')] = { type: 'vec4', needsUpdate: true, size: 4, value: this.colorscale.lowColor.value() };
        this.uniforms[this.uniformName('high_color')] = { type: 'vec4', needsUpdate: true, size: 4, value: this.colorscale.highColor.value() };
        this.uniforms[this.uniformName('scale')] = { type: 'float', needsUpdate: true, size: 1, value: scale };
        this.uniforms[this.uniformName('bias')] = { type: 'float', needsUpdate: true, size: 1, value: bias };
    }

    /**
     * Creates the colormap texture for magnitude visualization.
     * Samples colorscale at specified resolution, creates 1D RGBA texture,
     * configures appropriate texture filtering, and links texture with sampler.
     * 
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {Promise<void>}
     * @private
     */
    async createTextures(gl) {
        const colormap = this.colorscale.sample(this.maxSteps);
        let textureFilter = gl.LINEAR;
        if (this.colorscale.type == 'bar') {
            textureFilter = gl.NEAREST;
        }
        const tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, textureFilter);
        gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, textureFilter);
        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, this.maxSteps, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, colormap.buffer);
        this.getSampler('colormap').tex = tex; // Link tex to sampler
    }

    /**
     * Generates GLSL code for vector field visualization.
     * 
     * Shader Features:
     * - Tile-based arrow placement
     * - Signed distance field arrow rendering
     * - Multiple visualization modes
     * - Magnitude-based colormapping
     * - Smooth field interpolation
     * 
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {string} GLSL function definition
     * @private
     */
    fragDataSrc(gl) {
        return `
        // 2D vector field visualization by Matthias Reitinger, @mreitinger
        // Based on "2D vector field visualization by Morgan McGuire, http://casual-effects.com", https://www.shadertoy.com/view/4s23DG
        
        const float ARROW_TILE_SIZE = 16.0;
        const float ISQRT2 = 0.70710678118; // 1/sqrt(2)

        // Computes the center pixel of the tile containing pixel pos
        vec2 arrowTileCenterCoord(vec2 pos) {
            return (floor(pos / ARROW_TILE_SIZE) + 0.5) * ARROW_TILE_SIZE;
        }

        // Computes the distance from a line segment
        float line3(vec2 a, vec2 b, vec2 c) {
            vec2 ab = a - b;
            vec2 cb = c - b;
            float d = dot(ab, cb);
            float len2 = dot(cb, cb);
            float t = 0.0;
            if (len2 != 0.0) {
              t = clamp(d / len2, 0.0, 1.0);
            }
            vec2 r = b + cb * t;
            return distance(a, r);
        }

        // Computes the signed distance from a line segment
        float line(vec2 p, vec2 p1, vec2 p2) {
            vec2 center = (p1 + p2) * 0.5;
            float len = length(p2 - p1);
            vec2 dir = (p2 - p1) / len;
            vec2 rel_p = p - center;
            float dist1 = abs(dot(rel_p, vec2(dir.y, -dir.x)));
            float dist2 = abs(dot(rel_p, dir)) - 0.5*len;
            return max(dist1, dist2);
        }
        
        // v = field sampled at arrowTileCenterCoord(p), scaled by the length
        // desired in pixels for arrows
        // Returns a signed distance from the arrow
        float arrow(vec2 p, vec2 v) {
            if (${this.modeName('arrowNormalize')}) v = normalize(v);
            v *= ARROW_TILE_SIZE * 0.5; // Change from [-1,1] to pixels
            // Make everything relative to the center, which may be fractional
            p -= arrowTileCenterCoord(p);
                
            float mag_v = length(v), mag_p = length(p);
            
            if (mag_v > 0.0) {
                // Non-zero velocity case
                vec2 dir_v = normalize(v);
                
                // We can't draw arrows larger than the tile radius, so clamp magnitude.
                // Enforce a minimum length to help see direction
                mag_v = clamp(mag_v, 2.0, ARROW_TILE_SIZE * 0.4);
        
                // Arrow tip location
                v = dir_v * mag_v;
        
                // Signed distance from shaft
                float shaft = line3(p, v, -v);
                // Signed distance from head
                float head = min(line3(p, v, 0.4*v + 0.2*vec2(-v.y, v.x)),
                                 line3(p, v, 0.4*v + 0.2*vec2(v.y, -v.x)));
                return min(shaft, head);
            } else {
                // Signed distance from the center point
                return mag_p;
            }
        }
        
        vec4 lookupColormap(float cv) {            
            if(cv >= 1.0) 
                return ${this.uniformName('high_color')};
            else if(cv <= 0.0) 
                return ${this.uniformName('low_color')};
            return texture(${this.samplerName('colormap')}, vec2(cv, 0.5));
        }

        vec4 ${this.functionName()}(vec4 col){
            if(col.a == 0.0) return col;

            vec2 p = v_texcoord*tileSize; // point in pixel
            vec2 pc_coord = arrowTileCenterCoord(p)/tileSize; // center coordinate
            vec4 pc_val = texture(kd, pc_coord); // [0..1] - lookup color in center
            float s = 2.0;
            float b = -1.0;
            vec2 uvc = vec2(pc_val.x*s+b, pc_val.y*s+b); // [-1..1]
            vec2 uvr =  vec2(col.r*s+b, col.g*s+b); // [-1..1]

            // Colors
            float vc = length(uvc)*ISQRT2;
            float cvc = vc*${this.uniformName('scale')} + ${this.uniformName('bias')};
            float vr = length(uvr)*ISQRT2;
            float cvr = vr*${this.uniformName('scale')} + ${this.uniformName('bias')};
            vec4 cmapc = lookupColormap(cvc);
            vec4 cmapr = lookupColormap(cvr);
                
            // Arrow            
            float arrow_dist = arrow(p, uvc);
            
            vec4 arrow_col = cmapc;
            vec4 field_col = vec4(0.0, 0.0, 0.0, 0.0);

            switch (${this.modeName('arrowColor')}) {
                case 0:
                    arrow_col = cmapc;
                    break;
                case 1:
                    arrow_col = ${this.uniformName('arrow_color')};               
                    break;
            }

            switch (${this.modeName('fieldColor')}) {
                case 0:
                    field_col = vec4(0.0, 0.0, 0.0, 0.0);
                    break;
                case 1:
                    field_col = cmapr;              
                    break;
            }

            float t = clamp(arrow_dist, 0.0, 1.0);
            return  mix(arrow_col, field_col, t);
        }`;
    }


}

export { ShaderFilterVector }