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 2.0+
*
* 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(source, 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 }