# ТЗ #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 переносятся в одну плоскую изолированную группу. Нормативная структура: ```html … ``` При fallback группа получает `blend-normal` и `data-blend="normal"`. Одно capability-решение действует сразу на все pools документа: смешанных screen/normal состояний между источниками не бывает. `data-blend` находится на группе только для диагностики и тестов, не является persisted настройкой. ```css .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: ```text 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` в `WeakMap` и исполняется не более одного раза 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`.