//provide routes for applying canvases to workers import worker from './canvas.worker' import { proxyElementWorkerRoutes, initProxyElement } from './ProxyListener'; declare var WorkerGlobalScope; export type WorkerCanvasTransferProps = { //defined in main thread to send to worker canvas:HTMLCanvasElement, context?:string, _id?:string, draw?:string|((self:any,canvas:any,context:any)=>void), update?:string|((self:any,canvas:any,context:any,input:any)=>void), init?:string|((self,canvas:any,context:any)=>void), clear?:string|((self,canvas:any,context:any)=>void), transfer?:any[], animating?:boolean, //animation will start automatically, else you can call draw conditionally [key:string]:any //any transferrable props you want to use in your animation } export type CanvasProps = { //defined in worker thread canvas:any, //offscreen canvas context?:string|CanvasRenderingContext2D|WebGL2RenderingContext|WebGLRenderingContext, _id:string, width?:number, height?:number, draw?:string|((self:any,canvas:any,context:any)=>void), update?:string|((self:any,canvas:any,context:any,input:any)=>void), init?:string|((self,canvas:any,context:any)=>void), clear?:string|((self,canvas:any,context:any)=>void), animating?:boolean, preventDefault?:boolean, //we can generically prevent defaults on key events (except F1-12 for debug reasons) [key:string]:any } export type CanvasControls = { _id:string, draw:(props?:any,transfer?:any)=>void, //if you set props they will be available on "self" or "this" in the draw functions update:(props:{[key:string]:any},transfer?:any)=>void, clear:()=>void, init:()=>void, stop:()=>void, start:()=>void, set:(newDrawProps:CanvasProps,transfer?:any)=>void } export type WorkerCanvasControls = { worker:Worker|MessagePort, terminate:()=>void } & CanvasControls export type WorkerCanvas = { //this is the object stored on the worker to track this canvas context graph:any, //Graph or Service class canvas:any, //OffscreenCanvas context?:CanvasRenderingContext2D|WebGL2RenderingContext|WebGLRenderingContext, _id:string, draw?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'])=>void), //runs in animation loop or on drawFrame calls update?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'],input:any)=>void), init?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'])=>void), clear?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'])=>void), stop:()=>void, start:()=>void, set:(newDrawProps:CanvasProps,transfer?:any)=>void, terminate:()=>void, animating:boolean, //animation will start automatically, else you can call draw conditionally [key:string]:any //any transferrable props you want to use in your animation } export function Renderer( options:CanvasProps & { worker?:Worker|string|Blob|MessagePort|true, route?:string } ) { if(options.worker === true) { options.worker = worker } if(options.worker) { let worker = options.worker; let route = options.route; if(worker instanceof Blob || typeof worker === 'string') { worker = new Worker(worker as any); } delete options.worker; delete options.route; return transferCanvas(worker as any, options as WorkerCanvasTransferProps, route); } else { initProxyElement(options.canvas,undefined,options._id, options.preventDefault); return setupCanvas(options); } } export function transferCanvas( worker:Worker|MessagePort, options:WorkerCanvasTransferProps, route?:string //we can reroute from the default 'setupCanvas' e.g. for other rendering init processes like in threejs ) { //console.log(options); if(!options) return undefined; if(!options._id) options._id = `canvas${Math.floor(Math.random()*1000000000000000)}`; let offscreen = options.canvas instanceof OffscreenCanvas ? options.canvas : (options.canvas as any).transferControlToOffscreen(); if(!options.width) options.width = options.canvas.clientWidth; if(!options.height) options.height = options.canvas.clientHeight; let message:any = {route:route ? route : 'setupCanvas', args:{ ...options, canvas:offscreen, }}; let proxy; if(this?.__node?.graph) proxy = this.__node.graph.run('initProxyElement', options.canvas, worker, options._id, options.preventDefault); //initiate an element proxy else proxy = initProxyElement(options.canvas,worker,options._id, options.preventDefault); if(options.draw) { if(typeof options.draw === 'function') message.args.draw = options.draw.toString() else message.args.draw = options.draw; } if(options.update) { if(typeof options.update === 'function') message.args.update = options.update.toString() else message.args.update = options.update; } if(options.init) { if(typeof options.init === 'function') message.args.init = options.init.toString() else message.args.init = options.init; } if(options.clear) { if(typeof options.clear === 'function') message.args.clear = options.clear.toString() else message.args.clear = options.clear; } let tr = [offscreen]; if(options.transfer) { tr.push(...options.transfer); delete options.transfer; } //console.log(worker,message); worker.postMessage(message,tr); //lets add some utilities to make it easy to update the thread const canvascontrols = { _id:options._id, width:options.width, height:options.height, worker, draw:(props?:any,transfer?:any)=>{ worker.postMessage({route:'drawFrame',args:[props,options._id]},transfer); }, update:(props:{[key:string]:any},transfer?:any)=>{ worker.postMessage({route:'updateCanvas',args:[props,options._id]}, transfer); }, clear:()=>{ worker.postMessage({route:'clearCanvas',args:options._id}) }, init:()=>{ //console.log('Posting init') worker.postMessage({route:'initCanvas',args:options._id}); }, stop:()=>{ worker.postMessage({route:'stopAnim',args:options._id}) }, start:()=>{ worker.postMessage({route:'startAnim',args:options._id}) }, set:(newDrawProps:CanvasProps,transfer?:any)=>{ worker.postMessage({route:'setDraw',args:[newDrawProps,options._id]},transfer); }, terminate:()=>{ if(proxy) proxy.terminate(); (worker as Worker).terminate(); } } return canvascontrols as WorkerCanvasControls; } export function setDraw( settings:CanvasProps, _id?:string ) { let canvasopts; if(this?.__node?.graph) { if(_id) canvasopts = this.__node.graph.CANVASES?.[settings._id]; else if(settings._id) canvasopts = this.__node.graph.CANVASES?.[settings._id]; else canvasopts = this.__node.graph.CANVASES?.[Object.keys(this.__node.graph.CANVASES)[0]]; } else { if(_id) canvasopts = globalThis.CANVASES?.[settings._id]; else if(settings._id) canvasopts = globalThis.CANVASES?.[settings._id]; else canvasopts = globalThis.CANVASES?.[Object.keys(globalThis.CANVASES)[0]]; } if(canvasopts) { if(settings.canvas) { canvasopts.canvas = settings.canvas; if(canvasopts.proxy) canvasopts.proxy.terminate(); let proxy; //create an element proxy to add event listener functionality if(this?.__node?.graph) proxy = this.__node.graph.run('makeProxy', canvasopts._id, canvasopts.canvas); else proxy = proxyElementWorkerRoutes.makeProxy(canvasopts._id, canvasopts.canvas); canvasopts.proxy = proxy; //now the canvas can handle mouse and resize events, more can be implemented } if(typeof settings.context === 'string') canvasopts.context = canvasopts.canvas.getContext(settings.context); else if(settings.context) canvasopts.context = settings.context; if(settings.width) canvasopts.canvas.width = settings.width; if(settings.height) canvasopts.canvas.height = settings.height; if(typeof settings.draw === 'string') settings.draw = parseFunctionFromText(settings.draw); if(typeof settings.draw === 'function') { canvasopts.draw = settings.draw.bind(settings); } if(typeof settings.update === 'string') settings.update = parseFunctionFromText(settings.update); if(typeof settings.update === 'function') { canvasopts.update = settings.update.bind(settings); } if(typeof settings.init === 'string') settings.init = parseFunctionFromText(settings.init); if(typeof settings.init === 'function') { canvasopts.init = settings.init.bind(settings); } if(typeof settings.clear === 'string') settings.clear = parseFunctionFromText(settings.clear); if(typeof settings.clear === 'function') { canvasopts.clear = settings.clear.bind(settings); } return settings._id; } return undefined; } export function setupCanvas( options:CanvasProps ){ if(this?.__node?.graph) { if(!this.__node.graph.CANVASES) this.__node.graph.CANVASES = {} as {[key:string]:WorkerCanvas}; } else if(!globalThis.CANVASES) globalThis.CANVASES = {} as {[key:string]:WorkerCanvas}; let canvasOptions = options; options._id ? canvasOptions._id = options._id : canvasOptions._id = `canvas${Math.floor(Math.random()*1000000000000000)}`; typeof options.context === 'string' ? canvasOptions.context = options.canvas.getContext(options.context) : canvasOptions.context = options.context; //get the rendering context based on string passed ('animating' in options) ? canvasOptions.animating = options.animating : canvasOptions.animating = true; let proxy; if(this?.__node?.graph?.CANVASES[canvasOptions._id]) { this.__node.graph.run('setDraw',canvasOptions); } else if(globalThis.CANVASES?.[canvasOptions._id]) { setDraw(canvasOptions); } else { if(this?.__node?.graph) { canvasOptions.graph = this.__node.graph; if(!canvasOptions.__node) {canvasOptions.__node = {}} if(!canvasOptions.__node.tag) canvasOptions.__node.tag = canvasOptions._id; canvasOptions = this.__node.graph.add(canvasOptions); //replace with node canvasOptions.__addOndisconnected = () => { canvasOptions.stop(); delete this.__node.graph.CANVASES[canvasOptions._id]; } } if(this?.__node?.graph) this.__node.graph.CANVASES[canvasOptions._id] = canvasOptions; else globalThis.CANVASES[canvasOptions._id] = canvasOptions; //create an element proxy to add event listener functionality if(this?.__node?.graph) proxy = this.__node.graph.run('makeProxy', canvasOptions._id, canvasOptions.canvas); else proxy = proxyElementWorkerRoutes.makeProxy(canvasOptions._id, canvasOptions.canvas); //now the canvas can handle mouse and resize events, more can be implemented if(options.width) canvasOptions.canvas.width = options.width; if(options.height) canvasOptions.canvas.height = options.height; if(typeof canvasOptions.draw === 'string') { canvasOptions.draw = parseFunctionFromText(canvasOptions.draw); } else if(typeof canvasOptions.draw === 'function') { canvasOptions.draw = canvasOptions.draw.bind(canvasOptions); } if(typeof canvasOptions.update === 'string') { canvasOptions.update = parseFunctionFromText(canvasOptions.update); } else if(typeof canvasOptions.update === 'function') { canvasOptions.update = canvasOptions.update.bind(canvasOptions); } if(typeof canvasOptions.init === 'string') { canvasOptions.init = parseFunctionFromText(canvasOptions.init); } else if(typeof canvasOptions.init === 'function') { canvasOptions.init = canvasOptions.init.bind(canvasOptions); } if(typeof canvasOptions.clear === 'string') { canvasOptions.clear = parseFunctionFromText(canvasOptions.clear); } else if(typeof canvasOptions.clear === 'function') { canvasOptions.clear = canvasOptions.clear.bind(canvasOptions); } const finishSetup = () => { canvasOptions.stop = () => {stopAnim(canvasOptions._id);}; canvasOptions.start = (draw?:any) => {startAnim(canvasOptions._id,draw);}; canvasOptions.set = (settings:any) => {setDraw(settings,canvasOptions._id);} if(typeof canvasOptions.draw === 'function' && canvasOptions.animating) { let draw = (s,canvas,context) => { if(s.animating) { let res = s.draw(s,canvas,context); if(res?.then) { res.then(() => { requestAnimationFrame(()=>{ draw(s,canvas,context); }); }) } else requestAnimationFrame(()=>{ draw(s,canvas,context); }); } } draw(canvasOptions, canvasOptions.canvas,canvasOptions.context); } if(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) return canvasOptions._id as string; else { //lets add some utilities to make it easy to update the thread const canvascontrols = { _id:options._id, width:options.width, height:options.height, proxy, draw:(props?:any)=>{ drawFrame(props,options._id); }, update:(props:{[key:string]:any})=>{ updateCanvas(props,options._id); }, clear:()=>{ clearCanvas(options._id); }, init:()=>{ //console.log('Posting init') initCanvas(options._id); }, stop:()=>{ stopAnim(options._id); }, start:()=>{ startAnim(options._id); }, set:(newDrawProps:CanvasProps)=>{ setDraw(newDrawProps,options._id); }, terminate:()=>{ if(canvascontrols.proxy) { canvascontrols.proxy.terminate(); } if(this.__node?.graph) this.__node.graph.remove(options._id); else { stopAnim(options._id); if(this?.__node?.graph) delete this.__node.graph.CANVASES[canvasOptions._id]; else delete globalThis.CANVASES[canvasOptions._id]; } } } return canvascontrols as CanvasControls; } } if(typeof canvasOptions.init === 'function') { let res = (canvasOptions.init as any)(canvasOptions, canvasOptions.canvas,canvasOptions.context); if(res?.then) { return new Promise((resolve) => { res.then(()=>{ resolve(finishSetup()) }); }) as Promise; } } return finishSetup(); } } //local function copy, same thing but returns the whole canvas object export function drawFrame(props?:{[key:string]:any},_id?:string) { //can update props when calling draw let canvasopts = getCanvas.call(this, _id); if(canvasopts) { if(props) Object.assign(canvasopts,props); if(canvasopts.draw) { canvasopts.draw(canvasopts,canvasopts.canvas,canvasopts.context); return _id; } } return undefined; } export function clearCanvas(_id?:string) { let canvasopts = getCanvas.call(this, _id); if(canvasopts?.clear) { canvasopts.clear(canvasopts,canvasopts.canvas,canvasopts.context); return _id; } return undefined; } export function initCanvas(_id?:string){ let canvasopts = getCanvas.call(this, _id); if(canvasopts?.init) { canvasopts.init(canvasopts,canvasopts.canvas,canvasopts.context); return _id; } return undefined; } export function updateCanvas(input?:any,_id?:string){ let canvasopts = getCanvas.call(this, _id); if(canvasopts?.update) { canvasopts.update(canvasopts,canvasopts.canvas,canvasopts.context,input); return _id; } return undefined; } export function setProps(props?:{[key:string]:any},_id?:string){ //update animation props, e.g. the radius or color of a circle you are drawing with a stored value let canvasopts = getCanvas.call(this, _id); if(canvasopts && props) { Object.assign(canvasopts,props); if(props.width) canvasopts.canvas.width = props.width; if(props.height) canvasopts.canvas.height = props.height; return _id; } return undefined; } export function startAnim(_id?:string, draw?:string|((this:any,canvas:any,context:any)=>void)){ //run the draw function applied to the animation or provide a new one let canvasopts = getCanvas.call(this, _id); canvasopts.animating = true; if(canvasopts && draw) { if(typeof draw === 'string') draw = parseFunctionFromText(draw); if(typeof draw === 'function') { canvasopts.draw = draw; } return _id; } if(typeof canvasopts?.draw === 'function') { let draw = (s,canvas,context) => { if(s.animating) { s.draw(s,canvas,context); requestAnimationFrame(()=>{ draw(s,canvas,context); }) } } if(typeof canvasopts.clear === 'function') (canvasopts.clear as any)(canvasopts, canvasopts.canvas, canvasopts.context); if(typeof canvasopts.init === 'function') (canvasopts.init as any)(canvasopts, canvasopts.canvas, canvasopts.context); draw(canvasopts,canvasopts.canvas,canvasopts.context); return _id; } return undefined; } export function stopAnim(_id:string){ let canvasopts = getCanvas.call(this, _id); if(canvasopts) { canvasopts.animating = false; if(typeof canvasopts.clear === 'function') requestAnimationFrame(canvasopts.clear(canvasopts, canvasopts.canvas, canvasopts.context)); return _id; } return undefined; } export function getCanvas (_id?:string) { let canvasopts; if(this?.__node?.graph) { if(!_id) canvasopts = this.__node.graph.CANVASES?.[Object.keys(this.__node.graph.CANVASES)[0]]; else canvasopts = this.__node.graph.CANVASES?.[_id]; } else { if(!_id) canvasopts = globalThis.CANVASES?.[Object.keys(globalThis.CANVASES)[0]]; else canvasopts = globalThis.CANVASES?.[_id]; } return canvasopts; } //load on front and backend export const workerCanvasRoutes = { ...proxyElementWorkerRoutes, Renderer:Renderer, transferCanvas:transferCanvas, setupCanvas:setupCanvas, setDraw:setDraw, drawFrame:drawFrame, clearCanvas:clearCanvas, initCanvas:initCanvas, updateCanvas:updateCanvas, setProps:setProps, startAnim:startAnim, stopAnim:stopAnim, getCanvas:getCanvas }; function parseFunctionFromText(method='') { //Get the text inside of a function (regular or arrow); let getFunctionBody = (methodString) => { return methodString.replace(/^\W*(function[^{]+\{([\s\S]*)\}|[^=]+=>[^{]*\{([\s\S]*)\}|[^=]+=>(.+))/i, '$2$3$4'); } let getFunctionHead = (methodString) => { let startindex = methodString.indexOf('=>')+1; if(startindex <= 0) { startindex = methodString.indexOf('){'); } if(startindex <= 0) { startindex = methodString.indexOf(') {'); } return methodString.slice(0, methodString.indexOf('{',startindex) + 1); } let newFuncHead = getFunctionHead(method); let newFuncBody = getFunctionBody(method); let newFunc; if (newFuncHead.includes('function')) { let varName = newFuncHead.split('(')[1].split(')')[0] newFunc = new Function(varName, newFuncBody); } else { if(newFuncHead.substring(0,6) === newFuncBody.substring(0,6)) { //newFuncBody = newFuncBody.substring(newFuncHead.length); let varName = newFuncHead.split('(')[1].split(')')[0] //console.log(varName, newFuncHead ,newFuncBody); newFunc = new Function(varName, newFuncBody.substring(newFuncBody.indexOf('{')+1,newFuncBody.length-1)); } else { try {newFunc = (0,eval)(newFuncHead + newFuncBody + "}");} catch {} } } return newFunc; }