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

@lumaform/orb

v0.3.1

Published

WebGL runtime for ambient, reactive AI-assistant orbs: 45 shader engines, a modulation rack, and a versioned config format.

Readme

@lumaform/orb

The runtime behind Lumaform Orb: forty-five WebGL shader engines for ambient, reactive assistant orbs, a modulation rack that drives their parameters, and a versioned config format that round-trips a look between the studio and your app.

npm install @lumaform/orb three

Design a look in the studio: orb.lumaform.xyz. Every engine is there with live controls, a variation grid and an Export tab that writes the config this package loads.

New here? The guide goes from a look designed in the studio to an orb in your app, with audio.

Requires three as a peer, a browser with WebGL2, and Node 20+ to build. Pre-1.0, so a minor release may break the API; see Status.

Quick start

import { createOrb } from '@lumaform/orb';
import { nebula } from '@lumaform/orb/engines';

const orb = createOrb(document.querySelector('#orb'), {
  engines: { nebula },
  config,          // JSON exported from the studio's Export tab
});

createOrb constructs the runtime, registers the engines you handed it, reads the config and starts its own frame loop. That is the whole happy path.

You import the engines you want

createOrb never reaches for the catalog's factories. Catalog entries name theirs as a string (factoryName) rather than binding the function, so reading a parameter schema does not drag in all forty-five engines and their geometry.

The import list is the bundle. Name one engine, ship one engine. This is a structural property, not a bundler setting — sideEffects: false cannot drop a module whose export is named in a live binding, which is why the binding is not there.

API

createOrb(container, options) → Orb

| Option | Default | | |---|---|---| | engines | {} | Engine id → factory. Import from @lumaform/orb/engines. | | config | null | A parsed config object. Decides which engine mounts. | | template | null | { engine, config }, in place of engines and config. See States. | | state | null | Name of the state to start in; an unknown name falls back to the config's initialState. | | engine | null | Engine id, when there is no config. Falls back to the sole registered engine. | | params, global | null | Starting values, merged over schema defaults. | | autoStart | true | Start the loop immediately. | | controls | false | OrbitControls. An ambient orb rarely wants drag-to-rotate. | | autoRotate | false | Camera auto-rotation. Motion belongs to the engine. | | preserveDrawingBuffer | false | Only needed to read pixels back with toDataURL. | | pixelRatio | device, max 2 | Render density. A config file never sets it. |

The returned orb exposes start(), stop(), isRunning, setEngine(), setParams(), loadConfig(), setState(), state, states, setAudioSource(), dispose(), and dropped — the config keys the engine's schema does not define, which is usually a version mismatch worth surfacing. setState(name) eases to a named state from the config and returns false for an unknown name; state is the current name and states lists the names. See States.

Defaults are the embed's, not the studio's. The canvas is sized from the container with a ResizeObserver, not from the window; nothing rotates unless you ask; dispose() removes the canvas it added and leaves your other children alone. Opt in when you want more:

createOrb(el, { engines: { nebula }, controls: true, autoRotate: true });

OrbRuntime

The escape hatch, for a host that wants to own its frame loop. It has none: call advance(delta) and render(delta), or tick(delta) for both, and interleave whatever you like between them. The studio does exactly this — it advances its rehearsal and tween state, calls advance(), then either renders its variation grid or calls render().

mountEngine(type, { params, global, modulation }) takes one engine's parameters, not a store keyed by every engine type.

Config

parseConfigFile(text, knownEngines) validates and migrates; readConfig(config, defs) returns a playback record and mutates nothing. global and modulation come back null when the file omits them — a config written before modulation existed has no rack at all, and that must not be confused with an empty one, which would silently wipe a live rack.

Catalog

ENGINE_CATALOG, ENGINE_PARAM_DEFINITIONS, getDefaultEngineParams(id) and friends. Metadata and schemas only — no factories.

Subpaths

| | | |---|---| | @lumaform/orb | createOrb, OrbRuntime, catalog metadata, config I/O | | @lumaform/orb/engines | One named export per engine, keyed by id | | @lumaform/orb/audio | Microphone capture and level following | | @lumaform/orb/internal | Building blocks the studio shares. Not covered by semver |

Audio is a subpath, not a flag

A { audio: true } option could not be tree-shaken — a bundler cannot prove the value, so getUserMedia would ship to everyone and dependency scanners would flag the call rather than its use. Consent also belongs to you, not to a library reading a config field.

Not importing @lumaform/orb/audio is the off switch: zero bytes, no permission surface, nothing for a security review to find.

It also solves the wrong half. For an assistant orb the interesting signal is usually the assistant's own speech. setAudioSource() accepts any object with read() → 0..1 and isActive. An <audio> element or a Web Audio node isn't one by itself, but becomes one in a few lines with createLevelFollower and rmsFromTimeDomain from this subpath. The guide has the recipe:

orb.setAudioSource({ isActive, read }); // read() → loudness 0..1, once per frame

Status

What each version contains is in CHANGELOG.md. OrbRuntime stays on the root export as the escape hatch for hosts that drive their own loop; createOrb is the path for everyone else. Before 1.0 a minor release may break the API, and anything under /internal may change in any release.

Known limitation: a handful of engines still pass an unbounded time to a float32 uniform, which quantises motion over multi-day sessions. The periodic terms are fixed by phase accumulation; the remaining cases are aperiodic noise domains that need tileable noise. See src/core/phase.js.

License

MIT © Ahmet Bektes