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>
123 lines
6 KiB
HTML
123 lines
6 KiB
HTML
<!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>
|