Add per-layer LED preview for composite color strip sources
When a target uses a composite CSS source, the LED preview now shows individual layer strips below the blended composite result. Backend stores per-layer color snapshots and sends an extended binary wire format; frontend renders separate canvases with hover labels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -935,3 +935,47 @@ ul.section-tip li {
|
||||
.led-preview-zones:hover .led-preview-zone-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Per-layer LED preview (composite sources) */
|
||||
.led-preview-layers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.led-preview-layer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.led-preview-layer-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
image-rendering: pixelated;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.led-preview-layer-composite .led-preview-layer-canvas {
|
||||
height: 24px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.led-preview-layer-label {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.6rem;
|
||||
font-family: var(--font-mono, monospace);
|
||||
color: #fff;
|
||||
text-shadow: 0 0 3px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.6);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.led-preview-layers:hover .led-preview-layer-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1037,7 +1037,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${_buildLedPreviewHtml(target.id, device, bvsId)}`,
|
||||
${_buildLedPreviewHtml(target.id, device, bvsId, css, colorStripSourceMap)}`,
|
||||
actions: `
|
||||
${isProcessing ? `
|
||||
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('device.button.stop')}">
|
||||
@@ -1211,7 +1211,7 @@ const _ledPreviewLastFrame = {};
|
||||
* For OpenRGB devices in "separate" zone mode with 2+ zones, renders
|
||||
* one canvas per zone with labels. Otherwise, a single canvas.
|
||||
*/
|
||||
function _buildLedPreviewHtml(targetId, device, bvsId) {
|
||||
function _buildLedPreviewHtml(targetId, device, bvsId, cssSource, colorStripSourceMap) {
|
||||
const visible = ledPreviewWebSockets[targetId] ? '' : 'none';
|
||||
const bvsAttr = bvsId ? ' data-has-bvs="1"' : '';
|
||||
|
||||
@@ -1232,6 +1232,26 @@ function _buildLedPreviewHtml(targetId, device, bvsId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for composite source with per-layer preview
|
||||
if (cssSource && cssSource.source_type === 'composite' && cssSource.layers && cssSource.layers.length > 1) {
|
||||
const layerCanvases = cssSource.layers.filter(l => l.enabled !== false).map((l, i) => {
|
||||
const layerSrc = colorStripSourceMap ? colorStripSourceMap[l.source_id] : null;
|
||||
const layerName = layerSrc ? layerSrc.name : l.source_id;
|
||||
return `<div class="led-preview-layer">` +
|
||||
`<canvas class="led-preview-canvas led-preview-layer-canvas" data-layer-idx="${i}"></canvas>` +
|
||||
`<span class="led-preview-layer-label">${escapeHtml(layerName)}</span>` +
|
||||
`</div>`;
|
||||
}).join('');
|
||||
return `<div id="led-preview-panel-${targetId}" class="led-preview-panel led-preview-layers" data-composite="1" style="display:${visible}">` +
|
||||
`<div class="led-preview-layer led-preview-layer-composite">` +
|
||||
`<canvas class="led-preview-canvas led-preview-layer-canvas" data-layer-idx="composite"></canvas>` +
|
||||
`<span class="led-preview-layer-label">${escapeHtml(cssSource.name || 'Composite')}</span>` +
|
||||
`</div>` +
|
||||
layerCanvases +
|
||||
`<span id="led-preview-brightness-${targetId}" class="led-preview-brightness" style="display:none"${bvsAttr}></span>` +
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
// Default: single canvas
|
||||
return `<div id="led-preview-panel-${targetId}" class="led-preview-panel" style="display:${visible}">` +
|
||||
`<canvas id="led-preview-canvas-${targetId}" class="led-preview-canvas"></canvas>` +
|
||||
@@ -1329,18 +1349,44 @@ function connectLedPreviewWS(targetId) {
|
||||
ws.onmessage = (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const raw = new Uint8Array(event.data);
|
||||
// Wire format: [brightness_byte] [R G B R G B ...]
|
||||
const brightness = raw[0];
|
||||
const frame = raw.subarray(1);
|
||||
_ledPreviewLastFrame[targetId] = frame;
|
||||
|
||||
const panel = document.getElementById(`led-preview-panel-${targetId}`);
|
||||
if (panel) {
|
||||
if (panel.dataset.zoneMode === 'separate') {
|
||||
_renderLedStripZones(panel, frame);
|
||||
} else {
|
||||
const canvas = panel.querySelector('.led-preview-canvas');
|
||||
if (canvas) _renderLedStrip(canvas, frame);
|
||||
|
||||
// Composite wire format: [brightness] [0xFE] [layer_count] [led_count_hi] [led_count_lo] [layers...] [composite...]
|
||||
if (raw.length > 4 && raw[1] === 0xFE && panel && panel.dataset.composite === '1') {
|
||||
const layerCount = raw[2];
|
||||
const ledCount = (raw[3] << 8) | raw[4];
|
||||
const rgbSize = ledCount * 3;
|
||||
let offset = 5;
|
||||
|
||||
// Render per-layer canvases (individual layers)
|
||||
const layerCanvases = panel.querySelectorAll('.led-preview-layer-canvas[data-layer-idx]');
|
||||
for (let i = 0; i < layerCount; i++) {
|
||||
const layerRgb = raw.subarray(offset, offset + rgbSize);
|
||||
offset += rgbSize;
|
||||
// layer canvases: idx 0 = "composite", idx 1..N = individual layers
|
||||
const canvas = layerCanvases[i + 1]; // +1 because composite canvas is first
|
||||
if (canvas) _renderLedStrip(canvas, layerRgb);
|
||||
}
|
||||
|
||||
// Final composite result
|
||||
const compositeRgb = raw.subarray(offset, offset + rgbSize);
|
||||
_ledPreviewLastFrame[targetId] = compositeRgb;
|
||||
const compositeCanvas = panel.querySelector('[data-layer-idx="composite"]');
|
||||
if (compositeCanvas) _renderLedStrip(compositeCanvas, compositeRgb);
|
||||
} else {
|
||||
// Standard wire format: [brightness_byte] [R G B R G B ...]
|
||||
const frame = raw.subarray(1);
|
||||
_ledPreviewLastFrame[targetId] = frame;
|
||||
|
||||
if (panel) {
|
||||
if (panel.dataset.zoneMode === 'separate') {
|
||||
_renderLedStripZones(panel, frame);
|
||||
} else {
|
||||
const canvas = panel.querySelector('.led-preview-canvas');
|
||||
if (canvas) _renderLedStrip(canvas, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user