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:
@@ -110,6 +110,15 @@ class Calibration(BaseModel):
|
||||
leds_right: int = Field(default=0, ge=0, description="Number of LEDs on the right edge")
|
||||
leds_bottom: int = Field(default=0, ge=0, description="Number of LEDs on the bottom edge")
|
||||
leds_left: int = Field(default=0, ge=0, description="Number of LEDs on the left edge")
|
||||
# Per-edge span: fraction of screen side covered by LEDs (0.0–1.0)
|
||||
span_top_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of top edge coverage")
|
||||
span_top_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of top edge coverage")
|
||||
span_right_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of right edge coverage")
|
||||
span_right_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of right edge coverage")
|
||||
span_bottom_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of bottom edge coverage")
|
||||
span_bottom_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of bottom edge coverage")
|
||||
span_left_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of left edge coverage")
|
||||
span_left_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of left edge coverage")
|
||||
|
||||
|
||||
class CalibrationTestModeRequest(BaseModel):
|
||||
|
||||
@@ -65,6 +65,15 @@ class CalibrationConfig:
|
||||
leds_right: int = 0
|
||||
leds_bottom: int = 0
|
||||
leds_left: int = 0
|
||||
# Per-edge span: fraction of screen side covered by LEDs (0.0–1.0)
|
||||
span_top_start: float = 0.0
|
||||
span_top_end: float = 1.0
|
||||
span_right_start: float = 0.0
|
||||
span_right_end: float = 1.0
|
||||
span_bottom_start: float = 0.0
|
||||
span_bottom_end: float = 1.0
|
||||
span_left_start: float = 0.0
|
||||
span_left_end: float = 1.0
|
||||
|
||||
def build_segments(self) -> List[CalibrationSegment]:
|
||||
"""Derive segment list from core parameters."""
|
||||
@@ -99,6 +108,13 @@ class CalibrationConfig:
|
||||
"""Get derived segment list."""
|
||||
return self.build_segments()
|
||||
|
||||
def get_edge_span(self, edge: str) -> tuple[float, float]:
|
||||
"""Get span (start, end) for a given edge."""
|
||||
return (
|
||||
getattr(self, f"span_{edge}_start", 0.0),
|
||||
getattr(self, f"span_{edge}_end", 1.0),
|
||||
)
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Validate calibration configuration.
|
||||
|
||||
@@ -117,6 +133,13 @@ class CalibrationConfig:
|
||||
if count < 0:
|
||||
raise ValueError(f"LED count for {edge} must be non-negative, got {count}")
|
||||
|
||||
for edge in ["top", "right", "bottom", "left"]:
|
||||
start, end = self.get_edge_span(edge)
|
||||
if not (0.0 <= start <= 1.0) or not (0.0 <= end <= 1.0):
|
||||
raise ValueError(f"Span for {edge} must be in [0.0, 1.0], got ({start}, {end})")
|
||||
if end <= start:
|
||||
raise ValueError(f"Span end must be greater than start for {edge}, got ({start}, {end})")
|
||||
|
||||
return True
|
||||
|
||||
def get_total_leds(self) -> int:
|
||||
@@ -202,6 +225,20 @@ class PixelMapper:
|
||||
else: # left
|
||||
edge_pixels = border_pixels.left
|
||||
|
||||
# Slice to span region if not full coverage
|
||||
span_start, span_end = self.calibration.get_edge_span(edge_name)
|
||||
if span_start > 0.0 or span_end < 1.0:
|
||||
if edge_name in ("top", "bottom"):
|
||||
total_w = edge_pixels.shape[1]
|
||||
s = int(span_start * total_w)
|
||||
e = int(span_end * total_w)
|
||||
edge_pixels = edge_pixels[:, s:e, :]
|
||||
else:
|
||||
total_h = edge_pixels.shape[0]
|
||||
s = int(span_start * total_h)
|
||||
e = int(span_end * total_h)
|
||||
edge_pixels = edge_pixels[s:e, :, :]
|
||||
|
||||
# Divide edge into segments matching LED count
|
||||
try:
|
||||
pixel_segments = get_edge_segments(
|
||||
@@ -333,6 +370,14 @@ def calibration_from_dict(data: dict) -> CalibrationConfig:
|
||||
leds_right=data.get("leds_right", 0),
|
||||
leds_bottom=data.get("leds_bottom", 0),
|
||||
leds_left=data.get("leds_left", 0),
|
||||
span_top_start=data.get("span_top_start", 0.0),
|
||||
span_top_end=data.get("span_top_end", 1.0),
|
||||
span_right_start=data.get("span_right_start", 0.0),
|
||||
span_right_end=data.get("span_right_end", 1.0),
|
||||
span_bottom_start=data.get("span_bottom_start", 0.0),
|
||||
span_bottom_end=data.get("span_bottom_end", 1.0),
|
||||
span_left_start=data.get("span_left_start", 0.0),
|
||||
span_left_end=data.get("span_left_end", 1.0),
|
||||
)
|
||||
|
||||
config.validate()
|
||||
@@ -355,7 +400,7 @@ def calibration_to_dict(config: CalibrationConfig) -> dict:
|
||||
Returns:
|
||||
Dictionary representation
|
||||
"""
|
||||
return {
|
||||
result = {
|
||||
"layout": config.layout,
|
||||
"start_position": config.start_position,
|
||||
"offset": config.offset,
|
||||
@@ -364,3 +409,11 @@ def calibration_to_dict(config: CalibrationConfig) -> dict:
|
||||
"leds_bottom": config.leds_bottom,
|
||||
"leds_left": config.leds_left,
|
||||
}
|
||||
# Include span fields only when not default (full coverage)
|
||||
for edge in ["top", "right", "bottom", "left"]:
|
||||
start = getattr(config, f"span_{edge}_start")
|
||||
end = getattr(config, f"span_{edge}_end")
|
||||
if start != 0.0 or end != 1.0:
|
||||
result[f"span_{edge}_start"] = start
|
||||
result[f"span_{edge}_end"] = end
|
||||
return result
|
||||
|
||||
@@ -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