diff --git a/simulator/static/app.js b/simulator/static/app.js index 12a77f2..3a4b77f 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -1,111 +1,101 @@ -const DIALS = ["left", "right", "dark", "light"]; -const MODEL = ["brain_weight", "mood_weight", "pool_size"]; +// Thin renderer: post controls+calibration -> RenderPlan; render grade, Right +// variant crossfade, and the live Left overlay. All math stays in Python. +const $ = (id) => document.getElementById(id); +const vid = $("vid"), overlay = $("overlay"), black = $("black"), readout = $("readout"); -function buildGrid(el) { - el.innerHTML = ""; - // rows = first axis 0..4 top->bottom, cols = second axis 0..4 left->right - for (let a = 0; a < 5; a++) { - for (let b = 0; b < 5; b++) { - const cell = document.createElement("div"); - cell.className = "cell"; - cell.dataset.a = a; - cell.dataset.b = b; - el.appendChild(cell); - } +let clip = null; // active clip manifest entry +let currentVariant = -1; // last loaded Right strength + +async function loadClips() { + const data = await (await fetch("/api/clips")).json(); + clip = data.clips[0] || null; +} + +function mediaUrl(file) { return "/media/" + file; } + +function variantFile(strength) { + const v = clip.right_variants[String(strength)]; + return v ? v.file : clip.base_file; +} + +function applyGrade(tone) { + // Light: warm + brighten; Dark: cool + darken; 0: raw. + const warm = tone > 0 ? tone : 0, cool = tone < 0 ? -tone : 0; + const bright = 1 + 0.25 * tone, sat = 1 + 0.15 * Math.abs(tone); + vid.style.filter = + `brightness(${bright}) saturate(${sat}) ` + + `sepia(${(warm * 0.5).toFixed(3)}) hue-rotate(${(-cool * 200).toFixed(0)}deg)`; +} + +function loadVariant(strength) { + if (strength === currentVariant) return; + currentVariant = strength; + vid.style.opacity = "0"; + setTimeout(() => { + vid.src = mediaUrl(variantFile(strength)); + vid.play().catch(() => {}); + vid.style.opacity = "1"; + }, 150); +} + +function renderOverlay(level, intensity) { + overlay.innerHTML = ""; + if (!clip || level <= 0) { overlay.style.opacity = "0"; return; } + overlay.style.opacity = String(intensity); + const strings = (clip.strings && clip.strings.en) || {}; + for (const a of clip.annotations) { + if (a.min_level > level) continue; + const [x, y, w, h] = a.box.map((n) => n * 100); + const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.setAttribute("x", x); rect.setAttribute("y", y); + rect.setAttribute("width", w); rect.setAttribute("height", h); + rect.setAttribute("class", "anno-box"); + overlay.appendChild(rect); + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("x", x + 0.5); text.setAttribute("y", Math.max(y - 0.5, 2)); + text.setAttribute("class", "anno-label"); + text.textContent = strings[a.key] || a.key; + overlay.appendChild(text); } } -function paintGrid(el, axisA, axisB, point, pool) { - // clear - el.querySelectorAll(".cell").forEach((c) => { - c.className = "cell"; - c.innerHTML = ""; - }); - const counts = {}; - pool.forEach((c) => { - const r = c.record; - const key = `${r[axisA]},${r[axisB]}`; - counts[key] = (counts[key] || 0) + 1; - }); - el.querySelectorAll(".cell").forEach((c) => { - const a = +c.dataset.a, b = +c.dataset.b; - const key = `${a},${b}`; - if (counts[key]) { - c.classList.add("cand"); - const n = document.createElement("span"); - n.className = "n"; - n.textContent = counts[key]; - c.appendChild(n); - } - if (a === point[axisA] && b === point[axisB]) c.classList.add("point"); - }); +function controls() { + return { + content: $("content").value, + left: +$("left").value, right: +$("right").value, + dark: +$("dark").value, light: +$("light").value, + volume: 2, brightness: 2, + }; } -function readState() { - const s = { mode: document.getElementById("mode").value, approved_only: document.getElementById("approved_only").checked }; - DIALS.forEach((d) => (s[d] = +document.getElementById(d).value)); - s.brain_weight = +document.getElementById("brain_weight").value; - s.mood_weight = +document.getElementById("mood_weight").value; - s.pool_size = +document.getElementById("pool_size").value; - return s; +function calibration() { + return { mood_gain: +$("mood_gain").value, overlay_gain: +$("overlay_gain").value, + right_variant_map: [0, 1, 2, 3, 4] }; } -function syncOutputs() { - [...DIALS, ...MODEL].forEach((id) => { - const out = document.getElementById(`${id}-out`); - if (out) out.textContent = document.getElementById(id).value; - }); -} - -async function refresh() { - syncOutputs(); - const state = readState(); - const resp = await fetch("/api/select", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(state), +let timer = null; +async function update() { + const resp = await fetch("/api/alteration", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ controls: controls(), calibration: calibration() }), }); + if (!resp.ok) { readout.textContent = "invalid: " + resp.status; return; } const data = await resp.json(); + readout.textContent = JSON.stringify(data, null, 2); + if (!data.content.video) { black.classList.remove("hidden"); return; } + black.classList.add("hidden"); + applyGrade(data.plan.grade.tone); + loadVariant(data.plan.restyle.variant); + renderOverlay(data.plan.overlay.level, data.plan.overlay.intensity); +} - const pickEl = document.getElementById("pick"); - if (!data.pick) { - pickEl.innerHTML = '