From 9e68b2d9d3aa4cfde5d9ff3856ec0167683e85fa Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 1 Dec 2024 20:16:00 -0500 Subject: [PATCH 1/5] simplify a bit --- demo/distance.html | 1 + src/distance-field.ts | 72 ++++++++++++++++++++++++------------------- src/utils/tags.ts | 30 ++++++++++++------ 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/demo/distance.html b/demo/distance.html index 713566f..e518edd 100644 --- a/demo/distance.html +++ b/demo/distance.html @@ -13,6 +13,7 @@ min-height: 100%; position: relative; margin: 0; + overscroll-behavior: none; } fc-geometry { diff --git a/src/distance-field.ts b/src/distance-field.ts index 637ec05..730eef3 100644 --- a/src/distance-field.ts +++ b/src/distance-field.ts @@ -45,6 +45,8 @@ export class DistanceField extends HTMLElement { // Start the JFA process this.runJFA(); + + window.addEventListener('resize', this.handleResize); } // Lifecycle hooks @@ -62,11 +64,12 @@ export class DistanceField extends HTMLElement { geometry.removeEventListener('move', this.handleGeometryUpdate); geometry.removeEventListener('resize', this.handleGeometryUpdate); }); + + window.removeEventListener('resize', this.handleResize); } // Handle updates from geometries private handleGeometryUpdate = () => { - console.log('handleGeometryUpdate'); // Re-render seed points and rerun JFA this.initSeedPointRendering(); this.runJFA(); @@ -98,8 +101,6 @@ export class DistanceField extends HTMLElement { } private initShaders() { - const gl = this.gl; - // Shader sources const vertexShaderSource = vert`#version 300 es precision highp float; @@ -198,15 +199,18 @@ export class DistanceField extends HTMLElement { this.offsets = new Float32Array(offsets); - // Compile JFA shaders - const vertexShader = this.createShader(gl.VERTEX_SHADER, vertexShaderSource); - const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragmentShaderSource); - this.program = this.createProgram(vertexShader, fragmentShader); + // Compile JFA shaders using the utility function + this.program = this.compileShaderProgram(vertexShaderSource, fragmentShaderSource); - // Compile display shaders - const displayVertexShader = this.createShader(gl.VERTEX_SHADER, displayVertexShaderSource); - const displayFragmentShader = this.createShader(gl.FRAGMENT_SHADER, displayFragmentShaderSource); - this.displayProgram = this.createProgram(displayVertexShader, displayFragmentShader); + // Compile display shaders using the utility function + this.displayProgram = this.compileShaderProgram(displayVertexShaderSource, displayFragmentShaderSource); + } + + private compileShaderProgram(vertexSource: string, fragmentSource: string): WebGLProgram { + const gl = this.gl; + const vertexShader = this.createShader(gl.VERTEX_SHADER, vertexSource); + const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragmentSource); + return this.createProgram(vertexShader, fragmentShader); } private createShader(type: GLenum, source: string): WebGLShader { @@ -306,7 +310,7 @@ export class DistanceField extends HTMLElement { out vec4 outColor; void main() { - vec2 seedCoord = gl_FragCoord.xy / u_resolution; + vec2 seedCoord = gl_FragCoord.xy / vec2(${this.canvas.width.toFixed(1)}, ${this.canvas.height.toFixed(1)}); outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords, shape ID, initial distance 0 }`; @@ -423,29 +427,13 @@ export class DistanceField extends HTMLElement { gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0); - // Check framebuffer status - const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - if (status !== gl.FRAMEBUFFER_COMPLETE) { - console.error('Framebuffer is incomplete:', status.toString(16)); - return; - } - - gl.viewport(0, 0, this.canvas.width, this.canvas.height); - // Use shader program gl.useProgram(this.program); - // Adjust offsets based on step size and resolution - const adjustedOffsets = []; - for (let i = 0; i < this.offsets.length; i += 2) { - const offsetX = (this.offsets[i] * stepSize) / this.canvas.width; - const offsetY = (this.offsets[i + 1] * stepSize) / this.canvas.height; - adjustedOffsets.push(offsetX, offsetY); - } - - // Set the offsets uniform + // Compute and set the offsets uniform + const offsets = this.computeOffsets(stepSize); const offsetsLocation = gl.getUniformLocation(this.program, 'u_offsets'); - gl.uniform2fv(offsetsLocation, new Float32Array(adjustedOffsets)); + gl.uniform2fv(offsetsLocation, offsets); // Bind input texture gl.activeTexture(gl.TEXTURE0); @@ -517,4 +505,26 @@ export class DistanceField extends HTMLElement { gl.bindVertexArray(null); } + + // Handle window resize + private handleResize = () => { + // Update canvas size + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + + // Re-initialize WebGL resources + this.initPingPongTextures(); + this.renderSeedPoints(); + this.runJFA(); + }; + + private computeOffsets(stepSize: number): Float32Array { + const offsets = []; + for (let y = -1; y <= 1; y++) { + for (let x = -1; x <= 1; x++) { + offsets.push((x * stepSize) / this.canvas.width, (y * stepSize) / this.canvas.height); + } + } + return new Float32Array(offsets); + } } diff --git a/src/utils/tags.ts b/src/utils/tags.ts index f8fe24f..04b89a6 100644 --- a/src/utils/tags.ts +++ b/src/utils/tags.ts @@ -1,19 +1,29 @@ -export function glsl(strings: TemplateStringsArray) { - return strings[0]; +export function glsl(strings: TemplateStringsArray, ...values: any[]) { + return strings.reduce((result, str, i) => { + return result + str + (values[i] || ''); + }, ''); } -export function vert(strings: TemplateStringsArray) { - return strings[0]; +export function vert(strings: TemplateStringsArray, ...values: any[]) { + return strings.reduce((result, str, i) => { + return result + str + (values[i] || ''); + }, ''); } -export function frag(strings: TemplateStringsArray) { - return strings[0]; +export function frag(strings: TemplateStringsArray, ...values: any[]) { + return strings.reduce((result, str, i) => { + return result + str + (values[i] || ''); + }, ''); } -export function css(strings: TemplateStringsArray) { - return strings[0]; +export function css(strings: TemplateStringsArray, ...values: any[]) { + return strings.reduce((result, str, i) => { + return result + str + (values[i] || ''); + }, ''); } -export function html(strings: TemplateStringsArray) { - return strings[0]; +export function html(strings: TemplateStringsArray, ...values: any[]) { + return strings.reduce((result, str, i) => { + return result + str + (values[i] || ''); + }, ''); } From 6b8566fa3084966bf983c2f3a95946251c121fb1 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 1 Dec 2024 20:22:19 -0500 Subject: [PATCH 2/5] handle resize --- src/distance-field.ts | 53 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/distance-field.ts b/src/distance-field.ts index 730eef3..f46f1b8 100644 --- a/src/distance-field.ts +++ b/src/distance-field.ts @@ -249,6 +249,12 @@ export class DistanceField extends HTMLElement { const width = this.canvas.width; const height = this.canvas.height; + // Delete existing textures to prevent memory leaks + for (const texture of this.textures) { + gl.deleteTexture(texture); + } + this.textures = []; + // Enable the EXT_color_buffer_float extension const ext = gl.getExtension('EXT_color_buffer_float'); if (!ext) { @@ -282,8 +288,10 @@ export class DistanceField extends HTMLElement { this.textures.push(texture); } - // Create framebuffer - this.framebuffer = gl.createFramebuffer()!; + // Reuse existing framebuffer + if (!this.framebuffer) { + this.framebuffer = gl.createFramebuffer()!; + } } private initSeedPointRendering() { @@ -305,19 +313,17 @@ export class DistanceField extends HTMLElement { precision highp float; flat in float v_shapeID; - uniform vec2 u_resolution; + uniform vec2 u_canvasSize; out vec4 outColor; void main() { - vec2 seedCoord = gl_FragCoord.xy / vec2(${this.canvas.width.toFixed(1)}, ${this.canvas.height.toFixed(1)}); + vec2 seedCoord = gl_FragCoord.xy / u_canvasSize; outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords, shape ID, initial distance 0 }`; // Compile seed shaders - const seedVertexShader = this.createShader(gl.VERTEX_SHADER, seedVertexShaderSource); - const seedFragmentShader = this.createShader(gl.FRAGMENT_SHADER, seedFragmentShaderSource); - this.seedProgram = this.createProgram(seedVertexShader, seedFragmentShader); + this.seedProgram = this.compileShaderProgram(seedVertexShaderSource, seedFragmentShaderSource); // Set up VAO and buffer for shapes this.shapeVAO = gl.createVertexArray()!; @@ -390,8 +396,9 @@ export class DistanceField extends HTMLElement { // Use seed shader program gl.useProgram(this.seedProgram); - // Set uniforms - gl.uniform2f(gl.getUniformLocation(this.seedProgram, 'u_resolution'), this.canvas.width, this.canvas.height); + // Set the canvas size uniform + const canvasSizeLocation = gl.getUniformLocation(this.seedProgram, 'u_canvasSize'); + gl.uniform2f(canvasSizeLocation, this.canvas.width, this.canvas.height); // Bind VAO and draw shapes gl.bindVertexArray(this.shapeVAO); @@ -508,13 +515,26 @@ export class DistanceField extends HTMLElement { // Handle window resize private handleResize = () => { + console.log('handleResize'); + const gl = this.gl; + // Update canvas size this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; - // Re-initialize WebGL resources + // Update the viewport + gl.viewport(0, 0, this.canvas.width, this.canvas.height); + + // Re-initialize textures with the new dimensions this.initPingPongTextures(); - this.renderSeedPoints(); + + // Update uniforms dependent on canvas size + this.updateCanvasSizeUniforms(); + + // Re-initialize seed point rendering to update positions + this.initSeedPointRendering(); + + // Rerun JFA this.runJFA(); }; @@ -527,4 +547,15 @@ export class DistanceField extends HTMLElement { } return new Float32Array(offsets); } + + private updateCanvasSizeUniforms() { + const gl = this.gl; + + // Update seedProgram's canvas size uniform + gl.useProgram(this.seedProgram); + const canvasSizeLocation = gl.getUniformLocation(this.seedProgram, 'u_canvasSize'); + gl.uniform2f(canvasSizeLocation, this.canvas.width, this.canvas.height); + + // Update other programs if necessary + } } From 8f7ce62b5de450e8bafafa53cb908c9b8d1d9840 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 1 Dec 2024 20:38:09 -0500 Subject: [PATCH 3/5] fix corner bug --- src/distance-field.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/distance-field.ts b/src/distance-field.ts index f46f1b8..32b71b9 100644 --- a/src/distance-field.ts +++ b/src/distance-field.ts @@ -138,6 +138,10 @@ export class DistanceField extends HTMLElement { vec4 sampled = texture(u_previousTexture, sampleCoord); + if (sampled.z == 0.0) { + continue; // Skip background pixels + } + // Compute distance to the seed point stored in this neighbor float dist = distance(sampled.xy, v_texCoord); From a7804b27ac5d75bb18f9d570168638e874fd244b Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 1 Dec 2024 21:03:36 -0500 Subject: [PATCH 4/5] cleanup --- src/distance-field.ts | 128 ++++++++++-------------------------------- src/utils/webgl.ts | 63 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 99 deletions(-) create mode 100644 src/utils/webgl.ts diff --git a/src/distance-field.ts b/src/distance-field.ts index 32b71b9..2c52224 100644 --- a/src/distance-field.ts +++ b/src/distance-field.ts @@ -1,37 +1,41 @@ import { frag, vert } from './utils/tags.ts'; +import { WebGLUtils } from './utils/webgl.ts'; /** Previously used a CPU-based implementation. https://github.com/folk-canvas/folk-canvas/commit/fdd7fb9d84d93ad665875cad25783c232fd17bcc */ export class DistanceField extends HTMLElement { static tagName = 'distance-field'; - static define() { - customElements.define(this.tagName, this); - } - private geometries: NodeListOf; private textures: WebGLTexture[] = []; private pingPongIndex: number = 0; - private offsets!: Float32Array; private canvas!: HTMLCanvasElement; private gl!: WebGL2RenderingContext; - private program!: WebGLProgram; - private displayProgram!: WebGLProgram; - private seedProgram!: WebGLProgram; private framebuffer!: WebGLFramebuffer; private fullscreenQuadVAO!: WebGLVertexArrayObject; private shapeVAO!: WebGLVertexArrayObject; + private jfaProgram!: WebGLProgram; // Jump Flooding Algorithm shader program + private renderProgram!: WebGLProgram; // Final rendering shader program + private seedProgram!: WebGLProgram; // Seed point shader program + constructor() { super(); this.geometries = document.querySelectorAll('fc-geometry'); - const { gl } = this.createWebGLCanvas(window.innerWidth, window.innerHeight); + const { gl, canvas } = WebGLUtils.createWebGLCanvas( + window.innerWidth, + window.innerHeight, + this // Pass the parent element + ); - if (!gl) { + if (!gl || !canvas) { + console.error('Failed to initialize WebGL context.'); return; } + + this.canvas = canvas; this.gl = gl; // Initialize shaders @@ -49,9 +53,11 @@ export class DistanceField extends HTMLElement { window.addEventListener('resize', this.handleResize); } - // Lifecycle hooks + static define() { + customElements.define(this.tagName, this); + } + connectedCallback() { - // Update distance field when geometries move or resize this.geometries.forEach((geometry) => { geometry.addEventListener('move', this.handleGeometryUpdate); geometry.addEventListener('resize', this.handleGeometryUpdate); @@ -59,7 +65,6 @@ export class DistanceField extends HTMLElement { } disconnectedCallback() { - // Remove event listeners this.geometries.forEach((geometry) => { geometry.removeEventListener('move', this.handleGeometryUpdate); geometry.removeEventListener('resize', this.handleGeometryUpdate); @@ -68,38 +73,11 @@ export class DistanceField extends HTMLElement { window.removeEventListener('resize', this.handleResize); } - // Handle updates from geometries private handleGeometryUpdate = () => { - // Re-render seed points and rerun JFA this.initSeedPointRendering(); this.runJFA(); }; - private createWebGLCanvas(width: number, height: number) { - this.canvas = document.createElement('canvas'); - - // Set canvas styles - this.canvas.style.position = 'absolute'; - this.canvas.style.top = '0'; - this.canvas.style.left = '0'; - this.canvas.style.width = '100%'; - this.canvas.style.height = '100%'; - this.canvas.style.zIndex = '-1'; - - this.canvas.width = width; - this.canvas.height = height; - - // Initialize WebGL2 context - const gl = this.canvas.getContext('webgl2'); - if (!gl) { - console.error('WebGL2 is not available.'); - return {}; - } - - this.appendChild(this.canvas); - return { gl }; - } - private initShaders() { // Shader sources const vertexShaderSource = vert`#version 300 es @@ -201,51 +179,18 @@ export class DistanceField extends HTMLElement { } } - this.offsets = new Float32Array(offsets); - // Compile JFA shaders using the utility function - this.program = this.compileShaderProgram(vertexShaderSource, fragmentShaderSource); + this.jfaProgram = this.compileShaderProgram(vertexShaderSource, fragmentShaderSource); // Compile display shaders using the utility function - this.displayProgram = this.compileShaderProgram(displayVertexShaderSource, displayFragmentShaderSource); + this.renderProgram = this.compileShaderProgram(displayVertexShaderSource, displayFragmentShaderSource); } private compileShaderProgram(vertexSource: string, fragmentSource: string): WebGLProgram { const gl = this.gl; - const vertexShader = this.createShader(gl.VERTEX_SHADER, vertexSource); - const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragmentSource); - return this.createProgram(vertexShader, fragmentShader); - } - - private createShader(type: GLenum, source: string): WebGLShader { - const gl = this.gl; - const shader = gl.createShader(type)!; - gl.shaderSource(shader, source); - gl.compileShader(shader); - - const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - if (!success) { - console.error('Could not compile shader:', gl.getShaderInfoLog(shader)); - gl.deleteShader(shader); - throw new Error('Shader compilation failed'); - } - return shader; - } - - private createProgram(vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram { - const gl = this.gl; - const program = gl.createProgram()!; - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - const success = gl.getProgramParameter(program, gl.LINK_STATUS); - if (!success) { - console.error('Program failed to link:', gl.getProgramInfoLog(program)); - gl.deleteProgram(program); - throw new Error('Program linking failed'); - } - return program; + const vertexShader = WebGLUtils.createShader(gl, gl.VERTEX_SHADER, vertexSource); + const fragmentShader = WebGLUtils.createShader(gl, gl.FRAGMENT_SHADER, fragmentSource); + return WebGLUtils.createProgram(gl, vertexShader, fragmentShader); } private initPingPongTextures() { @@ -439,17 +384,17 @@ export class DistanceField extends HTMLElement { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0); // Use shader program - gl.useProgram(this.program); + gl.useProgram(this.jfaProgram); // Compute and set the offsets uniform const offsets = this.computeOffsets(stepSize); - const offsetsLocation = gl.getUniformLocation(this.program, 'u_offsets'); + const offsetsLocation = gl.getUniformLocation(this.jfaProgram, 'u_offsets'); gl.uniform2fv(offsetsLocation, offsets); // Bind input texture gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, inputTexture); - gl.uniform1i(gl.getUniformLocation(this.program, 'u_previousTexture'), 0); + gl.uniform1i(gl.getUniformLocation(this.jfaProgram, 'u_previousTexture'), 0); // Draw a fullscreen quad this.drawFullscreenQuad(); @@ -466,13 +411,13 @@ export class DistanceField extends HTMLElement { gl.viewport(0, 0, this.canvas.width, this.canvas.height); // Use display shader program - gl.useProgram(this.displayProgram); + gl.useProgram(this.renderProgram); // Bind the final texture const finalTexture = this.textures[this.pingPongIndex % 2]; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, finalTexture); - gl.uniform1i(gl.getUniformLocation(this.displayProgram, 'u_texture'), 0); + gl.uniform1i(gl.getUniformLocation(this.renderProgram, 'u_texture'), 0); // Draw a fullscreen quad this.drawFullscreenQuad(); @@ -503,7 +448,7 @@ export class DistanceField extends HTMLElement { gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); - const positionAttributeLocation = gl.getAttribLocation(this.program, 'a_position'); + const positionAttributeLocation = gl.getAttribLocation(this.jfaProgram, 'a_position'); gl.enableVertexAttribArray(positionAttributeLocation); gl.vertexAttribPointer( positionAttributeLocation, @@ -519,7 +464,6 @@ export class DistanceField extends HTMLElement { // Handle window resize private handleResize = () => { - console.log('handleResize'); const gl = this.gl; // Update canvas size @@ -532,9 +476,6 @@ export class DistanceField extends HTMLElement { // Re-initialize textures with the new dimensions this.initPingPongTextures(); - // Update uniforms dependent on canvas size - this.updateCanvasSizeUniforms(); - // Re-initialize seed point rendering to update positions this.initSeedPointRendering(); @@ -551,15 +492,4 @@ export class DistanceField extends HTMLElement { } return new Float32Array(offsets); } - - private updateCanvasSizeUniforms() { - const gl = this.gl; - - // Update seedProgram's canvas size uniform - gl.useProgram(this.seedProgram); - const canvasSizeLocation = gl.getUniformLocation(this.seedProgram, 'u_canvasSize'); - gl.uniform2f(canvasSizeLocation, this.canvas.width, this.canvas.height); - - // Update other programs if necessary - } } diff --git a/src/utils/webgl.ts b/src/utils/webgl.ts new file mode 100644 index 0000000..4ec505c --- /dev/null +++ b/src/utils/webgl.ts @@ -0,0 +1,63 @@ +export class WebGLUtils { + static createShader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader { + const shader = gl.createShader(type)!; + gl.shaderSource(shader, source); + gl.compileShader(shader); + + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!success) { + const error = gl.getShaderInfoLog(shader); + gl.deleteShader(shader); + throw new Error(`Shader compilation failed: ${error}`); + } + return shader; + } + + static createProgram( + gl: WebGL2RenderingContext, + vertexShader: WebGLShader, + fragmentShader: WebGLShader + ): WebGLProgram { + const program = gl.createProgram()!; + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + const success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!success) { + const error = gl.getProgramInfoLog(program); + gl.deleteProgram(program); + throw new Error(`Program linking failed: ${error}`); + } + return program; + } + + static createWebGLCanvas( + width: number, + height: number, + parent: HTMLElement + ): { gl: WebGL2RenderingContext | undefined; canvas: HTMLCanvasElement } { + const canvas = document.createElement('canvas'); + + // Set canvas styles + canvas.style.position = 'absolute'; + canvas.style.top = '0'; + canvas.style.left = '0'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.zIndex = '-1'; + + canvas.width = width; + canvas.height = height; + + // Initialize WebGL2 context + const gl = canvas.getContext('webgl2'); + if (!gl) { + console.error('WebGL2 is not available.'); + return { gl: undefined, canvas }; + } + + parent.appendChild(canvas); + return { gl, canvas }; + } +} From 10cd273481cef735d8ee80d81dc92c7e2bc35c7f Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Sun, 1 Dec 2024 21:23:05 -0500 Subject: [PATCH 5/5] simplify --- src/distance-field.ts | 333 ++++++++++++++++++++++-------------------- src/utils/webgl.ts | 12 ++ 2 files changed, 183 insertions(+), 162 deletions(-) diff --git a/src/distance-field.ts b/src/distance-field.ts index 2c52224..f672038 100644 --- a/src/distance-field.ts +++ b/src/distance-field.ts @@ -10,7 +10,7 @@ export class DistanceField extends HTMLElement { private pingPongIndex: number = 0; private canvas!: HTMLCanvasElement; - private gl!: WebGL2RenderingContext; + private glContext!: WebGL2RenderingContext; private framebuffer!: WebGLFramebuffer; private fullscreenQuadVAO!: WebGLVertexArrayObject; private shapeVAO!: WebGLVertexArrayObject; @@ -19,16 +19,14 @@ export class DistanceField extends HTMLElement { private renderProgram!: WebGLProgram; // Final rendering shader program private seedProgram!: WebGLProgram; // Seed point shader program + private static readonly MAX_DISTANCE = 99999.0; + constructor() { super(); this.geometries = document.querySelectorAll('fc-geometry'); - const { gl, canvas } = WebGLUtils.createWebGLCanvas( - window.innerWidth, - window.innerHeight, - this // Pass the parent element - ); + const { gl, canvas } = WebGLUtils.createWebGLCanvas(window.innerWidth, window.innerHeight, this); if (!gl || !canvas) { console.error('Failed to initialize WebGL context.'); @@ -36,7 +34,7 @@ export class DistanceField extends HTMLElement { } this.canvas = canvas; - this.gl = gl; + this.glContext = gl; // Initialize shaders this.initShaders(); @@ -49,8 +47,6 @@ export class DistanceField extends HTMLElement { // Start the JFA process this.runJFA(); - - window.addEventListener('resize', this.handleResize); } static define() { @@ -58,6 +54,7 @@ export class DistanceField extends HTMLElement { } connectedCallback() { + window.addEventListener('resize', this.handleResize); this.geometries.forEach((geometry) => { geometry.addEventListener('move', this.handleGeometryUpdate); geometry.addEventListener('resize', this.handleGeometryUpdate); @@ -65,12 +62,12 @@ export class DistanceField extends HTMLElement { } disconnectedCallback() { + window.removeEventListener('resize', this.handleResize); this.geometries.forEach((geometry) => { geometry.removeEventListener('move', this.handleGeometryUpdate); geometry.removeEventListener('resize', this.handleGeometryUpdate); }); - - window.removeEventListener('resize', this.handleResize); + this.cleanupWebGLResources(); } private handleGeometryUpdate = () => { @@ -79,122 +76,13 @@ export class DistanceField extends HTMLElement { }; private initShaders() { - // Shader sources - const vertexShaderSource = vert`#version 300 es - precision highp float; - in vec2 a_position; - out vec2 v_texCoord; - - void main() { - v_texCoord = a_position * 0.5 + 0.5; // Transform to [0, 1] range - gl_Position = vec4(a_position, 0.0, 1.0); - }`; - - const fragmentShaderSource = frag`#version 300 es - precision highp float; - precision mediump int; - - in vec2 v_texCoord; - out vec4 outColor; - - uniform sampler2D u_previousTexture; - uniform vec2 u_offsets[9]; - - void main() { - // Start with the current texel's nearest seed point and distance - vec4 nearest = texture(u_previousTexture, v_texCoord); - - // Initialize minDist with the current distance - float minDist = nearest.a; - - // Loop through neighbor offsets - for (int i = 0; i < 9; ++i) { - vec2 sampleCoord = v_texCoord + u_offsets[i]; - - // Clamp sampleCoord to [0, 1] to prevent sampling outside texture - sampleCoord = clamp(sampleCoord, vec2(0.0), vec2(1.0)); - - vec4 sampled = texture(u_previousTexture, sampleCoord); - - if (sampled.z == 0.0) { - continue; // Skip background pixels - } - - // Compute distance to the seed point stored in this neighbor - float dist = distance(sampled.xy, v_texCoord); - - if (dist < minDist) { - nearest = sampled; - nearest.a = dist; - minDist = dist; - } - } - - // Output the nearest seed point and updated distance - outColor = nearest; - }`; - - const displayVertexShaderSource = vert`#version 300 es - in vec2 a_position; - out vec2 v_texCoord; - - void main() { - v_texCoord = a_position * 0.5 + 0.5; - gl_Position = vec4(a_position, 0.0, 1.0); - }`; - - const displayFragmentShaderSource = frag`#version 300 es - precision highp float; - - in vec2 v_texCoord; - out vec4 outColor; - - uniform sampler2D u_texture; - - void main() { - vec4 texel = texture(u_texture, v_texCoord); - - // Extract shape ID and distance - float shapeID = texel.z; - float distance = texel.a; - - // Hash-based color for shape - vec3 shapeColor = vec3( - fract(sin(shapeID * 12.9898) * 43758.5453), - fract(sin(shapeID * 78.233) * 43758.5453), - fract(sin(shapeID * 93.433) * 43758.5453) - ); - - // Visualize distance (e.g., as intensity) - float intensity = exp(-distance * 10.0); - - outColor = vec4(shapeColor * intensity, 1.0); - }`; - - // Compute offsets on the CPU - const offsets = []; - for (let y = -1; y <= 1; y++) { - for (let x = -1; x <= 1; x++) { - offsets.push(x, y); - } - } - - // Compile JFA shaders using the utility function - this.jfaProgram = this.compileShaderProgram(vertexShaderSource, fragmentShaderSource); - - // Compile display shaders using the utility function - this.renderProgram = this.compileShaderProgram(displayVertexShaderSource, displayFragmentShaderSource); - } - - private compileShaderProgram(vertexSource: string, fragmentSource: string): WebGLProgram { - const gl = this.gl; - const vertexShader = WebGLUtils.createShader(gl, gl.VERTEX_SHADER, vertexSource); - const fragmentShader = WebGLUtils.createShader(gl, gl.FRAGMENT_SHADER, fragmentSource); - return WebGLUtils.createProgram(gl, vertexShader, fragmentShader); + this.jfaProgram = WebGLUtils.createShaderProgram(this.glContext, jfaVertShader, jfaFragShader); + this.seedProgram = WebGLUtils.createShaderProgram(this.glContext, seedVertexShaderSource, seedFragmentShaderSource); + this.renderProgram = WebGLUtils.createShaderProgram(this.glContext, renderVertShader, renderFragShader); } private initPingPongTextures() { - const gl = this.gl; + const gl = this.glContext; const width = this.canvas.width; const height = this.canvas.height; @@ -244,35 +132,7 @@ export class DistanceField extends HTMLElement { } private initSeedPointRendering() { - const gl = this.gl; - - // Shader sources for seed point rendering - const seedVertexShaderSource = vert`#version 300 es - precision highp float; - - in vec3 a_position; // x, y, shapeID - flat out float v_shapeID; - - void main() { - gl_Position = vec4(a_position.xy, 0.0, 1.0); - v_shapeID = a_position.z; // Pass shape ID to fragment shader - }`; - - const seedFragmentShaderSource = frag`#version 300 es - precision highp float; - - flat in float v_shapeID; - uniform vec2 u_canvasSize; - - out vec4 outColor; - - void main() { - vec2 seedCoord = gl_FragCoord.xy / u_canvasSize; - outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords, shape ID, initial distance 0 - }`; - - // Compile seed shaders - this.seedProgram = this.compileShaderProgram(seedVertexShaderSource, seedFragmentShaderSource); + const gl = this.glContext; // Set up VAO and buffer for shapes this.shapeVAO = gl.createVertexArray()!; @@ -319,6 +179,7 @@ export class DistanceField extends HTMLElement { gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); + gl.useProgram(this.seedProgram); const positionLocation = gl.getAttribLocation(this.seedProgram, 'a_position'); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0); @@ -330,7 +191,7 @@ export class DistanceField extends HTMLElement { } private renderSeedPoints() { - const gl = this.gl; + const gl = this.glContext; // Bind framebuffer to render to the seed texture const seedTexture = this.textures[this.pingPongIndex % 2]; @@ -339,7 +200,7 @@ export class DistanceField extends HTMLElement { // Clear the texture with a large initial distance gl.viewport(0, 0, this.canvas.width, this.canvas.height); - gl.clearColor(0.0, 0.0, 0.0, 99999.0); // Max initial distance + gl.clearColor(0.0, 0.0, 0.0, DistanceField.MAX_DISTANCE); gl.clear(gl.COLOR_BUFFER_BIT); // Use seed shader program @@ -373,7 +234,7 @@ export class DistanceField extends HTMLElement { } private renderPass(stepSize: number) { - const gl = this.gl; + const gl = this.glContext; // Swap textures for ping-pong rendering const inputTexture = this.textures[this.pingPongIndex % 2]; @@ -396,7 +257,6 @@ export class DistanceField extends HTMLElement { gl.bindTexture(gl.TEXTURE_2D, inputTexture); gl.uniform1i(gl.getUniformLocation(this.jfaProgram, 'u_previousTexture'), 0); - // Draw a fullscreen quad this.drawFullscreenQuad(); // Swap ping-pong index @@ -404,7 +264,7 @@ export class DistanceField extends HTMLElement { } private renderToScreen() { - const gl = this.gl; + const gl = this.glContext; // Unbind framebuffer to render to the canvas gl.bindFramebuffer(gl.FRAMEBUFFER, null); @@ -424,9 +284,8 @@ export class DistanceField extends HTMLElement { } private drawFullscreenQuad() { - const gl = this.gl; + const gl = this.glContext; - // Initialize VAO if not already done if (!this.fullscreenQuadVAO) { this.initFullscreenQuad(); } @@ -437,7 +296,7 @@ export class DistanceField extends HTMLElement { } private initFullscreenQuad() { - const gl = this.gl; + const gl = this.glContext; const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]); @@ -464,7 +323,7 @@ export class DistanceField extends HTMLElement { // Handle window resize private handleResize = () => { - const gl = this.gl; + const gl = this.glContext; // Update canvas size this.canvas.width = window.innerWidth; @@ -492,4 +351,154 @@ export class DistanceField extends HTMLElement { } return new Float32Array(offsets); } + + private cleanupWebGLResources() { + const gl = this.glContext; + + // Delete textures + this.textures.forEach((texture) => gl.deleteTexture(texture)); + this.textures = []; + + // Delete framebuffers + if (this.framebuffer) { + gl.deleteFramebuffer(this.framebuffer); + } + + // Delete VAOs + if (this.fullscreenQuadVAO) { + gl.deleteVertexArray(this.fullscreenQuadVAO); + } + if (this.shapeVAO) { + gl.deleteVertexArray(this.shapeVAO); + } + + if (this.jfaProgram) { + gl.deleteProgram(this.jfaProgram); + } + if (this.renderProgram) { + gl.deleteProgram(this.renderProgram); + } + if (this.seedProgram) { + gl.deleteProgram(this.seedProgram); + } + + // Clear other references + this.geometries = null!; + } } + +const jfaVertShader = vert`#version 300 es + precision highp float; + in vec2 a_position; + out vec2 v_texCoord; + + void main() { + v_texCoord = a_position * 0.5 + 0.5; // Transform to [0, 1] range + gl_Position = vec4(a_position, 0.0, 1.0); + }`; + +const jfaFragShader = frag`#version 300 es + precision highp float; + precision mediump int; + + in vec2 v_texCoord; + out vec4 outColor; + + uniform sampler2D u_previousTexture; + uniform vec2 u_offsets[9]; + + void main() { + // Start with the current texel's nearest seed point and distance + vec4 nearest = texture(u_previousTexture, v_texCoord); + + // Initialize minDist with the current distance + float minDist = nearest.a; + + // Loop through neighbor offsets + for (int i = 0; i < 9; ++i) { + vec2 sampleCoord = v_texCoord + u_offsets[i]; + + // Clamp sampleCoord to [0, 1] to prevent sampling outside texture + sampleCoord = clamp(sampleCoord, vec2(0.0), vec2(1.0)); + + vec4 sampled = texture(u_previousTexture, sampleCoord); + + if (sampled.z == 0.0) { + continue; // Skip background pixels + } + + // Compute distance to the seed point stored in this neighbor + float dist = distance(sampled.xy, v_texCoord); + + if (dist < minDist) { + nearest = sampled; + nearest.a = dist; + minDist = dist; + } + } + + // Output the nearest seed point and updated distance + outColor = nearest; + }`; + +const renderVertShader = vert`#version 300 es + in vec2 a_position; + out vec2 v_texCoord; + + void main() { + v_texCoord = a_position * 0.5 + 0.5; + gl_Position = vec4(a_position, 0.0, 1.0); + }`; + +const renderFragShader = frag`#version 300 es + precision highp float; + + in vec2 v_texCoord; + out vec4 outColor; + + uniform sampler2D u_texture; + + void main() { + vec4 texel = texture(u_texture, v_texCoord); + + // Extract shape ID and distance + float shapeID = texel.z; + float distance = texel.a; + + // Hash-based color for shape + vec3 shapeColor = vec3( + fract(sin(shapeID * 12.9898) * 43758.5453), + fract(sin(shapeID * 78.233) * 43758.5453), + fract(sin(shapeID * 93.433) * 43758.5453) + ); + + // Visualize distance (e.g., as intensity) + float intensity = exp(-distance * 10.0); + + outColor = vec4(shapeColor * intensity, 1.0); + }`; + +// Shader sources for seed point rendering +const seedVertexShaderSource = vert`#version 300 es + precision highp float; + + in vec3 a_position; // x, y, shapeID + flat out float v_shapeID; + + void main() { + gl_Position = vec4(a_position.xy, 0.0, 1.0); + v_shapeID = a_position.z; // Pass shape ID to fragment shader + }`; + +const seedFragmentShaderSource = frag`#version 300 es + precision highp float; + + flat in float v_shapeID; + uniform vec2 u_canvasSize; + + out vec4 outColor; + + void main() { + vec2 seedCoord = gl_FragCoord.xy / u_canvasSize; + outColor = vec4(seedCoord, v_shapeID, 0.0); // Seed coords, shape ID, initial distance 0 + }`; diff --git a/src/utils/webgl.ts b/src/utils/webgl.ts index 4ec505c..33d6722 100644 --- a/src/utils/webgl.ts +++ b/src/utils/webgl.ts @@ -60,4 +60,16 @@ export class WebGLUtils { parent.appendChild(canvas); return { gl, canvas }; } + + static createShaderProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram { + const vertexShader = this.createShader(gl, gl.VERTEX_SHADER, vertexSource); + const fragmentShader = this.createShader(gl, gl.FRAGMENT_SHADER, fragmentSource); + const program = this.createProgram(gl, vertexShader, fragmentShader); + + // Clean up shaders since they're now linked to the program + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + + return program; + } }