300 lines
19 KiB
HTML
300 lines
19 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>arthur</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.cel::before { content:'▣'; position:absolute; left:3px; top:1px; color:#c084fc;
|
||
font-size:10px; text-shadow:0 0 2px #000; }
|
||
.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; }
|
||
/* paint: a sketch, see js/paint.js */
|
||
#cv-paint { border:1px solid var(--line); cursor:crosshair; touch-action:none; }
|
||
#cv-paint.drop { border-color:var(--ok); }
|
||
#paintlayers { flex:0 0 280px; max-height:600px; overflow:auto; }
|
||
.lay { display:flex; align-items:center; gap:5px; padding:3px 4px; border-radius:3px;
|
||
border:1px solid transparent; cursor:pointer; }
|
||
.lay.sel { border-color:var(--accent); background:#1c2130; }
|
||
.lay i { width:12px; height:12px; border-radius:2px; border:1px solid #0006; flex:0 0 auto; }
|
||
.lay b { flex:1; font-weight:400; color:var(--dim); font-size:11px; }
|
||
.lay.sel b { color:var(--fg); }
|
||
.lay select { font:inherit; font-size:10px; background:#1c2130; color:var(--dim);
|
||
border:1px solid var(--line); border-radius:2px; padding:1px 2px; max-width:88px; }
|
||
.lay button { padding:0 5px; font-size:11px; line-height:18px; }
|
||
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>arthur <span>— rotoscope + frame removal</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="irisAnchor" title="what the iris hangs off">
|
||
<option value="steady" selected>iris: steady</option>
|
||
<option value="free">iris: free</option>
|
||
<option value="locked">iris: locked</option>
|
||
</select>
|
||
<select id="gazeOrigin" title="what counts as looking straight ahead">
|
||
<option value="median" selected>origin: median</option>
|
||
<option value="neutral">origin: neutral f</option>
|
||
</select>
|
||
<select id="exposure" title="exposure — how often the picture gets a new drawing">
|
||
<option value="1" selected>on 1s</option>
|
||
<option value="2">on 2s</option>
|
||
<option value="3">on 3s</option>
|
||
<option value="4">on 4s</option>
|
||
</select>
|
||
<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> ·
|
||
lids <b style="color:#60a5fa">—</b> · iris <b style="color:#fbbf24">—</b> ·
|
||
brows <b style="color:#c084fc">—</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 and eyes · grey = held plate outline<br>
|
||
dark green ghost = unshifted mouth when lead ≠ 0 · red lid ring = blink</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>
|
||
<b>exposure</b> holds the picture on a grid: rip at 24, render on 2s for
|
||
12. The dense track and the audio are untouched, so it is reversible<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>eye vertices</span><input type="range" id="eyeVerts" min="4" max="12" step="2" value="8"><output id="eyeVertsv"></output></label>
|
||
<label class="ctl"><span>lash line</span><input type="range" id="lashPx" min="0" max="3" value="1"><output id="lashPxv"></output></label>
|
||
<label class="ctl"><span>blink cut</span><input type="range" id="blinkCut" min="20" max="300" value="130"><output id="blinkCutv"></output></label>
|
||
<label class="ctl"><span>blink hold ±f</span><input type="range" id="blinkHold" min="1" max="5" value="2"><output id="blinkHoldv"></output></label>
|
||
<label class="ctl"><span>blink dwell</span><input type="range" id="blinkDwell" min="0" max="4" value="0"><output id="blinkDwellv"></output></label>
|
||
<label class="ctl"><span>gaze gain</span><input type="range" id="gazeGain" min="50" max="400" value="100"><output id="gazeGainv"></output></label>
|
||
<label class="ctl"><span>gaze step</span><input type="range" id="gazeStep" min="0" max="6" value="2"><output id="gazeStepv"></output></label>
|
||
<label class="ctl"><span>gaze dwell</span><input type="range" id="gazeDwell" min="0" max="6" value="2"><output id="gazeDwellv"></output></label>
|
||
<label class="ctl"><span>iris size</span><input type="range" id="irisSize" min="20" max="70" value="42"><output id="irisSizev"></output></label>
|
||
<label class="ctl"><span>pupil</span><input type="range" id="pupilPx" min="0" max="7" value="3"><output id="pupilPxv"></output></label>
|
||
<label class="ctl"><span>brow vertices</span><input type="range" id="browVerts" min="4" max="10" step="2" value="6"><output id="browVertsv"></output></label>
|
||
<label class="ctl"><span>brow weight</span><input type="range" id="browWeight" min="0" max="3" value="1"><output id="browWeightv"></output></label>
|
||
<label class="ctl"><span>brow raise gain</span><input type="range" id="browGain" min="50" max="300" value="100"><output id="browGainv"></output></label>
|
||
<label class="ctl"><span>brow step</span><input type="range" id="browStep" min="0" max="6" value="2"><output id="browStepv"></output></label>
|
||
<label class="ctl"><span>brow dwell</span><input type="range" id="browDwell" min="0" max="6" value="2"><output id="browDwellv"></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>blink cut</b> is lid gap over corner distance — normalised, so one
|
||
value carries across takes. <b>blink hold</b> is the minimum length of a
|
||
blink: a real blink is one frame at 12fps and a single frame of closed
|
||
eye reads as a dropout, so it is extended to a beat.
|
||
<b>brow step</b> and <b>brow dwell</b> quantise the brow's HEIGHT above
|
||
the eye, not its shape — the traced ring is his, the height snaps between
|
||
a few levels and holds. Both ends move independently, so raise and tilt
|
||
come out of one control: both up is surprise, inner up is worry, inner
|
||
down is anger. <b>brow weight</b> thickens the ring, which it needs at
|
||
three pixels tall.<br>
|
||
<b>pupil</b> is a square, in whole pixels, 0 to turn it off: at this size
|
||
a circle of radius 1.5 is a plus sign with the corners gnawed off and it
|
||
changes shape as it moves, where a square stays the mark you drew.<br>
|
||
<b>gaze step</b> is the grid the iris snaps to, in raster pixels, and
|
||
<b>gaze dwell</b> is how long a new cell must hold — together they turn
|
||
drift into saccades. <b>gaze gain</b> exaggerates or damps the throw;
|
||
measured excursion is small and a character usually wants more of it.<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 170px">
|
||
<h2>gaze field</h2>
|
||
<canvas id="cv-gaze"></canvas>
|
||
<div class="legend" id="eyeinfo" style="white-space:pre-line"></div>
|
||
<div class="legend">green = every cell the iris visits in the take ·
|
||
grey = raw · amber = where it is now, quantised</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>paint — background cels, drawn on the frames that get their own drawing</h2>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
|
||
<select id="paintTool" title="tool">
|
||
<option value="pen" selected>pen</option>
|
||
<option value="edit">edit</option>
|
||
</select>
|
||
<select id="paintColor" title="palette colour — indices only, never an RGB value"></select>
|
||
<label style="display:flex;align-items:center;gap:6px;color:var(--dim)"
|
||
title="dim the drawing to trace the frame underneath — editing aid only, never exported">
|
||
opacity
|
||
<input type="range" id="paintOpacity" min="10" max="100" value="100" style="width:96px">
|
||
<output id="paintOpacityv" style="color:var(--accent);min-width:34px"></output>
|
||
</label>
|
||
<button id="btn-celprev">Copy previous</button>
|
||
<button id="btn-celclear">Clear cel</button>
|
||
<span class="legend" id="paintframe" style="margin:0"></span>
|
||
</div>
|
||
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
||
<canvas id="cv-paint"></canvas>
|
||
<div id="paintlayers"></div>
|
||
</div>
|
||
<div class="legend" id="paintinfo"></div>
|
||
<div class="legend" id="paintcels"></div>
|
||
<div class="legend">
|
||
<b>pen</b> click to place vertices · click the green box on the first one, or
|
||
<kbd>Enter</kbd> / double-click, to close · <kbd>Esc</kbd> cancels ·
|
||
<kbd>Backspace</kbd> drops the last point<br>
|
||
<b>edit</b> click a shape to select · drag a vertex or the shape itself ·
|
||
<kbd>Shift</kbd>-click an edge inserts a vertex · <kbd>Alt</kbd>-click a vertex
|
||
removes it · <kbd>Del</kbd> deletes the layer<br>
|
||
<b>opacity</b> dims the drawing over the registered source frame so you can
|
||
trace it. It is an editing aid only — never rasterised, never exported, and
|
||
the flat render is untouched. At 100% the view is exactly the normal
|
||
composite; below it the placeholder oval is dropped, since it covers the
|
||
face you turned the slider down to see.<br>
|
||
<b>Copy previous</b> takes the last enabled frame that actually has a drawing
|
||
— skipping empty ones, so it behaves the same before and after you thin the
|
||
strip out. Frames carrying a drawing are marked <b style="color:#c084fc">▣</b>.<br>
|
||
<b>drag a frame</b> from the strip onto the canvas to copy its drawing, every
|
||
layer. Vertices snap to the 320×200 grid, and colours are palette indices —
|
||
you cannot pick one that is not in the ramp.
|
||
</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>
|