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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@spearwolf/offscreen-display

v0.4.0

Published

helpers for creating custom offscreen canvas elements

Readme

@spearwolf/offscreen-display

npm (scoped) License

A minimal javascript library that makes it pretty easy for you to create a custom element that hands its canvas over to a web worker as an OffscreenCanvas and renders it in the animation frame loop of the browser.

Whether webgl, webgpu or 2d context is up to the user, the class takes care of the synchronization of the element dimension and rendering of the frames.

📖 How To

1. Install

➜ npm i @spearwolf/offscreen-display @spearwolf/eventize

The worker adds its listeners with on() from @spearwolf/eventize, that is why it is installed as well.

To get started quickly, vite is recommended, but this is purely optional and a matter of taste.

just updated? there is also a CHANGELOG

2. Create custom element

awesome-display.js

import {OffscreenDisplay} from '@spearwolf/offscreen-display';

class AwesomeDisplay extends OffscreenDisplay {
  createWorker() {
    return new Worker(new URL('awesome-display-worker.js', import.meta.url), {
      type: 'module',
    });
  }
}

customElements.define('awesome-display', AwesomeDisplay);

3. Integrate the element in your page

index.html

<html>
  <body>
    <awesome-display></awesome-display>
    <script type="module" src="awesome-display.js"></script>
  </body>
</html>

4. Create your worker

awesome-display-worker.js

import {on} from '@spearwolf/eventize';
import {OffscreenWorkerDisplay} from '@spearwolf/offscreen-display/worker.js';

const display = new OffscreenWorkerDisplay();

let ctx = null;

on(display, {
  onCanvas({canvas}, contextAttributes) {
    ctx = canvas.getContext('2d', contextAttributes);
  },

  onFrame({now, canvasWidth: w, canvasHeight: h, pixelRatio}) {
    // canvasWidth and canvasHeight are physical pixels, pixelRatio converts css pixels into them
    ctx.clearRect(0, 0, w, h);
  },
});

self.addEventListener('message', (evt) => {
  display.parseMessageData(evt.data);
});

5. More features available

There are other features not listed here. For a complete example, see the rainbow line element.

API

OffscreenDisplay — the element, main thread

  • constructor(initialHTML) — the initial HTML of the shadow root, OffscreenDisplay.InitialHTML by default. It must contain a canvas that gets its displayed size from CSS (the default styles stretch it over the element): the pixel size of the canvas follows its displayed size in physical pixels.
  • Hooks to override:
    • createWorker() — required, returns the Worker that renders the canvas
    • getContextAttributes() — the attributes for getContext() in the worker; {alpha: true} by default, {alpha: false} with the attribute no-alpha
    • getInitialWorkerAttributes() — properties that go into the first message to the worker; none by default
    • queryCanvasElement() — finds the canvas in the shadow root, needed with a different initial HTML
  • asNumberValue(name, defaultValue) — the value of an attribute as a number, the default if the attribute is missing or not a number
  • Properties: worker (the rendering Worker, undefined while there is none) and canvas (the canvas whose control went to the worker)
  • Lifecycle: connecting the element starts the worker. Disconnecting it sends {isConnected: false} to the worker, one animation frame later the element calls dispose() and the worker is terminated; moving the element within the same task keeps the worker. Connecting the element again after that starts a fresh worker with a fresh canvas.
  • Pausing: the worker requests no animation frames while its canvas is 0×0 — display: none on the element or an ancestor, a collapsed container, a route that a framework keeps hidden in the DOM — or more than 200px outside the viewport of its document, which an IntersectionObserver on the canvas reports as {isVisible}. The worker and its canvas stay; the frames start again with the next size above 0×0 and with {isVisible: true}. A hidden element keeps its worker on purpose: a canvas can be handed to a worker only once, so showing it again would need a fresh canvas and worker and start with a blank frame — just what a framework that hides routes to make going back instant does not want.
  • dispose() — terminates the worker and stops observing the size of the canvas. Public and idempotent.

OffscreenWorkerDisplay — the display, worker

  • parseMessageData(data) — hand it every message from the main thread: self.addEventListener('message', (evt) => display.parseMessageData(evt.data))

  • Events:

    • onCanvas(display, contextAttributes) — the canvas has arrived
    • onInit(display) — the canvas is there and the element is connected
    • onResize(display) — the size or the pixel ratio has changed
    • onFrame(display) — once per animation frame, only while the element is connected and its canvas in or near the viewport, from the first size on and while the canvas is larger than 0×0

    onCanvas, onInit and onResize are retained: a listener added later still receives the last one. A throwing listener does not end the frame loop. Its error arrives as an error event at the Worker; the same error in the following frames arrives only once, until a frame runs without an error.

  • Properties: canvas (the OffscreenCanvas), canvasWidth and canvasHeight (physical pixels), pixelRatio (physical pixels per css pixel), now (the time of the current frame in seconds), isConnected, ready (canvas there and connected)

  • destroy() — ends the frame loop and releases all listeners, signals and effects; the display ignores every message afterwards

Messages from the main thread

  • {canvas, contextAttributes, ...getInitialWorkerAttributes()} — once, when the worker is created
  • {isConnected} — when the element is connected or disconnected
  • {isVisible} — whether the canvas is within 200px of the viewport of its document; frames pause while it is not. Until the first {isVisible} the worker takes the canvas as visible, so a main thread of your own that never sends it keeps the frames running.
  • {resize: {width, height, pixelRatio}} — the size of the canvas in physical pixels and the devicePixelRatio

TypeScript

The package ships type declarations for both entry points. The listeners of the worker are typed with EventListenerMethods from @spearwolf/eventize and the event map OffscreenWorkerDisplayEvents:

import {type EventListenerMethods, on} from '@spearwolf/eventize';
import {OffscreenWorkerDisplay, type OffscreenWorkerDisplayEvents} from '@spearwolf/offscreen-display/worker.js';

const display = new OffscreenWorkerDisplay();

const listeners: EventListenerMethods<OffscreenWorkerDisplayEvents> = {
  onFrame({now, canvasWidth, canvasHeight, pixelRatio}) {
    // all of them are numbers
  },
};

on(display, listeners);

The worker must import on() from the same copy of @spearwolf/eventize (major version 6) that @spearwolf/offscreen-display uses.

Copyright and License

Copyright © 2024 by Wolfger Schramm.

The source code and npm package is licensed under the Apache-2.0 License.

have fun! 🚀🌱