src/orb.js is an ES module, and a file:// document has an opaque
origin. Browsers fetch <script type="module"> with CORS, which an opaque origin can never
satisfy, so the import is refused before any of this page runs. Nothing in the library can change that.
Any static server from this folder will do:
python -m http.server 8000 # or: npx serve .WebGL1 · zero dependencies · no build step
A procedural galaxy sealed inside a glass sphere. The seed decides what it looks like, your agent's audio decides how it moves. No geometry anywhere in it: one four-vertex quad, and the sphere is solved analytically per fragment.
speak is audible synthesis, through the same analyser your agent's audio would use.
real premultiplied alpha
The orb composites with per-pixel alpha and transmits whatever sits behind it, so the same seed lands correctly on a white card and a black one. No halo, no grey box, no background baked into the texture. Flip the page and both cards below keep their own colour.
Rim lensing goes dark against the page, and the far wall of the sphere shows through.
Same seed, same frame. Only the ground behind the glass changed.
One seed on seven grounds. background: 'auto' walks
up to 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. Same string, same orb, on any device or session. Small orbs share an instanced atlas, so this whole roster is one draw call.
install
Two ways in. Point at the CDN and you are done, or download the file and serve it yourself. Both give you the same thing: one ES module, no dependencies, no build step.
Served by jsDelivr straight from the repo, pinned to a release tag, with open CORS. Not my bandwidth, and it does not break if this site goes away.
Pin the tag. @main would ship your users whatever I pushed last.
Grab the file, drop it next to your page, serve it over http(s). That is the whole install. Keep the licence header and you are square.
<script type="module" src="https://cdn.jsdelivr.net/gh/earlbalai/orb.js@v1.0.0/src/element.js"></script>
<orb-js seed="agent-42" size="320" state="speaking"></orb-js>
<script type="module">
const el = document.querySelector('orb-js');
el.listenTo(agentStream); // MediaStream, <audio>, AnalyserNode, 'microphone'
el.setAttribute('state', 'listening');
</script>
import { createOrb } from 'https://cdn.jsdelivr.net/gh/earlbalai/orb.js@v1.0.0/src/orb.js';
const orb = createOrb(document.getElementById('orb'), {
seed: 'agent-42', // hue, archetype, structure, spin all derive from this
size: 320, // css px, square
state: 'idle', // idle | listening | thinking | speaking
background: 'auto', // transmits whatever is behind it
});
// anything your voice stack hands you
const stop = orb.listenTo(agentStream);
orb.state = 'speaking'; // crossfades over ~350ms
stop(); // detach the audio
orb.destroy(); // and everything else
// bundlers cannot resolve a url import, so load it at runtime
import { useEffect, useRef } from 'react';
const SRC = 'https://cdn.jsdelivr.net/gh/earlbalai/orb.js@v1.0.0/src/orb.js';
export function Orb({ seed, size = 320, state = 'idle', audio }) {
const host = useRef(null);
const orb = useRef(null);
useEffect(() => {
let dead = false;
import(SRC).then(({ Orb }) => {
if (!dead) orb.current = Orb.mount(host.current, { seed, size, state });
});
return () => { dead = true; orb.current?.destroy(); orb.current = null; };
}, []);
useEffect(() => { orb.current?.update({ seed, size }); }, [seed, size]);
useEffect(() => { orb.current?.setState(state); }, [state]);
useEffect(() => audio ? orb.current?.listenTo(audio) : undefined, [audio]);
return <div ref={host} />;
}
<script src="https://cdn.jsdelivr.net/gh/earlbalai/orb.js@v1.0.0/src/orb.global.js"></script>
<orb-js seed="agent-42" size="320"></orb-js>
<!-- or build it by hand off the global -->
<script>
var orb = Orb.createOrb(document.getElementById('orb'), {
seed: 'agent-42', size: 320
});
</script>
the technique
No mesh at all. z = sqrt(1 - r²) over a four-vertex quad gives the near hemisphere
exactly, and the normal comes with it, which is everything lighting, fresnel and refraction need.
The view ray refracts into the glass, and on a unit sphere the chord to the far wall is exactly
-2·dot(N, R). So the galaxy gets sampled a second time there for the price of a dot
product. Without that back layer it reads as a disc.
Near the silhouette the shading function runs three times over, once per channel, at displaced
coordinates. The falloff is hand-expanded from (e²ˣ−1)/(e²ˣ+1) since GLSL ES 1.00 has
no tanh.
Premultiplied output plus a page-colour uniform, so the sphere transmits its ground instead of
carrying one. CSS cuts the silhouette with a half-pixel radial mask. The shader renders past
r = 1 on purpose, so the lens always has real pixels to bend.
RMS feeds 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 it lurch and a sentence makes it swim.
The four states are one smoothed scalar, ordered the way a conversation actually runs. Ordinary transitions are between neighbours, so the orb never passes through a third look on the way.