1.1.4 โ€ข Published 2 months ago

workercanvas v1.1.4

Weekly downloads
-
License
LGPL-3.0-or-later
Repository
-
Last release
2 months ago

Web Worker Canvas Library ๐Ÿš€

d-status d-downloads

Installation ๐Ÿ“ฆ

To include this library in your project, simply use:

npm install workercanvas
# or
yarn add workercanvas

Run Example

Demo Link

cd example

# if tinybuild is not installed
npm i -g tinybuild

npm i

npm start

Elevate your applications with seamless off-screen rendering!

This 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 ๐Ÿ–ผ๏ธ๐Ÿ’ก.

This is a pure implementation with type support, and adds about 32kb of total weight to your program when using the bundle.

Usage ๐Ÿ› ๏ธ

Import the necessary components from the library and let the magic happen:

index.js

import { Renderer, WorkerCanvas } from '../WorkerCanvas';

const canvas = document.createElement('canvas'); 
canvas.width = 800; canvas.height = 600;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.id = 'myCanvas';
document.body.appendChild(canvas);

const workerRenderer = Renderer({
  canvas: canvas,
  context:'2d',
  worker: true, //use our prebundled worker
  _id:canvas.id,
  init:(self:WorkerCanvas,canvas,context)=>{ //init called automatically before first draw on thread
      console.log('canvas', canvas)
      canvas.addEventListener('mousedown',(ev)=>{ //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
          console.log('clicked!', ev, canvas);
      })
  },
  draw:(self:WorkerCanvas,canvas:any,context:CanvasRenderingContext2D)=>{ //render loop starts automatically on thread after receiving canvas and instructions
      context.clearRect(0,0,canvas.width, canvas.height);
      
      context.fillStyle = `rgb(0,${Math.sin(Date.now()*0.001)*255},${Math.cos(Date.now()*0.001)*255})`;
      context.fillRect(0,0,canvas.width,canvas.height);
  }                        
});

1

Start creating fluid animations and graphics right away!

There 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.

Types

This speeds up learning the library in your editor (e.g. VSCode)

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
}

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
}

type CanvasControls = {
    _id:string,
    draw:(props?:any,transfer?:any)=>void,
    update:(props:{[key:string]:any},transfer?:any)=>void,
    clear:()=>void,
    init:()=>void,
    stop:()=>void,
    start:()=>void,
    set:(newDrawProps:CanvasProps,transfer?:any)=>void
}

type WorkerCanvasControls = {
    worker:Worker|MessagePort,
    terminate:()=>void
} & CanvasControls

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),
    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
}

Sample worker that reroutes for ThreeJS initialization

2

three.worker.ts

import { workerCanvasRoutes, CanvasProps } from '../WorkerCanvas';

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { PickHelper } from './PickHelper'
//etc...
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
//minimal web worker for running offscreen canvases, 

declare var WorkerGlobalScope;

if(typeof WorkerGlobalScope !== 'undefined') {

    const routes = {
        ...workerCanvasRoutes,
        receiveThreeCanvas:function(options:CanvasProps){ //modified canvas receiver that installs desired threejs modules
                const ThreeProps = { //e.g. install these systems to 'self', which is the worker canvas
                    THREE,
                    OrbitControls,
                    EffectComposer,
                    RenderPass,
                    SMAAPass,
                    UnrealBloomPass,
                    PickHelper
                }

                Object.assign(options, ThreeProps); //install desired props to our canvas's 'self' reference

                console.log(this);
                let renderId = routes.setupCanvas(options); //the the base canvas tools do the rest, all ThreeJS tools are on self, for self contained ThreeJS renders
                //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

                //let canvasopts = this.graph.CANVASES[renderId] as WorkerCanvas;

                return renderId;
            }
        //add more compatible routes that don't require graphscript
    };
    
    self.onmessage = (ev) => {
        if(ev.data.route) {
            if(Array.isArray(ev.data.args)) {
                routes[ev.data.route](...ev.data.args);
            } else routes[ev.data.route](ev.data.args);
        } //that's it! The functions handle worker communication internally
    
    }
    
}

export default self as any;

Then from main thread we'd call

index.js

import { Renderer, WorkerCanvas } from '../WorkerCanvas';
import threewrkr from './three.worker' //or use a bundled path (not ts)

