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 · native SDKs for iOS, Android & Windows
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>
native
The same orb for iOS, macOS, Android and Windows. Each SDK is idiomatic to its platform
and has zero third-party dependencies. Identity, the four-state dynamics and the galaxy shader are
ported line for line and pinned to this module by a shared conformance suite, so
agent-42 is the same orb in a browser, on a phone and in a desktop app.
Swift + Metal, iOS 15 and macOS 12. A SwiftUI Orb view and a UIKit/AppKit
OrbView on a transparent Metal layer. Audio from an AVAudioEngine tap,
any PCM callback, or the microphone. The shader compiles from source at runtime, so there is no
metallib to ship.
native/apple · swift test
Kotlin + OpenGL ES 2.0, API 21. OrbView is a TextureView on its own
EGL thread, so it composites with real alpha inside any layout, and Orb() is a one-line
composable. Audio from any PCM feed or AudioRecord. The galaxy GLSL is generated
verbatim from this module.
native/android · ./gradlew :orb:testDebugUnitTest
Direct3D 11, HLSL compiled at runtime. Draw into your own render target from a game or an engine, or hand it an HWND. Audio through WASAPI: the microphone, or loopback of whatever the machine is playing, which is how a desktop agent's own voice drives the orb without touching your playback path.
native/windows · orb_conformance.exe
import OrbKit
struct AgentView: View {
@State var state: OrbState = .idle
let mic = try? OrbAudioSource.microphone()
var body: some View {
Orb(seed: "agent-42", size: 320, state: state) // state crossfades over ~350 ms
.orbAudio(state == .listening ? mic : nil) // engine tap, PCM push, or the microphone
}
}
// UIKit / AppKit
let orb = OrbView(seed: "agent-42", size: 320, state: .listening)
orb.listen(OrbAudioSource.tap(engine.mainMixerNode)) // the agent's own TTS
orb.state = .speaking
import com.earlbalai.orb.compose.Orb
@Composable
fun Agent(state: OrbState, audio: OrbAudioSource?) {
Orb(seed = "agent-42", size = 320.dp, state = state, audio = audio)
}
// or the View
val orb = OrbView(context)
orb.update(OrbOptions(seed = "agent-42", state = OrbState.LISTENING))
orb.listen(OrbAudioSource.microphone()) // or pcm().push(...) from your voice SDK
orb.state = OrbState.SPEAKING // crossfades over ~350 ms
#include <orb/orb.h>
orb::Renderer renderer(device); // once per D3D11 device; compiles the HLSL
orb::Options o; o.seed = "agent-42"; o.size = 320;
orb::Orb agent(o);
agent.listen(orb::PCMSource::loopback()); // whatever the machine is playing: the agent's TTS
agent.setState(orb::State::Speaking);
// per frame, into your own render target
agent.tick(nowSeconds);
agent.draw(renderer, ctx, backBufferRTV, {x, y, agent.pixels(dpi)}, dpi,
orb::Composite::OverExisting);
// or just a window
orb::Window win(hwnd, o);
win.frame(); // tick, clear, draw, present
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.