Progressive HSV phase interpolation for multi-day events (spec §4.2) #74

Closed
opened 2026-07-16 07:25:46 +00:00 by claude-bot · 1 comment
Contributor

Target milestone: v1.1.0 (deferred from v1.0.0 #9).

Implement optional per-event HSV color interpolation across eve → opening → mid → closing at generation time, per spec §4.2. The current generator emits one independent scheme per phase (scheme_generator.generate_schemes_for_event), which is a different model from the interpolated per-day presets §4.2 describes.

Requires:

  • Moving from one-preset-per-phase to per-day pre-calculated presets (or an equivalent interpolation layer).
  • A per-event opt-in flag in phase_config (e.g. interpolate: true).
  • Hue-wraparound-aware HSV blending between phase anchor colors.
  • Review-UI + scheduler support for resolving the correct day's preset.

The gap is documented inline in backend/app/services/scheme_generator.py (deferred note from #9). Split out of #9 during the v1.0.0 push so v1 scope stayed honest (implemented-or-explicitly-deferred).

**Target milestone: v1.1.0** (deferred from v1.0.0 #9). Implement optional per-event HSV color interpolation across `eve → opening → mid → closing` at generation time, per spec §4.2. The current generator emits one independent scheme per phase (`scheme_generator.generate_schemes_for_event`), which is a different model from the interpolated per-day presets §4.2 describes. Requires: - Moving from one-preset-per-phase to per-day pre-calculated presets (or an equivalent interpolation layer). - A per-event opt-in flag in `phase_config` (e.g. `interpolate: true`). - Hue-wraparound-aware HSV blending between phase anchor colors. - Review-UI + scheduler support for resolving the correct day's preset. The gap is documented inline in `backend/app/services/scheme_generator.py` (deferred note from #9). Split out of #9 during the v1.0.0 push so v1 scope stayed honest (implemented-or-explicitly-deferred).
claude-bot added this to the v1.1.0 milestone 2026-07-16 07:26:20 +00:00
Author
Contributor

Landed in #128 (squash-merged to main).

The design question turned out not to be the colour maths — it was what a human approves. Interpolation wants a scheme per day; the review queue wants a scheme per phase, and twenty-six approvable rows for one holiday would make it unusable. So the four phase schemes stay the anchors (listed, edited, approved) and the per-day rows are derived: rebuilt whenever an anchor changes, carrying schemes.derived_from, filtered out of the review list, preset sync and config export.

A derived day is approved only when every anchor it blends is approved. Falling back to the plain anchor colours on a half-approved event would light the tree in something the user never signed off on because a different scheme was approved.

On the issue's four requirements:

  • Per-day pre-calculated presets — yes, at generation time as §4.2 specifies. Days landing exactly on an anchor get no row: the blend there is the anchor, so a three-day event produces zero derived rows, which is correct.
  • Per-event opt-inphase_config.interpolate, off by default.
  • Hue-wraparound-aware blending — shortest arc. Red → magenta is 60° backwards; a linear blend takes the 300° route and spends a fortnight passing through green. Exactly 180° apart is genuinely ambiguous, so the direction is fixed and pinned by a test. Blending is in HSV rather than channel-wise RGB, and an achromatic endpoint contributes no hue (colorsys reports white as hue 0, which reads as red).
  • Review-UI + scheduler support — both. GET /schemes/for-date/{date} returns what will actually light on a day, and the side panel previews that instead of the anchor. It resolves through the same helper the scheduler uses, so the preview and the push cannot disagree — the drift #93 fixed once already.

One thing deliberately not done: effect_id and palette don't interpolate. Effect 26 is not halfway between 12 and 40, it's an unrelated animation. They step to the nearer anchor at the midpoint.

Writing the tests found a real bug. sync_derived_schemes originally read anchor colours off scheme.segments, but generate_schemes_for_event rebuilds those rows with db.add(SchemeSegment(...)) rather than through the relationship — so the loaded collection can still hold the previous generation's values. It would have blended between colours no longer anchored anywhere, and looked entirely plausible. It now queries the rows directly. Three of my own tests were passing because of that staleness and were corrected too.

Verified: ruff/mypy clean, 654 backend tests, alembic upgrade/check/downgrade on a fresh DB, frontend tsc/eslint/81 vitest/build in node:22, CI green.

Landed in #128 (squash-merged to `main`). **The design question turned out not to be the colour maths — it was what a human approves.** Interpolation wants a scheme per day; the review queue wants a scheme per phase, and twenty-six approvable rows for one holiday would make it unusable. So the four phase schemes stay the **anchors** (listed, edited, approved) and the per-day rows are **derived**: rebuilt whenever an anchor changes, carrying `schemes.derived_from`, filtered out of the review list, preset sync and config export. A derived day is approved only when *every* anchor it blends is approved. Falling back to the plain anchor colours on a half-approved event would light the tree in something the user never signed off on because a different scheme was approved. **On the issue's four requirements:** - *Per-day pre-calculated presets* — yes, at generation time as §4.2 specifies. Days landing exactly on an anchor get no row: the blend there *is* the anchor, so a three-day event produces zero derived rows, which is correct. - *Per-event opt-in* — `phase_config.interpolate`, off by default. - *Hue-wraparound-aware blending* — shortest arc. Red → magenta is 60° backwards; a linear blend takes the 300° route and spends a fortnight passing through green. Exactly 180° apart is genuinely ambiguous, so the direction is fixed and pinned by a test. Blending is in HSV rather than channel-wise RGB, and an achromatic endpoint contributes no hue (`colorsys` reports white as hue 0, which reads as red). - *Review-UI + scheduler support* — both. `GET /schemes/for-date/{date}` returns what will actually light on a day, and the side panel previews that instead of the anchor. It resolves through the **same helper the scheduler uses**, so the preview and the push cannot disagree — the drift #93 fixed once already. **One thing deliberately not done:** `effect_id` and `palette` don't interpolate. Effect 26 is not halfway between 12 and 40, it's an unrelated animation. They step to the nearer anchor at the midpoint. **Writing the tests found a real bug.** `sync_derived_schemes` originally read anchor colours off `scheme.segments`, but `generate_schemes_for_event` rebuilds those rows with `db.add(SchemeSegment(...))` rather than through the relationship — so the loaded collection can still hold the *previous* generation's values. It would have blended between colours no longer anchored anywhere, and looked entirely plausible. It now queries the rows directly. Three of my own tests were passing *because* of that staleness and were corrected too. Verified: `ruff`/`mypy` clean, 654 backend tests, `alembic upgrade`/`check`/`downgrade` on a fresh DB, frontend `tsc`/`eslint`/81 vitest/build in `node:22`, CI green.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#74
No description provided.