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 = '
∅ Void / rest — walls dark, audio silent.
'; - } else { - const p = data.pick; - pickEl.innerHTML = - `
${p.title}
` + - `
mode ${p.mode} · coord (${p.left},${p.right},${p.dark},${p.light})
` + - `
${p.rationale || ""}
`; +function debounced() { clearTimeout(timer); timer = setTimeout(update, 80); } + +async function main() { + await loadClips(); + for (const id of ["content", "left", "right", "dark", "light", "mood_gain", "overlay_gain"]) { + $(id).addEventListener("input", debounced); } - - const poolEl = document.getElementById("pool"); - poolEl.innerHTML = ""; - data.pool.forEach((c, i) => { - const li = document.createElement("li"); - if (i === 0) li.className = "winner"; - li.innerHTML = `${c.record.title} d=${c.distance.toFixed(2)}`; - poolEl.appendChild(li); - }); - - const point = { left: state.left, right: state.right, dark: state.dark, light: state.light }; - paintGrid(document.getElementById("brain-grid"), "left", "right", point, data.pool); - paintGrid(document.getElementById("mood-grid"), "dark", "light", point, data.pool); + update(); } - -async function loadMeta() { - const data = await (await fetch("/api/catalog/meta")).json(); - const byMode = Object.entries(data.by_mode).map(([k, v]) => `${k}:${v}`).join(" "); - document.getElementById("meta").textContent = `${data.total} records · ${byMode}`; -} - -function init() { - buildGrid(document.getElementById("brain-grid")); - buildGrid(document.getElementById("mood-grid")); - document.querySelectorAll("input, select").forEach((el) => - el.addEventListener("input", refresh) - ); - loadMeta(); - refresh(); -} - -document.addEventListener("DOMContentLoaded", init); +main(); diff --git a/simulator/static/index.html b/simulator/static/index.html index 0a89203..c868754 100644 --- a/simulator/static/index.html +++ b/simulator/static/index.html @@ -1,64 +1,56 @@ - + - - - HEF — Curator's X-ray - + + + HEF — Alteration Simulator + -
-

Experience Filter — Curator's X-ray

-
-
- +

Human Experience Filter — Alteration Preview

-
-

Dials

- - - - - - -

Model knobs

- - - - +
+
+ + + +
-
-
-

Picked

-
-
-
-

Pool (nearest first)

-
    -
    -
    -

    Coordinate maps

    -
    Brain — Left × Right
    -
    Mood — Dark × Light
    -
    +
    +
    + Content dial + +
    + +
    + Experience knobs (0–4) + + + + +
    + +
    + Calibration + + +
    + +
    + RenderPlan readout +
    +
    - diff --git a/simulator/static/style.css b/simulator/static/style.css index 79371ce..66daf67 100644 --- a/simulator/static/style.css +++ b/simulator/static/style.css @@ -1,26 +1,22 @@ -:root { color-scheme: dark; } * { box-sizing: border-box; } -body { margin: 0; font-family: -apple-system, system-ui, sans-serif; background: #0e0e16; color: #e6e6ee; } -header { padding: 12px 20px; border-bottom: 1px solid #2a2a3a; display: flex; justify-content: space-between; align-items: baseline; } -header h1 { font-size: 18px; margin: 0; } -.meta { font-size: 12px; color: #9a9ab0; } -main { display: grid; grid-template-columns: 280px 1fr; gap: 20px; padding: 20px; } -.controls label { display: block; margin: 8px 0; font-size: 13px; } -.controls input[type=range] { width: 100%; } -.controls .check { display: flex; gap: 6px; align-items: center; } -h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .5px; color: #9a9ab0; } -.xray { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; } -.pick #pick { background: #1a1a2e; border: 1px solid #33334a; border-radius: 8px; padding: 14px; min-height: 80px; } -.pick .title { font-size: 16px; font-weight: 600; } -.pick .void { color: #7777aa; font-style: italic; } -.pool ol { margin: 0; padding-left: 18px; font-size: 13px; } -.pool li { margin: 4px 0; } -.pool li.winner { color: #7fffd4; font-weight: 600; } -.pool .dist { color: #9a9ab0; } -.maps { grid-column: 1 / -1; display: flex; gap: 40px; } -.grid5 { display: grid; grid-template-columns: repeat(5, 28px); grid-template-rows: repeat(5, 28px); gap: 3px; } -.grid5 .cell { background: #1c1c2c; border: 1px solid #2a2a3a; border-radius: 3px; position: relative; } -.grid5 .cell.cand { background: #3a3a66; } -.grid5 .cell.point { outline: 2px solid #7fffd4; } -.grid5 .cell .n { position: absolute; right: 2px; bottom: 1px; font-size: 9px; color: #aab; } -.label { font-size: 11px; color: #9a9ab0; margin-bottom: 4px; } +body { margin: 0; font: 14px/1.4 system-ui, sans-serif; background: #111; color: #eee; } +header { padding: 0.6rem 1rem; background: #000; } +h1 { font-size: 1rem; margin: 0; font-weight: 600; } +main { display: flex; gap: 1rem; padding: 1rem; flex-wrap: wrap; } +.stage { flex: 1 1 640px; } +.screen { position: relative; width: 100%; aspect-ratio: 16 / 9; background: #000; + border-radius: 6px; overflow: hidden; } +#vid { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.15s ease; } +#overlay { position: absolute; inset: 0; width: 100%; height: 100%; + pointer-events: none; transition: opacity 0.2s ease; } +.anno-box { fill: none; stroke: #6cf; stroke-width: 0.4; vector-effect: non-scaling-stroke; } +.anno-label { fill: #6cf; font-size: 3px; font-family: monospace; } +.black { position: absolute; inset: 0; background: #000; } +.hidden { display: none; } +.panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.8rem; } +fieldset { border: 1px solid #333; border-radius: 6px; } +legend { color: #9af; padding: 0 0.4rem; } +label { display: block; margin: 0.4rem 0; } +input[type=range], select { width: 100%; } +#readout { background: #000; padding: 0.5rem; border-radius: 4px; font-size: 12px; + white-space: pre-wrap; max-height: 240px; overflow: auto; }