Skip to content

Repository files navigation

Framebuffer Worker

Draw on a Canvas from a Web Worker.

This is definitely not optimized for real-time application, although possible. The main goal is to render a visualization of millions of data which usually take some seconds to render.

By doing it off-the-main-thread, in a Worker, it will never block the UI.

demonpm

How does it work?

As the OffscreenCanvas API is still experimental, we draw directly in a SharedArrayBuffer . The drawing is done via WebAssembly thanks to the embedded_graphics Rust crate, which is instantiated in a Web Worker. That's why everything is asynchronous.

Example

import{init,asyncThrottle,Style,Color,Point}from"framebuffer-worker";constcanvas=document.getElementById("canvas");constlayer=awaitinit(canvas);layer().then(async({ clear, render, line })=>{awaitclear();awaitline({startPoint: newPoint(0,0),endPoint: newPoint(canvas.width,canvas.height),style: newStyle(undefined,newColor(127,127,127),1),});awaitrender();});layer().then(async({ clear, render, line })=>{constcb=async(event)=>{constx=event.offsetX;consty=event.offsetY;awaitclear();awaitPromise.all([line({startPoint: newPoint(x,0),endPoint: newPoint(x,canvas.height),style: newStyle(undefined,newColor(65,105,225),1),}),line({startPoint: newPoint(0,y),endPoint: newPoint(canvas.width,y),style: newStyle(undefined,newColor(65,105,225),1),}),]);awaitrender();};canvas.addEventListener("pointermove",asyncThrottle(cb,16));});

Basics

Layers

Every time you create a new layer, it will instantiate a new Worker. Every layer has to be rendered individually, though. So the time that every layer will take to render, will never affect the other layers rendering speed. At every render the layers are merged together, in the order of creation at the moment, so that you do not have to sync between layers yourself.

Currently, the rendering is not optimized if you have multiple real-time layers, because every render call its own requestAnimationFrame and merge layers together. Opacity is not supported at the moment.

constcanvas=document.getElementById("canvas");constlayer=awaitinit(canvas);layer().then(async({ clear, render, line, circle, rectangle })=>{// -- snip --});// ORconst{ clear, render, line, circle, rectangle }=awaitlayer();

Clear

Calling await clear(); will simply fill the SharedArrayBuffer with OxO. It is way faster than "drawing" all pixels one by one with a specific color. Colors are defined as (red, green, blue, alpha). So here it will be a transparent black.

Render

Call await render(); every time you want the pixels to appear on the screen. It will merge all layers together, by the order of creation. Last layer on top.

Although at every drawings (clear, line, ...), the buffer is modified, we keep a copy of the previous one to draw it, until you call render.

Primitives

Line

awaitline({startPoint: newPoint(0,0),endPoint: newPoint(canvas.width,canvas.height),// no fillColor for the linestyle: newStyle(undefined,newColor(255,105,180),1),});

Circle

awaitcircle({topLeftPoint: newPoint(10,20),diameter: 20,style: newStyle(newColor(176,230,156),newColor(255,105,180),2),});

Rectangle

awaitrectangle({topLeftPoint: newPoint(50,100),size: newSize(100,40),style: newStyle(newColor(176,230,156),newColor(255,105,180),1),radius: 3,//optional});

Rounded Rectangle

awaitrounded_rectangle({topLeftPoint: newPoint(50,100),size: newSize(300,40),style: newStyle(newColor(255,255,255),newColor(255,10,18),1),corners: newCorners(newSize(3,6),newSize(9,12),newSize(10,10),newSize(4,4)),});

Ellipse

awaitellipse({topLeftPoint: newPoint(10,20),size: newSize(300,40),style: newStyle(newColor(176,230,156),newColor(255,105,180),2),});

Arc

awaitarc({topLeftPoint: newPoint(100,240),diameter: 130,angleStart: newAngle(0),angleSweep: newAngle(72),// no fillColor for the polylinestyle: newStyle(undefined,newColor(127,127,127),5),});

Sector

awaitsector({topLeftPoint: newPoint(80,260),diameter: 130,angleStart: newAngle(35),angleSweep: newAngle(300),style: newStyle(newColor(253,216,53)),});

Triangle

awaittriangle({vertex1: newPoint(10,64),vertex2: newPoint(50,64),vertex3: newPoint(60,44),style: newStyle(newColor(48,120,214)),});

Polyline

awaitpolyline({points: [newPoint(10,64),newPoint(50,64),newPoint(60,44),newPoint(70,64),newPoint(80,64),newPoint(90,74),newPoint(100,10),newPoint(110,84),newPoint(120,64),newPoint(300,64),],// no fillColor for the polylinestyle: newStyle(undefined,newColor(176,230,156),3),});

Text

Only a single monospaced font is available: ProFont. There is no italic nor bold version. But the bigger the font, the bolder.

Only few sizes are available: 7, 9, 10, 12, 14, 18, and 24 pixels. You can see the rendering on the GitHub page.

The textStyle argument is optional. The default alignment is left and the default baseline is alphabetic.

awaittext({position: newPoint(20,20),label: `Hello, world!`,size: 9,textColor: newColor(33,33,33),textStyle: newTextStyle(Alignment.Center,Baseline.Middle),// optional});

Interactivity

You can, since v1.1, add some interactivity. Each primitive returns a bounding box, a rectangle, which allow you to check the intersection with the pointer.

constcanvas=document.getElementById("canvas");constlayer=awaitinit(canvas);letotherLayerApi;layer().then(async({ clear, render, circle })=>{letcursor;letboundingBoxes=newMap();lethoverBounding;awaitclear();for(leti=0;i<900;i++){letid=`circle-${i}`;constdiameter=10;constperLine=Math.floor(canvas.width/(diameter+2))-1;awaitcircle({topLeftPoint: newPoint((diameter+2)*(i%perLine)+5,5+(diameter+2)*Math.floor(i/perLine),),
diameter,style: newStyle(newColor(176,230,156),newColor(255,105,180),1),}).then((bounding)=>{if(bounding)boundingBoxes.set(id,bounding);});}awaitrender();canvas.addEventListener("pointermove",asyncThrottle(async(event)=>{hoverBounding=undefined;cursor=newPoint(event.offsetX,event.offsetY);for(constboundingofboundingBoxes.values()){if(bounding.intersect(cursor)){hoverBounding=bounding.as_js();}}awaitotherLayerApi?.clear();if(hoverBounding){awaitotherLayerApi?.rectangle({topLeftPoint: newPoint(hoverBounding.top_left.x,hoverBounding.top_left.y),size: newSize(hoverBounding.size.width,hoverBounding.size.height),style: newStyle(undefined,newColor(100,180,255),2),});}awaitotherLayerApi?.render();},16),);});layer().then(async(api)=>{otherLayerApi=api;});

Server configuration

SharedArrayBuffer support

You need to set two HTTP Headers:

HeaderValue
Cross-Origin-Opener-Policysame-origin
Cross-Origin-Embedder-Policyrequire-corp

Vite

You need to exclude the framebuffer-worker module from the dependency pre-bundling as this module is an ES module and use import.meta.url internally to load the worker and wasm files.

You also need to set the mandatory headers to support SharedArrayBuffer.

import{defineConfig}from"vite";exportdefaultdefineConfig({optimizeDeps: {exclude: ["framebuffer-worker"],},server: {headers: {"Cross-Origin-Embedder-Policy": "require-corp","Cross-Origin-Opener-Policy": "same-origin",},},});

About

Draw on a Canvas from a Web Worker.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages