orber 0.3.0

Turn photos and videos into abstract orb mood images and short-form vertical videos
Documentation
# 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

|              | Static            | Animated                            |
| ------------ | ----------------- | ----------------------------------- |
| **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.