distance fields extend folk-set
This commit is contained in:
parent
1692307688
commit
35f61d853d
|
|
@ -28,10 +28,18 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
p {
|
||||||
|
width: fit-content;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<folk-distance-field>
|
<folk-distance-field>
|
||||||
|
<p>Hello</p>
|
||||||
|
<p>World</p>
|
||||||
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
|
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
|
||||||
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
|
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
|
||||||
<folk-shape x="100" y="300" width="50" height="50"></folk-shape>
|
<folk-shape x="100" y="300" width="50" height="50"></folk-shape>
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,7 @@ export class WebGLUtils {
|
||||||
|
|
||||||
static createWebGLCanvas(
|
static createWebGLCanvas(
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number
|
||||||
parent: HTMLElement
|
|
||||||
): { gl: WebGL2RenderingContext | undefined; canvas: HTMLCanvasElement } {
|
): { gl: WebGL2RenderingContext | undefined; canvas: HTMLCanvasElement } {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
|
@ -52,8 +51,6 @@ export class WebGLUtils {
|
||||||
console.error('WebGL2 is not available.');
|
console.error('WebGL2 is not available.');
|
||||||
return { gl: undefined, canvas };
|
return { gl: undefined, canvas };
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.appendChild(canvas);
|
|
||||||
return { gl, canvas };
|
return { gl, canvas };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,24 @@ export class FolkBaseSet extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#sourcesMap = new Map<Element, DOMRectReadOnly>();
|
#sourcesMap = new Map<Element, DOMRectReadOnly>();
|
||||||
get sourcesMap() {
|
get sourcesMap(): ReadonlyMap<Element, DOMRectReadOnly> {
|
||||||
return this.#sourcesMap;
|
return this.#sourcesMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
get sourceElements() {
|
get sourceRects() {
|
||||||
return Array.from(this.#sourcesMap.keys());
|
return Array.from(this.#sourcesMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
#sourceElements = new Set<Element>();
|
||||||
|
get sourceElements(): ReadonlySet<Element> {
|
||||||
|
return this.#sourceElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sourcesCallback = (entry: ClientRectObserverEntry) => {
|
#sourcesCallback = (entry: ClientRectObserverEntry) => {
|
||||||
this.#sourcesMap.set(entry.target, entry.contentRect);
|
this.#sourcesMap.set(entry.target, entry.contentRect);
|
||||||
this.update();
|
if (this.#sourceElements.size === this.#sourcesMap.size) {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -48,14 +55,12 @@ export class FolkBaseSet extends HTMLElement {
|
||||||
|
|
||||||
observeSources() {
|
observeSources() {
|
||||||
const elements = this.sources ? document.querySelectorAll(this.sources) : [];
|
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 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 = this.#sourceElements.difference(sourceElements);
|
||||||
|
|
||||||
const elementsToUnobserve = currentElements.difference(sourceElements);
|
|
||||||
|
|
||||||
this.unobserveSources(elementsToUnobserve);
|
this.unobserveSources(elementsToUnobserve);
|
||||||
|
|
||||||
|
|
@ -63,13 +68,14 @@ export class FolkBaseSet extends HTMLElement {
|
||||||
folkObserver.observe(el, this.#sourcesCallback);
|
folkObserver.observe(el, this.#sourcesCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update();
|
this.#sourceElements = sourceElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
unobserveSources(elements: Iterable<Element> = this.#sourcesMap.keys()) {
|
unobserveSources(elements: Set<Element> = this.#sourceElements) {
|
||||||
for (const el of elements) {
|
for (const el of elements) {
|
||||||
folkObserver.unobserve(el, this.#sourcesCallback);
|
folkObserver.unobserve(el, this.#sourcesCallback);
|
||||||
this.#sourcesMap.delete(el);
|
this.#sourcesMap.delete(el);
|
||||||
|
this.#sourceElements.delete(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { DOMRectTransform } from './common/DOMRectTransform.ts';
|
||||||
import { frag, vert } from './common/tags.ts';
|
import { frag, vert } from './common/tags.ts';
|
||||||
|
import { Point } from './common/types.ts';
|
||||||
import { WebGLUtils } from './common/webgl.ts';
|
import { WebGLUtils } from './common/webgl.ts';
|
||||||
|
import { FolkBaseSet } from './folk-base-set.ts';
|
||||||
import { FolkShape } from './folk-shape.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.
|
* 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
|
* 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 tagName = 'folk-distance-field';
|
||||||
|
|
||||||
|
static override define() {
|
||||||
|
FolkShape.define();
|
||||||
|
super.define();
|
||||||
|
}
|
||||||
|
|
||||||
|
#shadow = this.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
private textures: WebGLTexture[] = [];
|
private textures: WebGLTexture[] = [];
|
||||||
|
|
||||||
private shapes!: NodeListOf<FolkShape>;
|
|
||||||
private canvas!: HTMLCanvasElement;
|
private canvas!: HTMLCanvasElement;
|
||||||
private glContext!: WebGL2RenderingContext;
|
private glContext!: WebGL2RenderingContext;
|
||||||
private framebuffer!: WebGLFramebuffer;
|
private framebuffer!: WebGLFramebuffer;
|
||||||
|
|
@ -29,52 +38,48 @@ export class FolkDistanceField extends HTMLElement {
|
||||||
|
|
||||||
private isPingTexture: boolean = true;
|
private isPingTexture: boolean = true;
|
||||||
|
|
||||||
static define() {
|
constructor() {
|
||||||
if (customElements.get(this.tagName)) return;
|
super();
|
||||||
FolkShape.define();
|
|
||||||
customElements.define(this.tagName, this);
|
this.#shadow.appendChild(document.createElement('slot'));
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.shapes = document.querySelectorAll('folk-shape');
|
|
||||||
this.initWebGL();
|
this.initWebGL();
|
||||||
this.initShaders();
|
this.initShaders();
|
||||||
this.initPingPongTextures();
|
this.initPingPongTextures();
|
||||||
this.populateSeedPoints();
|
|
||||||
this.runJumpFloodingAlgorithm();
|
|
||||||
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
this.shapes.forEach((geometry) => {
|
|
||||||
geometry.addEventListener('transform', this.handleGeometryUpdate);
|
super.connectedCallback();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
this.shapes.forEach((geometry) => {
|
|
||||||
geometry.removeEventListener('transform', this.handleGeometryUpdate);
|
|
||||||
});
|
|
||||||
this.cleanupWebGLResources();
|
this.cleanupWebGLResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initWebGL() {
|
private initWebGL() {
|
||||||
const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight, this);
|
const { gl, canvas } = WebGLUtils.createWebGLCanvas(this.clientWidth, this.clientHeight);
|
||||||
|
|
||||||
if (!gl || !canvas) {
|
if (!gl || !canvas) {
|
||||||
throw new Error('Failed to initialize WebGL context.');
|
throw new Error('Failed to initialize WebGL context.');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
|
this.#shadow.prepend(canvas);
|
||||||
this.glContext = gl;
|
this.glContext = gl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles updates to geometry elements by re-initializing seed points and rerunning the JFA.
|
* Handles updates to geometry elements by re-initializing seed points and rerunning the JFA.
|
||||||
*/
|
*/
|
||||||
private handleGeometryUpdate = () => {
|
override update() {
|
||||||
this.populateSeedPoints();
|
this.populateSeedPoints();
|
||||||
this.runJumpFloodingAlgorithm();
|
this.runJumpFloodingAlgorithm();
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes all shader programs used in rendering.
|
* Initializes all shader programs used in rendering.
|
||||||
|
|
@ -149,12 +154,23 @@ export class FolkDistanceField extends HTMLElement {
|
||||||
const containerHeight = this.clientHeight;
|
const containerHeight = this.clientHeight;
|
||||||
|
|
||||||
// Collect positions and assign unique IDs to all shapes
|
// Collect positions and assign unique IDs to all shapes
|
||||||
this.shapes.forEach((geometry, index) => {
|
this.sourceRects.forEach((rect, index) => {
|
||||||
const rect = geometry.getTransformDOMRect();
|
let topLeftParent: Point;
|
||||||
const topLeftParent = rect.toParentSpace(rect.topLeft);
|
let topRightParent: Point;
|
||||||
const topRightParent = rect.toParentSpace(rect.topRight);
|
let bottomLeftParent: Point;
|
||||||
const bottomLeftParent = rect.toParentSpace(rect.bottomLeft);
|
let bottomRightParent: Point;
|
||||||
const bottomRightParent = rect.toParentSpace(rect.bottomRight);
|
|
||||||
|
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
|
// Convert rotated coordinates to NDC using container dimensions
|
||||||
const x1 = (topLeftParent.x / containerWidth) * 2 - 1;
|
const x1 = (topLeftParent.x / containerWidth) * 2 - 1;
|
||||||
|
|
@ -238,7 +254,7 @@ export class FolkDistanceField extends HTMLElement {
|
||||||
|
|
||||||
// Bind VAO and draw shapes
|
// Bind VAO and draw shapes
|
||||||
gl.bindVertexArray(this.shapeVAO);
|
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
|
// Unbind VAO and framebuffer
|
||||||
gl.bindVertexArray(null);
|
gl.bindVertexArray(null);
|
||||||
|
|
@ -443,9 +459,6 @@ export class FolkDistanceField extends HTMLElement {
|
||||||
if (this.seedProgram) {
|
if (this.seedProgram) {
|
||||||
gl.deleteProgram(this.seedProgram);
|
gl.deleteProgram(this.seedProgram);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear other references
|
|
||||||
this.shapes = null!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue