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>
This commit is contained in:
commit
7a6bdea157
13 changed files with 1277 additions and 0 deletions
83
js/raster.js
Normal file
83
js/raster.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Indexed flat-fill rasteriser.
|
||||
//
|
||||
// Canvas2D antialiases path fills, and antialiasing is exactly what the target
|
||||
// idiom does not have: Animator Pro fills polygons into a 256-colour indexed
|
||||
// raster with hard edges (csd_render_poly). A preview that antialiases would
|
||||
// misrepresent the look it exists to judge, so this writes palette indices into
|
||||
// a byte buffer with an even-odd scanline fill and expands to RGBA only at the
|
||||
// very end.
|
||||
|
||||
export class IndexedRaster {
|
||||
constructor(w, h) {
|
||||
this.w = w; this.h = h;
|
||||
this.buf = new Uint8Array(w * h);
|
||||
}
|
||||
|
||||
clear(index) { this.buf.fill(index); }
|
||||
|
||||
// Even-odd scanline fill. Samples at pixel centres (y + 0.5), so a polygon
|
||||
// edge landing exactly on a pixel boundary resolves consistently.
|
||||
fillPoly(pts, index) {
|
||||
const n = pts.length;
|
||||
if (n < 3) return;
|
||||
let minY = Infinity, maxY = -Infinity;
|
||||
for (const p of pts) { if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; }
|
||||
const y0 = Math.max(0, Math.ceil(minY - 0.5));
|
||||
const y1 = Math.min(this.h - 1, Math.floor(maxY - 0.5) + 1);
|
||||
const xs = [];
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
const sy = y + 0.5;
|
||||
xs.length = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = pts[i], b = pts[(i + 1) % n];
|
||||
if (a.y === b.y) continue;
|
||||
const lo = Math.min(a.y, b.y), hi = Math.max(a.y, b.y);
|
||||
if (sy < lo || sy >= hi) continue;
|
||||
xs.push(a.x + ((sy - a.y) / (b.y - a.y)) * (b.x - a.x));
|
||||
}
|
||||
if (xs.length < 2) continue;
|
||||
xs.sort((p, q) => p - q);
|
||||
for (let k = 0; k + 1 < xs.length; k += 2) {
|
||||
const xa = Math.max(0, Math.ceil(xs[k] - 0.5));
|
||||
const xb = Math.min(this.w - 1, Math.floor(xs[k + 1] - 0.5));
|
||||
const row = y * this.w;
|
||||
for (let x = xa; x <= xb; x++) this.buf[row + x] = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fillDisc(cx, cy, r, index) {
|
||||
const rr = r * r;
|
||||
const y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(this.h - 1, Math.ceil(cy + r));
|
||||
const x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(this.w - 1, Math.ceil(cx + r));
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const dx = x + 0.5 - cx, dy = y + 0.5 - cy;
|
||||
if (dx * dx + dy * dy <= rr) this.buf[y * this.w + x] = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand indices through the palette into an ImageData at integer zoom.
|
||||
// Nearest-neighbour by construction, so no filtering softens the result.
|
||||
toImageData(palette, zoom = 1) {
|
||||
const W = this.w * zoom, H = this.h * zoom;
|
||||
const img = new ImageData(W, H);
|
||||
const d = img.data;
|
||||
const rgb = palette.map(hexToRgb);
|
||||
for (let y = 0; y < H; y++) {
|
||||
const srow = Math.floor(y / zoom) * this.w;
|
||||
for (let x = 0; x < W; x++) {
|
||||
const c = rgb[this.buf[srow + Math.floor(x / zoom)]] || [255, 0, 255];
|
||||
const o = (y * W + x) * 4;
|
||||
d[o] = c[0]; d[o + 1] = c[1]; d[o + 2] = c[2]; d[o + 3] = 255;
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
export function hexToRgb(hex) {
|
||||
const s = hex.replace('#', '');
|
||||
return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue