#!/usr/bin/env node // REVIEW STRIP (A, 29 Oct): n frames of one kill between film t0 and t1, as one contact sheet. Headless (--headless=new, private profile). // node _tools/qa_strip.cjs [--n 8] [--gore 5] [--page 58k_x.html] [--pre 'K58.X=false'] [--w 480] [--out dir] // Writes /.strip_-_g.png (default out /Volumes/Workspace/Scratch/qa). Runs on the Mac or the VM // (_tools/vm_motionqa.sh --strip runs it there and copies the png back). const fs = require('fs'), os = require('os'), path = require('path'); let chromium; try { ({ chromium } = require('playwright')); } catch (e) { ({ chromium } = require('/Users/basha/node_modules/playwright')); } const A = process.argv.slice(2), pos = [], opt = { n: 8, gore: 5, w: 480, out: '/Volumes/Workspace/Scratch/qa' }; for (let i = 0; i < A.length; i++) { const a = A[i]; if (a.startsWith('--')) opt[a.slice(2)] = A[++i]; else pos.push(a); } const [key, t0s, t1s] = pos; if (!key || t0s == null || t1s == null) { console.log('usage: node _tools/qa_strip.cjs [--n 8] [--gore 5] [--page ..] [--pre ..] [--w 480] [--out dir]'); process.exit(2); } const t0 = +t0s, t1 = +t1s, n = Math.max(1, +opt.n | 0), TW = +opt.w | 0 || 480; if (!fs.existsSync(opt.out)) fs.mkdirSync(opt.out, { recursive: true }); const CHROME = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/google-chrome', '/usr/bin/chromium'].find((p) => fs.existsSync(p)); const ROOT = path.resolve(__dirname, '..'); (async () => { const udd = fs.mkdtempSync(path.join(os.tmpdir(), 'k58strip-')); let code = 0; const ctx = await chromium.launchPersistentContext(udd, Object.assign({ headless: true, args: ['--headless=new', '--allow-file-access-from-files'], viewport: { width: 1200, height: 900 } }, CHROME ? { executablePath: CHROME } : {})); try { const page = await ctx.newPage(); const pg = opt.page || '58k_' + key.split('.')[0] + '.html'; await page.goto('file://' + path.join(ROOT, pg)); await page.waitForFunction(() => document.documentElement.classList.contains('k58-loaded'), null, { timeout: 150000 }); const R = await page.evaluate(async ([key, t0, t1, n, TW, o]) => { if (o.pre) (0, eval)(o.pre); const def = K58.get(key); if (!def) return { err: 'no kill ' + key }; const FS = K58.FSTEP || 1 / 180, TH = Math.round(TW * 0.4625), cols = Math.min(4, n), rows = Math.ceil(n / cols), cv = document.createElement('canvas'); cv.width = cols * TW; cv.height = 24 + rows * (TH + 18); const g = cv.getContext('2d'); g.fillStyle = '#0E1018'; g.fillRect(0, 0, cv.width, cv.height); g.font = 'bold 14px sans-serif'; g.fillStyle = '#E8ECF8'; g.fillText(key + ' ' + (def.name || '') + ' gore ' + o.gore + (o.pre ? ' pre: ' + o.pre : ''), 8, 17); const v = K58.offscreen(TW, TH, { dpr: 1 }); v.load(def, { gore: +o.gore }); for (let i = 0; i < n; i++) { const t = n === 1 ? t0 : t0 + (t1 - t0) * i / (n - 1); v.advance(Math.round(t / FS), 1e9); v.render(); const X = (i % cols) * TW, Y = 24 + Math.floor(i / cols) * (TH + 18); g.drawImage(v.cv, X, Y + 16, TW - 4, TH); g.font = '12px sans-serif'; g.fillStyle = '#C8D0E8'; g.fillText(t.toFixed(2) + 's', X + 4, Y + 12); } K58.offRelease(v); return { png: cv.toDataURL('image/png') }; }, [key, t0, t1, n, TW, opt]); if (R.err) { console.log('ERROR ' + R.err); code = 1; } else { const f = path.join(opt.out, key + '.strip_' + t0.toFixed(2) + '-' + t1.toFixed(2) + '_g' + opt.gore + '.png'); fs.writeFileSync(f, Buffer.from(R.png.split(',')[1], 'base64')); console.log(' -> ' + f); } await Promise.race([page.close(), new Promise((r) => setTimeout(r, 8000))]); } catch (e) { console.error(e); code = 3; } finally { let closed = false; await Promise.race([ctx.close().then(() => { closed = true; }), new Promise((r) => setTimeout(r, 10000))]); if (!closed) { try { for (const ln of require('child_process').execSync('ps -axo pid=,command=', { encoding: 'utf8', maxBuffer: 1 << 26 }).split('\n')) if (ln.includes(udd)) { const pid = parseInt(ln, 10); if (pid && pid !== process.pid) try { process.kill(pid, 'SIGKILL'); } catch (e) {} } } catch (e) {} } try { fs.rmSync(udd, { recursive: true, force: true }); } catch (e) {} } process.exit(code); })();