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);