moved to worker (gets backed up, but no impact on main thread)
This commit is contained in:
parent
a0e00e9657
commit
db42811ea2
|
|
@ -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({
|
export default defineConfig({
|
||||||
plugins: [linkGenerator()],
|
plugins: [linkGenerator(), configureResponseHeaders()],
|
||||||
build: {
|
build: {
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
rollupOptions: { input },
|
rollupOptions: { input },
|
||||||
|
|
@ -38,4 +51,12 @@ export default defineConfig({
|
||||||
polyfill: false,
|
polyfill: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
worker: {
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
allow: [resolve(__dirname, '..')],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { FolkGeometry } from '../canvas/fc-geometry.ts';
|
import type { FolkGeometry } from '../canvas/fc-geometry.ts';
|
||||||
import type { Vector2 } from '../utils/Vector2.ts';
|
import type { Vector2 } from '../utils/Vector2.ts';
|
||||||
import { Fields } from './fields.ts';
|
|
||||||
|
|
||||||
export class DistanceField extends HTMLElement {
|
export class DistanceField extends HTMLElement {
|
||||||
static tagName = 'distance-field';
|
static tagName = 'distance-field';
|
||||||
|
|
@ -11,12 +10,14 @@ export class DistanceField extends HTMLElement {
|
||||||
|
|
||||||
private canvas!: HTMLCanvasElement;
|
private canvas!: HTMLCanvasElement;
|
||||||
private ctx: CanvasRenderingContext2D;
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
private offscreenCanvas: HTMLCanvasElement;
|
||||||
private offscreenCtx: CanvasRenderingContext2D;
|
private offscreenCtx: CanvasRenderingContext2D;
|
||||||
private fields: Fields;
|
|
||||||
private resolution: number;
|
private resolution: number;
|
||||||
private imageSmoothing: boolean;
|
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');
|
private geometries = document.querySelectorAll('fc-geometry');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -24,9 +25,8 @@ export class DistanceField extends HTMLElement {
|
||||||
|
|
||||||
this.resolution = 800; // default resolution
|
this.resolution = 800; // default resolution
|
||||||
this.imageSmoothing = true;
|
this.imageSmoothing = true;
|
||||||
this.fields = new Fields(this.resolution);
|
|
||||||
|
|
||||||
const { ctx, offscreenCtx } = this.createCanvas(
|
const { ctx, offscreenCtx, offscreenCanvas } = this.createCanvas(
|
||||||
window.innerWidth,
|
window.innerWidth,
|
||||||
window.innerHeight,
|
window.innerHeight,
|
||||||
this.resolution,
|
this.resolution,
|
||||||
|
|
@ -35,6 +35,16 @@ export class DistanceField extends HTMLElement {
|
||||||
|
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.offscreenCtx = offscreenCtx;
|
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();
|
this.renderDistanceField();
|
||||||
}
|
}
|
||||||
|
|
@ -48,17 +58,19 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
// Update distance field when geometries move or resize
|
// Remove event listeners and terminate the worker
|
||||||
this.geometries.forEach((geometry) => {
|
this.geometries.forEach((geometry) => {
|
||||||
geometry.removeEventListener('move', this.handleGeometryUpdate);
|
geometry.removeEventListener('move', this.handleGeometryUpdate);
|
||||||
geometry.removeEventListener('resize', this.handleGeometryUpdate);
|
geometry.removeEventListener('resize', this.handleGeometryUpdate);
|
||||||
});
|
});
|
||||||
|
this.worker.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
||||||
if (name === 'resolution') {
|
if (name === 'resolution') {
|
||||||
this.resolution = parseInt(newValue, 10);
|
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') {
|
} else if (name === 'image-smoothing') {
|
||||||
this.imageSmoothing = newValue === 'true';
|
this.imageSmoothing = newValue === 'true';
|
||||||
if (this.ctx) {
|
if (this.ctx) {
|
||||||
|
|
@ -68,30 +80,39 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDistanceField() {
|
private renderDistanceField() {
|
||||||
// Get the computed ImageData from Fields
|
// Request the worker to generate ImageData
|
||||||
const imageData = this.fields.generateImageData();
|
this.worker.postMessage({ type: '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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
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 {
|
private transformToFieldCoordinates(point: Vector2): Vector2 {
|
||||||
|
|
@ -103,14 +124,15 @@ export class DistanceField extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
addShape(points: Vector2[]) {
|
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));
|
const transformedPoints = points.map((point) => this.transformToFieldCoordinates(point));
|
||||||
this.fields.addShape(transformedPoints);
|
this.worker.postMessage({ type: 'addShape', data: { points: transformedPoints } });
|
||||||
this.renderDistanceField();
|
this.renderDistanceField();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeShape(index: number) {
|
removeShape(index: number) {
|
||||||
this.fields.removeShape(index);
|
// Inform the worker to remove a shape
|
||||||
|
this.worker.postMessage({ type: 'removeShape', data: { index } });
|
||||||
this.renderDistanceField();
|
this.renderDistanceField();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +140,7 @@ export class DistanceField extends HTMLElement {
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
const offscreenCanvas = 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.position = 'absolute';
|
||||||
this.canvas.style.top = '0';
|
this.canvas.style.top = '0';
|
||||||
this.canvas.style.left = '0';
|
this.canvas.style.left = '0';
|
||||||
|
|
@ -140,14 +162,12 @@ export class DistanceField extends HTMLElement {
|
||||||
ctx.imageSmoothingEnabled = this.imageSmoothing;
|
ctx.imageSmoothingEnabled = this.imageSmoothing;
|
||||||
|
|
||||||
this.appendChild(this.canvas);
|
this.appendChild(this.canvas);
|
||||||
return { ctx, offscreenCtx };
|
return { ctx, offscreenCtx, offscreenCanvas };
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGeometryUpdate = (event: Event) => {
|
handleGeometryUpdate = (event: Event) => {
|
||||||
const geometry = event.target as HTMLElement;
|
const geometry = event.target as HTMLElement;
|
||||||
// TODO: store as array from getgo
|
const shapeId = this.geometryShapeIds.get(geometry);
|
||||||
const index = Array.from(this.geometries).indexOf(geometry as FolkGeometry);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
const rect = geometry.getBoundingClientRect();
|
const rect = geometry.getBoundingClientRect();
|
||||||
const points = [
|
const points = [
|
||||||
|
|
@ -157,10 +177,20 @@ export class DistanceField extends HTMLElement {
|
||||||
{ x: rect.x, y: rect.y + rect.height },
|
{ x: rect.x, y: rect.y + rect.height },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (index < this.fields.shapes.length) {
|
const transformedPoints = this.transformPoints(points);
|
||||||
this.fields.updateShape(index, this.transformPoints(points));
|
|
||||||
|
if (shapeId) {
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: 'updateShape',
|
||||||
|
data: { id: shapeId, points: transformedPoints },
|
||||||
|
});
|
||||||
} else {
|
} 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();
|
this.renderDistanceField();
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import type { Vector2 } from '../utils/Vector2.ts';
|
import type { Vector2 } from '../utils/Vector2.ts';
|
||||||
import { computeCPT } from './cpt.ts';
|
import { computeCPT } from './cpt.ts';
|
||||||
|
|
||||||
|
interface Shape {
|
||||||
|
id: string;
|
||||||
|
points: Vector2[];
|
||||||
|
color: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class Fields {
|
export class Fields {
|
||||||
private edt: Float32Array[] = [];
|
private edt: Float32Array[] = [];
|
||||||
private cpt: Vector2[][] = [];
|
private cpt: Vector2[][] = [];
|
||||||
|
|
@ -8,15 +14,11 @@ export class Fields {
|
||||||
private xcoords: Float32Array[] = [];
|
private xcoords: Float32Array[] = [];
|
||||||
private ycoords: Float32Array[] = [];
|
private ycoords: Float32Array[] = [];
|
||||||
private resolution: number;
|
private resolution: number;
|
||||||
shapes: Array<{
|
private shapes: Map<string, Shape> = new Map();
|
||||||
points: Vector2[];
|
|
||||||
color: number;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
constructor(resolution: number) {
|
constructor(resolution: number) {
|
||||||
this.resolution = resolution + 1;
|
this.resolution = resolution + 1;
|
||||||
this.initializeArrays();
|
this.initializeArrays();
|
||||||
this.updateFields();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeArrays() {
|
private initializeArrays() {
|
||||||
|
|
@ -39,15 +41,24 @@ export class Fields {
|
||||||
return this.colorField[x][y];
|
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);
|
const shapeColor = color ?? Math.floor(Math.random() * 255);
|
||||||
this.shapes.push({ points, color: shapeColor });
|
this.shapes.set(id, { id, points, color: shapeColor });
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeShape(index: number) {
|
removeShape(id: string) {
|
||||||
this.shapes.splice(index, 1);
|
if (this.shapes.delete(id)) {
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateShape(id: string, points: Vector2[]) {
|
||||||
|
const shape = this.shapes.get(id);
|
||||||
|
if (shape) {
|
||||||
|
shape.points = points;
|
||||||
|
this.updateFields();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 LARGE_NUMBER = 1000000000000;
|
||||||
const size = distanceField.length;
|
const size = distanceField.length;
|
||||||
const cellSize = 1;
|
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;
|
const { points, color } = shape;
|
||||||
|
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
const start = points[i];
|
const start = points[i];
|
||||||
const end = points[(i + 1) % points.length];
|
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(
|
private renderer(
|
||||||
pixelRenderer: (
|
pixelRenderer: (
|
||||||
distance: number,
|
distance: number,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue