Files
houseplan-card/docs/specs/019-glow-additive-blending.md
T

16 KiB
Raw Blame History

ТЗ #19 — Аддитивное смешивание Glow-источников

  • Issue: https://github.com/Matysh/houseplan-card/issues/19
  • Приоритет: P2
  • Статус ТЗ: реализовано в кандидате v1.61.0-beta.2; exact-SHA verification/golden approval — release gate
  • Связано: независимый Glow overlay #55 повторно использует эту композицию

Цель

Пересекающиеся radial pools смешиваются как свет, а не перекрывают друг друга по DOM order:

  • тёплый и холодный источник дают смешанный цвет в пересечении;
  • два одинаковых dim-источника дают более светлое пересечение;
  • room/data fill, Glow base, opening tunnel fills, backdrop и sun не участвуют в аддитивной группе.

Модель источников, resolvedLightSources(room), радиусы, clipping светом стен и сохранённый config не меняются.

Scope guards

  • blend применяется только к radial pools;
  • никакого UA sniffing и программного pixel-by-pixel polyfill;
  • unsupported или сомнительный engine получает текущий normal layering;
  • feature не имеет пользовательского/скрытого toggle: при провале correctness или performance gate issue возвращается в research;
  • #55 не создаёт вторую blend group и не смешивает Glow base с pools.

Render architecture

Все pools одного lighting layer переносятся в одну плоскую изолированную группу. Нормативная структура:

<g class="glow-pools-frame" pointer-events="none">
  <g class="glow-pools blend-screen" data-blend="screen">
    <g class="glow-spot">
      <circle class="glow-pool" clip-path="url(#per-source-visibility)">…</circle>
    </g>
  </g>
</g>

При fallback группа получает blend-normal и data-blend="normal". Одно capability-решение действует сразу на все pools документа: смешанных screen/normal состояний между источниками не бывает. data-blend находится на группе только для диагностики и тестов, не является persisted настройкой.

.glow-pools { isolation: isolate; }
.glow-spot { isolation: isolate; }
.glow-pools.blend-screen > .glow-spot { mix-blend-mode: screen; }
.glow-pools.blend-normal > .glow-spot { mix-blend-mode: normal; }

После #71 screen-композиция принадлежит .glow-spot, а единственная форма источника — один circle, обрезанный полигоном видимости, пересечённым с полом. Per-source luminance mask и отдельный shadow layer удалены: тень — это пол, не попавший в visibility polygon.

glow_base, resolved room/data fill, opening tunnel fills и sun rays остаются sibling layers вне glow-pools-frame. Их порядок относительно друг друга сохраняется по #55.

Каждый circle сохраняет собственный visibility clip-path после re-parenting. Clip ids остаются уникальными и стабильными в пределах SVG; объединять per-source clips в один общий clip запрещено, иначе свет начнёт проходить сквозь чужие стены.

Нормативная alpha/blend семантика

Альфа отдельного pool задаётся только stop-opacity его radial gradient:

Acenter = clamp(fill_colors.glow_light.a * 0.7 * (0.4 + 0.6 * sourceBrightness^(1/2.2)), 0, 1)
A(r = 0%) = Acenter
A(r = 45%) = 0.88 * Acenter
A(r = 70%) = 0.62 * Acenter
A(r = 86%) = 0.32 * Acenter
A(r = 100%) = 0
screen(S, D) = 1 - (1 - S) * (1 - D)   // отдельно для R, G, B в 0..1

На .glow-pool запрещены opacity, fill-opacity, filter и дополнительная alpha: полупрозрачность должна участвовать в blend через gradient stops, а не создавать отдельную element-композицию. Коэффициент 0.7 уже входит в Acenter; внешний glow-pools-frame не имеет постоянного opacity и не применяет его повторно.

.glow-spot может временно анимировать opacity 0→1/1→0 в течение 500 мс. Это только transition появления/исчезновения; в steady-state opacity равна 1 и не меняет alpha-формулу.

Glow base использует собственный alpha-контракт #55 и не входит в эту формулу. Blend не меняет base/tunnel opacity и не осветляет paper/backdrop.

Runtime capability probe

CSS.supports('mix-blend-mode','screen') разрешён только как быстрый отрицательный pre-check. Положительный ответ не считается доказательством поддержки SVG.

Фактическая capability определяется одноразовым render probe:

  1. Создать детерминированный мини-SVG с той же парой isolation:isolate + mix-blend-mode:screen, двумя частично перекрывающимися фигурами известных непрозрачных RGB-цветов и контрольным фоном.
  2. Без внешних ресурсов сериализовать SVG, растеризовать его в canvas с фиксированными физическими размерами и прочитать source/overlap/background pixels через getImageData.
  3. Для overlap вычислить ожидаемый RGB по формуле screen; каждый канал должен совпасть с допуском не более 2/255. Source pixels должны совпасть со своими цветами, background — остаться неизменным. Проверка только overlap недостаточна: артефакт isolation не должен дать false positive.
  4. Любая ошибка, timeout, transparent/zero sample, security exception или несовпадение переводит capability в false.

Результат кешируется как Promise<boolean> в WeakMap<Document, …> и исполняется не более одного раза per document. Он не сохраняется в config, localStorage или по UA: обновившийся WebView должен пройти пробу заново.

Пока Promise pending, карточка рисует normal fallback, не пустой слой. После успешного результата все смонтированные карточки получают один update и переходят на screen; при false повторного update нет. Print/screenshot path и houseplan-space-card, если она в будущем начнёт показывать live pools, используют то же capability-решение. prefers-reduced-motion не влияет на статичное смешивание.

Для тестов capability dependency можно инъецировать как true|false; это неэкспортируемый test hook, не пользовательская настройка.

Fallback contract

Fallback сохраняет текущую визуальную семантику:

  • pools идут в normal DOM layering;
  • gradient alpha (включая единственный коэффициент 0.7), radius и per-source clips не меняются;
  • порядок остальных SVG layers идентичен screen-path;
  • отсутствие поддержки не создаёт warning/toast: это штатная деградация.

Baseline fallback проверяется отдельно, а не выводится из golden screen-path.

Детерминированная fixture

Добавляется общая fixture test/fixtures/glow/additive-pools.json со сценариями 1, 10, 30 и 60 перекрывающихся pools. Она содержит schema-valid Houseplan config и отдельный детерминированный HA state snapshot:

  • фиксированные geometry, colors, brightness, radii и marker order;
  • минимум два помещения с физической стеной и разными per-source clip paths;
  • warm/cool и identical-dim контрольные пары;
  • отключены sun/daynight/transition и другие недетерминированные эффекты.

Один и тот же JSON загружается Node-тестом и Python backend schema test. Ни frontend, ни performance runner не держат собственную расходящуюся копию.

Performance gate до включения

Создаётся отдельный профиль large-light-blend-v1; существующий large-house-v1 не меняет смысл молча. Baseline и candidate строятся из точных SHA и запускаются последовательно одним runner/process family на pinned Chromium при:

  • DPR = 1;
  • CPU throttling ×4 как воспроизводимом proxy старого kiosk-железа;
  • минимум одном warm-up и семи measured samples на вариантах 1/10/30/60 pools.

Отчёт содержит stateUpdate p50/p95, render count, Long Tasks, heap/cache growth и screenshot capture time. Ship gate применяет одновременно relative-to-base и отдельные absolute budgets этого профиля; более строгий предел побеждает. Budget нельзя ослаблять только ради прохождения #19 без письменного обоснования по артефактам нескольких запусков.

Неизмеримый критерий «не более 10% на старом reference WebView» удалён: такого устройства нет в CI. Correctness сомнительных SVG-движков закрывает runtime probe, а производительность старого железа — throttled профиль. Полевая beta на реальных kiosk WebView желательна, но не заменяет автоматический gate.

При провале correctness или performance feature не ship-ится и возвращается в research без скрытого fallback toggle.

Разделение визуальных проверок

Pure unit

  • формула screen на известных RGB, clamp и округление;
  • gradient alpha использует только stop-opacity;
  • capability cache вызывается один раз per document;
  • pending/false/true state machine и единое решение для всех pools.

Browser pixel smoke

demo/smoke_glow_blending.mjs работает при фиксированном DPR=1 и сэмплирует реальные pixels детерминированной SVG-сцены, а не сравнивает только DOM:

  • warm+cool overlap соответствует screen-формуле с установленным допуском;
  • две одинаковые dim-лампы дают overlap светлее одного pool без channel clip;
  • background/base и opening tunnel fill сохраняют baseline pixels;
  • перестановка markers не меняет overlap;
  • принудительный test-only fallback совпадает с current normal baseline;
  • per-room clips переживают re-parenting: pool не появляется за физической стеной и остаётся видимым в разрешённой части своего clip.

Smoke использует canvas/PNG pixel sampling. Он не заменяется golden: обычный screenshot diff может заметить изменение, но не доказывает математическую формулу смеси.

Golden

Golden-сцена фиксирует целостную композицию в light/dark theme, 1/2/много источников, tunnel рядом с overlap и интеграцию с независимым overlay #55. Candidate утверждается владельцем глазами и после принятия становится визуальным контрактом; capture никогда не перезаписывает baseline сам.

Edge cases

  • 0 и 1 pool не создают лишнего визуального изменения;
  • несколько карточек Houseplan в одном document используют один probe;
  • разные комнаты, nested holes, doors/gates и physical/virtual walls;
  • hidden/removed/disabled/unavailable источники не попадают в pools по существующему resolver;
  • dark/light theme, print/screenshot, kiosk и browser zoom;
  • порядок markers, одинаковые цвета и полностью совпавшие центры;
  • probe pending/fail/timeout и восстановление после reload документа.

Документация и release artifacts

В том же feature commit обязательны:

  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md;
  • пользовательская документация Glow с объяснением смешивания и штатного fallback старых движков;
  • обновление ARCHITECTURE/render-order, если структура слоя меняется;
  • owner-reviewed golden и performance comparison artifacts.

Release body описывает видимую пользователю смесь света; probe, классы и кеши относятся к small fixes/improvements, если не требуют отдельного предупреждения.

Приёмка

  1. Положительный screen-path включается только после успешного фактического SVG render probe; CSS.supports сам по себе недостаточен.
  2. Все pools документа используют одно screen/normal решение; смешанного режима нет.
  3. Альфа steady-state живёт в gradient stops; outer-frame opacity отсутствует, а opacity .glow-spot используется только для 500 ms transition; коэффициент 0.7 применяется ровно один раз внутри формулы stop-opacity.
  4. Glow base, data fill, paper/backdrop, opening tunnel fills и sun остаются вне isolated group и сохраняют baseline pixels.
  5. Каждый pool сохраняет собственный clip-path после re-parenting.
  6. Shared fixture 1/10/30/60 проходит frontend и backend schema tests.
  7. Pixel smoke доказывает screen-формулу, isolation, order independence, clipping и normal fallback; golden фиксирует целостную картинку отдельно.
  8. large-light-blend-v1 проходит relative и absolute budgets при CPU ×4.
  9. #55 повторно использует ту же pool group и не создаёт второе смешивание.
  10. Документация и ru/en changelog обновлены.

Фактическая структура после #71 зафиксирована выше; каноническая модель транспорта света и её ограничения живут в docs/LIGHT.md.