feat: add overlay, soft light, hard light, difference, exclusion blend modes to composite
Some checks failed
Lint & Test / test (push) Failing after 27s
Some checks failed
Lint & Test / test (push) Failing after 27s
Integer-math implementations with pre-allocated scratch buffers. IconSelect picker updated with 10 blend modes. i18n for en/ru/zh.
This commit is contained in:
@@ -70,7 +70,7 @@ Need to research HAOS communication options first (WebSocket API, REST API, MQTT
|
||||
### `composite`
|
||||
|
||||
- [ ] Allow nested composites (with cycle detection)
|
||||
- [ ] More blend modes: overlay, soft light, hard light, difference, exclusion
|
||||
- [x] More blend modes: overlay, soft light, hard light, difference, exclusion
|
||||
- [x] Per-layer LED range masks (optional start/end/reverse on each composite layer)
|
||||
|
||||
### `notification`
|
||||
|
||||
@@ -17,6 +17,11 @@ _BLEND_ADD = "add"
|
||||
_BLEND_MULTIPLY = "multiply"
|
||||
_BLEND_SCREEN = "screen"
|
||||
_BLEND_OVERRIDE = "override"
|
||||
_BLEND_OVERLAY = "overlay"
|
||||
_BLEND_SOFT_LIGHT = "soft_light"
|
||||
_BLEND_HARD_LIGHT = "hard_light"
|
||||
_BLEND_DIFFERENCE = "difference"
|
||||
_BLEND_EXCLUSION = "exclusion"
|
||||
|
||||
|
||||
class CompositeColorStripStream(ColorStripStream):
|
||||
@@ -348,12 +353,125 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
u16a >>= 8
|
||||
np.copyto(out, u16a, casting="unsafe")
|
||||
|
||||
def _blend_overlay(self, bottom: np.ndarray, top: np.ndarray, alpha: int,
|
||||
out: np.ndarray) -> None:
|
||||
"""Overlay blend: multiply darks, screen lights, then lerp with alpha.
|
||||
|
||||
if bottom < 128: blended = 2*bottom*top >> 8
|
||||
else: blended = 255 - 2*(255-bottom)*(255-top) >> 8
|
||||
"""
|
||||
u16a, u16b = self._u16_a, self._u16_b
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
np.copyto(u16b, top, casting="unsafe")
|
||||
# Multiply path: 2*b*t >> 8
|
||||
mul = (u16a * u16b) >> 7 # * 2 >> 8 == >> 7
|
||||
# Screen path: 255 - 2*(255-b)*(255-t) >> 8
|
||||
scr = 255 - (((255 - u16a) * (255 - u16b)) >> 7)
|
||||
# Select based on bottom < 128
|
||||
mask = u16a < 128
|
||||
blended = np.where(mask, mul, scr)
|
||||
np.clip(blended, 0, 255, out=blended)
|
||||
# Lerp: result = (bottom * (256-a) + blended * a) >> 8
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
u16a *= (256 - alpha)
|
||||
blended *= alpha
|
||||
u16a += blended
|
||||
u16a >>= 8
|
||||
np.copyto(out, u16a, casting="unsafe")
|
||||
|
||||
def _blend_soft_light(self, bottom: np.ndarray, top: np.ndarray, alpha: int,
|
||||
out: np.ndarray) -> None:
|
||||
"""Soft light blend (Pegtop formula), then lerp with alpha.
|
||||
|
||||
blended = (1 - 2*t/255) * b*b/255 + 2*t*b/255
|
||||
"""
|
||||
u16a, u16b = self._u16_a, self._u16_b
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
np.copyto(u16b, top, casting="unsafe")
|
||||
# term1 = (255 - 2*t) * b * b / (255*255)
|
||||
# term2 = 2 * t * b / 255
|
||||
# Use intermediate 32-bit to avoid overflow
|
||||
b32 = u16a.astype(np.uint32)
|
||||
t32 = u16b.astype(np.uint32)
|
||||
blended = ((255 - 2 * t32) * b32 * b32 + 2 * t32 * b32 * 255) // (255 * 255)
|
||||
np.clip(blended, 0, 255, out=blended)
|
||||
# Lerp
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
u16a *= (256 - alpha)
|
||||
blended_u16 = blended.astype(np.uint16)
|
||||
blended_u16 *= alpha
|
||||
u16a += blended_u16
|
||||
u16a >>= 8
|
||||
np.copyto(out, u16a, casting="unsafe")
|
||||
|
||||
def _blend_hard_light(self, bottom: np.ndarray, top: np.ndarray, alpha: int,
|
||||
out: np.ndarray) -> None:
|
||||
"""Hard light blend: overlay with top/bottom roles swapped.
|
||||
|
||||
if top < 128: blended = 2*bottom*top >> 8
|
||||
else: blended = 255 - 2*(255-bottom)*(255-top) >> 8
|
||||
"""
|
||||
u16a, u16b = self._u16_a, self._u16_b
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
np.copyto(u16b, top, casting="unsafe")
|
||||
mul = (u16a * u16b) >> 7
|
||||
scr = 255 - (((255 - u16a) * (255 - u16b)) >> 7)
|
||||
# Select based on top < 128 (differs from overlay)
|
||||
mask = u16b < 128
|
||||
blended = np.where(mask, mul, scr)
|
||||
np.clip(blended, 0, 255, out=blended)
|
||||
# Lerp
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
u16a *= (256 - alpha)
|
||||
blended *= alpha
|
||||
u16a += blended
|
||||
u16a >>= 8
|
||||
np.copyto(out, u16a, casting="unsafe")
|
||||
|
||||
def _blend_difference(self, bottom: np.ndarray, top: np.ndarray, alpha: int,
|
||||
out: np.ndarray) -> None:
|
||||
"""Difference blend: |bottom - top|, then lerp with alpha."""
|
||||
u16a, u16b = self._u16_a, self._u16_b
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
np.copyto(u16b, top, casting="unsafe")
|
||||
# abs diff using signed subtraction
|
||||
blended = np.abs(u16a.astype(np.int16) - u16b.astype(np.int16)).astype(np.uint16)
|
||||
# Lerp
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
u16a *= (256 - alpha)
|
||||
blended *= alpha
|
||||
u16a += blended
|
||||
u16a >>= 8
|
||||
np.copyto(out, u16a, casting="unsafe")
|
||||
|
||||
def _blend_exclusion(self, bottom: np.ndarray, top: np.ndarray, alpha: int,
|
||||
out: np.ndarray) -> None:
|
||||
"""Exclusion blend: bottom + top - 2*bottom*top/255, then lerp with alpha."""
|
||||
u16a, u16b = self._u16_a, self._u16_b
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
np.copyto(u16b, top, casting="unsafe")
|
||||
# blended = b + t - 2*b*t/255
|
||||
blended = u16a + u16b - ((u16a * u16b) >> 7)
|
||||
np.clip(blended, 0, 255, out=blended)
|
||||
# Lerp
|
||||
np.copyto(u16a, bottom, casting="unsafe")
|
||||
u16a *= (256 - alpha)
|
||||
blended *= alpha
|
||||
u16a += blended
|
||||
u16a >>= 8
|
||||
np.copyto(out, u16a, casting="unsafe")
|
||||
|
||||
_BLEND_DISPATCH = {
|
||||
_BLEND_NORMAL: "_blend_normal",
|
||||
_BLEND_ADD: "_blend_add",
|
||||
_BLEND_MULTIPLY: "_blend_multiply",
|
||||
_BLEND_SCREEN: "_blend_screen",
|
||||
_BLEND_OVERRIDE: "_blend_override",
|
||||
_BLEND_OVERLAY: "_blend_overlay",
|
||||
_BLEND_SOFT_LIGHT: "_blend_soft_light",
|
||||
_BLEND_HARD_LIGHT: "_blend_hard_light",
|
||||
_BLEND_DIFFERENCE: "_blend_difference",
|
||||
_BLEND_EXCLUSION: "_blend_exclusion",
|
||||
}
|
||||
|
||||
# ── Processing loop ─────────────────────────────────────────
|
||||
|
||||
@@ -53,9 +53,14 @@ function _getCompositeBlendItems() {
|
||||
return [
|
||||
{ value: 'normal', icon: _icon(P.square), label: t('color_strip.composite.blend_mode.normal'), desc: t('color_strip.composite.blend_mode.normal.desc') },
|
||||
{ value: 'add', icon: _icon(P.sun), label: t('color_strip.composite.blend_mode.add'), desc: t('color_strip.composite.blend_mode.add.desc') },
|
||||
{ value: 'multiply', icon: _icon(P.eye), label: t('color_strip.composite.blend_mode.multiply'), desc: t('color_strip.composite.blend_mode.multiply.desc') },
|
||||
{ value: 'multiply', icon: _icon(P.moon), label: t('color_strip.composite.blend_mode.multiply'), desc: t('color_strip.composite.blend_mode.multiply.desc') },
|
||||
{ value: 'screen', icon: _icon(P.monitor), label: t('color_strip.composite.blend_mode.screen'), desc: t('color_strip.composite.blend_mode.screen.desc') },
|
||||
{ value: 'override', icon: _icon(P.zap), label: t('color_strip.composite.blend_mode.override'), desc: t('color_strip.composite.blend_mode.override.desc') },
|
||||
{ value: 'overlay', icon: _icon(P.copy), label: t('color_strip.composite.blend_mode.overlay'), desc: t('color_strip.composite.blend_mode.overlay.desc') },
|
||||
{ value: 'soft_light', icon: _icon(P.sunDim), label: t('color_strip.composite.blend_mode.soft_light'), desc: t('color_strip.composite.blend_mode.soft_light.desc') },
|
||||
{ value: 'hard_light', icon: _icon(P.zap), label: t('color_strip.composite.blend_mode.hard_light'), desc: t('color_strip.composite.blend_mode.hard_light.desc') },
|
||||
{ value: 'difference', icon: _icon(P.activity), label: t('color_strip.composite.blend_mode.difference'), desc: t('color_strip.composite.blend_mode.difference.desc') },
|
||||
{ value: 'exclusion', icon: _icon(P.circleOff), label: t('color_strip.composite.blend_mode.exclusion'), desc: t('color_strip.composite.blend_mode.exclusion.desc') },
|
||||
{ value: 'override', icon: _icon(P.eye), label: t('color_strip.composite.blend_mode.override'), desc: t('color_strip.composite.blend_mode.override.desc') },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -127,11 +132,9 @@ export function compositeRenderList() {
|
||||
<div class="composite-layer-row">
|
||||
<select class="composite-layer-source" data-idx="${i}">${srcOptions}</select>
|
||||
<select class="composite-layer-blend" data-idx="${i}">
|
||||
<option value="normal"${layer.blend_mode === 'normal' ? ' selected' : ''}>${t('color_strip.composite.blend_mode.normal')}</option>
|
||||
<option value="add"${layer.blend_mode === 'add' ? ' selected' : ''}>${t('color_strip.composite.blend_mode.add')}</option>
|
||||
<option value="multiply"${layer.blend_mode === 'multiply' ? ' selected' : ''}>${t('color_strip.composite.blend_mode.multiply')}</option>
|
||||
<option value="screen"${layer.blend_mode === 'screen' ? ' selected' : ''}>${t('color_strip.composite.blend_mode.screen')}</option>
|
||||
<option value="override"${layer.blend_mode === 'override' ? ' selected' : ''}>${t('color_strip.composite.blend_mode.override')}</option>
|
||||
${_getCompositeBlendItems().map(b =>
|
||||
`<option value="${b.value}"${layer.blend_mode === b.value ? ' selected' : ''}>${b.label}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="composite-layer-row">
|
||||
|
||||
@@ -1183,6 +1183,16 @@
|
||||
"color_strip.composite.blend_mode.screen": "Screen",
|
||||
"color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply",
|
||||
"color_strip.composite.blend_mode.override": "Override",
|
||||
"color_strip.composite.blend_mode.overlay": "Overlay",
|
||||
"color_strip.composite.blend_mode.overlay.desc": "Multiply darks, screen lights",
|
||||
"color_strip.composite.blend_mode.soft_light": "Soft Light",
|
||||
"color_strip.composite.blend_mode.soft_light.desc": "Gentle contrast adjustment",
|
||||
"color_strip.composite.blend_mode.hard_light": "Hard Light",
|
||||
"color_strip.composite.blend_mode.hard_light.desc": "Strong contrast, vivid colors",
|
||||
"color_strip.composite.blend_mode.difference": "Difference",
|
||||
"color_strip.composite.blend_mode.difference.desc": "Absolute color difference",
|
||||
"color_strip.composite.blend_mode.exclusion": "Exclusion",
|
||||
"color_strip.composite.blend_mode.exclusion.desc": "Like difference, lower contrast",
|
||||
"color_strip.composite.blend_mode.override.desc": "Black = transparent, bright = opaque",
|
||||
"color_strip.composite.opacity": "Opacity",
|
||||
"color_strip.composite.brightness": "Brightness",
|
||||
|
||||
@@ -1148,6 +1148,16 @@
|
||||
"color_strip.composite.blend_mode.screen": "Экран",
|
||||
"color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение",
|
||||
"color_strip.composite.blend_mode.override": "Замена",
|
||||
"color_strip.composite.blend_mode.overlay": "Наложение",
|
||||
"color_strip.composite.blend_mode.overlay.desc": "Умножение тёмных, осветление светлых",
|
||||
"color_strip.composite.blend_mode.soft_light": "Мягкий свет",
|
||||
"color_strip.composite.blend_mode.soft_light.desc": "Мягкая коррекция контраста",
|
||||
"color_strip.composite.blend_mode.hard_light": "Жёсткий свет",
|
||||
"color_strip.composite.blend_mode.hard_light.desc": "Сильный контраст, яркие цвета",
|
||||
"color_strip.composite.blend_mode.difference": "Разница",
|
||||
"color_strip.composite.blend_mode.difference.desc": "Абсолютная разница цветов",
|
||||
"color_strip.composite.blend_mode.exclusion": "Исключение",
|
||||
"color_strip.composite.blend_mode.exclusion.desc": "Как разница, но мягче",
|
||||
"color_strip.composite.blend_mode.override.desc": "Чёрный = прозрачный, яркий = непрозрачный",
|
||||
"color_strip.composite.opacity": "Непрозрачность",
|
||||
"color_strip.composite.brightness": "Яркость",
|
||||
|
||||
@@ -1148,6 +1148,16 @@
|
||||
"color_strip.composite.blend_mode.screen": "滤色",
|
||||
"color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转",
|
||||
"color_strip.composite.blend_mode.override": "覆盖",
|
||||
"color_strip.composite.blend_mode.overlay": "叠加",
|
||||
"color_strip.composite.blend_mode.overlay.desc": "暗部相乘,亮部滤色",
|
||||
"color_strip.composite.blend_mode.soft_light": "柔光",
|
||||
"color_strip.composite.blend_mode.soft_light.desc": "柔和对比度调整",
|
||||
"color_strip.composite.blend_mode.hard_light": "强光",
|
||||
"color_strip.composite.blend_mode.hard_light.desc": "强对比度,鲜艳色彩",
|
||||
"color_strip.composite.blend_mode.difference": "差值",
|
||||
"color_strip.composite.blend_mode.difference.desc": "绝对颜色差异",
|
||||
"color_strip.composite.blend_mode.exclusion": "排除",
|
||||
"color_strip.composite.blend_mode.exclusion.desc": "类似差值,对比度更低",
|
||||
"color_strip.composite.blend_mode.override.desc": "黑色=透明,亮色=不透明",
|
||||
"color_strip.composite.opacity": "不透明度",
|
||||
"color_strip.composite.brightness": "亮度",
|
||||
|
||||
Reference in New Issue
Block a user