Every day becomes a unique fingerprint of spilled color.
Mood sets the hue · the meaning of what you wrote sets the pattern · how much you logged sets the grain. Similar days look like siblings — no two are ever identical.
A day (or a note, a track, a commit — any record) is reduced to a handful of numbers, then those numbers are drawn. Three independent axes, one image:
| driven by | what it does | |
|---|---|---|
| Color | mood | Position on the circumplex (valence × energy) bilinearly blends the four quadrant colors in OKLab. The orb's dominant hue is the mood. Intensity drives saturation. |
| Pattern | meaning | Warp, rotation and seed come from a hash of the day's embedding. A 1% shift in meaning scrambles the pattern — while the color stays put. That's the fingerprint. |
| Grain | volume | A quiet day (one or two short notes) spills into a few huge pools. A dense day breaks into fine veins. Volume scales the field, it doesn't crop the same image. |
It's deterministic: the same ~10 floats always produce the same orb, on every platform. A record is stored as parameters, never as a rendered image — so every orb can be re-rendered crisp at any size, and re-skinned retroactively.
Seven days, each frozen into its own orb. Color = mood · pattern = meaning · grain = volume.
![]() MON long · energized |
![]() TUE productive |
![]() WED short · 2 notes |
![]() THU anxious |
![]() FRI sad · quiet |
![]() SAT depleted |
![]() SUN calm · recover |
When no mood is logged, color simply falls back to meaning — the embedding alone tints the orb:
The whole orb is one pure function plus a fragment shader. Drop orb.js in and feed it numbers:
<script src="orb.js"></script>
<canvas id="orb" style="width:240px;height:240px"></canvas>
<script>
const { initOrb, sizeCanvas, orbParams, draw } = window.MoodOrb;
const canvas = document.getElementById('orb');
sizeCanvas(canvas);
const orb = initOrb(canvas);
const params = orbParams({
valence: 0.55, // -1 unpleasant … +1 pleasant (mood)
energy: 0.80, // -1 depleted … +1 energized (mood)
volume: 0.92, // 0 a couple of notes … 1 a full, wordy day
embedding: [0.12, 0.84, 0.33, 0.61, 0.20, 0.74, 0.48, 0.07], // the day's meaning vector
moodLogged: true // false → hue falls back to the embedding
});
// params = { c1, c2, c3 (OKLCH stops), warp, freq, rot, seed, detail } — ~10 floats
(function loop(t){ draw(orb, params, t / 1000); requestAnimationFrame(loop); })(0);
</script>Pass a constant time instead of the clock for a frozen, deterministic still.
orb.js is web/WebGL, but the math is portable and intentionally tiny:
- Same renderer (web): copy
orb.jsmore or less verbatim. - iOS / Metal, Android, a different engine, or server-side raster: don't copy the GLSL — port the logic. The OKLab/OKLCH ↔ sRGB conversion, the domain-warped fBm, and the
orbParamsmapping are all straightforward to re-implement. Keep the math identical and orbs look the same everywhere. - Storage: persist the
orbParamsoutput (~10 floats), not an image. - Similarity axis: if your embeddings are high-dimensional, fit PCA offline and take the top 3–4 components for hue & flow; refit as data grows.
index.html the interactive one-pager (the live demo)
orb.js the core: orbParams() + shader + initOrb/draw — zero dependencies
gallery/ pre-rendered example orbs







