diff --git a/demo/vite.config.ts b/demo/vite.config.ts index 7554060..df41d64 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -29,8 +29,21 @@ const linkGenerator = (): Plugin => { }; }; +const configureResponseHeaders = (): Plugin => { + return { + name: 'configure-response-headers', + configureServer: (server) => { + server.middlewares.use((_req, res, next) => { + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + next(); + }); + }, + }; +}; + export default defineConfig({ - plugins: [linkGenerator()], + plugins: [linkGenerator(), configureResponseHeaders()], build: { target: 'esnext', rollupOptions: { input }, @@ -38,4 +51,12 @@ export default defineConfig({ polyfill: false, }, }, + worker: { + format: 'es', + }, + server: { + fs: { + allow: [resolve(__dirname, '..')], + }, + }, }); diff --git a/src/distanceField/distance-field.ts b/src/distanceField/distance-field.ts index 8dfdc35..1e3f42e 100644 --- a/src/distanceField/distance-field.ts +++ b/src/distanceField/distance-field.ts @@ -1,6 +1,5 @@ import type { FolkGeometry } from '../canvas/fc-geometry.ts'; import type { Vector2 } from '../utils/Vector2.ts'; -import { Fields } from './fields.ts'; export class DistanceField extends HTMLElement { static tagName = 'distance-field'; @@ -11,12 +10,14 @@ export class DistanceField extends HTMLElement { private canvas!: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; + private offscreenCanvas: HTMLCanvasElement; private offscreenCtx: CanvasRenderingContext2D; - private fields: Fields; private resolution: number; private imageSmoothing: boolean; + private worker!: Worker; + private geometryShapeIds: Map = new Map(); - // Get all geometry elements and create points for the distance field + // Get all geometry elements private geometries = document.querySelectorAll('fc-geometry'); constructor() { @@ -24,9 +25,8 @@ export class DistanceField extends HTMLElement { this.resolution = 800; // default resolution this.imageSmoothing = true; - this.fields = new Fields(this.resolution); - const { ctx, offscreenCtx } = this.createCanvas( + const { ctx, offscreenCtx, offscreenCanvas } = this.createCanvas( window.innerWidth, window.innerHeight, this.resolution, @@ -35,6 +35,16 @@ export class DistanceField extends HTMLElement { this.ctx = ctx; this.offscreenCtx = offscreenCtx; + this.offscreenCanvas = offscreenCanvas; + + // Initialize the Web Worker + try { + this.worker = new Worker(new URL('./distance-field.worker.ts', import.meta.url).href, { type: 'module' }); + this.worker.onmessage = this.handleWorkerMessage; + this.worker.postMessage({ type: 'initialize', data: { resolution: this.resolution } }); + } catch (error) { + console.error('Error initializing worker', error); + } this.renderDistanceField(); } @@ -48,17 +58,19 @@ export class DistanceField extends HTMLElement { } disconnectedCallback() { - // Update distance field when geometries move or resize + // Remove event listeners and terminate the worker this.geometries.forEach((geometry) => { geometry.removeEventListener('move', this.handleGeometryUpdate); geometry.removeEventListener('resize', this.handleGeometryUpdate); }); + this.worker.terminate(); } attributeChangedCallback(name: string, _oldValue: string, newValue: string) { if (name === 'resolution') { this.resolution = parseInt(newValue, 10); - this.fields = new Fields(this.resolution); + // Re-initialize the worker with the new resolution + this.worker.postMessage({ type: 'initialize', data: { resolution: this.resolution } }); } else if (name === 'image-smoothing') { this.imageSmoothing = newValue === 'true'; if (this.ctx) { @@ -68,30 +80,39 @@ export class DistanceField extends HTMLElement { } private renderDistanceField() { - // Get the computed ImageData from Fields - const imageData = this.fields.generateImageData(); - - // Put the ImageData onto the offscreen canvas - this.offscreenCtx.putImageData(imageData, 0, 0); - - // Draw scaled version to main canvas - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage( - this.offscreenCtx.canvas, - 0, - 0, - this.resolution, - this.resolution, - 0, - 0, - this.canvas.width, - this.canvas.height - ); + // Request the worker to generate ImageData + this.worker.postMessage({ type: 'generateImageData' }); } - // Public methods + // Handle messages from the worker + private handleWorkerMessage = (event: MessageEvent) => { + const { type, imageData } = event.data; + + if (type === 'imageData') { + // Reconstruct ImageData from the transferred buffer + const imgData = new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height); + + // Update the canvas with the new image data + this.offscreenCtx.putImageData(imgData, 0, 0); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.drawImage( + this.offscreenCanvas, + 0, + 0, + this.resolution, + this.resolution, + 0, + 0, + this.canvas.width, + this.canvas.height + ); + } + }; + + // Public method to reset fields reset() { - this.fields = new Fields(this.resolution); + // Reset the fields in the worker + this.worker.postMessage({ type: 'initialize', data: { resolution: this.resolution } }); } private transformToFieldCoordinates(point: Vector2): Vector2 { @@ -103,14 +124,15 @@ export class DistanceField extends HTMLElement { } addShape(points: Vector2[]) { - // Transform each point from screen coordinates to field coordinates + // Transform and send points to the worker const transformedPoints = points.map((point) => this.transformToFieldCoordinates(point)); - this.fields.addShape(transformedPoints); + this.worker.postMessage({ type: 'addShape', data: { points: transformedPoints } }); this.renderDistanceField(); } removeShape(index: number) { - this.fields.removeShape(index); + // Inform the worker to remove a shape + this.worker.postMessage({ type: 'removeShape', data: { index } }); this.renderDistanceField(); } @@ -118,7 +140,7 @@ export class DistanceField extends HTMLElement { this.canvas = document.createElement('canvas'); const offscreenCanvas = document.createElement('canvas'); - // Set canvas styles to ensure it stays behind other elements + // Set canvas styles this.canvas.style.position = 'absolute'; this.canvas.style.top = '0'; this.canvas.style.left = '0'; @@ -140,14 +162,12 @@ export class DistanceField extends HTMLElement { ctx.imageSmoothingEnabled = this.imageSmoothing; this.appendChild(this.canvas); - return { ctx, offscreenCtx }; + return { ctx, offscreenCtx, offscreenCanvas }; } handleGeometryUpdate = (event: Event) => { const geometry = event.target as HTMLElement; - // TODO: store as array from getgo - const index = Array.from(this.geometries).indexOf(geometry as FolkGeometry); - if (index === -1) return; + const shapeId = this.geometryShapeIds.get(geometry); const rect = geometry.getBoundingClientRect(); const points = [ @@ -157,10 +177,20 @@ export class DistanceField extends HTMLElement { { x: rect.x, y: rect.y + rect.height }, ]; - if (index < this.fields.shapes.length) { - this.fields.updateShape(index, this.transformPoints(points)); + const transformedPoints = this.transformPoints(points); + + if (shapeId) { + this.worker.postMessage({ + type: 'updateShape', + data: { id: shapeId, points: transformedPoints }, + }); } else { - this.fields.addShape(this.transformPoints(points)); + const newId = crypto.randomUUID(); + this.geometryShapeIds.set(geometry, newId); + this.worker.postMessage({ + type: 'addShape', + data: { id: newId, points: transformedPoints }, + }); } this.renderDistanceField(); diff --git a/src/distanceField/distance-field.worker.ts b/src/distanceField/distance-field.worker.ts new file mode 100644 index 0000000..f74d0c8 --- /dev/null +++ b/src/distanceField/distance-field.worker.ts @@ -0,0 +1,40 @@ +/// +import { Fields } from './fields.ts'; + +declare const self: DedicatedWorkerGlobalScope; + +// Initialize the Fields instance +let fields: Fields; + +// Listen for messages from the main thread +self.onmessage = (event) => { + const { type, data } = event.data; + + switch (type) { + case 'initialize': + fields = new Fields(data.resolution); + break; + + case 'addShape': + fields.addShape(data.id, data.points, data.color); + break; + + case 'removeShape': + fields.removeShape(data.id); + break; + + case 'updateShape': + fields.updateShape(data.id, data.points); + break; + + case 'generateImageData': { + const imageData = fields.generateImageData(); + // Post the ImageData back to the main thread + postMessage({ type: 'imageData', imageData }, [imageData.data.buffer]); + break; + } + + default: + console.warn(`Unknown message type: ${type}`); + } +}; diff --git a/src/distanceField/fields.ts b/src/distanceField/fields.ts index 10bb3ff..f68f30d 100644 --- a/src/distanceField/fields.ts +++ b/src/distanceField/fields.ts @@ -1,6 +1,12 @@ import type { Vector2 } from '../utils/Vector2.ts'; import { computeCPT } from './cpt.ts'; +interface Shape { + id: string; + points: Vector2[]; + color: number; +} + export class Fields { private edt: Float32Array[] = []; private cpt: Vector2[][] = []; @@ -8,15 +14,11 @@ export class Fields { private xcoords: Float32Array[] = []; private ycoords: Float32Array[] = []; private resolution: number; - shapes: Array<{ - points: Vector2[]; - color: number; - }> = []; + private shapes: Map = new Map(); constructor(resolution: number) { this.resolution = resolution + 1; this.initializeArrays(); - this.updateFields(); } private initializeArrays() { @@ -39,15 +41,24 @@ export class Fields { return this.colorField[x][y]; } - addShape(points: Vector2[], color?: number) { + addShape(id: string, points: Vector2[], color?: number) { const shapeColor = color ?? Math.floor(Math.random() * 255); - this.shapes.push({ points, color: shapeColor }); + this.shapes.set(id, { id, points, color: shapeColor }); this.updateFields(); } - removeShape(index: number) { - this.shapes.splice(index, 1); - this.updateFields(); + removeShape(id: string) { + if (this.shapes.delete(id)) { + this.updateFields(); + } + } + + updateShape(id: string, points: Vector2[]) { + const shape = this.shapes.get(id); + if (shape) { + shape.points = points; + this.updateFields(); + } } updateFields() { @@ -66,7 +77,7 @@ export class Fields { } } - boolifyFields(distanceField: Float32Array[], colorField: Float32Array[]): void { + private boolifyFields(distanceField: Float32Array[], colorField: Float32Array[]): void { const LARGE_NUMBER = 1000000000000; const size = distanceField.length; const cellSize = 1; @@ -126,9 +137,8 @@ export class Fields { } }; - for (const shape of this.shapes) { + for (const shape of this.shapes.values()) { const { points, color } = shape; - for (let i = 0; i < points.length; i++) { const start = points[i]; const end = points[(i + 1) % points.length]; @@ -137,14 +147,6 @@ export class Fields { } } - updateShape(index: number, points: Vector2[]) { - if (index >= 0 && index < this.shapes.length) { - const existingColor = this.shapes[index].color; - this.shapes[index] = { points, color: existingColor }; - this.updateFields(); - } - } - private renderer( pixelRenderer: ( distance: number,