distance fields extend folk-set
This commit is contained in:
parent
1692307688
commit
35f61d853d
|
|
@ -28,10 +28,18 @@
|
|||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
p {
|
||||
width: fit-content;
|
||||
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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="200" 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(
|
||||
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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,17 +25,24 @@ export class FolkBaseSet extends HTMLElement {
|
|||
}
|
||||
|
||||
#sourcesMap = new Map<Element, DOMRectReadOnly>();
|
||||
get sourcesMap() {
|
||||
get sourcesMap(): ReadonlyMap<Element, DOMRectReadOnly> {
|
||||
return this.#sourcesMap;
|
||||
}
|
||||
|
||||
get sourceElements() {
|
||||
return Array.from(this.#sourcesMap.keys());
|
||||
get sourceRects() {
|
||||
return Array.from(this.#sourcesMap.values());
|
||||
}
|
||||
|
||||
#sourceElements = new Set<Element>();
|
||||
get sourceElements(): ReadonlySet<Element> {
|
||||
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<Element> = this.#sourcesMap.keys()) {
|
||||
unobserveSources(elements: Set<Element> = this.#sourceElements) {
|
||||
for (const el of elements) {
|
||||
folkObserver.unobserve(el, this.#sourcesCallback);
|
||||
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 { 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<FolkShape>;
|
||||
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!;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue