@ubox-lib/ubox-xcreen
v0.1.2
Published
Virtual screen layout and synchronized multi-screen canvas animations for Ubox installations
Maintainers
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+Xor callxcreen.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) |