const canvas2 = document.createElement('canvas'); 
canvas2.width = 800; canvas2.height = 600;
canvas2.id = 'myCanvas2';
document.body.appendChild(canvas2);
const myOffscreenCanvas = Renderer(
    {
        canvas: canvas2,
        worker: threewrkr, //use our prebundled worker
        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
        _id:canvas2.id,
        context:undefined, //Threejs sets the context
        init:(self:WorkerCanvas,canvas,context)=>{

            //these are installed to the 'self' reference
            const THREE = self.THREE;
            const OrbitControls = self.OrbitControls;
            const PickHelper = self.PickHelper;
            
            const renderer = new THREE.WebGLRenderer({canvas});
            let time = 0;
            let lastFrame = Date.now();

            const fov = 75;
            const aspect = 2;
            const near = 0.1;
            const far = 100;
            
            const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
            camera.position.z = 4;

            renderer.setSize(canvas.width, canvas.height, false);
                if(camera) {
                    camera.aspect = canvas.clientWidth / canvas.clientHeight;
                    camera.updateProjectionMatrix();
                }

            const controls = new OrbitControls(camera, canvas);
            controls.target.set(0,0,0);
            controls.update();

            const scene = new THREE.Scene();

            {
                const color = 0xFFFFFF;
                const intensity = 1;
                const light = new THREE.DirectionalLight(color, intensity);
                light.position.set(-1, 2, 4);
                scene.add(light);
            }
        
            const boxWidth = 1;
            const boxHeight = 1;
            const boxDepth = 1;
            const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
        
            const makeInstance = (geometry, color, x) => {
                const material = new THREE.MeshPhongMaterial({
                    color,
                });
            
                const cube = new THREE.Mesh(geometry, material);
                scene.add(cube);
            
                cube.position.x = x;
            
                return cube;
            }
        
            const cubes = [
                makeInstance(geometry, 0x44aa88, 0),
                makeInstance(geometry, 0x8844aa, -2),
                makeInstance(geometry, 0xaa8844, 2),
            ];

            let getCanvasRelativePosition = (event) => {
                const rect = canvas.getBoundingClientRect();
                return {
                    x: event.clientX - rect.left,
                    y: event.clientY - rect.top,
                };
            }
        
            const pickPosition = {x: -2, y: -2};
            const pickHelper = new PickHelper();

            let setPickPosition = (event) => {
                const pos = getCanvasRelativePosition(event);
                pickPosition.x = (pos.x / canvas.clientWidth ) *  2 - 1;
                pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1;  // note we flip Y
            }
        
            let clearPickPosition = () => {
                // unlike the mouse which always has a position
                // if the user stops touching the screen we want
                // to stop picking. For now we just pick a value
                // unlikely to pick something
                pickPosition.x = -100000;
                pickPosition.y = -100000;
            }
            
            canvas.addEventListener('mousemove', setPickPosition);
            canvas.addEventListener('mouseout', clearPickPosition);
            canvas.addEventListener('mouseleave', clearPickPosition);
        
            canvas.addEventListener('touchstart', (event) => {
                // prevent the window from scrolling
                event.preventDefault();
                setPickPosition(event.touches[0]);
            }, {passive: false});
        
            canvas.addEventListener('touchmove', (event) => {
                setPickPosition(event.touches[0]);
            });
        
            canvas.addEventListener('touchend', clearPickPosition);

            canvas.addEventListener('resize', (ev) => {
                renderer.setSize(canvas.width, canvas.height, false);
                if(camera) {
                    camera.aspect = canvas.clientWidth / canvas.clientHeight;
                    camera.updateProjectionMatrix();
                }
            });

            Object.assign(self, {
                renderer,
                camera,
                controls,
                scene,
                cubes,
                time,
                lastFrame,
                pickPosition,
                pickHelper
            }); //assign these to self for the draw function

            clearPickPosition();
            //this.renderer.setAnimationLoop(this.draw);


        },
        draw:(self:WorkerCanvas,canvas:any,context:any)=>{
            let now = Date.now();
            self.time += (now - self.lastFrame) * 0.001;
            self.lastFrame = now;

            self.cubes.forEach((cube, ndx) => {
                const speed = 1 + ndx * .1;
                const rot = self.time * speed;
                cube.rotation.x = rot;
                cube.rotation.y = rot;
                });
            
                
                self.pickHelper.pick(self.pickPosition, self.scene, self.camera, self.time);
                //console.log(this.pickPosition);
                self.renderer.render(self.scene, self.camera);
        },
        clear:(self:WorkerCanvas, canvas, context) => {
            if(self.renderer) {
                self.render.domElement = null;
                self.renderer = null;
                self.composer = null;
                self.gui = null;
                self.controls = null;
                self.camera = null;
                self.scene = null;
            }
        }
    }                
);

Use Cases ๐ŸŽจ

  • Games: Build immersive, high-performance game engines that run in the browser.
  • Data Visualizations: Handle large datasets and real-time data rendering without a hitch.
  • Interactive UIs: Keep your user interfaces snappy even with complex background processes.
  • Educational Tools: Create dynamic simulations and educational content that require heavy rendering.

Documentation Directory ๐Ÿ“‚

Each of these documents provides detailed information about the individual files, including their purposes, interfaces, types, and functions.

Getting Started ๐ŸŒŸ

Dive into the docs 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! ๐Ÿš€

Explore 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!


We hope you love using this library as much as we enjoyed creating it! Happy coding! ๐Ÿ’ปโœจ

(Note to self: Remember to always code responsibly and keep the main thread clean! ๐Ÿงน)

1.1.4

2 months ago

1.1.3

2 months ago

1.1.1

2 months ago

1.1.2

2 months ago

1.1.0

5 months ago

1.0.9

5 months ago

1.0.8

5 months ago

1.0.11

5 months ago

1.0.10

5 months ago

1.0.15

5 months ago

1.0.14

5 months ago

1.0.13

5 months ago

1.0.12

5 months ago

1.0.7

5 months ago

1.0.6

5 months ago

1.0.5

5 months ago

1.0.4

5 months ago

1.0.3

5 months ago

1.0.2

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago