Tighten web surfaces and clean handoff
This commit is contained in:
180
web/v1/app.js
180
web/v1/app.js
@@ -1,9 +1,9 @@
|
||||
(function () {
|
||||
const PREVIEW_RENDER_INTERVAL_MS = 90;
|
||||
const PREVIEW_RENDER_INTERVAL_MS = 16;
|
||||
const STATE_RENDER_INTERVAL_MS = 180;
|
||||
const POSITION_ORDER = { top: 0, middle: 1, bottom: 2 };
|
||||
const COLOR_PARAM_KEYS = new Set(["color_mode", "palette", "primary_color", "secondary_color"]);
|
||||
const BRIGHTNESS_PARAM_KEYS = new Set(["brightness"]);
|
||||
const HIDDEN_PARAMETER_KEYS = new Set(["speed", "brightness"]);
|
||||
const TEMPO_BPM_MIN = 10;
|
||||
const TEMPO_BPM_MAX = 300;
|
||||
const TEMPO_BPM_DEFAULT = 120;
|
||||
@@ -46,16 +46,60 @@
|
||||
palette: {
|
||||
control: "enum",
|
||||
label: "Palette",
|
||||
default_value: "Laser Club",
|
||||
default_value: "Deep Blue",
|
||||
groups: [
|
||||
{
|
||||
label: "White / Temperature Presets",
|
||||
options: [
|
||||
{ value: "Candle", label: "Candle (#FF9329)" },
|
||||
{ value: "Tungsten 40W", label: "Tungsten 40W (#FFC58F)" },
|
||||
{ value: "Tungsten 100W", label: "Tungsten 100W (#FFD6AA)" },
|
||||
{ value: "Halogen", label: "Halogen (#FFF1E0)" },
|
||||
{ value: "Carbon Arc", label: "Carbon Arc (#FFFAF4)" },
|
||||
{ value: "High Noon Sun", label: "High Noon Sun (#FFFFFB)" },
|
||||
{ value: "Overcast Sky", label: "Overcast Sky (#C9E2FF)" },
|
||||
{ value: "Clear Blue Sky", label: "Clear Blue Sky (#409CFF)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Club / Show Presets",
|
||||
options: [
|
||||
{ value: "Deep Blue", label: "Deep Blue (#0047FF)" },
|
||||
{ value: "Electric Cyan", label: "Electric Cyan (#00D8FF)" },
|
||||
{ value: "Emerald", label: "Emerald (#00C853)" },
|
||||
{ value: "Acid Lime", label: "Acid Lime (#AEEA00)" },
|
||||
{ value: "Amber", label: "Amber (#FF9D00)" },
|
||||
{ value: "Signal Red", label: "Signal Red (#FF2D2D)" },
|
||||
{ value: "Hot Magenta", label: "Hot Magenta (#FF00A8)" },
|
||||
{ value: "Violet", label: "Violet (#7A3CFF)" },
|
||||
{ value: "Warm White", label: "Warm White (#FFD6AA)" },
|
||||
{ value: "Neutral White", label: "Neutral White (#FFF1E0)" },
|
||||
{ value: "Cool White", label: "Cool White (#C9E2FF)" },
|
||||
{ value: "Blacklight Violet", label: "Blacklight Violet (#A700FF)" },
|
||||
],
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{ value: "Laser Club", label: "Laser Club" },
|
||||
{ value: "Magenta Drive", label: "Magenta Drive" },
|
||||
{ value: "Warehouse Heat", label: "Warehouse Heat" },
|
||||
{ value: "UV Riot", label: "UV Riot" },
|
||||
{ value: "Redline", label: "Redline" },
|
||||
{ value: "Sodium Haze", label: "Sodium Haze" },
|
||||
{ value: "Afterhours", label: "Afterhours" },
|
||||
{ value: "Voltage", label: "Voltage" },
|
||||
{ value: "Candle", label: "Candle (#FF9329)" },
|
||||
{ value: "Tungsten 40W", label: "Tungsten 40W (#FFC58F)" },
|
||||
{ value: "Tungsten 100W", label: "Tungsten 100W (#FFD6AA)" },
|
||||
{ value: "Halogen", label: "Halogen (#FFF1E0)" },
|
||||
{ value: "Carbon Arc", label: "Carbon Arc (#FFFAF4)" },
|
||||
{ value: "High Noon Sun", label: "High Noon Sun (#FFFFFB)" },
|
||||
{ value: "Overcast Sky", label: "Overcast Sky (#C9E2FF)" },
|
||||
{ value: "Clear Blue Sky", label: "Clear Blue Sky (#409CFF)" },
|
||||
{ value: "Deep Blue", label: "Deep Blue (#0047FF)" },
|
||||
{ value: "Electric Cyan", label: "Electric Cyan (#00D8FF)" },
|
||||
{ value: "Emerald", label: "Emerald (#00C853)" },
|
||||
{ value: "Acid Lime", label: "Acid Lime (#AEEA00)" },
|
||||
{ value: "Amber", label: "Amber (#FF9D00)" },
|
||||
{ value: "Signal Red", label: "Signal Red (#FF2D2D)" },
|
||||
{ value: "Hot Magenta", label: "Hot Magenta (#FF00A8)" },
|
||||
{ value: "Violet", label: "Violet (#7A3CFF)" },
|
||||
{ value: "Warm White", label: "Warm White (#FFD6AA)" },
|
||||
{ value: "Neutral White", label: "Neutral White (#FFF1E0)" },
|
||||
{ value: "Cool White", label: "Cool White (#C9E2FF)" },
|
||||
{ value: "Blacklight Violet", label: "Blacklight Violet (#A700FF)" },
|
||||
],
|
||||
},
|
||||
direction: {
|
||||
@@ -171,11 +215,11 @@
|
||||
previewLayoutSignature: null,
|
||||
viewOutputSignature: null,
|
||||
stateTimer: null,
|
||||
previewTimer: null,
|
||||
lastStateRenderAt: 0,
|
||||
lastPreviewRenderAt: 0,
|
||||
stateRenderQueued: false,
|
||||
previewRenderQueued: false,
|
||||
previewAnimationFrame: 0,
|
||||
eventFilterSignature: null,
|
||||
},
|
||||
};
|
||||
@@ -225,7 +269,6 @@
|
||||
snapshotList: document.getElementById("snapshot-list"),
|
||||
motionParams: document.getElementById("motion-params"),
|
||||
colorParams: document.getElementById("color-params"),
|
||||
brightnessParams: document.getElementById("brightness-params"),
|
||||
previewGrid: document.getElementById("preview-grid"),
|
||||
summaryCards: document.getElementById("summary-cards"),
|
||||
selectedTileCard: document.getElementById("selected-tile-card"),
|
||||
@@ -565,15 +608,28 @@
|
||||
return;
|
||||
}
|
||||
apiState.ui.previewRenderQueued = true;
|
||||
const now = window.performance.now();
|
||||
const waitMs = Math.max(0, PREVIEW_RENDER_INTERVAL_MS - (now - apiState.ui.lastPreviewRenderAt));
|
||||
window.setTimeout(() => {
|
||||
const scheduleFrame = () => {
|
||||
apiState.ui.previewAnimationFrame = window.requestAnimationFrame(() => {
|
||||
const now = window.performance.now();
|
||||
if (now - apiState.ui.lastPreviewRenderAt < PREVIEW_RENDER_INTERVAL_MS) {
|
||||
scheduleFrame();
|
||||
return;
|
||||
}
|
||||
apiState.ui.previewAnimationFrame = 0;
|
||||
apiState.ui.previewRenderQueued = false;
|
||||
apiState.ui.lastPreviewRenderAt = now;
|
||||
renderPreview(false);
|
||||
renderSelectedTile();
|
||||
renderSnapshotJson();
|
||||
});
|
||||
};
|
||||
|
||||
if (apiState.ui.previewAnimationFrame) {
|
||||
apiState.ui.previewRenderQueued = false;
|
||||
apiState.ui.lastPreviewRenderAt = window.performance.now();
|
||||
renderPreview(false);
|
||||
renderSelectedTile();
|
||||
renderSnapshotJson();
|
||||
}, waitMs);
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleFrame();
|
||||
}
|
||||
|
||||
async function handlePrimitive(primitive, options) {
|
||||
@@ -1120,7 +1176,6 @@
|
||||
if (!scene || !global) {
|
||||
dom.motionParams.innerHTML = "";
|
||||
dom.colorParams.innerHTML = "";
|
||||
dom.brightnessParams.innerHTML = "";
|
||||
apiState.ui.parameterCards.clear();
|
||||
return;
|
||||
}
|
||||
@@ -1128,13 +1183,14 @@
|
||||
if (
|
||||
!force &&
|
||||
(dom.motionParams.contains(document.activeElement) ||
|
||||
dom.colorParams.contains(document.activeElement) ||
|
||||
dom.brightnessParams.contains(document.activeElement))
|
||||
dom.colorParams.contains(document.activeElement))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parameters = scene.parameters || [];
|
||||
const parameters = (scene.parameters || []).filter(
|
||||
(parameter) => !HIDDEN_PARAMETER_KEYS.has(parameter.key)
|
||||
);
|
||||
const signature = parameters
|
||||
.map((parameter) => `${parameter.key}:${parameter.kind}`)
|
||||
.join("|");
|
||||
@@ -1144,7 +1200,6 @@
|
||||
apiState.ui.parameterCards.clear();
|
||||
dom.motionParams.innerHTML = "";
|
||||
dom.colorParams.innerHTML = "";
|
||||
dom.brightnessParams.innerHTML = "";
|
||||
|
||||
parameters.forEach((parameter) => {
|
||||
const card = createParameterCard(parameter, global);
|
||||
@@ -1165,9 +1220,6 @@
|
||||
if (COLOR_PARAM_KEYS.has(parameter.key)) {
|
||||
return dom.colorParams;
|
||||
}
|
||||
if (BRIGHTNESS_PARAM_KEYS.has(parameter.key)) {
|
||||
return dom.brightnessParams;
|
||||
}
|
||||
return dom.motionParams;
|
||||
}
|
||||
|
||||
@@ -1251,13 +1303,28 @@
|
||||
card.appendChild(readout);
|
||||
} else if (controlKind === "enum") {
|
||||
primaryInput = document.createElement("select");
|
||||
const options = contract && Array.isArray(contract.options) ? contract.options : [];
|
||||
options.forEach((option) => {
|
||||
const node = document.createElement("option");
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
primaryInput.appendChild(node);
|
||||
});
|
||||
const groupedOptions = contract && Array.isArray(contract.groups) ? contract.groups : [];
|
||||
if (groupedOptions.length) {
|
||||
groupedOptions.forEach((group) => {
|
||||
const groupNode = document.createElement("optgroup");
|
||||
groupNode.label = group.label;
|
||||
group.options.forEach((option) => {
|
||||
const node = document.createElement("option");
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
groupNode.appendChild(node);
|
||||
});
|
||||
primaryInput.appendChild(groupNode);
|
||||
});
|
||||
} else {
|
||||
const options = contract && Array.isArray(contract.options) ? contract.options : [];
|
||||
options.forEach((option) => {
|
||||
const node = document.createElement("option");
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
primaryInput.appendChild(node);
|
||||
});
|
||||
}
|
||||
primaryInput.addEventListener("change", async (event) => {
|
||||
const optionValue = event.target.value;
|
||||
const payloadValue =
|
||||
@@ -1550,8 +1617,8 @@
|
||||
dom.previewUpdated.textContent = `${apiState.previewResponse.generated_at_millis} ms`;
|
||||
dom.previewGrid.className = `preview-grid preview-grid-mode-${apiState.ui.previewMode}`;
|
||||
|
||||
const columnCount = uniqueNodeIds(panels).length;
|
||||
dom.previewGrid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(112px, 1fr))`;
|
||||
const columnCount = 6;
|
||||
dom.previewGrid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(0, 1fr))`;
|
||||
|
||||
const layoutSignature = panels.map((panel) => panel.key).join("|");
|
||||
if (force || layoutSignature !== apiState.ui.previewLayoutSignature) {
|
||||
@@ -2437,40 +2504,16 @@
|
||||
const sampleColors = (panel.sample_led_hex || [])
|
||||
.map((hex) => normalizeColorHex(hex))
|
||||
.filter((hex) => isHexColorString(hex));
|
||||
const palette = sampleColors.length
|
||||
? sampleColors
|
||||
: [normalizeColorHex(panel.representative_color_hex)];
|
||||
|
||||
if (palette.length === 1) {
|
||||
return TILE_LED_GEOMETRY.map(() => palette[0]);
|
||||
if (!sampleColors.length) {
|
||||
return TILE_LED_GEOMETRY.map(() => normalizeColorHex(panel.representative_color_hex));
|
||||
}
|
||||
|
||||
return TILE_LED_GEOMETRY.map((_led, index) => {
|
||||
const factor = TILE_LED_GEOMETRY.length === 1 ? 0 : index / (TILE_LED_GEOMETRY.length - 1);
|
||||
return interpolatePalette(palette, factor);
|
||||
const sourceIndex = Math.round((index / Math.max(1, TILE_LED_GEOMETRY.length - 1)) * (sampleColors.length - 1));
|
||||
return sampleColors[sourceIndex] || normalizeColorHex(panel.representative_color_hex);
|
||||
});
|
||||
}
|
||||
|
||||
function interpolatePalette(palette, factor) {
|
||||
const scaled = factor * (palette.length - 1);
|
||||
const lowerIndex = Math.floor(scaled);
|
||||
const upperIndex = Math.min(palette.length - 1, Math.ceil(scaled));
|
||||
if (lowerIndex === upperIndex) {
|
||||
return palette[lowerIndex];
|
||||
}
|
||||
return mixHexColors(palette[lowerIndex], palette[upperIndex], scaled - lowerIndex);
|
||||
}
|
||||
|
||||
function mixHexColors(leftHex, rightHex, factor) {
|
||||
const left = parseHexColor(leftHex);
|
||||
const right = parseHexColor(rightHex);
|
||||
return rgbToHex(
|
||||
Math.round(lerp(left.r, right.r, factor)),
|
||||
Math.round(lerp(left.g, right.g, factor)),
|
||||
Math.round(lerp(left.b, right.b, factor))
|
||||
);
|
||||
}
|
||||
|
||||
function parseHexColor(hex) {
|
||||
const normalized = normalizeColorHex(hex).slice(1);
|
||||
return {
|
||||
@@ -2486,9 +2529,10 @@
|
||||
|
||||
function previewLedOpacity(energyPercent) {
|
||||
if (!energyPercent) {
|
||||
return "0.18";
|
||||
return "0.1";
|
||||
}
|
||||
return String(Math.min(1, 0.42 + energyPercent / 160));
|
||||
const steppedPercent = Math.round(Math.max(0, Math.min(100, energyPercent)) / 10) * 10;
|
||||
return String(Math.max(0.1, steppedPercent / 100));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
|
||||
Reference in New Issue
Block a user