src/orb.js is a real ES module, and a file:// document has an
opaque origin. Every current engine fetches <script type="module"> with CORS, which an
opaque origin can never satisfy — so the import is refused before any of this page runs. That is a browser
rule; there is no build step involved and no flag inside the library that changes it.
Run any static server from this folder and reload:
python -m http.server 8000 # or: npx serve .WebGL1 · zero dependencies · no build step
XyOrb is a procedural galaxy sealed inside a glass sphere — deterministic from a seed, driven by real audio, and honest about what your agent is doing right now. There is no geometry in it: one four-vertex quad, and the sphere is solved analytically per fragment.
speak is audible synthesis, routed through the same analyser your agent's audio would be.
real premultiplied alpha
The orb composites with true per-pixel alpha and transmits whatever is behind it, so the same seed sits correctly on a white card and a black one — no halo, no grey box, no baked-in background. These two cards keep their own colour whichever way you flip the page.
Rim lensing goes dark against the page; the far wall of the sphere shows through.
Same seed, same frame. Only the ground behind the glass changed.
One seed across seven grounds. background: 'auto'
reads the nearest opaque ancestor, so an orb dropped into your UI transmits your UI.
deterministic identity
FNV-1a over the seed string picks hue, archetype, structural phase, meteor cadence and starting spin. The same string is the same orb forever — across sessions, devices and users. Small orbs ride an instanced atlas, so this whole roster is one draw call.
one script tag
Hosted by XyLabs. Nothing to install, nothing to build, nothing to self-host — point at the url and you are done. It is served with open CORS, so it works from any origin.
<!-- the whole integration -->
<script type="module" src="https://xyorb.vidome.app/v1/element.js"></script>
<xy-orb seed="agent-42" size="320" state="speaking"></xy-orb>
<!-- drive it from your voice stack -->
<script type="module">
const el = document.querySelector('xy-orb');
el.listenTo(agentStream); // MediaStream, <audio>, AnalyserNode, 'microphone'
el.setAttribute('state', 'listening');
</script>
<div id="orb"></div>
<script type="module">
import { createXyOrb } from 'https://xyorb.vidome.app/v1/orb.js';
const orb = createXyOrb(document.getElementById('orb'), {
seed: 'agent-42', // hue, archetype, structure, spin — all of it
size: 320, // css px, square
state: 'idle', // idle | listening | thinking | speaking
background: 'auto', // transmits whatever is behind it
});
// Anything your voice stack hands you: MediaStream, MediaStreamTrack,
// <audio>, AnalyserNode, AudioNode, 'microphone', or (t) => 0..1
const stop = orb.listenTo(agentStream);
orb.state = 'speaking'; // crossfades over ~350ms
stop(); // detach the audio
orb.destroy(); // and everything else
</script>
// A wrapper ships at /v1/react.js. Bundlers cannot resolve a url import,
// so either vendor orb.js into your app or load it natively as below.
import { useEffect, useRef } from 'react';
export function Orb({ seed, size = 320, state = 'idle', audio }) {
const host = useRef(null);
const orb = useRef(null);
useEffect(() => {
let dead = false;
import('https://xyorb.vidome.app/v1/orb.js').then(({ XyOrb }) => {
if (dead) return;
orb.current = XyOrb.mount(host.current, { seed, size, state });
});
return () => { dead = true; orb.current?.destroy(); orb.current = null; };
}, []); // mount once
useEffect(() => { orb.current?.update({ seed, size }); }, [seed, size]);
useEffect(() => { orb.current?.setState(state); }, [state]);
// listenTo returns its own disposer — exactly what an effect wants back
useEffect(() => audio ? orb.current?.listenTo(audio) : undefined, [audio]);
return <div ref={host} />;
}
<!-- No modules. Works in Tag Manager, Webflow, Squarespace, Wordpress. -->
<script src="https://xyorb.vidome.app/v1/xyorb.global.js"></script>
<xy-orb seed="agent-42" size="320"></xy-orb>
<!-- or build it by hand from the global -->
<script>
var orb = XyOrb.createXyOrb(document.getElementById('orb'), {
seed: 'agent-42', size: 320
});
</script>
the technique
Not one triangle of mesh. z = sqrt(1 - r²) over a four-vertex quad gives the near
hemisphere exactly, and its normal for free — lighting, fresnel and refraction all fall out of it.
The view ray refracts into the glass, and for a unit sphere the chord to the far wall is exactly
-2·dot(N, R). The galaxy is sampled a second time there — one dot product, not a ray
march. That back layer is what sells a ball instead of a disc.
Near the silhouette the whole shading function is re-evaluated three times at per-channel displaced
coordinates, with an erf falloff hand-expanded from (e²ˣ−1)/(e²ˣ+1) because GLSL ES 1.00
has no tanh.
Premultiplied output and a page-colour uniform, so the sphere transmits its ground rather than
carrying one. The silhouette is cut in CSS with a half-pixel radial mask — the shader deliberately
renders past r = 1 so the lens always has real pixels to bend.
RMS runs into two envelopes: fast (40 ms attack / 180 ms release) drives motion, slow (110/300 ms) drives colour. The positive derivative of the fast one kicks a spin integrator, so a consonant makes the ball lurch and a sentence makes it swim.
The four states are one smoothed scalar, ordered as the conversational loop itself. Every ordinary transition is between adjacent states, so the orb never passes through a third look on its way somewhere.