{"id":22120155,"url":"https://github.com/joshbrew/worker_canvas","last_synced_at":"2025-03-24T06:26:39.823Z","repository":{"id":212002201,"uuid":"730470215","full_name":"joshbrew/worker_canvas","owner":"joshbrew","description":"Take your canvas-based rendering off the main thread","archived":false,"fork":false,"pushed_at":"2024-03-23T23:03:05.000Z","size":448,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-29T12:14:50.717Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshbrew.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-12-12T02:01:52.000Z","updated_at":"2023-12-12T02:07:07.000Z","dependencies_parsed_at":"2024-03-13T11:56:43.678Z","dependency_job_id":null,"html_url":"https://github.com/joshbrew/worker_canvas","commit_stats":null,"previous_names":["joshbrew/worker_canvas"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fworker_canvas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fworker_canvas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fworker_canvas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshbrew%2Fworker_canvas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshbrew","download_url":"https://codeload.github.com/joshbrew/worker_canvas/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245220058,"owners_count":20579697,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-01T14:21:09.074Z","updated_at":"2025-03-24T06:26:39.796Z","avatar_url":"https://github.com/joshbrew.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Web Worker Canvas Library 🚀\n\n\n![d-status](https://img.shields.io/npm/v/workercanvas.svg) \n![d-downloads](https://img.shields.io/npm/dt/workercanvas.svg)\n\n## Installation 📦\n\nTo include this library in your project, simply use:\n\n```bash\nnpm install workercanvas\n# or\nyarn add workercanvas\n```\n\n## Run Example\n\n### [Demo Link](https://workercanvas.netlify.app/)\n```bash\ncd example\n\n# if tinybuild is not installed\nnpm i -g tinybuild\n\nnpm i\n\nnpm start\n```\n\n## Elevate your applications with seamless off-screen rendering!\n\nThis library harnesses the power of Web Workers to take your canvas-based rendering off the main thread 🧵. Experience buttery smooth animations, complex visual computations, and responsive user interfaces—all while keeping your application's main thread free for other critical tasks 🖼️💡.\n\nThis is a pure implementation with type support, and adds about 32kb of total weight to your program when using the bundle.\n\n## Usage 🛠️\n\nImport the necessary components from the library and let the magic happen:\n\n### index.js\n```ts\nimport { Renderer, WorkerCanvas } from '../WorkerCanvas';\n\nconst canvas = document.createElement('canvas'); \ncanvas.width = 800; canvas.height = 600;\ncanvas.style.width = '100%';\ncanvas.style.height = '100%';\ncanvas.id = 'myCanvas';\ndocument.body.appendChild(canvas);\n\nconst workerRenderer = Renderer({\n  canvas: canvas,\n  context:'2d',\n  worker: true, //use our prebundled worker\n  _id:canvas.id,\n  init:(self:WorkerCanvas,canvas,context)=\u003e{ //init called automatically before first draw on thread\n      console.log('canvas', canvas)\n      canvas.addEventListener('mousedown',(ev)=\u003e{ //ProxyListener mimics most of the necessary mouse and key events e.g. for proxying threejs controls on thread for processing event logic locally on the thread. Also includes resize events\n          console.log('clicked!', ev, canvas);\n      })\n  },\n  draw:(self:WorkerCanvas,canvas:any,context:CanvasRenderingContext2D)=\u003e{ //render loop starts automatically on thread after receiving canvas and instructions\n      context.clearRect(0,0,canvas.width, canvas.height);\n      \n      context.fillStyle = `rgb(0,${Math.sin(Date.now()*0.001)*255},${Math.cos(Date.now()*0.001)*255})`;\n      context.fillRect(0,0,canvas.width,canvas.height);\n  }                        \n});\n```\n\n![1](./example/result1.PNG)\n\nStart creating fluid animations and graphics right away!\n\nThere is a prebundled worker when you use 'true' or you can follow the canvas.worker.ts example to ensure functionality and provide a worker url instead. If you want to invoke threejs etc on the thread you need to define your own worker and have an alternative route to initialize what you need. \n\n## Types\n\nThis speeds up learning the library in your editor (e.g. VSCode)\n\n```ts\ntype WorkerCanvasTransferProps = { //defined in main thread to send to worker\n    canvas:HTMLCanvasElement,  \n    context?:string, \n    _id?:string,\n    draw?:string|((self:any,canvas:any,context:any)=\u003evoid),\n    update?:string|((self:any,canvas:any,context:any,input:any)=\u003evoid),\n    init?:string|((self,canvas:any,context:any)=\u003evoid),\n    clear?:string|((self,canvas:any,context:any)=\u003evoid),\n    transfer?:any[],\n    animating?:boolean, //animation will start automatically, else you can call draw conditionally\n    [key:string]:any //any transferrable props you want to use in your animation\n}\n\ntype CanvasProps = { //defined in worker thread\n    canvas:any, //offscreen canvas\n    context?:string|CanvasRenderingContext2D|WebGL2RenderingContext|WebGLRenderingContext,\n    _id:string,\n    width?:number,\n    height?:number,\n    draw?:string|((self:any,canvas:any,context:any)=\u003evoid),\n    update?:string|((self:any,canvas:any,context:any,input:any)=\u003evoid),\n    init?:string|((self,canvas:any,context:any)=\u003evoid),\n    clear?:string|((self,canvas:any,context:any)=\u003evoid),\n    animating?:boolean,\n    preventDefault?:boolean, //we can generically prevent defaults on key events (except F1-12 for debug reasons)\n    [key:string]:any\n}\n\ntype CanvasControls = {\n    _id:string,\n    draw:(props?:any,transfer?:any)=\u003evoid,\n    update:(props:{[key:string]:any},transfer?:any)=\u003evoid,\n    clear:()=\u003evoid,\n    init:()=\u003evoid,\n    stop:()=\u003evoid,\n    start:()=\u003evoid,\n    set:(newDrawProps:CanvasProps,transfer?:any)=\u003evoid\n}\n\ntype WorkerCanvasControls = {\n    worker:Worker|MessagePort,\n    terminate:()=\u003evoid\n} \u0026 CanvasControls\n\ntype WorkerCanvas = { //this is the object stored on the worker to track this canvas context\n    graph:any, //Graph or Service class\n    canvas:any, //OffscreenCanvas\n    context?:CanvasRenderingContext2D|WebGL2RenderingContext|WebGLRenderingContext,\n    _id:string,\n    draw?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'])=\u003evoid), //runs in animation loop or on drawFrame calls\n    update?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'],input:any)=\u003evoid),\n    init?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'])=\u003evoid),\n    clear?:((self:WorkerCanvas,canvas:WorkerCanvas['canvas'],context:WorkerCanvas['context'])=\u003evoid),\n    animating:boolean, //animation will start automatically, else you can call draw conditionally\n    [key:string]:any //any transferrable props you want to use in your animation\n}\n\n\n```\n\n## Sample worker that reroutes for ThreeJS initialization\n\n![2](./example/result2.PNG)\n\n\n### three.worker.ts\n```ts\n\nimport { workerCanvasRoutes, CanvasProps } from '../WorkerCanvas';\n\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport { PickHelper } from './PickHelper'\n//etc...\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'\nimport { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'\n//minimal web worker for running offscreen canvases, \n\ndeclare var WorkerGlobalScope;\n\nif(typeof WorkerGlobalScope !== 'undefined') {\n\n    const routes = {\n        ...workerCanvasRoutes,\n        receiveThreeCanvas:function(options:CanvasProps){ //modified canvas receiver that installs desired threejs modules\n                const ThreeProps = { //e.g. install these systems to 'self', which is the worker canvas\n                    THREE,\n                    OrbitControls,\n                    EffectComposer,\n                    RenderPass,\n                    SMAAPass,\n                    UnrealBloomPass,\n                    PickHelper\n                }\n\n                Object.assign(options, ThreeProps); //install desired props to our canvas's 'self' reference\n\n                console.log(this);\n                let renderId = routes.setupCanvas(options); //the the base canvas tools do the rest, all ThreeJS tools are on self, for self contained ThreeJS renders\n                //you can use the canvas render loop by default, or don't provide a draw function and just use the init and the Three animate() callback\n\n                //let canvasopts = this.graph.CANVASES[renderId] as WorkerCanvas;\n\n                return renderId;\n            }\n        //add more compatible routes that don't require graphscript\n    };\n    \n    self.onmessage = (ev) =\u003e {\n        if(ev.data.route) {\n            if(Array.isArray(ev.data.args)) {\n                routes[ev.data.route](...ev.data.args);\n            } else routes[ev.data.route](ev.data.args);\n        } //that's it! The functions handle worker communication internally\n    \n    }\n    \n}\n\nexport default self as any;\n\n\n```\n\nThen from main thread we'd call\n\n### index.js\n```ts\nimport { Renderer, WorkerCanvas } from '../WorkerCanvas';\nimport threewrkr from './three.worker' //or use a bundled path (not ts)\n\nconst canvas2 = document.createElement('canvas'); \ncanvas2.width = 800; canvas2.height = 600;\ncanvas2.id = 'myCanvas2';\ndocument.body.appendChild(canvas2);\nconst myOffscreenCanvas = Renderer(\n    {\n        canvas: canvas2,\n        worker: threewrkr, //use our prebundled worker\n        route:\"receiveThreeCanvas\", //set a custom function to pass our Renderer creation logic to, e.g. to create a stage for setting up ThreeJS on the worker\n        _id:canvas2.id,\n        context:undefined, //Threejs sets the context\n        init:(self:WorkerCanvas,canvas,context)=\u003e{\n\n            //these are installed to the 'self' reference\n            const THREE = self.THREE;\n            const OrbitControls = self.OrbitControls;\n            const PickHelper = self.PickHelper;\n            \n            const renderer = new THREE.WebGLRenderer({canvas});\n            let time = 0;\n            let lastFrame = Date.now();\n\n            const fov = 75;\n            const aspect = 2;\n            const near = 0.1;\n            const far = 100;\n            \n            const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);\n            camera.position.z = 4;\n\n            renderer.setSize(canvas.width, canvas.height, false);\n                if(camera) {\n                    camera.aspect = canvas.clientWidth / canvas.clientHeight;\n                    camera.updateProjectionMatrix();\n                }\n\n            const controls = new OrbitControls(camera, canvas);\n            controls.target.set(0,0,0);\n            controls.update();\n\n            const scene = new THREE.Scene();\n\n            {\n                const color = 0xFFFFFF;\n                const intensity = 1;\n                const light = new THREE.DirectionalLight(color, intensity);\n                light.position.set(-1, 2, 4);\n                scene.add(light);\n            }\n        \n            const boxWidth = 1;\n            const boxHeight = 1;\n            const boxDepth = 1;\n            const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);\n        \n            const makeInstance = (geometry, color, x) =\u003e {\n                const material = new THREE.MeshPhongMaterial({\n                    color,\n                });\n            \n                const cube = new THREE.Mesh(geometry, material);\n                scene.add(cube);\n            \n                cube.position.x = x;\n            \n                return cube;\n            }\n        \n            const cubes = [\n                makeInstance(geometry, 0x44aa88, 0),\n                makeInstance(geometry, 0x8844aa, -2),\n                makeInstance(geometry, 0xaa8844, 2),\n            ];\n\n            let getCanvasRelativePosition = (event) =\u003e {\n                const rect = canvas.getBoundingClientRect();\n                return {\n                    x: event.clientX - rect.left,\n                    y: event.clientY - rect.top,\n                };\n            }\n        \n            const pickPosition = {x: -2, y: -2};\n            const pickHelper = new PickHelper();\n\n            let setPickPosition = (event) =\u003e {\n                const pos = getCanvasRelativePosition(event);\n                pickPosition.x = (pos.x / canvas.clientWidth ) *  2 - 1;\n                pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1;  // note we flip Y\n            }\n        \n            let clearPickPosition = () =\u003e {\n                // unlike the mouse which always has a position\n                // if the user stops touching the screen we want\n                // to stop picking. For now we just pick a value\n                // unlikely to pick something\n                pickPosition.x = -100000;\n                pickPosition.y = -100000;\n            }\n            \n            canvas.addEventListener('mousemove', setPickPosition);\n            canvas.addEventListener('mouseout', clearPickPosition);\n            canvas.addEventListener('mouseleave', clearPickPosition);\n        \n            canvas.addEventListener('touchstart', (event) =\u003e {\n                // prevent the window from scrolling\n                event.preventDefault();\n                setPickPosition(event.touches[0]);\n            }, {passive: false});\n        \n            canvas.addEventListener('touchmove', (event) =\u003e {\n                setPickPosition(event.touches[0]);\n            });\n        \n            canvas.addEventListener('touchend', clearPickPosition);\n\n            canvas.addEventListener('resize', (ev) =\u003e {\n                renderer.setSize(canvas.width, canvas.height, false);\n                if(camera) {\n                    camera.aspect = canvas.clientWidth / canvas.clientHeight;\n                    camera.updateProjectionMatrix();\n                }\n            });\n\n            Object.assign(self, {\n                renderer,\n                camera,\n                controls,\n                scene,\n                cubes,\n                time,\n                lastFrame,\n                pickPosition,\n                pickHelper\n            }); //assign these to self for the draw function\n\n            clearPickPosition();\n            //this.renderer.setAnimationLoop(this.draw);\n\n\n        },\n        draw:(self:WorkerCanvas,canvas:any,context:any)=\u003e{\n            let now = Date.now();\n            self.time += (now - self.lastFrame) * 0.001;\n            self.lastFrame = now;\n\n            self.cubes.forEach((cube, ndx) =\u003e {\n                const speed = 1 + ndx * .1;\n                const rot = self.time * speed;\n                cube.rotation.x = rot;\n                cube.rotation.y = rot;\n                });\n            \n                \n                self.pickHelper.pick(self.pickPosition, self.scene, self.camera, self.time);\n                //console.log(this.pickPosition);\n                self.renderer.render(self.scene, self.camera);\n        },\n        clear:(self:WorkerCanvas, canvas, context) =\u003e {\n            if(self.renderer) {\n                self.render.domElement = null;\n                self.renderer = null;\n                self.composer = null;\n                self.gui = null;\n                self.controls = null;\n                self.camera = null;\n                self.scene = null;\n            }\n        }\n    }                \n);\n```\n\n\n## Use Cases 🎨\n\n- **Games**: Build immersive, high-performance game engines that run in the browser.\n- **Data Visualizations**: Handle large datasets and real-time data rendering without a hitch.\n- **Interactive UIs**: Keep your user interfaces snappy even with complex background processes.\n- **Educational Tools**: Create dynamic simulations and educational content that require heavy rendering.\n\n---\n\n## Documentation Directory 📂\n\n- [WorkerCanvas](./documentation/WorkerCanvas.ts.md)\n- [ProxyListener](./documentation/ProxyListener.ts.md)\n- [canvas.worker](./documentation/canvas.worker.ts.md)\n\nEach of these documents provides detailed information about the individual files, including their purposes, interfaces, types, and functions.\n\n## Getting Started 🌟\n\nDive into the [docs](#documentation-directory-) to understand what each module can do for you. Then, integrate them into your project, tailor their behavior through simple configurations, and watch your web app's performance take off! 🚀\n\nExplore the repository, and don't hesitate to contribute or reach out if you have any questions or suggestions. Together, we can push the boundaries of what's possible on the web!\n\n---\n\nWe hope you love using this library as much as we enjoyed creating it! Happy coding! 💻✨\n\n(Note to self: Remember to always code responsibly and keep the main thread clean! 🧹)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshbrew%2Fworker_canvas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshbrew%2Fworker_canvas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshbrew%2Fworker_canvas/lists"}