refactor(color-strips): move Key Colors test from lightbox into test-css-source modal
Lint & Test / test (push) Successful in 6m37s
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:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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} 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">✕</button>
|
<button class="lightbox-close" onclick="closeLightbox()" title="Close">✕</button>
|
||||||
<button id="lightbox-auto-refresh" class="lightbox-refresh-btn" onclick="toggleKCTestAutoRefresh()" title="Stream live" style="display:none">▶</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user