moved to worker (gets backed up, but no impact on main thread)

This commit is contained in:
Orion Reed 2024-12-01 08:18:11 -05:00
parent a0e00e9657
commit db42811ea2
4 changed files with 154 additions and 61 deletions

View File

@ -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, '..')],
},
},
});

View File

@ -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<HTMLElement, string> = 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();

View File

@ -0,0 +1,40 @@
/// <reference lib="webworker" />
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}`);
}
};

View File

@ -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<string, Shape> = 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,