npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

framebuffer-worker

v1.1.0-beta.1

Published

Draw on a Canvas from a Web Worker

Downloads

7

Readme

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.

demo npm

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";

const canvas = document.getElementById("canvas");
const layer = await init(canvas);

layer().then(async ({ clear, render, line }) => {
  await clear();
  await line({
    startPoint: new Point(0, 0),
    endPoint: new Point(canvas.width, canvas.height),
    style: new Style(undefined, new Color(127, 127, 127), 1),
  });
  await render();
});

layer().then(async ({ clear, render, line }) => {
  const cb = async (event) => {
    const x = event.offsetX;
    const y = event.offsetY;

    await clear();

    await Promise.all([
      line({
        startPoint: new Point(x, 0),
        endPoint: new Point(x, canvas.height),
        style: new Style(undefined, new Color(65, 105, 225), 1),
      }),
      line({
        startPoint: new Point(0, y),
        endPoint: new Point(canvas.width, y),
        style: new Style(undefined, new Color(65, 105, 225), 1),
      }),
    ]);

    await render();
  };

  canvas.addEventListener("pointermove", asyncThrottle(cb, 16));
});

You can play with it on StackBlitz. Open the preview in a new tab because the vite config changes the headers. See bellow.

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.

const canvas = document.getElementById("canvas");
const layer = await init(canvas);

layer().then(async ({ clear, render, line, circle, rectangle }) => {
  // -- snip --
});

// OR

const { clear, render, line, circle, rectangle } = await layer();

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

await line({
  startPoint: new Point(0, 0),
  endPoint: new Point(canvas.width, canvas.height),
  // no fillColor for the line
  style: new Style(undefined, new Color(255, 105, 180), 1),
});

Circle

await circle({
  topLeftPoint: new Point(10, 20),
  diameter: 20,
  style: new Style(new Color(176, 230, 156), new Color(255, 105, 180), 2),
});

Rectangle

await rectangle({
  topLeftPoint: new Point(50, 100),
  size: new Size(100, 40),
  style: new Style(new Color(176, 230, 156), new Color(255, 105, 180), 1),
  radius: 3, //optional
});

Rounded Rectangle

await rounded_rectangle({
  topLeftPoint: new Point(50, 100),
  size: new Size(300, 40),
  style: new Style(new Color(255, 255, 255), new Color(255, 10, 18), 1),
  corners: new Corners(new Size(3, 6), new Size(9, 12), new Size(10, 10), new Size(4, 4)),
});

Ellipse

await ellipse({
  topLeftPoint: new Point(10, 20),
  size: new Size(300, 40),
  style: new Style(new Color(176, 230, 156), new Color(255, 105, 180), 2),
});

Arc

await arc({
  topLeftPoint: new Point(100, 240),
  diameter: 130,
  angleStart: new Angle(0),
  angleSweep: new Angle(72),
  // no fillColor for the polyline
  style: new Style(undefined, new Color(127, 127, 127), 5),
});

Sector

await sector({
  topLeftPoint: new Point(80, 260),
  diameter: 130,
  angleStart: new Angle(35),
  angleSweep: new Angle(300),
  style: new Style(new Color(253, 216, 53)),
});

Triangle

await triangle({
  vertex1: new Point(10, 64),
  vertex2: new Point(50, 64),
  vertex3: new Point(60, 44),
  style: new Style(new Color(48, 120, 214)),
});

Polyline

await polyline({
  points: [
    new Point(10, 64),
    new Point(50, 64),
    new Point(60, 44),
    new Point(70, 64),
    new Point(80, 64),
    new Point(90, 74),
    new Point(100, 10),
    new Point(110, 84),
    new Point(120, 64),
    new Point(300, 64),
  ],
  // no fillColor for the polyline
  style: new Style(undefined, new Color(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.

await text({
  position: new Point(20, 20),
  label: `Hello, world!`,
  size: 9,
  textColor: new Color(33, 33, 33),
  textStyle: new TextStyle(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.

const canvas = document.getElementById("canvas");
const layer = await init(canvas);

let otherLayerApi;

layer().then(async ({ clear, render, circle }) => {
  let cursor;
  let boundingBoxes = new Map();
  let hoverBounding;

  await clear();
  for (let i = 0; i < 900; i++) {
    let id = `circle-${i}`;
    const diameter = 10;
    const perLine = Math.floor(canvas.width / (diameter + 2)) - 1;
    await circle({
      topLeftPoint: new Point(
        (diameter + 2) * (i % perLine) + 5,
        5 + (diameter + 2) * Math.floor(i / perLine),
      ),
      diameter,
      style: new Style(new Color(176, 230, 156), new Color(255, 105, 180), 1),
    }).then((bounding) => {
      if (bounding) boundingBoxes.set(id, bounding);
    });
  }
  await render();

  canvas.addEventListener(
    "pointermove",
    asyncThrottle(async (event) => {
      hoverBounding = undefined;
      cursor = new Point(event.offsetX, event.offsetY);

      for (const bounding of boundingBoxes.values()) {
        if (bounding.intersect(cursor)) {
          hoverBounding = bounding.as_js();
        }
      }

      await otherLayerApi?.clear();

      if (hoverBounding) {
        await otherLayerApi?.rectangle({
          topLeftPoint: new Point(hoverBounding.top_left.x, hoverBounding.top_left.y),
          size: new Size(hoverBounding.size.width, hoverBounding.size.height),
          style: new Style(undefined, new Color(100, 180, 255), 2),
        });
      }

      await otherLayerApi?.render();
    }, 16),
  );
});

layer().then(async (api) => {
  otherLayerApi = api;
});

Server configuration

SharedArrayBuffer support

You need to set two HTTP Headers:

| Header | Value | | ---------------------------- | ------------ | | Cross-Origin-Opener-Policy | same-origin | | Cross-Origin-Embedder-Policy | require-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";

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