Skip to content

Feat/transitions#12

Open
anders130 wants to merge 10 commits into
devfrom
feat/transitions
Open

Feat/transitions#12
anders130 wants to merge 10 commits into
devfrom
feat/transitions

Conversation

@anders130
Copy link
Copy Markdown
Contributor

Summary

Phase 1 of the upgrade plan. Addresses the TV-mode workflow pain point: arbitrary per-call fade durations, a state-stack primitive for snapshot/restore, and per-layer opacity LiveParams that survive reloads. Also fixes a long-standing dark-band bug at zone soft-edges.

Why

  • TV-mode was painful: only a single config-time crossfade_ms and no way to snapshot/restore state, so "quickly dim part of the strip, then slowly fade back" wasn't expressible.
  • Layer opacity didn't exist as a first-class concept, so adding/removing a layer was an instant pop-in/pop-out.
  • Zone soft-edges produced visibly darker bands than a clean gradient because blend_pixel was dimming the overlay twice.

What's new

  • TransitionSpec { duration_ms, curve } (domain) threaded through commands, runtime, and every scene/device route - every mutating endpoint accepts ?fade_ms=...&curve=....
  • TransitionEffect is effect-aware now (FrameIter blend instead of snapshot lerp), so the source effect keeps animating during the crossfade.
  • State stack (POST /scenes/active/push, POST /scenes/active/pop?fade_ms=..., GET /scenes/active/stack, DELETE /scenes/active/stack) - LIFO snapshot of active layers + device state.
  • Per-layer opacity (default 1.0, persisted) wrapped in Arc<LiveParam<f32>> and multiplied into blend_pixel. The runtime keeps an id-keyed map so continuing layers reuse the same LiveParam across reloads.
  • New layers fade-in over ?fade_ms; PATCH /layers/{id} {"opacity": ...} retargets the existing LiveParam (smooth tween, no restart); DELETE /layers/{id}?fade_ms=... tweens to 0 then drops the row.
  • Fix: blend_pixel restructured to "fully blend, then lerp by opacity" - each blend mode now respects opacity uniformly.

Breaking changes

  • DB migrations land automatically: 0002_state_stack (new table) and 0003_layer_opacity (new column, default 1.0). Existing rows back-fill to 1.0; no manual action needed.
  • LayerResponse JSON gains an opacity field - clients expecting a fixed shape may need an update.
  • LayerSpec.opacity (internal) changed from f32 to Arc<LiveParam<f32>> - only relevant if you have out-of-tree code building composites directly.
  • crossfade_ms in config/default.toml is superseded by [transitions] default_fade_ms / default_curve. The old key is ignored (not removed yet).

How to use the new things

Fade a brightness/color change:

PATCH /device/state?fade_ms=1500&curve=ease_in_out
{"brightness": 240}

Swap scenes with a crossfade:

PUT /scenes/active?fade_ms=2000
[{"effect_id": "builtin:fill", "zone_id": "all"}]

TV-mode workflow (fast fade-out on a zone, slow fade-in on return):

POST /scenes/active/push                                    # snapshot current
PUT  /scenes/active?fade_ms=400                             # overwrite -> fast fade
... TV is on ...
POST /scenes/active/pop?fade_ms=5000&curve=ease_in_out      # restore -> slow fade

Add a layer that ramps in over 3s:

POST /scenes/active/layers?fade_ms=3000&curve=ease_in_out
{"effect_id": "builtin:lava", "zone_id": "<zone-id>", "blend_mode": "add"}

Tween a layer's opacity:

PATCH /scenes/active/layers/{id}?fade_ms=2000
{"opacity": 0.3}

Delete a layer with a fade-out:

DELETE /scenes/active/layers/{id}?fade_ms=3000

(Returns 204 immediately; row stays in the DB with opacity=0 while it visually fades, gets dropped after fade_ms.)

Available curves: linear, ease_in, ease_out, ease_in_out, exp.

@anders130 anders130 self-assigned this May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant