arthur/index.html
Your Name d18791e6ad Mouth lead: nudge performance tracks ahead of the clock
Smoothing does not literally delay anything - a centred moving average has zero
phase lag - but it blurs onsets, so the visually salient moment of a mouth
opening moves later even though the mean does not. Animators also draw mouth
shapes a frame or two ahead of the sound as standard practice, so this is the
normal control rather than a workaround.

Only the performance tracks shift; the head and audio stay put, since it is the
mouth that should anticipate. In photo-underlay modes the vector mouth will
therefore no longer match the frame behind it, which is expected.

The lead is baked into the exported take - key f carries the pose from source
frame f+lead - so the Animator Pro renderer never needs to know about it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 15:34:08 -04:00

181 lines
10 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: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="speed" title="playback speed"><option value="1">1x</option><option value="0.5">½x</option><option value="0.25">¼x</option></select>
<audio id="audio" controls hidden style="height:28px;vertical-align:middle"></audio>
<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">
<select id="plateMode" title="plate representation (B to cycle)">
<option value="photo-dim">photo dim</option>
<option value="photo">photo</option>
<option value="posterize">posterized</option>
<option value="oval" selected>oval</option>
<option value="oval+photo">oval + photo</option>
<option value="off">none</option>
</select>
<button id="btn-saveframe">Save frame 4x</button>
<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 class="legend">audio drives the clock — dropped frames, never drift<br>
plate representation: <kbd>B</kbd> cycles · photo modes are <b>registered</b>
into raster space, so tracing them lands on the mouth</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 · <kbd>B</kbd> background ·
<kbd>[</kbd> <kbd>]</kbd> lead ·
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>mouth lead ±f</span><input type="range" id="lead" min="-6" max="6" value="0"><output id="leadv"></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>teeth contrast</span><input type="range" id="teethOn" min="1" max="60" value="16"><output id="teethOnv"></output></label>
<label class="ctl"><span>cavity erode</span><input type="range" id="teethErode" min="0" max="45" value="18"><output id="teethErodev"></output></label>
<label class="ctl"><span>blob grow/erode</span><input type="range" id="blobGrow" min="-4" max="4" value="0"><output id="blobGrowv"></output></label>
<label class="ctl"><span>tongue reject</span><input type="range" id="tongueReject" min="2" max="40" value="18"><output id="tongueRejectv"></output></label>
<label class="ctl"><span>prefer upper</span><input type="range" id="topBias" min="0" max="120" value="60"><output id="topBiasv"></output></label>
<label class="ctl"><span>teeth vertices</span><input type="range" id="teethVerts" min="5" max="20" value="10"><output id="teethVertsv"></output></label>
<label class="ctl"><span>teeth avg ±f</span><input type="range" id="teethSmooth" min="0" max="4" value="1"><output id="teethSmoothv"></output></label>
<label class="ctl"><span>teeth dwell</span><input type="range" id="teethDwell" min="0" max="6" value="1"><output id="teethDwellv"></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>mouth lead</b> shifts the performance tracks earlier (positive) against
the audio and the head. Averaging has no phase lag but it blurs onsets, so
an opening reads later than it is; animators also draw mouths a frame or
two ahead of the sound as standard practice. <kbd>[</kbd> <kbd>]</kbd>.<br>
<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>teeth contrast</b> gates on how far apart the cavity's dark and bright
halves are — Otsu always returns <i>some</i> threshold, so this is what
stops it inventing teeth in a dark mouth.
<b>cavity erode</b> pulls the sampled region in from the lip edge;
<b>blob grow/erode</b> resizes the found blob itself.
<b>tongue reject</b> drops pixels that are red relative to their own
brightness; <b>prefer upper</b> biases component choice toward the top of
the cavity, where teeth are and the tongue is not.
<b>dwell</b> is how many frames a presence change must persist.<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:0 1 190px">
<h2>teeth measurement</h2>
<div id="cv-teeth"></div>
<div class="legend" id="teethinfo"></div>
<div class="legend">green = kept pixels · amber = extracted contour</div>
</div>
<div class="panel" style="flex:1 1 240px">
<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 — registered reference 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>