I've got some heavy canvas code that I want to offload into a WebWorker. When I follow the examples in this page and I pass the path to the worker typescript file into the constructor
New Worker("./PaintCanvas.ts") the code successfully compiles but when it runs the worker doesn't seem to have found the code correctly because it throws an error saying Uncaught SyntaxError: Unexpected token '<' and the file it appears to be attempting to execute is actually my index.html file.
This is the component that I'm trying to run the worker from:
import React, { RefObject } from 'react';
//eslint-disable-next-line import/no-webpack-loader-syntax
import * as workerPath from "file-loader?name=[name].js!./PaintCanvas";
import './Canvas.css';
interface IProps {
}
interface IState {
}
class Canvas extends React.Component<IProps, IState> {
private canvasRef: RefObject<HTMLCanvasElement>;
private offscreen?: OffscreenCanvas;
private worker?: Worker;
constructor(props: IProps) {
super(props);
this.canvasRef = React.createRef();
this.resizeListener = this.resizeListener.bind(this);
window.addEventListener('resize', this.resizeListener);
}
componentDidMount() {
let canvas = this.canvasRef.current;
if (canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
this.offscreen = canvas.transferControlToOffscreen();
this.worker = new Worker("./PaintCanvas.ts");
this.worker.postMessage(this.offscreen, [this.offscreen]);
}
}
resizeListener() {
let canvas = this.canvasRef.current;
if (canvas) {
canvas.width = window.innerWidth > canvas.width ? window.innerWidth : canvas.width;
canvas.height = window.innerHeight > canvas.height ? window.innerHeight : canvas.height;
this.offscreen = canvas.transferControlToOffscreen();
this.worker = new Worker("./PaintCanvas.ts");
this.worker.postMessage(this.offscreen, [this.offscreen]);
}
}
render() {
return (
<>
<canvas className="noiseCanvas" ref={this.canvasRef}/>
<div className="overlay">
</div>
</>
);
}
}
export default Canvas;
And this is the worker that I'm trying to load:
export default class PaintCanvas extends Worker {
private canvas?: OffscreenCanvas;
private intervalId?: number;
private frame: number;
private frameSet: number;
private noiseData: ImageData[][];
private noiseNum: number[];
private overlayFrame: number[];
private overlayData: Uint8ClampedArray[][];
private overlayNum: number[];
private workers: (Worker|undefined)[];
constructor(stringUrl: string | URL) {
super(stringUrl);
this.frame = 0;
this.frameSet = 0;
this.noiseData = [[], [], []];
this.noiseNum = [0, 0, 0];
this.overlayFrame = [0, 0, 0];
this.overlayData = [[], [], []];
this.overlayNum = [0, 0, 0];
this.workers = [undefined, undefined, undefined];
}
onmessage = (event: MessageEvent) => {
this.canvas = event.data;
this.frame = 0;
this.frameSet = 0;
this.noiseData = [[], [], []];
this.noiseNum = [0, 0, 0];
this.overlayFrame = [0, 0, 0];
this.overlayData = [[], [], []];
this.overlayNum = [0, 0, 0];
if (this.workers[0]) {
this.workers[0].terminate();
}
if (this.workers[1]) {
this.workers[1].terminate();
}
if (this.workers[2]) {
this.workers[2].terminate();
}
this.makeNoise(0);
this.makeNoise(1);
this.makeNoise(2);
if (this.intervalId) {
window.clearInterval(this.intervalId);
}
this.intervalId = window.setInterval(this.paintNoise, 100);
}
makeNoise(index: number) {
if (this.canvas) {
const width = this.canvas.width;
const height = this.canvas.height;
this.workers[index] = new Worker("./FillCanvas.ts");
if (this.workers[index]) {
this.workers[index]!.onmessage = (event) => {
if (this.overlayNum[index] < 4 || !event.data[0]) {
this.overlayData[index].push(event.data);
this.overlayNum[index]++;
if (this.overlayNum[index] < 4) {
this.workers[index]!.postMessage([width, height]);
} else {
this.workers[index]!.postMessage([width, height, new Uint8ClampedArray(width * height * 4), this.overlayData[index][0]]);
this.overlayFrame[index]++;
}
} else {
if (event.data[0]) {
this.noiseData[index].push(new ImageData(event.data[0], width, height));
this.noiseNum[index]++;
if (this.noiseNum[index] < 30) {
this.workers[index]!.postMessage([width, height, event.data[1], this.overlayData[index][Math.ceil(this.overlayFrame[index] / 4) % 4]]);
this.overlayFrame[index]++;
} else {
this.workers[index] = undefined;
}
}
}
}
this.workers[index]!.postMessage([width, height]);
}
}
}
paintNoise() {
if (this.noiseNum[0] > 10) {
this.frame++;
if (this.frame % this.noiseNum[this.frameSet % 3] === 0) {
this.frameSet++;
}
if (this.canvas) {
let ctx = this.canvas.getContext("2d");
if (ctx) {
ctx.putImageData(this.noiseData[this.frameSet % 2][this.frame % this.noiseNum[this.frameSet % 2]], 0, 0);
}
}
}
}
}
As you can see my worker will also be creating its own workers once this works properly.
You should also notice the workerPath import at the top. I tried implementing the top answer from this similar stackoverflow question from a few years ago, and that did make the code itself visible but it wouldn't work as long as the code remains a typescript module. Instead I tried just instantiating the worker using the constructor of the class, eg new PaintCanvas(), but that still required a url and it still only finds the index.html file.
I also tried putting the worker file in the react public folder and using the public url to reference it. The tsconfig file automatically noticed this and added the file to the "include" section, which I thought looked promising, but it still just tried to execute index.html.
So my question is, is there an idiomatic non-hacky way to implement Web Workers in typescript in react or should I use one of the hacky methods I've seen elsewhere? I know that the Web Worker API is newly matured so hopefully the workarounds that a lot of the answers I'm finding online offer up aren't necessary anymore.