CSS: add led_count field; calibration dialog improvements; color corrections collapsible section
- Add explicit led_count to PictureColorStripSource (0 = auto from calibration) - Stream pads with black or truncates to match led_count exactly - Calibration dialog: show led_count input above visual editor in CSS mode - Calibration dialog: pre-populate led_count with effective count (cal sum) when stored value is 0 - Calibration dialog: sync preview label live as led_count input changes - CSS editor: group brightness/saturation/gamma into collapsible "Color Corrections" section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ def _css_to_response(source) -> ColorStripSourceResponse:
|
||||
gamma=getattr(source, "gamma", None),
|
||||
smoothing=getattr(source, "smoothing", None),
|
||||
interpolation_mode=getattr(source, "interpolation_mode", None),
|
||||
led_count=getattr(source, "led_count", 0),
|
||||
calibration=calibration,
|
||||
description=source.description,
|
||||
created_at=source.created_at,
|
||||
@@ -93,6 +94,7 @@ async def create_color_strip_source(
|
||||
gamma=data.gamma,
|
||||
smoothing=data.smoothing,
|
||||
interpolation_mode=data.interpolation_mode,
|
||||
led_count=data.led_count,
|
||||
calibration=calibration,
|
||||
description=data.description,
|
||||
)
|
||||
@@ -143,6 +145,7 @@ async def update_color_strip_source(
|
||||
gamma=data.gamma,
|
||||
smoothing=data.smoothing,
|
||||
interpolation_mode=data.interpolation_mode,
|
||||
led_count=data.led_count,
|
||||
calibration=calibration,
|
||||
description=data.description,
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ class ColorStripSourceCreate(BaseModel):
|
||||
gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0)
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||
led_count: int = Field(default=0, description="Total LED count (0 = auto from calibration)", ge=0)
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration (position and count per edge)")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
@@ -35,6 +36,7 @@ class ColorStripSourceUpdate(BaseModel):
|
||||
gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0)
|
||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode (average, median, dominant)")
|
||||
led_count: Optional[int] = Field(None, description="Total LED count (0 = auto from calibration)", ge=0)
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
@@ -52,6 +54,7 @@ class ColorStripSourceResponse(BaseModel):
|
||||
gamma: Optional[float] = Field(None, description="Gamma correction")
|
||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing")
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
||||
led_count: int = Field(0, description="Total LED count (0 = auto from calibration)")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
|
||||
@@ -142,7 +142,8 @@ class PictureColorStripStream(ColorStripStream):
|
||||
self._pixel_mapper = PixelMapper(
|
||||
self._calibration, interpolation_mode=self._interpolation_mode
|
||||
)
|
||||
self._led_count: int = self._calibration.get_total_leds()
|
||||
cal_leds = self._calibration.get_total_leds()
|
||||
self._led_count: int = source.led_count if source.led_count > 0 else cal_leds
|
||||
self._gamma_lut: np.ndarray = _build_gamma_lut(self._gamma)
|
||||
|
||||
# Thread-safe color cache
|
||||
@@ -224,10 +225,12 @@ class PictureColorStripStream(ColorStripStream):
|
||||
if (
|
||||
source.interpolation_mode != self._interpolation_mode
|
||||
or source.calibration != self._calibration
|
||||
or source.led_count != self._led_count
|
||||
):
|
||||
self._interpolation_mode = source.interpolation_mode
|
||||
self._calibration = source.calibration
|
||||
self._led_count = source.calibration.get_total_leds()
|
||||
cal_leds = source.calibration.get_total_leds()
|
||||
self._led_count = source.led_count if source.led_count > 0 else cal_leds
|
||||
self._pixel_mapper = PixelMapper(
|
||||
source.calibration, interpolation_mode=source.interpolation_mode
|
||||
)
|
||||
@@ -263,6 +266,15 @@ class PictureColorStripStream(ColorStripStream):
|
||||
led_colors = self._pixel_mapper.map_border_to_leds(border_pixels)
|
||||
t2 = time.perf_counter()
|
||||
|
||||
# Pad or truncate to match the declared led_count
|
||||
target_count = self._led_count
|
||||
if target_count > 0 and len(led_colors) != target_count:
|
||||
if len(led_colors) < target_count:
|
||||
pad = np.zeros((target_count - len(led_colors), 3), dtype=np.uint8)
|
||||
led_colors = np.concatenate([led_colors, pad])
|
||||
else:
|
||||
led_colors = led_colors[:target_count]
|
||||
|
||||
# Temporal smoothing
|
||||
smoothing = self._smoothing
|
||||
if (
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
color: #FFC107;
|
||||
}
|
||||
|
||||
|
||||
.inputs-dimmed .edge-led-input {
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -221,6 +221,47 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-collapse {
|
||||
margin-bottom: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.form-collapse > summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
padding: 4px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.form-collapse > summary::-webkit-details-marker { display: none; }
|
||||
|
||||
.form-collapse > summary::before {
|
||||
content: '▶';
|
||||
font-size: 0.6rem;
|
||||
opacity: 0.6;
|
||||
transition: transform 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-collapse[open] > summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.form-collapse > summary:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-collapse-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid var(--danger-color);
|
||||
|
||||
@@ -30,6 +30,7 @@ class CalibrationModal extends Modal {
|
||||
skip_start: this.$('cal-skip-start').value,
|
||||
skip_end: this.$('cal-skip-end').value,
|
||||
border_width: this.$('cal-border-width').value,
|
||||
led_count: this.$('cal-css-led-count').value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,6 +113,7 @@ export async function showCalibration(deviceId) {
|
||||
|
||||
document.getElementById('calibration-device-id').value = device.id;
|
||||
document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
|
||||
document.getElementById('cal-css-led-count-group').style.display = 'none';
|
||||
|
||||
document.getElementById('cal-start-position').value = calibration.start_position;
|
||||
document.getElementById('cal-layout').value = calibration.layout;
|
||||
@@ -216,6 +218,11 @@ export async function showCSSCalibration(cssId) {
|
||||
const preview = document.querySelector('.calibration-preview');
|
||||
preview.style.aspectRatio = '';
|
||||
document.getElementById('cal-device-led-count-inline').textContent = '—';
|
||||
const ledCountGroup = document.getElementById('cal-css-led-count-group');
|
||||
ledCountGroup.style.display = '';
|
||||
const calLeds = (calibration.leds_top || 0) + (calibration.leds_right || 0) +
|
||||
(calibration.leds_bottom || 0) + (calibration.leds_left || 0);
|
||||
document.getElementById('cal-css-led-count').value = source.led_count || calLeds || 0;
|
||||
|
||||
document.getElementById('cal-start-position').value = calibration.start_position || 'bottom_left';
|
||||
document.getElementById('cal-layout').value = calibration.layout || 'clockwise';
|
||||
@@ -286,8 +293,17 @@ export function updateCalibrationPreview() {
|
||||
parseInt(document.getElementById('cal-left-leds').value || 0);
|
||||
const totalEl = document.querySelector('.preview-screen-total');
|
||||
const inCSS = _isCSS();
|
||||
const deviceCount = inCSS ? null : parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0);
|
||||
const mismatch = !inCSS && total !== deviceCount;
|
||||
const declaredCount = inCSS
|
||||
? parseInt(document.getElementById('cal-css-led-count').value || 0)
|
||||
: parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0);
|
||||
if (inCSS) {
|
||||
document.getElementById('cal-device-led-count-inline').textContent = declaredCount || '—';
|
||||
}
|
||||
// In device mode: calibration total must exactly equal device LED count
|
||||
// In CSS mode: warn only if calibrated LEDs exceed the declared total (padding handles the rest)
|
||||
const mismatch = inCSS
|
||||
? (declaredCount > 0 && total > declaredCount)
|
||||
: (total !== declaredCount);
|
||||
document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total;
|
||||
if (totalEl) totalEl.classList.toggle('mismatch', mismatch);
|
||||
|
||||
@@ -831,10 +847,18 @@ export async function saveCalibration() {
|
||||
const leftLeds = parseInt(document.getElementById('cal-left-leds').value || 0);
|
||||
const total = topLeds + rightLeds + bottomLeds + leftLeds;
|
||||
|
||||
const declaredLedCount = cssMode
|
||||
? parseInt(document.getElementById('cal-css-led-count').value) || 0
|
||||
: parseInt(document.getElementById('cal-device-led-count-inline').textContent) || 0;
|
||||
if (!cssMode) {
|
||||
const deviceLedCount = parseInt(document.getElementById('cal-device-led-count-inline').textContent);
|
||||
if (total !== deviceLedCount) {
|
||||
error.textContent = `Total LEDs (${total}) must equal device LED count (${deviceLedCount})`;
|
||||
if (total !== declaredLedCount) {
|
||||
error.textContent = `Total LEDs (${total}) must equal device LED count (${declaredLedCount})`;
|
||||
error.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (declaredLedCount > 0 && total > declaredLedCount) {
|
||||
error.textContent = `Calibrated LEDs (${total}) exceed total LED count (${declaredLedCount})`;
|
||||
error.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
@@ -862,7 +886,7 @@ export async function saveCalibration() {
|
||||
if (cssMode) {
|
||||
response = await fetchWithAuth(`/color-strip-sources/${cssId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ calibration }),
|
||||
body: JSON.stringify({ calibration, led_count: declaredLedCount }),
|
||||
});
|
||||
} else {
|
||||
response = await fetchWithAuth(`/devices/${deviceId}/calibration`, {
|
||||
|
||||
@@ -22,6 +22,7 @@ class CSSEditorModal extends Modal {
|
||||
brightness: document.getElementById('css-editor-brightness').value,
|
||||
saturation: document.getElementById('css-editor-saturation').value,
|
||||
gamma: document.getElementById('css-editor-gamma').value,
|
||||
led_count: document.getElementById('css-editor-led-count').value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -35,7 +36,8 @@ export function createColorStripCard(source, pictureSourceMap) {
|
||||
? pictureSourceMap[source.picture_source_id].name
|
||||
: source.picture_source_id || '—';
|
||||
const cal = source.calibration || {};
|
||||
const ledCount = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
|
||||
const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
|
||||
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
|
||||
|
||||
return `
|
||||
<div class="card" data-css-id="${source.id}">
|
||||
@@ -107,6 +109,8 @@ export async function showCSSEditor(cssId = null) {
|
||||
document.getElementById('css-editor-gamma').value = gamma;
|
||||
document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2);
|
||||
|
||||
document.getElementById('css-editor-led-count').value = css.led_count ?? 0;
|
||||
|
||||
document.getElementById('css-editor-title').textContent = t('color_strip.edit');
|
||||
} else {
|
||||
document.getElementById('css-editor-id').value = '';
|
||||
@@ -122,6 +126,7 @@ export async function showCSSEditor(cssId = null) {
|
||||
document.getElementById('css-editor-saturation-value').textContent = '1.00';
|
||||
document.getElementById('css-editor-gamma').value = 1.0;
|
||||
document.getElementById('css-editor-gamma-value').textContent = '1.00';
|
||||
document.getElementById('css-editor-led-count').value = 0;
|
||||
document.getElementById('css-editor-title').textContent = t('color_strip.add');
|
||||
}
|
||||
|
||||
@@ -160,6 +165,7 @@ export async function saveCSSEditor() {
|
||||
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
||||
saturation: parseFloat(document.getElementById('css-editor-saturation').value),
|
||||
gamma: parseFloat(document.getElementById('css-editor-gamma').value),
|
||||
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -100,9 +100,7 @@ export function createDeviceCard(device) {
|
||||
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
||||
⚙️
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="showCalibration('${device.id}')" title="${t('device.button.calibrate')}">
|
||||
📐
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -552,6 +552,7 @@
|
||||
"color_strip.interpolation.dominant": "Dominant",
|
||||
"color_strip.smoothing": "Smoothing:",
|
||||
"color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
|
||||
"color_strip.color_corrections": "Color Corrections",
|
||||
"color_strip.brightness": "Brightness:",
|
||||
"color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.",
|
||||
"color_strip.saturation": "Saturation:",
|
||||
@@ -561,6 +562,8 @@
|
||||
"color_strip.test_device": "Test on Device:",
|
||||
"color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles",
|
||||
"color_strip.leds": "LED count",
|
||||
"color_strip.led_count": "LED Count:",
|
||||
"color_strip.led_count.hint": "Total number of LEDs on the physical strip. Set to 0 to use the sum from calibration. Useful when the strip has LEDs behind the TV that are not mapped to screen edges — those LEDs will be sent black.",
|
||||
"color_strip.created": "Color strip source created",
|
||||
"color_strip.updated": "Color strip source updated",
|
||||
"color_strip.deleted": "Color strip source deleted",
|
||||
|
||||
@@ -552,6 +552,7 @@
|
||||
"color_strip.interpolation.dominant": "Доминирующий",
|
||||
"color_strip.smoothing": "Сглаживание:",
|
||||
"color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.",
|
||||
"color_strip.color_corrections": "Цветокоррекция",
|
||||
"color_strip.brightness": "Яркость:",
|
||||
"color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.",
|
||||
"color_strip.saturation": "Насыщенность:",
|
||||
@@ -561,6 +562,8 @@
|
||||
"color_strip.test_device": "Тестировать на устройстве:",
|
||||
"color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку",
|
||||
"color_strip.leds": "Количество светодиодов",
|
||||
"color_strip.led_count": "Количество LED:",
|
||||
"color_strip.led_count.hint": "Общее число светодиодов на физической полосе. 0 = взять из калибровки. Укажите явно, если на полосе есть светодиоды за телевизором, не привязанные к краям экрана — им будет отправлен чёрный цвет.",
|
||||
"color_strip.created": "Источник цветовой полосы создан",
|
||||
"color_strip.updated": "Источник цветовой полосы обновлён",
|
||||
"color_strip.deleted": "Источник цветовой полосы удалён",
|
||||
|
||||
@@ -20,7 +20,6 @@ from wled_controller.core.capture.calibration import (
|
||||
CalibrationConfig,
|
||||
calibration_from_dict,
|
||||
calibration_to_dict,
|
||||
create_default_calibration,
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +52,7 @@ class ColorStripSource:
|
||||
"smoothing": None,
|
||||
"interpolation_mode": None,
|
||||
"calibration": None,
|
||||
"led_count": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -82,7 +82,7 @@ class ColorStripSource:
|
||||
calibration = (
|
||||
calibration_from_dict(calibration_data)
|
||||
if calibration_data
|
||||
else create_default_calibration(0)
|
||||
else CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||
)
|
||||
|
||||
# Only "picture" type for now; extend with elif branches for future types
|
||||
@@ -97,6 +97,7 @@ class ColorStripSource:
|
||||
smoothing=data["smoothing"] if data.get("smoothing") is not None else 0.3,
|
||||
interpolation_mode=data.get("interpolation_mode") or "average",
|
||||
calibration=calibration,
|
||||
led_count=data.get("led_count") or 0,
|
||||
)
|
||||
|
||||
|
||||
@@ -115,7 +116,10 @@ class PictureColorStripSource(ColorStripSource):
|
||||
gamma: float = 1.0 # 1.0 = no correction; <1 = brighter, >1 = darker mids
|
||||
smoothing: float = 0.3 # temporal smoothing (0.0 = none, 1.0 = full)
|
||||
interpolation_mode: str = "average" # "average" | "median" | "dominant"
|
||||
calibration: CalibrationConfig = field(default_factory=lambda: create_default_calibration(0))
|
||||
calibration: CalibrationConfig = field(
|
||||
default_factory=lambda: CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||
)
|
||||
led_count: int = 0 # explicit LED count; 0 = auto (derived from calibration)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
@@ -127,4 +131,5 @@ class PictureColorStripSource(ColorStripSource):
|
||||
d["smoothing"] = self.smoothing
|
||||
d["interpolation_mode"] = self.interpolation_mode
|
||||
d["calibration"] = calibration_to_dict(self.calibration)
|
||||
d["led_count"] = self.led_count
|
||||
return d
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from wled_controller.core.capture.calibration import calibration_to_dict
|
||||
from wled_controller.core.capture.calibration import CalibrationConfig, calibration_to_dict
|
||||
from wled_controller.storage.color_strip_source import (
|
||||
ColorStripSource,
|
||||
PictureColorStripSource,
|
||||
@@ -98,6 +98,7 @@ class ColorStripStore:
|
||||
smoothing: float = 0.3,
|
||||
interpolation_mode: str = "average",
|
||||
calibration=None,
|
||||
led_count: int = 0,
|
||||
description: Optional[str] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Create a new color strip source.
|
||||
@@ -105,8 +106,6 @@ class ColorStripStore:
|
||||
Raises:
|
||||
ValueError: If validation fails
|
||||
"""
|
||||
from wled_controller.core.capture.calibration import create_default_calibration
|
||||
|
||||
if not name or not name.strip():
|
||||
raise ValueError("Name is required")
|
||||
|
||||
@@ -115,7 +114,7 @@ class ColorStripStore:
|
||||
raise ValueError(f"Color strip source with name '{name}' already exists")
|
||||
|
||||
if calibration is None:
|
||||
calibration = create_default_calibration(0)
|
||||
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||
|
||||
source_id = f"css_{uuid.uuid4().hex[:8]}"
|
||||
now = datetime.utcnow()
|
||||
@@ -135,6 +134,7 @@ class ColorStripStore:
|
||||
smoothing=smoothing,
|
||||
interpolation_mode=interpolation_mode,
|
||||
calibration=calibration,
|
||||
led_count=led_count,
|
||||
)
|
||||
|
||||
self._sources[source_id] = source
|
||||
@@ -155,6 +155,7 @@ class ColorStripStore:
|
||||
smoothing: Optional[float] = None,
|
||||
interpolation_mode: Optional[str] = None,
|
||||
calibration=None,
|
||||
led_count: Optional[int] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Update an existing color strip source.
|
||||
@@ -193,6 +194,8 @@ class ColorStripStore:
|
||||
source.interpolation_mode = interpolation_mode
|
||||
if calibration is not None:
|
||||
source.calibration = calibration
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
|
||||
source.updated_at = datetime.utcnow()
|
||||
self._save()
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.test_device.hint">Select a device to send test pixels to when clicking edge toggles</small>
|
||||
<select id="calibration-test-device"></select>
|
||||
</div>
|
||||
<!-- LED count input (CSS calibration mode only) -->
|
||||
<div id="cal-css-led-count-group" class="form-group" style="display:none; margin-bottom: 12px; padding: 0 4px;">
|
||||
<div class="label-row">
|
||||
<label for="cal-css-led-count" data-i18n="color_strip.led_count">LED Count:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the physical strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
|
||||
<input type="number" id="cal-css-led-count" min="0" max="1500" step="1" value="0" oninput="updateCalibrationPreview()">
|
||||
</div>
|
||||
|
||||
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
||||
<div style="margin-bottom: 12px; padding: 0 24px;">
|
||||
<div class="calibration-preview">
|
||||
|
||||
@@ -63,40 +63,54 @@
|
||||
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-brightness">
|
||||
<span data-i18n="color_strip.brightness">Brightness:</span>
|
||||
<span id="css-editor-brightness-value">1.00</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
<details class="form-collapse">
|
||||
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
|
||||
<div class="form-collapse-body">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-brightness">
|
||||
<span data-i18n="color_strip.brightness">Brightness:</span>
|
||||
<span id="css-editor-brightness-value">1.00</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
|
||||
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-saturation">
|
||||
<span data-i18n="color_strip.saturation">Saturation:</span>
|
||||
<span id="css-editor-saturation-value">1.00</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
|
||||
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-gamma">
|
||||
<span data-i18n="color_strip.gamma">Gamma:</span>
|
||||
<span id="css-editor-gamma-value">1.00</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)</small>
|
||||
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
</div>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
|
||||
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-saturation">
|
||||
<span data-i18n="color_strip.saturation">Saturation:</span>
|
||||
<span id="css-editor-saturation-value">1.00</span>
|
||||
</label>
|
||||
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
|
||||
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-gamma">
|
||||
<span data-i18n="color_strip.gamma">Gamma:</span>
|
||||
<span id="css-editor-gamma-value">1.00</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)</small>
|
||||
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
|
||||
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
|
||||
</div>
|
||||
|
||||
<div id="css-editor-error" class="error-message" style="display: none;"></div>
|
||||
|
||||
Reference in New Issue
Block a user