refactor(color-strips): move Key Colors test from lightbox into test-css-source modal
Lint & Test / test (push) Successful in 6m37s

Removes the inlined FPS select and auto-refresh button from the shared
image lightbox and rehosts the Key Colors live preview inside the
dedicated test-css-source modal alongside the other CSS test views.

- Drop initLightbox() / lightbox-fps-select IconSelect — the lightbox no
  longer owns streaming controls.
- Add #css-test-kc-view (canvas + meta) and .css-test-kc-* styles.
- Reroute _testKeyColorsSource() through the existing modal session
  lifecycle so KC, CSPT, and standard CSS tests share teardown paths.
This commit is contained in:
2026-04-22 20:18:46 +03:00
parent 5db6eddcf8
commit be2d5e1670
6 changed files with 214 additions and 154 deletions
+65 -52
View File
@@ -274,6 +274,71 @@
display: block; display: block;
} }
/* Key Colors test view — frame with region overlays */
.css-test-kc-wrap {
position: relative;
width: 100%;
border-radius: 4px;
overflow: hidden;
background: #000;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.css-test-kc-canvas {
display: block;
width: 100%;
height: auto;
max-height: 70vh;
object-fit: contain;
}
.css-test-kc-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 6px 12px;
padding: 8px 0 2px;
font-size: 0.8rem;
color: var(--text-color-muted, #aaa);
}
.css-test-kc-swatch {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 6px 2px 2px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border-color, #333);
border-radius: 999px;
font-size: 0.78rem;
color: var(--text-color, #e0e0e0);
}
.css-test-kc-swatch-chip {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.css-test-kc-swatch-hex {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.72rem;
opacity: 0.65;
}
.css-test-kc-mode {
opacity: 0.7;
font-variant: small-caps;
letter-spacing: 0.04em;
}
.css-test-status { .css-test-status {
text-align: center; text-align: center;
padding: 8px 0; padding: 8px 0;
@@ -1246,58 +1311,6 @@
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
} }
.lightbox-refresh-btn {
position: absolute;
top: 16px;
right: 64px;
background: rgba(255, 255, 255, 0.15);
border: none;
color: white;
font-size: 1.2rem;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
z-index: 1;
}
.lightbox-refresh-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.lightbox-refresh-btn.active {
background: var(--primary-color);
}
.lightbox-fps-select {
position: absolute;
top: 16px;
right: 116px;
background: rgba(0, 0, 0, 0.65);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 6px;
padding: 4px 6px;
font-size: 0.8rem;
cursor: pointer;
z-index: 1;
appearance: none;
-webkit-appearance: none;
text-align: center;
}
.lightbox-fps-select:hover {
background: rgba(255, 255, 255, 0.15);
}
.lightbox-fps-select:focus {
outline: 1px solid var(--primary-color);
}
.lightbox-stats { .lightbox-stats {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
+1 -4
View File
@@ -22,7 +22,7 @@ import {
toggleHint, lockBody, unlockBody, closeLightbox, toggleHint, lockBody, unlockBody, closeLightbox,
showToast, showUndoToast, showConfirm, closeConfirmModal, showToast, showUndoToast, showConfirm, closeConfirmModal,
openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner,
setFieldError, clearFieldError, setupBlurValidation, initLightbox, setFieldError, clearFieldError, setupBlurValidation,
} from './core/ui.ts'; } from './core/ui.ts';
// Layer 3: displays, tutorials // Layer 3: displays, tutorials
@@ -754,9 +754,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize command palette // Initialize command palette
initCommandPalette(); initCommandPalette();
// Enhance lightbox FPS <select> with IconSelect
initLightbox();
// Setup form handler // Setup form handler
const addDeviceForm = queryEl('add-device-form'); const addDeviceForm = queryEl('add-device-form');
if (addDeviceForm) addDeviceForm.addEventListener('submit', handleAddDevice); if (addDeviceForm) addDeviceForm.addEventListener('submit', handleAddDevice);
-24
View File
@@ -5,30 +5,6 @@
import { confirmResolve, setConfirmResolve } from './state.ts'; import { confirmResolve, setConfirmResolve } from './state.ts';
import { API_BASE, getHeaders } from './api.ts'; import { API_BASE, getHeaders } from './api.ts';
import { t } from './i18n.ts'; import { t } from './i18n.ts';
import { IconSelect } from './icon-select.ts';
let _lightboxFpsIconSelect: IconSelect | null = null;
/** Enhance the lightbox FPS <select> with an IconSelect. Idempotent. */
export function initLightbox(): void {
if (_lightboxFpsIconSelect) return;
const sel = document.getElementById('lightbox-fps-select') as HTMLSelectElement | null;
if (!sel) return;
_lightboxFpsIconSelect = new IconSelect({
target: sel,
items: [
{ value: '1', icon: '<span style="font-weight:700">1</span>', label: '1 fps' },
{ value: '2', icon: '<span style="font-weight:700">2</span>', label: '2 fps' },
{ value: '3', icon: '<span style="font-weight:700">3</span>', label: '3 fps' },
{ value: '5', icon: '<span style="font-weight:700">5</span>', label: '5 fps' },
],
columns: 2,
onChange: (val: string) => {
const fn = (window as any).onLightboxFpsChange;
if (typeof fn === 'function') fn(val);
},
});
}
/** Returns true on touch devices where auto-focus would pop up the virtual keyboard */ /** Returns true on touch devices where auto-focus would pop up the virtual keyboard */
export function isTouchDevice() { export function isTouchDevice() {
@@ -7,7 +7,7 @@ import { fetchWithAuth, escapeHtml } from '../../core/api.ts';
import { logError } from '../../core/log.ts'; import { logError } from '../../core/log.ts';
import { colorStripSourcesCache } from '../../core/state.ts'; import { colorStripSourcesCache } from '../../core/state.ts';
import { t } from '../../core/i18n.ts'; import { t } from '../../core/i18n.ts';
import { showToast, openLightbox, closeLightbox } from '../../core/ui.ts'; import { showToast } from '../../core/ui.ts';
import { createFpsSparkline } from '../../core/chart-utils.ts'; import { createFpsSparkline } from '../../core/chart-utils.ts';
import { import {
getColorStripIcon, getColorStripIcon,
@@ -97,6 +97,10 @@ let _cssTestTransientConfig: any = null;
const _CSS_TEST_LED_KEY = 'css_test_led_count'; const _CSS_TEST_LED_KEY = 'css_test_led_count';
const _CSS_TEST_FPS_KEY = 'css_test_fps'; const _CSS_TEST_FPS_KEY = 'css_test_fps';
const _CSS_TEST_KC_FPS_KEY = 'css_test_kc_fps';
const _CSS_TEST_KC_FPS_DEFAULT = 5;
const _CSS_TEST_KC_FPS_MIN = 1;
const _CSS_TEST_KC_FPS_MAX = 30;
let _cssTestWs: WebSocket | null = null; let _cssTestWs: WebSocket | null = null;
let _cssTestRaf: number | null = null; let _cssTestRaf: number | null = null;
let _cssTestLatestRgb: Uint8Array | null = null; let _cssTestLatestRgb: Uint8Array | null = null;
@@ -109,6 +113,7 @@ let _cssTestNotificationIds: string[] = []; // notification source IDs to fire (
let _cssTestCSPTMode: boolean = false; // true when testing a CSPT template let _cssTestCSPTMode: boolean = false; // true when testing a CSPT template
let _cssTestCSPTId: string | null = null; // CSPT template ID when in CSPT mode let _cssTestCSPTId: string | null = null; // CSPT template ID when in CSPT mode
let _cssTestIsApiInput: boolean = false; let _cssTestIsApiInput: boolean = false;
let _cssTestIsKeyColors: boolean = false;
let _cssTestFpsTimestamps: number[] = []; // raw timestamps for current-second FPS calculation let _cssTestFpsTimestamps: number[] = []; // raw timestamps for current-second FPS calculation
let _cssTestFpsActualHistory: number[] = []; // rolling FPS samples for sparkline let _cssTestFpsActualHistory: number[] = []; // rolling FPS samples for sparkline
let _cssTestFpsChart: any = null; let _cssTestFpsChart: any = null;
@@ -125,6 +130,11 @@ function _getCssTestFps() {
return (stored >= 1 && stored <= 60) ? stored : 20; return (stored >= 1 && stored <= 60) ? stored : 20;
} }
function _getKCTestFps() {
const stored = parseInt(localStorage.getItem(_CSS_TEST_KC_FPS_KEY) ?? '', 10);
return (stored >= _CSS_TEST_KC_FPS_MIN && stored <= _CSS_TEST_KC_FPS_MAX) ? stored : _CSS_TEST_KC_FPS_DEFAULT;
}
function _populateCssTestSourceSelector(preselectId: any) { function _populateCssTestSourceSelector(preselectId: any) {
const sources = (colorStripSourcesCache.data || []) as any[]; const sources = (colorStripSourcesCache.data || []) as any[];
const nonProcessed = sources.filter(s => s.source_type !== 'processed'); const nonProcessed = sources.filter(s => s.source_type !== 'processed');
@@ -162,81 +172,139 @@ export function testColorStrip(sourceId: string) {
} }
let _kcTestWs: WebSocket | null = null; let _kcTestWs: WebSocket | null = null;
const _kcTestCanvas = document.createElement('canvas');
const BORDER_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96e6a1', '#dda0dd', '#f9ca24', '#ff9ff3', '#54a0ff']; const BORDER_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96e6a1', '#dda0dd', '#f9ca24', '#ff9ff3', '#54a0ff'];
function _testKeyColorsSource(sourceId: string) { function _testKeyColorsSource(sourceId: string) {
// Show lightbox with spinner _cssTestCSPTMode = false;
const lightbox = document.getElementById('image-lightbox')!; _cssTestCSPTId = null;
const spinner = lightbox.querySelector('.lightbox-spinner') as HTMLElement | null; _cssTestIsApiInput = false;
const content = lightbox.querySelector('.lightbox-content') as HTMLElement | null; _cssTestIsKeyColors = true;
if (content) content.style.width = '90vw'; // Fill viewport for KC preview _cssTestSourceId = sourceId;
const img = document.getElementById('lightbox-image') as HTMLImageElement;
img.src = '';
img.style.display = 'none'; // Hide until first frame arrives
if (spinner) spinner.style.display = '';
document.getElementById('lightbox-stats')!.style.display = 'none';
lightbox.classList.add('active');
// Close any previous WS // Close any previous sessions
if (_cssTestWs) { _cssTestWs.close(); _cssTestWs = null; }
if (_kcTestWs) { _kcTestWs.close(); _kcTestWs = null; }
if (_cssTestRaf) { cancelAnimationFrame(_cssTestRaf); _cssTestRaf = null; }
_cssTestLatestRgb = null;
_cssTestMeta = null;
_cssTestLayerData = null;
const modal = document.getElementById('test-css-source-modal') as HTMLElement | null;
if (!modal) return;
modal.style.display = 'flex';
modal.onclick = (e) => { if (e.target === modal) closeTestCssSourceModal(); };
// Show only the KC view; hide all others
(document.getElementById('css-test-strip-view') as HTMLElement).style.display = 'none';
(document.getElementById('css-test-rect-view') as HTMLElement).style.display = 'none';
(document.getElementById('css-test-layers-view') as HTMLElement).style.display = 'none';
(document.getElementById('css-test-kc-view') as HTMLElement).style.display = '';
(document.getElementById('css-test-fps-chart-group') as HTMLElement).style.display = 'none';
// CSPT input selector is not relevant for KC
const csptGroup = document.getElementById('css-test-cspt-input-group') as HTMLElement | null;
if (csptGroup) csptGroup.style.display = 'none';
// LED count doesn't apply to KC — hide LED group; keep FPS input visible
(document.getElementById('css-test-led-fps-group') as HTMLElement).style.display = '';
(document.getElementById('css-test-led-group') as HTMLElement).style.display = 'none';
const fpsInput = document.getElementById('css-test-fps-input') as HTMLInputElement | null;
if (fpsInput) {
fpsInput.min = String(_CSS_TEST_KC_FPS_MIN);
fpsInput.max = String(_CSS_TEST_KC_FPS_MAX);
fpsInput.value = String(_getKCTestFps());
fpsInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } };
}
// Widen modal to give the frame room to breathe
const modalContent = modal.querySelector('.modal-content') as HTMLElement | null;
if (modalContent) modalContent.style.maxWidth = '900px';
// Clear any stale KC state
const canvas = document.getElementById('css-test-kc-canvas') as HTMLCanvasElement | null;
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
}
const metaEl = document.getElementById('css-test-kc-meta') as HTMLElement | null;
if (metaEl) metaEl.innerHTML = '';
const statusEl = document.getElementById('css-test-status') as HTMLElement;
statusEl.textContent = t('color_strip.test.connecting');
statusEl.style.display = '';
_kcTestConnect(sourceId, _getKCTestFps());
}
function _kcTestConnect(sourceId: string, fps: number) {
if (_kcTestWs) { _kcTestWs.close(); _kcTestWs = null; } if (_kcTestWs) { _kcTestWs.close(); _kcTestWs = null; }
// Build WS URL const gen = ++_cssTestGeneration;
const loc = window.location; const loc = window.location;
const wsProto = loc.protocol === 'https:' ? 'wss:' : 'ws:'; const wsProto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProto}//${loc.host}/api/v1/color-strip-sources/${sourceId}/key-colors/test/ws?fps=5&preview_width=960`; const clamped = Math.max(_CSS_TEST_KC_FPS_MIN, Math.min(_CSS_TEST_KC_FPS_MAX, fps));
const wsUrl = `${wsProto}//${loc.host}/api/v1/color-strip-sources/${sourceId}/key-colors/test/ws?fps=${clamped}&preview_width=960`;
openAuthedWs(wsUrl).then((ws) => { openAuthedWs(wsUrl).then((ws) => {
if (gen !== _cssTestGeneration) { ws.close(); return; }
_kcTestWs = ws; _kcTestWs = ws;
ws.onmessage = (ev) => { ws.onmessage = (ev) => {
if (gen !== _cssTestGeneration) return;
try { try {
const data = JSON.parse(ev.data); const data = JSON.parse(ev.data);
if (data.type === 'frame') { if (data.type === 'frame') {
_renderKCTestFrame(data); _renderKCTestFrame(data);
const statusEl = document.getElementById('css-test-status') as HTMLElement | null;
if (statusEl) statusEl.style.display = 'none';
} }
} catch (err) { logError('color-strips.test.kcWs.message', err); } } catch (err) { logError('color-strips.test.kcWs.message', err); }
}; };
ws.onerror = () => { ws.onerror = () => {
showToast('Key Colors test connection failed', 'error'); if (gen !== _cssTestGeneration) return;
closeLightbox(); const statusEl = document.getElementById('css-test-status') as HTMLElement;
statusEl.textContent = t('color_strip.test.error');
statusEl.style.display = '';
}; };
ws.onclose = () => { ws.onclose = (ev) => {
if (gen !== _cssTestGeneration) return;
_kcTestWs = null; _kcTestWs = null;
}; if (ev.reason) {
const statusEl = document.getElementById('css-test-status') as HTMLElement;
// Stop WS when lightbox closes statusEl.textContent = ev.reason;
const origClose = (window as any).closeLightbox; statusEl.style.display = '';
lightbox.onclick = (e) => { }
if ((e.target as HTMLElement).closest('.lightbox-content')) return;
if (_kcTestWs) { _kcTestWs.close(); _kcTestWs = null; }
closeLightbox();
}; };
}).catch(() => { }).catch(() => {
showToast('Key Colors test connection failed', 'error'); if (gen !== _cssTestGeneration) return;
closeLightbox(); const statusEl = document.getElementById('css-test-status') as HTMLElement;
statusEl.textContent = t('color_strip.test.error');
statusEl.style.display = '';
}); });
} }
function _renderKCTestFrame(data: any) { function _renderKCTestFrame(data: any) {
const rects = data.rectangles || []; const rects = data.rectangles || [];
const mode = data.interpolation_mode || 'average'; const mode = data.interpolation_mode || 'average';
const canvas = document.getElementById('css-test-kc-canvas') as HTMLCanvasElement | null;
const metaEl = document.getElementById('css-test-kc-meta') as HTMLElement | null;
if (!canvas) return;
// Draw frame + rectangles onto offscreen canvas
const tmpImg = new Image(); const tmpImg = new Image();
tmpImg.onload = () => { tmpImg.onload = () => {
_kcTestCanvas.width = tmpImg.naturalWidth; canvas.width = tmpImg.naturalWidth;
_kcTestCanvas.height = tmpImg.naturalHeight; canvas.height = tmpImg.naturalHeight;
const ctx = _kcTestCanvas.getContext('2d')!; const ctx = canvas.getContext('2d')!;
ctx.drawImage(tmpImg, 0, 0); ctx.drawImage(tmpImg, 0, 0);
rects.forEach((r: any, i: number) => { rects.forEach((r: any, i: number) => {
const x = r.x * _kcTestCanvas.width; const x = r.x * canvas.width;
const y = r.y * _kcTestCanvas.height; const y = r.y * canvas.height;
const w = r.width * _kcTestCanvas.width; const w = r.width * canvas.width;
const h = r.height * _kcTestCanvas.height; const h = r.height * canvas.height;
const borderColor = BORDER_COLORS[i % BORDER_COLORS.length]; const borderColor = BORDER_COLORS[i % BORDER_COLORS.length];
ctx.fillStyle = r.color.hex + '33'; ctx.fillStyle = r.color.hex + '33';
@@ -258,36 +326,19 @@ function _renderKCTestFrame(data: any) {
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.strokeRect(x + w - 24, y + 2, 22, 22); ctx.strokeRect(x + w - 24, y + 2, 22, 22);
}); });
// Update lightbox image directly (use data URL for full-size display)
const lbImg = document.getElementById('lightbox-image') as HTMLImageElement;
if (lbImg) {
lbImg.src = _kcTestCanvas.toDataURL('image/jpeg', 0.9);
lbImg.style.display = '';
lbImg.style.maxWidth = '100%';
lbImg.style.width = '100%';
}
// Hide spinner after first frame
const spinner = document.querySelector('#image-lightbox .lightbox-spinner') as HTMLElement | null;
if (spinner) spinner.style.display = 'none';
// Update swatches
const statsEl = document.getElementById('lightbox-stats')!;
const swatches = rects.map((r: any) =>
`<div style="display:inline-flex;align-items:center;gap:6px;margin:4px 8px;">
<span style="display:inline-block;width:20px;height:20px;background:${r.color.hex};border:1px solid #888;border-radius:3px;"></span>
<span>${escapeHtml(r.name)}</span>
<small style="opacity:0.6;">${r.color.hex}</small>
</div>`
).join('');
statsEl.innerHTML = `
<div style="display:flex;flex-wrap:wrap;justify-content:center;">${swatches}</div>
<div style="margin-top:4px;opacity:0.6;text-align:center;">Mode: ${mode} | ${rects.length} region${rects.length !== 1 ? 's' : ''}</div>
`;
statsEl.style.display = '';
}; };
tmpImg.src = data.image; tmpImg.src = data.image;
if (metaEl) {
const swatches = rects.map((r: any) =>
`<span class="css-test-kc-swatch">
<span class="css-test-kc-swatch-chip" style="background:${r.color.hex}"></span>
<span>${escapeHtml(r.name)}</span>
<span class="css-test-kc-swatch-hex">${r.color.hex}</span>
</span>`
).join('');
metaEl.innerHTML = `${swatches}<span class="css-test-kc-mode">${mode} · ${rects.length}&nbsp;region${rects.length !== 1 ? 's' : ''}</span>`;
}
} }
export async function testCSPT(templateId: string) { export async function testCSPT(templateId: string) {
@@ -310,10 +361,12 @@ export async function testCSPT(templateId: string) {
function _openTestModal(sourceId: string) { function _openTestModal(sourceId: string) {
// Clean up any previous session fully // Clean up any previous session fully
if (_cssTestWs) { _cssTestWs.close(); _cssTestWs = null; } if (_cssTestWs) { _cssTestWs.close(); _cssTestWs = null; }
if (_kcTestWs) { _kcTestWs.close(); _kcTestWs = null; }
if (_cssTestRaf) { cancelAnimationFrame(_cssTestRaf); _cssTestRaf = null; } if (_cssTestRaf) { cancelAnimationFrame(_cssTestRaf); _cssTestRaf = null; }
_cssTestLatestRgb = null; _cssTestLatestRgb = null;
_cssTestMeta = null; _cssTestMeta = null;
_cssTestIsComposite = false; _cssTestIsComposite = false;
_cssTestIsKeyColors = false;
_cssTestLayerData = null; _cssTestLayerData = null;
const modal = document.getElementById('test-css-source-modal') as HTMLElement | null; const modal = document.getElementById('test-css-source-modal') as HTMLElement | null;
@@ -326,6 +379,8 @@ function _openTestModal(sourceId: string) {
(document.getElementById('css-test-strip-view') as HTMLElement).style.display = 'none'; (document.getElementById('css-test-strip-view') as HTMLElement).style.display = 'none';
(document.getElementById('css-test-rect-view') as HTMLElement).style.display = 'none'; (document.getElementById('css-test-rect-view') as HTMLElement).style.display = 'none';
(document.getElementById('css-test-layers-view') as HTMLElement).style.display = 'none'; (document.getElementById('css-test-layers-view') as HTMLElement).style.display = 'none';
const kcView = document.getElementById('css-test-kc-view') as HTMLElement | null;
if (kcView) kcView.style.display = 'none';
// Clear all test canvases to prevent stale frames from previous sessions // Clear all test canvases to prevent stale frames from previous sessions
modal.querySelectorAll('canvas').forEach(c => { modal.querySelectorAll('canvas').forEach(c => {
const ctx = c.getContext('2d'); const ctx = c.getContext('2d');
@@ -363,8 +418,12 @@ function _openTestModal(sourceId: string) {
const fpsVal = _getCssTestFps(); const fpsVal = _getCssTestFps();
const fpsInput = document.getElementById('css-test-fps-input') as HTMLInputElement | null; const fpsInput = document.getElementById('css-test-fps-input') as HTMLInputElement | null;
fpsInput!.value = fpsVal as any; if (fpsInput) {
fpsInput!.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } }; fpsInput.min = '1';
fpsInput.max = '60';
fpsInput.value = String(fpsVal);
fpsInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); applyCssTestSettings(); } };
}
_cssTestConnect(sourceId, ledCount, fpsVal); _cssTestConnect(sourceId, ledCount, fpsVal);
} }
@@ -621,6 +680,18 @@ function _cssTestUpdateBrightness(values: any) {
export function applyCssTestSettings() { export function applyCssTestSettings() {
if (!_cssTestSourceId) return; if (!_cssTestSourceId) return;
// Key Colors test: FPS only — different range and storage key
if (_cssTestIsKeyColors) {
const fpsInput = document.getElementById('css-test-fps-input') as HTMLInputElement | null;
let fps = parseInt(fpsInput?.value ?? '', 10);
if (isNaN(fps) || fps < _CSS_TEST_KC_FPS_MIN) fps = _CSS_TEST_KC_FPS_MIN;
if (fps > _CSS_TEST_KC_FPS_MAX) fps = _CSS_TEST_KC_FPS_MAX;
if (fpsInput) fpsInput.value = String(fps);
localStorage.setItem(_CSS_TEST_KC_FPS_KEY, String(fps));
_kcTestConnect(_cssTestSourceId, fps);
return;
}
const ledInput = document.getElementById('css-test-led-input') as HTMLInputElement | null; const ledInput = document.getElementById('css-test-led-input') as HTMLInputElement | null;
let leds = parseInt(ledInput?.value ?? '', 10); let leds = parseInt(ledInput?.value ?? '', 10);
if (isNaN(leds) || leds < 1) leds = 1; if (isNaN(leds) || leds < 1) leds = 1;
@@ -1060,11 +1131,13 @@ function _cssTestStopFpsSampling() {
export function closeTestCssSourceModal() { export function closeTestCssSourceModal() {
if (_cssTestWs) { _cssTestWs.close(); _cssTestWs = null; } if (_cssTestWs) { _cssTestWs.close(); _cssTestWs = null; }
if (_kcTestWs) { _kcTestWs.close(); _kcTestWs = null; }
if (_cssTestRaf) { cancelAnimationFrame(_cssTestRaf); _cssTestRaf = null; } if (_cssTestRaf) { cancelAnimationFrame(_cssTestRaf); _cssTestRaf = null; }
_cssTestLatestRgb = null; _cssTestLatestRgb = null;
_cssTestMeta = null; _cssTestMeta = null;
_cssTestSourceId = null; _cssTestSourceId = null;
_cssTestIsComposite = false; _cssTestIsComposite = false;
_cssTestIsKeyColors = false;
_cssTestLayerData = null; _cssTestLayerData = null;
_cssTestNotificationIds = []; _cssTestNotificationIds = [];
_cssTestIsApiInput = false; _cssTestIsApiInput = false;
@@ -44,6 +44,14 @@
<canvas id="css-test-layers-axis" class="css-test-strip-axis"></canvas> <canvas id="css-test-layers-axis" class="css-test-strip-axis"></canvas>
</div> </div>
<!-- Key Colors view (frame + region overlays) -->
<div id="css-test-kc-view" style="display:none">
<div class="css-test-kc-wrap">
<canvas id="css-test-kc-canvas" class="css-test-kc-canvas"></canvas>
</div>
<div id="css-test-kc-meta" class="css-test-kc-meta"></div>
</div>
<!-- CSPT test: input source selector (hidden by default) --> <!-- CSPT test: input source selector (hidden by default) -->
<div id="css-test-cspt-input-group" style="display:none" class="css-test-led-control"> <div id="css-test-cspt-input-group" style="display:none" class="css-test-led-control">
<label for="css-test-cspt-input-select" data-i18n="color_strip.processed.input">Source:</label> <label for="css-test-cspt-input-select" data-i18n="color_strip.processed.input">Source:</label>
@@ -1,13 +1,6 @@
<!-- Image Lightbox --> <!-- Image Lightbox -->
<div id="image-lightbox" class="lightbox" onclick="closeLightbox(event)"> <div id="image-lightbox" class="lightbox" onclick="closeLightbox(event)">
<button class="lightbox-close" onclick="closeLightbox()" title="Close">&#x2715;</button> <button class="lightbox-close" onclick="closeLightbox()" title="Close">&#x2715;</button>
<button id="lightbox-auto-refresh" class="lightbox-refresh-btn" onclick="toggleKCTestAutoRefresh()" title="Stream live" style="display:none">&#x25B6;</button>
<select id="lightbox-fps-select" class="lightbox-fps-select" style="display:none" title="Frames per second">
<option value="1">1 fps</option>
<option value="2">2 fps</option>
<option value="3" selected>3 fps</option>
<option value="5">5 fps</option>
</select>
<div class="lightbox-content"> <div class="lightbox-content">
<img id="lightbox-image" src="" alt="Full size preview"> <img id="lightbox-image" src="" alt="Full size preview">
<div id="lightbox-stats" class="lightbox-stats" style="display: none;"></div> <div id="lightbox-stats" class="lightbox-stats" style="display: none;"></div>