Source: ShaderLens.js

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import { Shader } from './Shader.js'

/**
 * @typedef {Object} ShaderLens~Uniforms
 * Uniform definitions for lens shader
 * @property {number[]} u_lens - Lens parameters [centerX, centerY, radius, borderWidth]
 * @property {number[]} u_width_height - Viewport dimensions [width, height]
 * @property {number[]} u_border_color - RGBA border color [r, g, b, a]
 * @property {boolean} u_border_enable - Whether to show lens border
 */

/**
 * @typedef {Object} ShaderLens~Options
 * Configuration options for lens shader
 * @property {string} [label='ShaderLens'] - Display label
 * @property {boolean} [overlayLayerEnabled=false] - Enable overlay layer
 * @property {Object} [uniforms] - Custom uniform values
 * @extends Shader~Options
 */

/**
 * ShaderLens implements a circular magnification lens effect with optional overlay.
 * 
 * Features:
 * - Circular lens with smooth borders
 * - Configurable lens size and position
 * - Optional border with customizable color
 * - Optional overlay layer with grayscale outside lens
 * - Smooth transition between lens and background
 * - Real-time lens movement
 * 
 * Technical Implementation:
 * - Pixel-based distance calculations
 * - Smooth border transitions
 * - Alpha blending for overlays
 * - WebGL 1.0 and 2.0 compatibility
 * - Viewport coordinate mapping
 * 
 *
 * Example usage:
 * ```javascript
 * // Create lens shader
 * const lens = new ShaderLens();
 * 
 * // Configure lens
 * lens.setLensUniforms(
 *     [400, 300, 100, 10],  // center at (400,300), radius 100, border 10
 *     [800, 600],           // viewport size
 *     [0.8, 0.8, 0.8, 1],   // gray border
 *     true                  // show border
 * );
 * 
 * // Enable overlay
 * lens.setOverlayLayerEnabled(true);
 * ```
 * 
 * Advanced usage with custom configuration:
 * ```javascript
 * const lens = new ShaderLens({
 *     uniforms: {
 *         u_lens: { value: [0, 0, 150, 15] },
 *         u_border_color: { value: [1, 0, 0, 1] }  // red border
 *     },
 *     overlayLayerEnabled: true
 * });
 * ```
 *
 * GLSL Implementation Details
 * 
 * Key Components:
 * 1. Lens Function:
 *    - Distance-based circle calculation
 *    - Smooth border transitions
 *    - Color mixing and blending
 * 
 * 2. Overlay Processing:
 *    - Grayscale conversion
 *    - Alpha blending
 *    - Border preservation
 * 
 * Functions:
 * - lensColor(): Handles color transitions between lens regions
 * - data(): Main processing function
 * 
 * Uniforms:
 * - {vec4} u_lens - Lens parameters [cx, cy, radius, border]
 * - {vec2} u_width_height - Viewport dimensions
 * - {vec4} u_border_color - Border color and alpha
 * - {bool} u_border_enable - Border visibility flag
 * - {sampler2D} source0 - Main texture
 * - {sampler2D} source1 - Optional overlay texture
 *
 * @extends Shader
 */
class ShaderLens extends Shader {
    /**
     * Creates a new lens shader
     * @param {ShaderLens~Options} [options] - Configuration options
     * 
     * @example
     * ```javascript
     * // Create basic lens shader
     * const lens = new ShaderLens({
     *     label: 'MyLens',
     *     overlayLayerEnabled: false
     * });
     * ```
     */
    constructor(options) {
        super(options);

        this.samplers = [
            { id: 0, name: 'source0' }, { id: 1, name: 'source1' }
        ];

        this.uniforms = {
            u_lens: { type: 'vec4', needsUpdate: true, size: 4, value: [0, 0, 100, 10] },
            u_width_height: { type: 'vec2', needsUpdate: true, size: 2, value: [1, 1] },
            u_border_color: { type: 'vec4', needsUpdate: true, size: 4, value: [0.8, 0.8, 0.8, 1] },
            u_border_enable: { type: 'bool', needsUpdate: true, size: 1, value: false }
        };
        this.label = "ShaderLens";
        this.needsUpdate = true;
        this.overlayLayerEnabled = false;
    }

    /**
     * Enables or disables the overlay layer
     * When enabled, adds a second texture layer with grayscale outside lens
     * @param {boolean} enabled - Whether to enable overlay
     */
    setOverlayLayerEnabled(x) {
        this.overlayLayerEnabled = x;
        this.needsUpdate = true;
    }

