# orber overview
`orber` turns a photo or short video into an abstract **orb mood** rendition — colorful, blurry light spheres that drift slowly. The original subject is intentionally lost; what survives is the *vibe* of the colors.
## Pipeline
```
input image / video
├─ (video only) extract representative frames via ffmpeg
├─ extract color clusters → N representative colors [implemented]
├─ place orbs → position, size, base color per orb [implemented for static PNG]
├─ render frame(s) → RGBA buffer with radial-gradient orbs [implemented via tiny-skia]
├─ (animated) interpolate → frame sequence over time t [implemented]
└─ encode → PNG / MP4 / WebM / SVG / CSS [PNG / MP4 / WebM / SVG / CSS implemented]
```
## Output formats
| **Raster** | PNG, WebP | MP4, WebM (vertical 9:16 by default)|
| **Style** | CSS gradient (implemented) | CSS gradient + `@keyframes` (planned) |
| **Vector** | SVG (implemented) | — |
CSS / SVG output is attractive because it is essentially zero-byte, infinitely loopable, resolution-independent, and cheap to render in a browser compared to a video element.
## Parameters
The CLI exposes the following flags (run `orber --help` for the authoritative list):
- `--orb-size` — relative orb size multiplier (small = many tiny orbs, large = few soft blobs)
- `--blur` — blur intensity in 0.0..=1.0 (sharp ↔ fully diffused)
- `--count` — orbs visible on screen at once (1..=200, default 20)
- `--direction` — conveyor flow direction: `lr` / `rl` / `tb` / `bt`
- `--speed` — conveyor pace: `very-slow` / `slow` (cross counts per clip)
- `--shape` — `circle` or `aquarelle` (watercolor bleed)
- `--saturation` — saturation multiplier
- `--duration-ms` — clip duration for animated outputs
- `--seed` — random seed for reproducibility
- `--variations N --output-dir DIR` — emit a curated set of N alternate looks for the same input (direction × speed × count × size × blur combinations)
Background color is not a CLI flag — it is derived from the input image (see "Background derivation" below).
## Background derivation (v0.3.0)
There is no `--background` flag. The background color is **derived automatically**
from the k-means clusters of the input image:
- the dominant cluster (highest weight) becomes the canvas color (alpha = 255)
- the remaining K − 1 clusters become the orb pool
- if k-means returns zero clusters (degenerate input), the canvas falls back to
opaque black
Concretely:
- a nightscape (mostly dark sky) → black canvas + bright neon points
- a daytime sky → sky-blue canvas + clouds / silhouettes drifting on it
- a beige interior → beige canvas + small accent-color orbs
The dominant color is the most "this is what the photo looks like" channel, so
making it the canvas and letting the sub-colors float as orbs produces a
composition that already feels right without parameter tuning. To get a
different canvas, feed in a different image — the design intentionally has no
override path.
Auto-derived backgrounds are always opaque (alpha = 255), so animated outputs
(`mp4` / `webm`) never collide with `yuv420p`'s lack of alpha.
## Motion model (v0.3.0)
Animated outputs use a **one-way conveyor belt**. The whole clip flows in exactly one
direction (`lr` / `rl` / `tb` / `bt`); orbs do not reflect, oscillate, or return to
their start. When an orb exits one edge, a fresh orb enters from the opposite edge
— but the seam happens **fully off-screen**: each orb's progress range is `[-r, 1+r]`
where `r` is its radius normalized by the progress-axis length, so orbs spawn and
despawn beyond the canvas edge and never visibly pop in or out. Each orb has a
randomized initial phase so the field looks scattered rather than synchronized.
A baseline breathing is applied to every orb automatically — there is no opt-in flag.
The breathing has **three independent axes**, each driven by its own seed-derived
phase offset and looping once per clip duration:
- radius: ±10%
- blur: ±15%
- opacity: ±5%
Each orb is also assigned an integer **speed multiplier** (`1x` / `2x`)
deterministically from the seed, so individual orbs visibly travel at different
paces inside the same clip. Combined with the global `--speed` cycle count
(`very-slow` / `slow` = 1 / 2), per-orb effective traversal counts spread over
`{1, 2, 4}` per clip. Because every factor is an integer, the loop closure at
`t = 0 ≡ t = 1` remains pixel-exact.
`--speed` itself is the global cycle count (1 / 2 screen-crosses per clip for
the slowest orbs). Real-time pacing is set by `--duration-ms`: `--speed slow
--duration-ms 8000` means the slowest orbs cross the screen twice in 8 seconds
(4 s/cross), with `2x` orbs proportionally faster.
> Note: the aquarelle shape uses the legacy `[0, 1]` wrap. Its bleed / bloom / halo
> textures clip cleanly enough that the off-screen wrap buffer would interfere with
> the halo rendering. The `[-r, 1+r]` off-screen wrap described above applies to
> the `circle` shape only.
## Orb count and visual mix (v0.3.0)
The k-means palette gives K colors (5 in the variations path). `--count <N>`
*expands* those K colors into N orbs by:
1. weight-proportional color sampling (more dominant clusters spawn more orbs)
2. per-orb cross-axis scattering (orbs spread across the full width/height instead of
sticking to cluster centroids)
Each orb is also assigned one of two visual styles deterministically from the seed:
- `Rim` — a mid stop drops the gradient to half-alpha, producing a ring-emphasized look
- `Soft` — center → transparent monotonic fade with no mid stop
The two styles mix roughly 50:50 inside a single frame, so some orbs look like
ring-haloed lights and others like plain soft glows.
> Note: the aquarelle shape ignores `--count` (palette-only rendering). It renders
> one orb per k-means cluster so the bleed / bloom / halo texture set stays coherent.
## Variation preset (v0.3.0)
The `--variations` mode draws from a 10-entry hand-tuned preset that combines five
independent axes — conveyor direction, conveyor speed, orb count, orb size, and blur.
Color is **not** an axis: the input image's k-means palette is used unchanged across
all ten outputs. Differentiation comes from layout (count / size / blur) and motion
(direction / speed).
- 4 stills: `snapshot_lr_dense`, `snapshot_rl_huge`, `snapshot_tb_fine`,
`snapshot_bt_blurry`
- 6 animations (8 s each): `flow_lr_slow`, `flow_rl_very_slow`, `flow_tb_dense`,
`flow_bt_blurry`, `flow_lr_dense_small`, `flow_rl_huge`
Stills are not pure `render_static` snapshots — they are the `t = 0` frame of the
conveyor, so orbs are phase-scattered and the off-screen wrap buffer means a fraction
of the requested `--count` will sit just outside the visible area, matching the
visual language of the videos.
## Use cases
- Background plates for video edits
- Streaming "be right back" idle screens
- Social story / TikTok / Reels backgrounds
- Phone or desktop wallpapers from your own photos
- Privacy-friendly mood snapshot of a place (looks nothing like the original)
## Non-goals (for the prototype)
- Web frontend (planned later as a separate effort)
- WASM build (planned later)
- Realtime / interactive editing (CLI-only for now)
## Relationship to aquarelle
The aquarelle (watercolor bleed) shape generator will eventually be split out into its own crate, shared between `orber` (irregular orb shapes) and `blueprinter` (sumi / watercolor diagram themes). For the prototype it lives inside `orber` under `src/aquarelle/` so the module boundary is already in place.