Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

ADR-016: Ambient dominant color — Rust-side extraction, DB-cached

ADR-016: Ambient dominant color — Rust-side extraction, DB-cached

Status

Accepted

Context

The now-playing surfaces (Music Room NowPlayingView, MusicPlayerExpansion — ui-design-system.md §8.8) are where users spend the most time. A static background behind gray artwork feels dead. Ambient color pulls the dominant color from the current track’s cover art and applies it as a subtle tint — felt, not noticed (~8% over the card surface, blended in oklch) — on the now-playing surfaces only; browse/detail views keep the static theme background.

data-models.md gives Track a cover_art_url but no color field. Playback events deliver TrackStub (tauri-events.md), which carries neither cover art nor color. The question is where the dominant color is computed and how the frontend receives it.

Decision

Rust-side extraction, cached in the library service. Image decoding and dominant-color computation are added to the library service (it owns content resolution) as part of this change:

  • Computed when a cover is first indexed/resolved: 1×1 resize + oklch average — cheap, and run once per unique cover.
  • Cached with the resolved metadata; invalidated only when the cover URL changes. null when there is no cover art — the surface then uses the static theme background (no tint).
  • Served to the frontend as a nullable cover_dominant_color field (a string like "oklch(0.55 0.12 45)") on both Track and TrackStub — events deliver TrackStub, so the field must exist there, not only on the full Track.

The UI applies it through the token channel: NowPlayingView and MusicPlayerExpansion set --ambient-color on their own root and the base layer builds the gradient (ui-design-system.md §6.3). No client-side extraction as a fallback in v1.

Alternatives considered

  • JS canvas extraction in the frontend. Rejected. It runs on the render thread and causes a visible layout shift when the cover resolves after the view mounts; also duplicates computation across every display of the same cover.
  • No ambient treatment. Rejected — the now-playing surface reads as dead without it, and the tint is the main “audio identity” cue the design system calls for (ui-design-system.md §1).
  • Frontend-resolved full Track (events stay stub-only; the view calls get_track/library for the color). Works, but adds a lookup and a loading state on the primary surface; carrying the small nullable field on the stub avoids it.

Consequences

Positive:

  • Dominant color is computed once per unique cover, then served from cache for every subsequent display — near-zero runtime cost.
  • Off the render thread; no layout shift from late-arriving cover art.
  • The field is nullable and tolerant — the UI degrades to the static theme background when there is no cover, no color, or an unprocessed track.
  • Rides the existing --ambient-color token channel, so the design system owns the blend and components stay token-clean.

Negative:

  • A data-model change (cover_dominant_color on Track and TrackStub) and a Rust/TS contract update (contracts/types.ts, serde structs) with the associated migrations — tracked in ui-design-system.md §14 open items.
  • The library service gains an image-decode capability (downscale + oklch average) it did not have; adds a dependency or code for image decoding.
  • Tint is limited to now-playing surfaces in v1; extending it is a per-room decision later.

References

  • ui-design-system.md §6 (ambient color), §6.2 (extraction), §6.3 (application), §14 (open items — cover_dominant_color)
  • api-specs/data-models.md (Track, TrackStub)
  • api-specs/tauri-events.md (events deliver TrackStub)
  • services/library-service.md (resolution/cache owner)
  • ADR-015 (token architecture — --ambient-color channel)
  • FR-MUSIC-2 (player expansion surface)

Was this page helpful?