Working
This commit is contained in:
212
ui/app.js
Normal file
212
ui/app.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const shaderSelect = document.getElementById("shader-select");
|
||||
const mixSlider = document.getElementById("mix-slider");
|
||||
const bypassToggle = document.getElementById("bypass-toggle");
|
||||
const reloadButton = document.getElementById("reload-button");
|
||||
const runtimeStatus = document.getElementById("runtime-status");
|
||||
const videoStatus = document.getElementById("video-status");
|
||||
const compileStatus = document.getElementById("compile-status");
|
||||
const parameterForm = document.getElementById("parameter-form");
|
||||
|
||||
let appState = null;
|
||||
let websocket = null;
|
||||
|
||||
function createKv(target, values) {
|
||||
target.innerHTML = "";
|
||||
values.forEach(([key, value]) => {
|
||||
const dt = document.createElement("dt");
|
||||
dt.textContent = key;
|
||||
const dd = document.createElement("dd");
|
||||
dd.textContent = value;
|
||||
target.append(dt, dd);
|
||||
});
|
||||
}
|
||||
|
||||
function postJson(path, payload) {
|
||||
return fetch(path, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
function renderParameters(shader) {
|
||||
parameterForm.innerHTML = "";
|
||||
if (!shader) {
|
||||
return;
|
||||
}
|
||||
|
||||
shader.parameters.forEach((parameter) => {
|
||||
const section = document.createElement("section");
|
||||
section.className = "parameter";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = parameter.label;
|
||||
section.appendChild(label);
|
||||
|
||||
const valueLabel = document.createElement("div");
|
||||
valueLabel.className = "parameter__value";
|
||||
|
||||
const sendValue = (value) => {
|
||||
postJson("/api/update-parameter", {
|
||||
shaderId: shader.id,
|
||||
parameterId: parameter.id,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
if (parameter.type === "float") {
|
||||
const range = document.createElement("input");
|
||||
range.type = "range";
|
||||
range.min = parameter.min?.[0] ?? 0;
|
||||
range.max = parameter.max?.[0] ?? 1;
|
||||
range.step = parameter.step?.[0] ?? 0.01;
|
||||
range.value = parameter.value;
|
||||
const number = document.createElement("input");
|
||||
number.type = "number";
|
||||
number.min = range.min;
|
||||
number.max = range.max;
|
||||
number.step = range.step;
|
||||
number.value = parameter.value;
|
||||
const pair = document.createElement("div");
|
||||
pair.className = "parameter__pair";
|
||||
pair.append(range, number);
|
||||
section.append(pair, valueLabel);
|
||||
const update = (value) => {
|
||||
valueLabel.textContent = Number(value).toFixed(3);
|
||||
range.value = value;
|
||||
number.value = value;
|
||||
};
|
||||
update(parameter.value);
|
||||
range.addEventListener("input", () => update(range.value));
|
||||
range.addEventListener("change", () => sendValue(Number(range.value)));
|
||||
number.addEventListener("change", () => {
|
||||
update(number.value);
|
||||
sendValue(Number(number.value));
|
||||
});
|
||||
} else if (parameter.type === "vec2" || parameter.type === "color") {
|
||||
const pair = document.createElement("div");
|
||||
pair.className = "parameter__pair";
|
||||
const values = parameter.value.slice();
|
||||
values.forEach((component, index) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.step = parameter.step?.[index] ?? 0.01;
|
||||
input.min = parameter.min?.[index] ?? "";
|
||||
input.max = parameter.max?.[index] ?? "";
|
||||
input.value = component;
|
||||
input.addEventListener("change", () => {
|
||||
values[index] = Number(input.value);
|
||||
valueLabel.textContent = values.map((value) => Number(value).toFixed(3)).join(", ");
|
||||
sendValue(values);
|
||||
});
|
||||
pair.appendChild(input);
|
||||
});
|
||||
valueLabel.textContent = values.map((value) => Number(value).toFixed(3)).join(", ");
|
||||
section.append(pair, valueLabel);
|
||||
} else if (parameter.type === "bool") {
|
||||
const toggle = document.createElement("input");
|
||||
toggle.type = "checkbox";
|
||||
toggle.checked = parameter.value;
|
||||
valueLabel.textContent = parameter.value ? "Enabled" : "Disabled";
|
||||
toggle.addEventListener("change", () => {
|
||||
valueLabel.textContent = toggle.checked ? "Enabled" : "Disabled";
|
||||
sendValue(toggle.checked);
|
||||
});
|
||||
section.append(toggle, valueLabel);
|
||||
} else if (parameter.type === "enum") {
|
||||
const select = document.createElement("select");
|
||||
parameter.options.forEach((option) => {
|
||||
const item = document.createElement("option");
|
||||
item.value = option.value;
|
||||
item.textContent = option.label;
|
||||
if (option.value === parameter.value) {
|
||||
item.selected = true;
|
||||
}
|
||||
select.appendChild(item);
|
||||
});
|
||||
valueLabel.textContent = parameter.value;
|
||||
select.addEventListener("change", () => {
|
||||
valueLabel.textContent = select.value;
|
||||
sendValue(select.value);
|
||||
});
|
||||
section.append(select, valueLabel);
|
||||
}
|
||||
|
||||
parameterForm.appendChild(section);
|
||||
});
|
||||
}
|
||||
|
||||
function renderState(state) {
|
||||
appState = state;
|
||||
const shaders = state.shaders || [];
|
||||
const activeShaderId = state.runtime.activeShaderId;
|
||||
const activeShader = shaders.find((shader) => shader.id === activeShaderId) || shaders[0];
|
||||
|
||||
shaderSelect.innerHTML = "";
|
||||
shaders.forEach((shader) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = shader.id;
|
||||
option.textContent = shader.name;
|
||||
if (shader.id === activeShaderId) {
|
||||
option.selected = true;
|
||||
}
|
||||
shaderSelect.appendChild(option);
|
||||
});
|
||||
|
||||
mixSlider.value = state.runtime.mixAmount ?? 1;
|
||||
bypassToggle.checked = Boolean(state.runtime.bypass);
|
||||
compileStatus.textContent = state.runtime.compileMessage || "No compiler output.";
|
||||
|
||||
createKv(runtimeStatus, [
|
||||
["Active Shader", activeShader?.name || "None"],
|
||||
["Auto Reload", state.app.autoReload ? "On" : "Off"],
|
||||
["Control URL", `http://127.0.0.1:${state.app.serverPort}`],
|
||||
["Compile Status", state.runtime.compileSucceeded ? "Ready" : "Error"],
|
||||
]);
|
||||
|
||||
createKv(videoStatus, [
|
||||
["Signal", state.video.hasSignal ? "Present" : "Missing"],
|
||||
["Mode", state.video.modeName || "Unknown"],
|
||||
["Resolution", `${state.video.width || 0} x ${state.video.height || 0}`],
|
||||
]);
|
||||
|
||||
renderParameters(activeShader);
|
||||
}
|
||||
|
||||
async function loadInitialState() {
|
||||
const response = await fetch("/api/state");
|
||||
renderState(await response.json());
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||
websocket = new WebSocket(`${protocol}://${location.host}/ws`);
|
||||
websocket.onmessage = (event) => {
|
||||
try {
|
||||
renderState(JSON.parse(event.data));
|
||||
} catch (error) {
|
||||
console.error("Failed to parse state update", error);
|
||||
}
|
||||
};
|
||||
websocket.onclose = () => {
|
||||
setTimeout(connectWebSocket, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
shaderSelect.addEventListener("change", () => {
|
||||
postJson("/api/select-shader", { shaderId: shaderSelect.value });
|
||||
});
|
||||
|
||||
mixSlider.addEventListener("change", () => {
|
||||
postJson("/api/set-mix", { mixAmount: Number(mixSlider.value) });
|
||||
});
|
||||
|
||||
bypassToggle.addEventListener("change", () => {
|
||||
postJson("/api/set-bypass", { bypass: bypassToggle.checked });
|
||||
});
|
||||
|
||||
reloadButton.addEventListener("click", () => {
|
||||
postJson("/api/reload", {});
|
||||
});
|
||||
|
||||
loadInitialState().then(connectWebSocket);
|
||||
Reference in New Issue
Block a user