UNPKG

p5

Version:

[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

527 lines (475 loc) 17.5 kB
import { Shader } from '../webgl/p5.Shader.js'; import { s as setWebGLUniformValue, e as setWebGLTextureParams, g as getWebGLUniformMetadata, h as getWebGLShaderAttributes, j as populateGLSLHooks, T as Texture } from '../rendering-C5SM-3b6.js'; import { I as Image } from '../p5.Renderer-N-APumjv.js'; import { C as CLAMP, ab as WEBGL2, n as CORNER, B as BLEND, ac as BLUR, ad as WEBGL } from '../constants-DwbuOBz3.js'; import { filterParamDefaults } from './const.js'; import { w as webgl2CompatibilityShader, f as filterBaseVert, a as filterBaseFrag } from '../webgl2Compatibility-DA7DLMuq.js'; import { g as glslBackend, r as randomGLSL, a as randomVertGLSL } from '../strands_glslBackend-DMhOnoGl.js'; import { getShaderHookTypes } from '../webgl/shaderHookUtils.js'; import { makeFilterShader } from '../core/filterShaders.js'; import '../creating_reading-Be7_6X4p.js'; import 'colorjs.io/fn'; import '../color/color_spaces/hsb.js'; import '../strands/ir_types.js'; import '../dom/p5.Element.js'; import '../dom/p5.File.js'; import '../io/p5.XML.js'; import '../dom/p5.MediaElement.js'; import '../shape/2d_primitives.js'; import '../core/helpers.js'; import '../shape/attributes.js'; import '../shape/curves.js'; import '../shape/vertex.js'; import '../color/setting.js'; import 'omggif'; import '../io/csv.js'; import '../io/utilities.js'; import './pixels.js'; import './filters.js'; import '../core/transform.js'; import '../webgl/GeometryBuilder.js'; import '../math/p5.Matrix.js'; import '../math/Matrices/Matrix.js'; import '../math/p5.Vector.js'; import '../math/Matrices/MatrixInterface.js'; import '../webgl/p5.Geometry.js'; import '../webgl/p5.DataArray.js'; import '../webgl/p5.Quat.js'; import '../webgl/ShapeBuilder.js'; import 'libtess'; import '../webgl/p5.RenderBuffer.js'; import '../webgl/GeometryBufferCache.js'; import '../shape/custom_shapes.js'; import '../type/textCore.js'; import '../core/States.js'; import '../webgl/enums.js'; import '../math/trigonometry.js'; import '../strands/ir_dag.js'; import '../strands/strands_FES.js'; import '../ir_builders-CMXkjMoV.js'; import '../strands/ir_cfg.js'; import '../strands/strands_builtins.js'; class FilterRenderer2D { /** * Creates a new FilterRenderer2D instance. * @param {p5} parentRenderer - The p5.js instance. */ constructor(parentRenderer) { this.parentRenderer = parentRenderer; // Create a canvas for applying WebGL-based filters this.canvas = document.createElement('canvas'); this.canvas.width = parentRenderer.width; this.canvas.height = parentRenderer.height; // Initialize the WebGL context let webglVersion = WEBGL2; this.gl = this.canvas.getContext('webgl2'); if (!this.gl) { webglVersion = WEBGL; this.gl = this.canvas.getContext('webgl'); } if (!this.gl) { console.error('WebGL not supported, cannot apply filter.'); return; } this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); this.textures = new Map(); // Minimal renderer object required by p5.Shader and p5.Texture this._renderer = { GL: this.gl, registerEnabled: new Set(), _curShader: null, _emptyTexture: null, webglVersion, states: { textureWrapX: CLAMP, textureWrapY: CLAMP, }, _arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b), _getEmptyTexture: () => { if (!this._emptyTexture) { const im = new Image(1, 1); im.set(0, 0, 255); this._emptyTexture = new Texture(this._renderer, im); } return this._emptyTexture; }, _initShader: (shader) => { const gl = this.gl; const vertShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertShader, shader.vertSrc()); gl.compileShader(vertShader); if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { throw new Error(`Yikes! An error occurred compiling the vertex shader: ${ gl.getShaderInfoLog(vertShader) }`); } const fragShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragShader, shader.fragSrc()); gl.compileShader(fragShader); if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { throw new Error(`Darn! An error occurred compiling the fragment shader: ${ gl.getShaderInfoLog(fragShader) }`); } const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error( `Snap! Error linking shader program: ${gl.getProgramInfoLog(program)}` ); } shader._glProgram = program; shader._vertShader = vertShader; shader._fragShader = fragShader; }, getTexture: (input) => { let src = input; if (src instanceof Framebuffer) { src = src.color; } const texture = this.textures.get(src); if (texture) { return texture; } const tex = new Texture(this._renderer, src); this.textures.set(src, tex); return tex; }, populateHooks: (shader, src, shaderType) => { return populateGLSLHooks(shader, src, shaderType); }, _getShaderAttributes: (shader) => { return getWebGLShaderAttributes(shader, this.gl); }, getUniformMetadata: (shader) => { return getWebGLUniformMetadata(shader, this.gl); }, _finalizeShader: () => {}, _useShader: (shader) => { this.gl.useProgram(shader._glProgram); }, bindTexture: (tex) => { // bind texture using gl context + glTarget and // generated gl texture object this.gl.bindTexture(this.gl.TEXTURE_2D, tex.getTexture().texture); }, unbindTexture: () => { // unbind per above, disable texturing on glTarget this.gl.bindTexture(this.gl.TEXTURE_2D, null); }, _unbindFramebufferTexture: (uniform) => { // Make sure an empty texture is bound to the slot so that we don't // accidentally leave a framebuffer bound, causing a feedback loop // when something else tries to write to it const gl = this.gl; const empty = this._getEmptyTexture(); gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); empty.bindTexture(); gl.uniform1i(uniform.location, uniform.samplerIndex); }, createTexture: ({ width, height, format, dataType }) => { const gl = this.gl; const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // TODO use format and data type return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE }; }, uploadTextureFromSource: ({ texture, glFormat, glDataType }, source) => { const gl = this.gl; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source); }, uploadTextureFromData: ({ texture, glFormat, glDataType }, data, width, height) => { const gl = this.gl; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( gl.TEXTURE_2D, 0, glFormat, width, height, 0, glFormat, glDataType, data ); }, setTextureParams: (texture) => { return setWebGLTextureParams(texture, this.gl, this._renderer.webglVersion); }, updateUniformValue: (shader, uniform, data) => { return setWebGLUniformValue( shader, uniform, data, (tex) => this._renderer.getTexture(tex), this.gl ); }, _enableAttrib: (_shader, attr, size, type, normalized, stride, offset) => { const loc = attr.location; const gl = this.gl; // Enable register even if it is disabled if (!this._renderer.registerEnabled.has(loc)) { gl.enableVertexAttribArray(loc); // Record register availability this._renderer.registerEnabled.add(loc); } gl.vertexAttribPointer( loc, size, type || gl.FLOAT, normalized || false, stride || 0, offset || 0 ); }, _disableRemainingAttributes: (shader) => { for (const location of this._renderer.registerEnabled.values()) { if ( !Object.keys(shader.attributes).some( key => shader.attributes[key].location === location ) ) { this.gl.disableVertexAttribArray(location); this._renderer.registerEnabled.delete(location); } } }, _updateTexture: (uniform, tex) => { const gl = this.gl; gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); tex.bindTexture(); tex.update(); gl.uniform1i(uniform.location, uniform.samplerIndex); }, baseFilterShader: () => this.baseFilterShader(), strandsBackend: glslBackend, getShaderHookTypes: (shader, hookName) => getShaderHookTypes(shader, hookName), uniformNameFromHookKey: (key) => key.slice(key.indexOf(' ') + 1), }; this._baseFilterShader = undefined; // Store initialized shaders for each operation this.filterShaders = {}; // These will be set by setOperation this.operation = null; this.filterParameter = 1; this.customShader = null; this._shader = null; // Create buffers once this.vertexBuffer = this.gl.createBuffer(); this.texcoordBuffer = this.gl.createBuffer(); // Set up the vertices and texture coordinates for a full-screen quad this.vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); this.texcoords = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]); // Upload vertex data once this._bindBufferData( this.vertexBuffer, this.gl.ARRAY_BUFFER, this.vertices ); // Upload texcoord data once this._bindBufferData( this.texcoordBuffer, this.gl.ARRAY_BUFFER, this.texcoords ); } _webGL2CompatibilityPrefix(shaderType, floatPrecision) { let code = ''; if (this._renderer.webglVersion === WEBGL2) { code += '#version 300 es\n#define WEBGL2\n'; } if (shaderType === 'vert') { code += '#define VERTEX_SHADER\n'; } else if (shaderType === 'frag') { code += '#define FRAGMENT_SHADER\n'; } if (floatPrecision) { code += `precision ${floatPrecision} float;\n`; } return code; } baseFilterShader() { if (!this._baseFilterShader) { this._baseFilterShader = new Shader( this._renderer, this._webGL2CompatibilityPrefix('vert', 'highp') + webgl2CompatibilityShader + filterBaseVert, this._webGL2CompatibilityPrefix('frag', 'highp') + webgl2CompatibilityShader + filterBaseFrag, { vertex: {}, fragment: { 'vec4 getColor': `(FilterInputs inputs, in sampler2D canvasContent) { return getTexture(canvasContent, inputs.texCoord); }` }, hookAliases: { 'getColor': ['filterColor'], }, } ); } return this._baseFilterShader; } getRandomFragmentShaderSnippet() { return randomGLSL; } getRandomVertexShaderSnippet() { return randomVertGLSL; } /** * Set the current filter operation and parameter. If a customShader is provided, * that overrides the operation-based shader. * @param {String} operation - The filter operation type (e.g., constants.BLUR). * @param {Number} filterParameter - The strength of the filter. * @param {p5.Shader} customShader - Optional custom shader. */ setOperation(operation, filterParameter, customShader = null) { this.operation = operation; this.filterParameter = filterParameter; let useDefaultParam = operation in filterParamDefaults && filterParameter === undefined; if (useDefaultParam) { this.filterParameter = filterParamDefaults[operation]; } this.customShader = customShader; this._initializeShader(); } /** * Initializes or retrieves the shader program for the current operation. * If a customShader is provided, that is used. * Otherwise, returns a cached shader if available, or creates a new one, caches it, and sets it as current. */ _initializeShader() { if (this.customShader) { this._shader = this.customShader; return; } if (!this.operation) { console.error('No operation set for FilterRenderer2D, cannot initialize shader.'); return; } // If we already have a compiled shader for this operation, reuse it if (this.filterShaders[this.operation]) { this._shader = this.filterShaders[this.operation]; return; } // Use the shared makeFilterShader function from filterShaders.js const newShader = makeFilterShader(this._renderer, this.operation, this.parentRenderer._pInst); this.filterShaders[this.operation] = newShader; this._shader = newShader; } /** * Binds a buffer to the drawing context * when passed more than two arguments it also updates or initializes * the data associated with the buffer */ _bindBufferData(buffer, target, values) { const gl = this.gl; gl.bindBuffer(target, buffer); gl.bufferData(target, values, gl.STATIC_DRAW); } get canvasTexture() { if (!this._canvasTexture) { this._canvasTexture = new Texture(this._renderer, this.parentRenderer); } return this._canvasTexture; } /** * Prepares and runs the full-screen quad draw call. */ _renderPass() { const gl = this.gl; this._shader.bindShader('fill'); const pixelDensity = this.parentRenderer.pixelDensity ? this.parentRenderer.pixelDensity() : 1; const texelSize = [ 1 / (this.parentRenderer.width * pixelDensity), 1 / (this.parentRenderer.height * pixelDensity) ]; const canvasTexture = this.canvasTexture; // Set uniforms for the shader this._shader.setUniform('tex0', canvasTexture); this._shader.setUniform('texelSize', texelSize); this._shader.setUniform('canvasSize', [this.parentRenderer.width, this.parentRenderer.height]); this._shader.setUniform('radius', Math.max(1, this.filterParameter)); this._shader.setUniform('filterParameter', this.filterParameter); this._shader.setDefaultUniforms(); this.parentRenderer.states.setValue('rectMode', CORNER); this.parentRenderer.states.setValue('imageMode', CORNER); this.parentRenderer.blendMode(BLEND); this.parentRenderer.resetMatrix(); const identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; this._shader.setUniform('uModelViewMatrix', identityMatrix); this._shader.setUniform('uProjectionMatrix', identityMatrix); // Bind and enable vertex attributes gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); this._shader.enableAttrib(this._shader.attributes.aPosition, 2); gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffer); this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2); this._shader.bindTextures(); this._renderer._disableRemainingAttributes(this._shader); // Draw the quad gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // Unbind the shader this._shader.unbindShader(); } /** * Applies the current filter operation. If the filter requires multiple passes (e.g. blur), * it handles those internally. Make sure setOperation() has been called before applyFilter(). */ applyFilter() { if (!this._shader) { console.error('Cannot apply filter: shader not initialized.'); return; } this.parentRenderer.push(); this.parentRenderer.resetMatrix(); // For blur, we typically do two passes: one horizontal, one vertical. if (this.operation === BLUR && !this.customShader) { // Horizontal pass this._shader.setUniform('direction', [1, 0]); this._renderPass(); // Draw the result onto itself this.parentRenderer.clear(); this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, this.parentRenderer.width, this.parentRenderer.height ); // Vertical pass this._shader.setUniform('direction', [0, 1]); this._renderPass(); this.parentRenderer.clear(); this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, this.parentRenderer.width, this.parentRenderer.height ); } else { // Single-pass filters this._renderPass(); this.parentRenderer.clear(); // con this.parentRenderer.blendMode(BLEND); this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, this.parentRenderer.width, this.parentRenderer.height ); } this.parentRenderer.pop(); } } export { FilterRenderer2D as default };