    /**
     * Updates lens parameters and appearance
     * @param {number[]} lensViewportCoords - Lens parameters [centerX, centerY, radius, borderWidth]
     * @param {number[]} windowWH - Viewport dimensions [width, height]
     * @param {number[]} borderColor - RGBA border color
     * @param {boolean} borderEnable - Whether to show border
     */
    setLensUniforms(lensViewportCoords, windowWH, borderColor, borderEnable) {
        this.setUniform('u_lens', lensViewportCoords);
        this.setUniform('u_width_height', windowWH);
        this.setUniform('u_border_color', borderColor);
        this.setUniform('u_border_enable', borderEnable);
    }

    /**
     * Generates fragment shader source code.
     * 
     * Shader Features:
     * - Circular lens implementation
     * - Smooth border transitions
     * - Optional overlay support
     * - Grayscale conversion outside lens
     * 
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {string} Fragment shader source code
     * @private
     */
    fragShaderSrc(gl) {
        let gl2 = !(gl instanceof WebGLRenderingContext);

        let samplerDeclaration = `uniform sampler2D ` + this.samplers[0].name + `;`;
        let overlaySamplerCode = "";

        if (this.overlayLayerEnabled) { //FIXME two cases with transparence or not.
            samplerDeclaration += `uniform sampler2D ` + this.samplers[1].name + `;`;

            overlaySamplerCode =
                `vec4 c1 = texture${gl2 ? '' : '2D'}(source1, v_texcoord);
            if (r > u_lens.z) {
                float k = (c1.r + c1.g + c1.b) / 3.0;
                c1 = vec4(k, k, k, c1.a);
            } else if (u_border_enable && r > innerBorderRadius) {
                // Preserve border keeping c1 alpha at zero
                c1.a = 0.0; 
            }
            color = color * (1.0 - c1.a) + c1 * c1.a;
            `
        }
        return `

        ${samplerDeclaration}
        uniform vec4 u_lens; // [cx, cy, radius, border]
        uniform vec2 u_width_height; // Keep wh to map to pixels. TexCoords cannot be integer unless using texture_rectangle
        uniform vec4 u_border_color;
        uniform bool u_border_enable;
        ${gl2 ? 'in' : 'varying'} vec2 v_texcoord;

        vec4 lensColor(in vec4 c_in, in vec4 c_border, in vec4 c_out,
            float r, float R, float B) {
            vec4 result;
            if (u_border_enable) {
                float B_SMOOTH = B < 8.0 ? B/8.0 : 1.0;
                if (r<R-B+B_SMOOTH) {
                    float t=smoothstep(R-B, R-B+B_SMOOTH, r);
                    result = mix(c_in, c_border, t);
                } else if (r<R-B_SMOOTH) {
                    result = c_border;  
                } else {
                    float t=smoothstep(R-B_SMOOTH, R, r);
                    result = mix(c_border, c_out, t);
                }
            } else {
                result = (r<R) ? c_in : c_out;
            }
            return result;
        }

        vec4 data() {
            vec4 color;
            float innerBorderRadius = (u_lens.z - u_lens.w);
            float dx = v_texcoord.x * u_width_height.x - u_lens.x;
            float dy = v_texcoord.y * u_width_height.y - u_lens.y;
            float r = sqrt(dx*dx + dy*dy);

            vec4 c_in = texture${gl2 ? '' : '2D'}(source0, v_texcoord);
            vec4 c_out = u_border_color; c_out.a=0.0;
            
            color = lensColor(c_in, u_border_color, c_out, r, u_lens.z, u_lens.w);

            ${overlaySamplerCode}
            return color;
        }
        `
    }

    /**
     * Generates vertex shader source code.
     * 
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {string} Vertex shader source code
     * @private
     */
    vertShaderSrc(gl) {
        let gl2 = !(gl instanceof WebGLRenderingContext);
        return `${gl2 ? '#version 300 es' : ''}
 

${gl2 ? 'in' : 'attribute'} vec4 a_position;
${gl2 ? 'in' : 'attribute'} vec2 a_texcoord;

${gl2 ? 'out' : 'varying'} vec2 v_texcoord;
void main() {
    gl_Position = a_position;
    v_texcoord = a_texcoord;
}`;
    }
}

export { ShaderLens }