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

@ubox-lib/ubox-xcreen

v0.1.2

Published

Virtual screen layout and synchronized multi-screen canvas animations for Ubox installations

Readme

ubox-xcreen

Virtual screen layout environment for synchronized multi-screen Ubox experiences.

Screens on separate machines share a common virtual coordinate space. Animations defined in that space play out across all screens simultaneously — a ball traveling from vx:0 to vx:3840 will appear to cross physically from one screen to another.


How It Works

Think of the virtual space as an invisible canvas that spans all your physical screens. Each screen occupies a rectangle within that canvas defined by its x, y, width, and height. An object at virtual coordinate (1920, 540) only renders on the screen whose rectangle contains that point.

Instead of broadcasting position updates every frame, each screen runs the same animation formula independently using a synchronized clock. This eliminates network jitter from the animation itself.

Virtual space: 3840 × 1080
┌──────────────────────┬──────────────────────┐
│  Screen A (left)     │  Screen B (right)    │
│  x:0  y:0            │  x:1920  y:0         │
│  1920 × 1080         │  1920 × 1080         │
└──────────────────────┴──────────────────────┘

Setup

Load after ubox-data-xpace:

<script src="https://unpkg.com/@ubox-lib/ubox-data-xpace/ubox-data-xpace.min.js"></script>
<script src="https://unpkg.com/@ubox-lib/ubox-xcreen/ubox-xcreen.min.js"></script>

Basic Usage

data.js

const client = connectUboxClient({
  appName: "display",
  expName: "my-experience",
  session: "1234",
});

const xcreen = connectUboxXcreen({
  client,
  canvas: document.getElementById("stage"),
});

On every load, a screen picker overlay appears. The operator selects a saved screen name to resume that identity, or types a new name and clicks Join. Saved names can be deleted with the ✕ button. This design lets two browser tabs on the same machine independently pick different screen identities (e.g. left-wall and right-wall).


Animations

Playing an animation (director screen)

class Ball extends XPObject {
  draw(ctx, x, y) {
    ctx.beginPath();
    ctx.arc(x, y, 40, 0, Math.PI * 2);
    ctx.fillStyle = "#fff";
    ctx.fill();
  }
}

// Start and broadcast the animation
xcreen.animate({
  id:       "ball_cross",
  from:     { vx: 0,    vy: 540 },
  to:       { vx: 3840, vy: 540 },
  duration: 4000,
  easing:   "easeInOut",
  object:   new Ball(),
});

Joining an animation (follower screens)

Follower screens listen for xp_anim_play and register their own XPObject with broadcast: false:

window.addEventListener("xp_anim_play", (e) => {
  xcreen.animate({
    ...e.detail,      // reuses sentAt for clock sync
    object: new Ball(),
  }, false);          // false = don't re-broadcast
});

Cancelling

xcreen.cancelAnimation("ball_cross");

Coordinate Conversion

// Virtual → local (returns null if outside this screen's bounds)
const local = xcreen.virtualToLocal(960, 540);
if (local) console.log(local.x, local.y);

// Local → virtual
const virt = xcreen.localToVirtual(100, 200);
console.log(virt.vx, virt.vy);

Updating Screen Layout

// Reposition this screen in the virtual space
xcreen.updateMyLayout({ x: 1920, y: 0 });

// Resize
xcreen.updateMyLayout({ width: 1920, height: 1080 });

Window Resize

ubox-xcreen automatically handles browser window and canvas resize events using a ResizeObserver. When the canvas changes size:

  • The canvas bitmap is rescaled to match the new CSS dimensions (DPR-aware, no blurry rendering).
  • This screen's layout entry (width/height) is broadcast to all peers.

No setup is required — resize is handled internally by the library.


Configuration Panel

An operator-facing minimap showing all screens in the virtual space.

  • Toggle: Ctrl+Shift+X or call xcreen.openConfigPanel()
  • Drag any screen rectangle to reposition it
  • Use the numeric inputs to set exact x/y/width/height for this screen
  • Changes broadcast to all peers in real time

Boundary Events

Fires when an animated object enters or exits a screen, enters a gap, or reaches the virtual edge:

xcreen.onBoundary((e) => {
  console.log(e.type);     // "screen-enter" | "screen-exit" | "gap-enter" | "gap-exit" | "edge"
  console.log(e.animId);   // animation id
  console.log(e.vx, e.vy); // virtual position at the crossing moment
  console.log(e.screenId); // only on "screen-enter" / "screen-exit"
});

XPObject API

Extend XPObject to create renderable animated objects.

class MySprite extends XPObject {
  constructor() {
    super();
    this.size = 80;
  }

  // Called every frame — virtual coordinates are already set before this
  update(vx, vy, progress) {
    super.update(vx, vy, progress);
    this.size = 80 + Math.sin(progress * Math.PI) * 20; // pulse
  }

  // Called every frame when the object is within this screen's bounds
  draw(ctx, x, y) {
    ctx.fillStyle = `hsl(${progress * 360}, 80%, 60%)`;
    ctx.fillRect(x - this.size / 2, y - this.size / 2, this.size, this.size);
  }
}

Clock Sync

ubox-xcreen uses a one-shot ping-pong exchange (NTP-style) to estimate the clock delta between machines. The correction is applied automatically when evaluating sentAt timestamps. Accuracy is ~2–10ms on a LAN, which is imperceptible in animations above 200ms.

The sync refreshes every 10 minutes to account for drift on long installations.


Screen Identity & Persistence

Screen names are stored in localStorage keyed to expName as an array:

ubox_xcreen__{expName}  →  [{ name: "left-wall", expiresAt: ... }, ...]
  • Expiry: 30 days per entry, refreshed on each successful connect.
  • Stale entries are pruned automatically on read.
  • On every boot: a screen picker overlay appears — click a saved name to resume it, or enter a new name to join fresh. Delete stale entries with ✕.
  • Two tabs, one machine: because the picker always appears, each tab can independently choose a different screen identity.

API Reference

connectUboxXcreen(options)

| Option | Type | Required | Description | |---|---|---|---| | client | UboxClientHandle | ✓ | From connectUboxClient() | | canvas | HTMLCanvasElement | ✓ | Canvas to render animations on | | debug | boolean | — | Log internal events to console |

Returned handle

| Property / Method | Description | |---|---| | screenId | This screen's name | | layout | Read-only map of all registered screens | | virtualSpace | { width, height } — bounding box of all screens | | virtualToLocal(vx, vy) | → { x, y } or null if out of bounds | | localToVirtual(x, y) | → { vx, vy } | | animate(descriptor, broadcast?) | Register and optionally broadcast an animation | | cancelAnimation(id) | Cancel a running animation on all screens | | updateMyLayout(patches) | Update this screen's position/size | | openConfigPanel() | Toggle the operator config panel | | onBoundary(handler) | Register a boundary event handler | | now() | Clock-corrected Date.now() |

Animation descriptor

| Field | Type | Required | Description | |---|---|---|---| | id | string | ✓ | Unique identifier — reusing replaces the animation | | from | { vx, vy } | ✓ | Start virtual coordinates | | to | { vx, vy } | ✓ | End virtual coordinates | | duration | number | ✓ | Duration in ms | | object | XPObject | ✓ | Renderer instance (one per screen) | | easing | string | — | linear (default), easeIn, easeOut, easeInOut | | loop | boolean | — | Loop continuously (default: false) |