From 35f61d853d2b7125ed5f449892a5f719949c1ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cchrisshank=E2=80=9D?= Date: Mon, 9 Dec 2024 14:28:19 -0800 Subject: [PATCH] distance fields extend folk-set --- demo/distance-field-visualization.html | 8 +++ src/common/webgl.ts | 5 +- src/folk-base-set.ts | 28 +++++++---- src/folk-distance-field.ts | 69 +++++++++++++++----------- 4 files changed, 67 insertions(+), 43 deletions(-) diff --git a/demo/distance-field-visualization.html b/demo/distance-field-visualization.html index 65cf7b3..a394ee5 100644 --- a/demo/distance-field-visualization.html +++ b/demo/distance-field-visualization.html @@ -28,10 +28,18 @@ position: absolute; inset: 0; } + + h1, + p { + width: fit-content; + border: 2px solid rgba(0, 0, 0, 0.5); + } +

Hello

+

World

diff --git a/src/common/webgl.ts b/src/common/webgl.ts index ab58108..0c441bb 100644 --- a/src/common/webgl.ts +++ b/src/common/webgl.ts @@ -34,8 +34,7 @@ export class WebGLUtils { static createWebGLCanvas( width: number, - height: number, - parent: HTMLElement + height: number ): { gl: WebGL2RenderingContext | undefined; canvas: HTMLCanvasElement } { const canvas = document.createElement('canvas'); @@ -52,8 +51,6 @@ export class WebGLUtils { console.error('WebGL2 is not available.'); return { gl: undefined, canvas }; } - - parent.appendChild(canvas); return { gl, canvas }; } diff --git a/src/folk-base-set.ts b/src/folk-base-set.ts index 7975040..afc5ab2 100644 --- a/src/folk-base-set.ts +++ b/src/folk-base-set.ts @@ -25,17 +25,24 @@ export class FolkBaseSet extends HTMLElement { } #sourcesMap = new Map(); - get sourcesMap() { + get sourcesMap(): ReadonlyMap { return this.#sourcesMap; } - get sourceElements() { - return Array.from(this.#sourcesMap.keys()); + get sourceRects() { + return Array.from(this.#sourcesMap.values()); + } + + #sourceElements = new Set(); + get sourceElements(): ReadonlySet { + return this.#sourceElements; } #sourcesCallback = (entry: ClientRectObserverEntry) => { this.#sourcesMap.set(entry.target, entry.contentRect); - this.update(); + if (this.#sourceElements.size === this.#sourcesMap.size) { + this.update(); + } }; connectedCallback() { @@ -48,14 +55,12 @@ export class FolkBaseSet extends HTMLElement { observeSources() { const elements = this.sources ? document.querySelectorAll(this.sources) : []; - const childElements = new Set(this.querySelectorAll('*')); + const childElements = new Set(this.children); const sourceElements = new Set(elements).union(childElements); - const currentElements = new Set(this.#sourcesMap.keys()); + const elementsToObserve = sourceElements.difference(this.#sourceElements); - const elementsToObserve = sourceElements.difference(currentElements); - - const elementsToUnobserve = currentElements.difference(sourceElements); + const elementsToUnobserve = this.#sourceElements.difference(sourceElements); this.unobserveSources(elementsToUnobserve); @@ -63,13 +68,14 @@ export class FolkBaseSet extends HTMLElement { folkObserver.observe(el, this.#sourcesCallback); } - this.update(); + this.#sourceElements = sourceElements; } - unobserveSources(elements: Iterable = this.#sourcesMap.keys()) { + unobserveSources(elements: Set = this.#sourceElements) { for (const el of elements) { folkObserver.unobserve(el, this.#sourcesCallback); this.#sourcesMap.delete(el); + this.#sourceElements.delete(el); } } diff --git a/src/folk-distance-field.ts b/src/folk-distance-field.ts index 0d6443a..b07ccc1 100644 --- a/src/folk-distance-field.ts +++ b/src/folk-distance-field.ts @@ -1,5 +1,8 @@ +import { DOMRectTransform } from './common/DOMRectTransform.ts'; import { frag, vert } from './common/tags.ts'; +import { Point } from './common/types.ts'; import { WebGLUtils } from './common/webgl.ts'; +import { FolkBaseSet } from './folk-base-set.ts'; import { FolkShape } from './folk-shape.ts'; /** @@ -7,12 +10,18 @@ import { FolkShape } from './folk-shape.ts'; * It renders shapes as seed points and computes the distance from each pixel to the nearest seed point. * Previous CPU-based implementation: github.com/folk-canvas/folk-canvas/commit/fdd7fb9d84d93ad665875cad25783c232fd17bcc */ -export class FolkDistanceField extends HTMLElement { +export class FolkDistanceField extends FolkBaseSet { static tagName = 'folk-distance-field'; + static override define() { + FolkShape.define(); + super.define(); + } + + #shadow = this.attachShadow({ mode: 'open' }); + private textures: WebGLTexture[] = []; - private shapes!: NodeListOf; private canvas!: HTMLCanvasElement; private glContext!: WebGL2RenderingContext; private framebuffer!: WebGLFramebuffer; @@ -29,52 +38,48 @@ export class FolkDistanceField extends HTMLElement { private isPingTexture: boolean = true; - static define() { - if (customElements.get(this.tagName)) return; - FolkShape.define(); - customElements.define(this.tagName, this); + constructor() { + super(); + + this.#shadow.appendChild(document.createElement('slot')); } connectedCallback() { - this.shapes = document.querySelectorAll('folk-shape'); this.initWebGL(); this.initShaders(); this.initPingPongTextures(); - this.populateSeedPoints(); - this.runJumpFloodingAlgorithm(); window.addEventListener('resize', this.handleResize); - this.shapes.forEach((geometry) => { - geometry.addEventListener('transform', this.handleGeometryUpdate); - }); + + super.connectedCallback(); } disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener('resize', this.handleResize); - this.shapes.forEach((geometry) => { - geometry.removeEventListener('transform', this.handleGeometryUpdate); - }); this.cleanupWebGLResources(); } private initWebGL() { - const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight, this); + const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight); if (!gl || !canvas) { throw new Error('Failed to initialize WebGL context.'); } this.canvas = canvas; + this.#shadow.prepend(canvas); this.glContext = gl; } /** * Handles updates to geometry elements by re-initializing seed points and rerunning the JFA. */ - private handleGeometryUpdate = () => { + override update() { this.populateSeedPoints(); this.runJumpFloodingAlgorithm(); - }; + } /** * Initializes all shader programs used in rendering. @@ -149,12 +154,23 @@ export class FolkDistanceField extends HTMLElement { const containerHeight = this.clientHeight; // Collect positions and assign unique IDs to all shapes - this.shapes.forEach((geometry, index) => { - const rect = geometry.getTransformDOMRect(); - const topLeftParent = rect.toParentSpace(rect.topLeft); - const topRightParent = rect.toParentSpace(rect.topRight); - const bottomLeftParent = rect.toParentSpace(rect.bottomLeft); - const bottomRightParent = rect.toParentSpace(rect.bottomRight); + this.sourceRects.forEach((rect, index) => { + let topLeftParent: Point; + let topRightParent: Point; + let bottomLeftParent: Point; + let bottomRightParent: Point; + + if (rect instanceof DOMRectTransform) { + topLeftParent = rect.toParentSpace(rect.topLeft); + topRightParent = rect.toParentSpace(rect.topRight); + bottomLeftParent = rect.toParentSpace(rect.bottomLeft); + bottomRightParent = rect.toParentSpace(rect.bottomRight); + } else { + topLeftParent = { x: rect.left, y: rect.top }; + topRightParent = { x: rect.right, y: rect.top }; + bottomLeftParent = { x: rect.left, y: rect.bottom }; + bottomRightParent = { x: rect.right, y: rect.bottom }; + } // Convert rotated coordinates to NDC using container dimensions const x1 = (topLeftParent.x / containerWidth) * 2 - 1; @@ -238,7 +254,7 @@ export class FolkDistanceField extends HTMLElement { // Bind VAO and draw shapes gl.bindVertexArray(this.shapeVAO); - gl.drawArrays(gl.TRIANGLES, 0, this.shapes.length * 6); + gl.drawArrays(gl.TRIANGLES, 0, this.sourcesMap.size * 6); // Unbind VAO and framebuffer gl.bindVertexArray(null); @@ -443,9 +459,6 @@ export class FolkDistanceField extends HTMLElement { if (this.seedProgram) { gl.deleteProgram(this.seedProgram); } - - // Clear other references - this.shapes = null!; } }