arthur/index.html
Your Name 7a6bdea157 roto: video -> take file builder with interactive tuning
Analysis half of the pipeline in docs/roto-puppet.md. Stabilises a face out
of a clip via a similarity fit on rigid landmarks, reduces the lip contour to
a fixed vertex budget, selects sparse keys on velocity minima, and previews
the result as flat indexed fills so timing can be judged without an Animator
Pro render.

- landmarks.js  ordered lip/oval rings; slot position is vertex identity
- mathutil.js   closed-form 2D similarity, Procrustes mean, transform smoothing
- pipeline.js   stabilise -> subsample -> key-select
- raster.js     indexed scanline fill, no antialiasing
- take.js       take-file writer
- selftest.js   29 assertions, incl. ring simplicity at every vertex budget

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 14:38:07 -04:00

123 lines
6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>roto — take builder</title>
<style>
:root {
--bg: #0b0d13; --panel: #12151f; --line: #232836; --fg: #e8eaf0;
--dim: #8891a5; --accent: #fbbf24; --ok: #4ade80; --err: #f87171;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg);
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
header { padding: 14px 18px; border-bottom: 1px solid var(--line);
display: flex; gap: 14px; align-items: baseline; flex-wrap: wrap; }
h1 { font-size: 14px; margin: 0; letter-spacing: .04em; }
h1 span { color: var(--dim); font-weight: 400; }
main { padding: 18px; display: flex; flex-direction: column; gap: 18px; }
.row { display: flex; gap: 18px; flex-wrap: wrap; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: 12px; }
.panel h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
color: var(--dim); margin: 0 0 8px; font-weight: 500; }
canvas { display: block; image-rendering: pixelated; max-width: 100%; border-radius: 2px; }
#cv-timeline { width: 100%; height: 90px; }
button, input[type=text] {
font: inherit; background: #1c2130; color: var(--fg);
border: 1px solid var(--line); border-radius: 3px; padding: 5px 11px; cursor: pointer; }
button:hover { border-color: var(--accent); }
input[type=text] { cursor: text; }
label.ctl { display: grid; grid-template-columns: 116px 1fr 44px;
gap: 10px; align-items: center; margin-bottom: 6px; }
label.ctl span:first-child { color: var(--dim); }
label.ctl output { text-align: right; color: var(--accent); }
input[type=range] { width: 100%; accent-color: var(--accent); }
#status { color: var(--dim); }
#status.ok { color: var(--ok); } #status.err { color: var(--err); } #status.warn { color: var(--accent); }
#readout { color: var(--dim); font-size: 12px; }
#sheet { display: flex; flex-wrap: wrap; gap: 8px; }
#sheet .cell { display: flex; flex-direction: column; gap: 3px; cursor: pointer; }
#sheet .cell span { color: var(--dim); font-size: 11px; }
#sheet .cell:hover span { color: var(--accent); }
#palette { display: flex; gap: 12px; flex-wrap: wrap; }
.sw { display: flex; align-items: center; gap: 5px; color: var(--dim); font-size: 11px; }
.sw input { width: 26px; height: 20px; padding: 0; border: 1px solid var(--line); background: none; }
.legend { color: var(--dim); font-size: 11px; margin-top: 6px; }
.legend b { font-weight: 400; }
.k { color: var(--accent); } .s { color: var(--err); } .c { color: #5b6478; }
</style>
</head>
<body>
<header>
<h1>roto <span>— video → take builder</span></h1>
<button id="btn-synth">Synthetic take</button>
<input type="text" id="framedir" value="frames" size="8" title="frame directory">
<button id="btn-frames">Load frames</button>
<button id="btn-play">Play</button>
<input type="text" id="takename" value="line_01" size="10" title="take name">
<button id="btn-export">Export .take</button>
<span id="status"></span>
</header>
<main>
<div class="row">
<div class="panel">
<h2>source + landmarks</h2>
<canvas id="cv-source"></canvas>
<div class="legend">outer lip <b style="color:#4ade80">—</b> · inner lip <b style="color:#f87171">—</b></div>
</div>
<div class="panel">
<h2>stabilised (head-local)</h2>
<canvas id="cv-stab"></canvas>
<div class="legend">live contour · <b class="k">—</b> active key pose · should sit still except the mouth</div>
</div>
<div class="panel">
<h2>flat render — 320×200 indexed</h2>
<canvas id="cv-render"></canvas>
<div class="legend" id="framelabel"></div>
</div>
</div>
<div class="row">
<div class="panel" style="flex:1 1 380px">
<h2>knobs</h2>
<label class="ctl"><span>vertices</span><input type="range" id="verts" min="4" max="16" step="2" value="8"><output id="vertsv"></output></label>
<label class="ctl"><span>min hold</span><input type="range" id="minHold" min="1" max="8" value="2"><output id="minHoldv"></output></label>
<label class="ctl"><span>change gate</span><input type="range" id="distThresh" min="0" max="40" value="6"><output id="distThreshv"></output></label>
<label class="ctl"><span>vel smoothing</span><input type="range" id="velSmooth" min="1" max="11" step="2" value="3"><output id="velSmoothv"></output></label>
<label class="ctl"><span>anchor smoothing</span><input type="range" id="smoothWin" min="1" max="21" step="2" value="5"><output id="smoothWinv"></output></label>
<label class="ctl"><span>exposure</span><input type="range" id="exposure" min="1" max="4" value="2"><output id="exposurev"></output></label>
<label class="ctl"><span>closed-mouth cut</span><input type="range" id="apertureThresh" min="0" max="400" value="120"><output id="apertureThreshv"></output></label>
<div class="legend" id="readout"></div>
</div>
<div class="panel" style="flex:1 1 300px">
<h2>palette</h2>
<div id="palette"></div>
<div class="legend" style="margin-top:12px">
Flat indexed fills, no antialiasing — the rasteriser writes palette
indices, the way <code>csd_render_poly</code> does.
</div>
</div>
</div>
<div class="panel">
<h2>timeline</h2>
<canvas id="cv-timeline"></canvas>
<input type="range" id="scrub" min="0" max="0" value="0" style="width:100%;margin-top:8px">
<div class="legend">
velocity curve · <b class="c">|</b> candidate minima ·
<b class="k">|</b> key as rendered (<code>f</code>) ·
<b class="s">|</b> true extreme (<code>src</code>) — a large gap means min-hold and exposure are fighting
</div>
</div>
<div class="panel">
<h2>selected poses — the vocabulary the take actually contains</h2>
<div id="sheet"></div>
</div>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>