Add partial LED side coverage (edge spans) for calibration
Some checks failed
Validate / validate (push) Failing after 8s
Some checks failed
Validate / validate (push) Failing after 8s
Allow LEDs to cover only a fraction of each screen edge via draggable span bars in the calibration UI. Per-edge start/end (0.0-1.0) values control which portion of the screen border is sampled for LED colors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1057,6 +1057,14 @@ async function showCalibration(deviceId) {
|
||||
document.getElementById('cal-bottom-leds').value = calibration.leds_bottom || 0;
|
||||
document.getElementById('cal-left-leds').value = calibration.leds_left || 0;
|
||||
|
||||
// Initialize edge spans
|
||||
window.edgeSpans = {
|
||||
top: { start: calibration.span_top_start ?? 0, end: calibration.span_top_end ?? 1 },
|
||||
right: { start: calibration.span_right_start ?? 0, end: calibration.span_right_end ?? 1 },
|
||||
bottom: { start: calibration.span_bottom_start ?? 0, end: calibration.span_bottom_end ?? 1 },
|
||||
left: { start: calibration.span_left_start ?? 0, end: calibration.span_left_end ?? 1 },
|
||||
};
|
||||
|
||||
// Snapshot initial values for dirty checking
|
||||
calibrationInitialValues = {
|
||||
start_position: calibration.start_position,
|
||||
@@ -1066,6 +1074,7 @@ async function showCalibration(deviceId) {
|
||||
right: String(calibration.leds_right || 0),
|
||||
bottom: String(calibration.leds_bottom || 0),
|
||||
left: String(calibration.leds_left || 0),
|
||||
spans: JSON.stringify(window.edgeSpans),
|
||||
};
|
||||
|
||||
// Initialize test mode state for this device
|
||||
@@ -1079,7 +1088,8 @@ async function showCalibration(deviceId) {
|
||||
modal.style.display = 'flex';
|
||||
lockBody();
|
||||
|
||||
// Render canvas after layout settles
|
||||
// Initialize span drag and render canvas after layout settles
|
||||
initSpanDrag();
|
||||
requestAnimationFrame(() => renderCalibrationCanvas());
|
||||
|
||||
} catch (error) {
|
||||
@@ -1096,7 +1106,8 @@ function isCalibrationDirty() {
|
||||
document.getElementById('cal-top-leds').value !== calibrationInitialValues.top ||
|
||||
document.getElementById('cal-right-leds').value !== calibrationInitialValues.right ||
|
||||
document.getElementById('cal-bottom-leds').value !== calibrationInitialValues.bottom ||
|
||||
document.getElementById('cal-left-leds').value !== calibrationInitialValues.left
|
||||
document.getElementById('cal-left-leds').value !== calibrationInitialValues.left ||
|
||||
JSON.stringify(window.edgeSpans) !== calibrationInitialValues.spans
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1176,7 +1187,8 @@ function updateCalibrationPreview() {
|
||||
}
|
||||
});
|
||||
|
||||
// Render canvas overlay (ticks, arrows, start label)
|
||||
// Position span bars and render canvas overlay
|
||||
updateSpanBars();
|
||||
renderCalibrationCanvas();
|
||||
}
|
||||
|
||||
@@ -1237,12 +1249,16 @@ function renderCalibrationCanvas() {
|
||||
const cw = 56;
|
||||
const ch = 36;
|
||||
|
||||
// Edge midlines (center of each edge bar) - in canvas coords
|
||||
// Span-aware edge geometry: ticks/arrows render only within the span region
|
||||
const spans = window.edgeSpans || {};
|
||||
const edgeLenH = cW - 2 * cw;
|
||||
const edgeLenV = cH - 2 * ch;
|
||||
|
||||
const edgeGeometry = {
|
||||
top: { x1: ox + cw, x2: ox + cW - cw, midY: oy + ch / 2, horizontal: true },
|
||||
bottom: { x1: ox + cw, x2: ox + cW - cw, midY: oy + cH - ch / 2, horizontal: true },
|
||||
left: { y1: oy + ch, y2: oy + cH - ch, midX: ox + cw / 2, horizontal: false },
|
||||
right: { y1: oy + ch, y2: oy + cH - ch, midX: ox + cW - cw / 2, horizontal: false },
|
||||
top: { x1: ox + cw + (spans.top?.start || 0) * edgeLenH, x2: ox + cw + (spans.top?.end || 1) * edgeLenH, midY: oy + ch / 2, horizontal: true },
|
||||
bottom: { x1: ox + cw + (spans.bottom?.start || 0) * edgeLenH, x2: ox + cw + (spans.bottom?.end || 1) * edgeLenH, midY: oy + cH - ch / 2, horizontal: true },
|
||||
left: { y1: oy + ch + (spans.left?.start || 0) * edgeLenV, y2: oy + ch + (spans.left?.end || 1) * edgeLenV, midX: ox + cw / 2, horizontal: false },
|
||||
right: { y1: oy + ch + (spans.right?.start || 0) * edgeLenV, y2: oy + ch + (spans.right?.end || 1) * edgeLenV, midX: ox + cW - cw / 2, horizontal: false },
|
||||
};
|
||||
|
||||
// Axis positions for labels (outside the container bounds, in the padding area)
|
||||
@@ -1397,6 +1413,126 @@ function renderCalibrationCanvas() {
|
||||
|
||||
}
|
||||
|
||||
function updateSpanBars() {
|
||||
const spans = window.edgeSpans || {};
|
||||
['top', 'right', 'bottom', 'left'].forEach(edge => {
|
||||
const bar = document.querySelector(`.edge-span-bar[data-edge="${edge}"]`);
|
||||
if (!bar) return;
|
||||
const span = spans[edge] || { start: 0, end: 1 };
|
||||
const edgeEl = bar.parentElement;
|
||||
const isHorizontal = (edge === 'top' || edge === 'bottom');
|
||||
|
||||
if (isHorizontal) {
|
||||
const totalWidth = edgeEl.clientWidth;
|
||||
bar.style.left = (span.start * totalWidth) + 'px';
|
||||
bar.style.width = ((span.end - span.start) * totalWidth) + 'px';
|
||||
} else {
|
||||
const totalHeight = edgeEl.clientHeight;
|
||||
bar.style.top = (span.start * totalHeight) + 'px';
|
||||
bar.style.height = ((span.end - span.start) * totalHeight) + 'px';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSpanDrag() {
|
||||
const MIN_SPAN = 0.05;
|
||||
|
||||
document.querySelectorAll('.edge-span-bar').forEach(bar => {
|
||||
const edge = bar.dataset.edge;
|
||||
const isHorizontal = (edge === 'top' || edge === 'bottom');
|
||||
|
||||
// Prevent edge click-through when interacting with span bar
|
||||
bar.addEventListener('click', e => e.stopPropagation());
|
||||
|
||||
// Handle resize via handles
|
||||
bar.querySelectorAll('.edge-span-handle').forEach(handle => {
|
||||
handle.addEventListener('mousedown', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const handleType = handle.dataset.handle;
|
||||
const edgeEl = bar.parentElement;
|
||||
const rect = edgeEl.getBoundingClientRect();
|
||||
|
||||
function onMouseMove(ev) {
|
||||
const span = window.edgeSpans[edge];
|
||||
let fraction;
|
||||
if (isHorizontal) {
|
||||
fraction = (ev.clientX - rect.left) / rect.width;
|
||||
} else {
|
||||
fraction = (ev.clientY - rect.top) / rect.height;
|
||||
}
|
||||
fraction = Math.max(0, Math.min(1, fraction));
|
||||
|
||||
if (handleType === 'start') {
|
||||
span.start = Math.min(fraction, span.end - MIN_SPAN);
|
||||
} else {
|
||||
span.end = Math.max(fraction, span.start + MIN_SPAN);
|
||||
}
|
||||
|
||||
updateSpanBars();
|
||||
renderCalibrationCanvas();
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle body drag (move entire span)
|
||||
bar.addEventListener('mousedown', e => {
|
||||
if (e.target.classList.contains('edge-span-handle')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const edgeEl = bar.parentElement;
|
||||
const rect = edgeEl.getBoundingClientRect();
|
||||
const span = window.edgeSpans[edge];
|
||||
const spanWidth = span.end - span.start;
|
||||
|
||||
let startFraction;
|
||||
if (isHorizontal) {
|
||||
startFraction = (e.clientX - rect.left) / rect.width;
|
||||
} else {
|
||||
startFraction = (e.clientY - rect.top) / rect.height;
|
||||
}
|
||||
const offsetInSpan = startFraction - span.start;
|
||||
|
||||
function onMouseMove(ev) {
|
||||
let fraction;
|
||||
if (isHorizontal) {
|
||||
fraction = (ev.clientX - rect.left) / rect.width;
|
||||
} else {
|
||||
fraction = (ev.clientY - rect.top) / rect.height;
|
||||
}
|
||||
|
||||
let newStart = fraction - offsetInSpan;
|
||||
newStart = Math.max(0, Math.min(1 - spanWidth, newStart));
|
||||
span.start = newStart;
|
||||
span.end = newStart + spanWidth;
|
||||
|
||||
updateSpanBars();
|
||||
renderCalibrationCanvas();
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
});
|
||||
|
||||
// Initial positioning
|
||||
updateSpanBars();
|
||||
}
|
||||
|
||||
function setStartPosition(position) {
|
||||
document.getElementById('cal-start-position').value = position;
|
||||
updateCalibrationPreview();
|
||||
@@ -1501,6 +1637,7 @@ async function saveCalibration() {
|
||||
const layout = document.getElementById('cal-layout').value;
|
||||
const offset = parseInt(document.getElementById('cal-offset').value || 0);
|
||||
|
||||
const spans = window.edgeSpans || {};
|
||||
const calibration = {
|
||||
layout: layout,
|
||||
start_position: startPosition,
|
||||
@@ -1508,7 +1645,15 @@ async function saveCalibration() {
|
||||
leds_top: topLeds,
|
||||
leds_right: rightLeds,
|
||||
leds_bottom: bottomLeds,
|
||||
leds_left: leftLeds
|
||||
leds_left: leftLeds,
|
||||
span_top_start: spans.top?.start ?? 0,
|
||||
span_top_end: spans.top?.end ?? 1,
|
||||
span_right_start: spans.right?.start ?? 0,
|
||||
span_right_end: spans.right?.end ?? 1,
|
||||
span_bottom_start: spans.bottom?.start ?? 0,
|
||||
span_bottom_end: spans.bottom?.end ?? 1,
|
||||
span_left_start: spans.left?.start ?? 0,
|
||||
span_left_end: spans.left?.end ?? 1,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -102,18 +102,34 @@
|
||||
|
||||
<!-- Clickable edge bars with LED count inputs -->
|
||||
<div class="preview-edge edge-top" onclick="toggleTestEdge('top')">
|
||||
<div class="edge-span-bar" data-edge="top">
|
||||
<div class="edge-span-handle edge-span-handle-start" data-edge="top" data-handle="start"></div>
|
||||
<div class="edge-span-handle edge-span-handle-end" data-edge="top" data-handle="end"></div>
|
||||
</div>
|
||||
<input type="number" id="cal-top-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
<div class="preview-edge edge-right" onclick="toggleTestEdge('right')">
|
||||
<div class="edge-span-bar" data-edge="right">
|
||||
<div class="edge-span-handle edge-span-handle-start" data-edge="right" data-handle="start"></div>
|
||||
<div class="edge-span-handle edge-span-handle-end" data-edge="right" data-handle="end"></div>
|
||||
</div>
|
||||
<input type="number" id="cal-right-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
<div class="preview-edge edge-bottom" onclick="toggleTestEdge('bottom')">
|
||||
<div class="edge-span-bar" data-edge="bottom">
|
||||
<div class="edge-span-handle edge-span-handle-start" data-edge="bottom" data-handle="start"></div>
|
||||
<div class="edge-span-handle edge-span-handle-end" data-edge="bottom" data-handle="end"></div>
|
||||
</div>
|
||||
<input type="number" id="cal-bottom-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
<div class="preview-edge edge-left" onclick="toggleTestEdge('left')">
|
||||
<div class="edge-span-bar" data-edge="left">
|
||||
<div class="edge-span-handle edge-span-handle-start" data-edge="left" data-handle="start"></div>
|
||||
<div class="edge-span-handle edge-span-handle-end" data-edge="left" data-handle="end"></div>
|
||||
</div>
|
||||
<input type="number" id="cal-left-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
|
||||
@@ -519,10 +519,15 @@ section {
|
||||
|
||||
.layout-index-label {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.layout-display-label {
|
||||
@@ -546,11 +551,12 @@ section {
|
||||
|
||||
.primary-indicator {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
top: 2px;
|
||||
right: 4px;
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.layout-legend {
|
||||
@@ -1084,6 +1090,98 @@ input:-webkit-autofill:focus {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
/* Edge span bars */
|
||||
.edge-span-bar {
|
||||
position: absolute;
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
||||
border-radius: 3px;
|
||||
cursor: grab;
|
||||
z-index: 1;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.edge-span-bar:hover {
|
||||
background: rgba(76, 175, 80, 0.45);
|
||||
}
|
||||
|
||||
.edge-span-bar:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Horizontal edges: bar spans left-right */
|
||||
.edge-top .edge-span-bar,
|
||||
.edge-bottom .edge-span-bar {
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
/* Vertical edges: bar spans top-bottom */
|
||||
.edge-left .edge-span-bar,
|
||||
.edge-right .edge-span-bar {
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
/* Resize handles */
|
||||
.edge-span-handle {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(76, 175, 80, 0.7);
|
||||
border-radius: 2px;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.edge-span-bar:hover .edge-span-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Horizontal handles */
|
||||
.edge-top .edge-span-handle,
|
||||
.edge-bottom .edge-span-handle {
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
width: 6px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.edge-top .edge-span-handle-start,
|
||||
.edge-bottom .edge-span-handle-start {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.edge-top .edge-span-handle-end,
|
||||
.edge-bottom .edge-span-handle-end {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Vertical handles */
|
||||
.edge-left .edge-span-handle,
|
||||
.edge-right .edge-span-handle {
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.edge-left .edge-span-handle-start,
|
||||
.edge-right .edge-span-handle-start {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.edge-left .edge-span-handle-end,
|
||||
.edge-right .edge-span-handle-end {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Ensure LED input is above span bar */
|
||||
.edge-led-input {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Corner start-position buttons */
|
||||
.preview-corner {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user