Sparseness was being applied for two different reasons at once. Aesthetic sparseness is set by the extraction rate; labour sparseness only binds on the plate, because a human draws each one. The mouth is traced and therefore free, and in limited animation lip sync is routinely the densest element - on 1s while heads hold on 2s and 3s. So: the mouth gets a key on every frame, and the frame strip is now the editing surface for deciding which frames need their own plate drawing. All frames start kept; delete the ones you don't want. - strip of face-cropped thumbnails, keep/drop per frame, keyboard driven - worksheet panel lists the drawings needed and the range each one holds - Suggest runs error-tolerance decimation on head pose as a starting point - export writes sparse plate keys + dense mouth keys, with a hold manifest - smoothContours: bounded exception to "never smooth the contour", which held only while keys were sparse enough to reject detector noise by sampling - averages are now a RADIUS in frames: 0 is off, 1 is +-1 - GPU delegate falls back to CPU instead of failing - #synth / #frames autorun for headless smoke tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
6.8 KiB
HTML
139 lines
6.8 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:12px 18px; border-bottom:1px solid var(--line);
|
||
display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
||
h1 { font-size:14px; margin:0 8px 0 0; letter-spacing:.04em; }
|
||
h1 span { color:var(--dim); font-weight:400; }
|
||
main { padding:16px 18px 40px; display:flex; flex-direction:column; gap:16px; }
|
||
.row { display:flex; gap:16px; 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; }
|
||
button,input[type=text],select { font:inherit; background:#1c2130; color:var(--fg);
|
||
border:1px solid var(--line); border-radius:3px; padding:5px 10px; cursor:pointer; }
|
||
button:hover { border-color:var(--accent); }
|
||
input[type=text] { cursor:text; }
|
||
label.ctl { display:grid; grid-template-columns:132px 1fr 46px; 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; }
|
||
|
||
/* the frame strip is the editing surface */
|
||
#strip { display:flex; flex-wrap:wrap; gap:4px; }
|
||
.fr { position:relative; cursor:pointer; border:2px solid transparent; border-radius:3px; line-height:0; }
|
||
.fr canvas { border-radius:1px; }
|
||
.fr span { position:absolute; left:2px; bottom:2px; font-size:10px; line-height:1.2;
|
||
padding:0 3px; border-radius:2px; background:#000a; color:#fff; }
|
||
.fr.keep { border-color:var(--ok); }
|
||
.fr.drop { border-color:#2a2f3e; }
|
||
.fr.drop canvas { opacity:.26; filter:grayscale(1); }
|
||
.fr.cur { border-color:var(--accent); }
|
||
.fr.keep::after { content:'●'; position:absolute; right:3px; top:1px; color:var(--ok); font-size:10px; }
|
||
|
||
#sheet { display:flex; flex-wrap:wrap; gap:10px; }
|
||
#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; }
|
||
kbd { background:#1c2130; border:1px solid var(--line); border-radius:2px;
|
||
padding:0 4px; color:var(--fg); font-size:11px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>roto <span>— frame removal + take builder</span></h1>
|
||
<button id="btn-synth">Synthetic</button>
|
||
<input type="text" id="framedir" value="frames" size="7" title="frame directory">
|
||
<button id="btn-frames">Load frames</button>
|
||
<button id="btn-play">Play</button>
|
||
<select id="playfps" title="playback fps"><option>6</option><option selected>12</option><option>24</option></select>
|
||
<button id="btn-keepall">Keep all</button>
|
||
<button id="btn-suggest">Suggest</button>
|
||
<input type="text" id="takename" value="line_01" size="9" 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">should sit still except the mouth · grey = held plate outline</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="panel">
|
||
<h2>frames — green kept (gets its own drawing) · dim held from the last kept frame</h2>
|
||
<div id="strip"></div>
|
||
<input type="range" id="scrub" min="0" max="0" value="0" style="width:100%;margin-top:10px">
|
||
<div class="legend">
|
||
<kbd>←</kbd> <kbd>→</kbd> step · <kbd>X</kbd> delete · <kbd>K</kbd> keep ·
|
||
click to select · double-click or shift-click to toggle ·
|
||
the mouth keeps <b>every</b> frame regardless
|
||
</div>
|
||
<div class="legend" id="readout"></div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="panel" style="flex:1 1 400px">
|
||
<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>contour avg ±f</span><input type="range" id="contourSmooth" min="0" max="4" value="1"><output id="contourSmoothv"></output></label>
|
||
<label class="ctl"><span>anchor avg ±f</span><input type="range" id="smoothWin" min="0" max="8" value="2"><output id="smoothWinv"></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>
|
||
<label class="ctl"><span>suggest tolerance</span><input type="range" id="tol" min="2" max="60" value="14"><output id="tolv"></output></label>
|
||
<div class="legend">
|
||
<b>contour avg</b> 0 = off, 1 = ±1 frame. Removes per-frame landmark
|
||
jitter. Push past 2 and it starts eating articulation.<br>
|
||
<b>anchor avg</b> smooths the head transform only — never the contour.<br>
|
||
<b>suggest tolerance</b> only affects the Suggest button: max head movement
|
||
allowed before a new drawing is required.
|
||
</div>
|
||
</div>
|
||
<div class="panel" style="flex:1 1 280px">
|
||
<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>drawings needed — one per kept frame, with the range it holds</h2>
|
||
<div id="sheet"></div>
|
||
</div>
|
||
</main>
|
||
|
||
<script type="module" src="./js/app.js"></script>
|
||
</body>
|
||
</html>
|