213 lines
7.0 KiB
JavaScript
213 lines
7.0 KiB
JavaScript
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);
|