teaser: the reject shot redone (7-11 s, 4 s): the common rejections storebound catches as real scan finding cards landing one per beat (2.1(a) demo account, 5.1.1(v) account deletion, 5.1.2(i) ATT from tests/fixtures/scan/teaser; ITMS-91064 from the new teaser-manifest fixture), guideline + one plain line each, then our own REJECTED stamp hits the stack on the beat at 9.5 s, no shake (one 4 px 1-frame jolt), a 3% push, the picture darkens into the cut; caption 'Rejected. Again.'; later shots +1.5 s (42.5 s, every cut on a beat, music follows the scenes, a thud per card and a hit on the stamp); render.ts --animatic --range a,b and --sheet a,b,fps, and the reject cards' boxes per frame in animatic-checks.json; tests: real finding lines, beat timing, and every card inside 6% safe margins at every frame

This commit is contained in:
CRTkafa 2026-10-14 05:22:47 +08:00
parent 511716bfd5
commit 20a764b3ee
11 changed files with 205 additions and 86 deletions

View file

@ -4,7 +4,7 @@ import { ASK_AT, BUILD_LINES, CAMERA, CAP_FADE, CAPTIONS, captionTimes, CONSOLE_
DATA_SAFETY_CSV, DATA_SAFETY_ROWS, DECL_ASK, DECL_AT, DECL_LINES, DECL_STEP, DECL_TOTAL, DESK, DESK_SCENES, DURATION, END, END_CMD, END_URL,
FINDINGS, FINDINGS_AT, FINDINGS_STEP, FIX_AT, FORM_AT, FORM_STEP, FORMAT, FORMS_COUNT_AT, FPS, GAME_SCREENS, GAME_SPEED, MONTAGE,
PILE, PILE_TITLES, PRICE_ROWS, PRIVACY_ANSWERS, PRIVACY_STEPS, PRODUCT_LINES, PROMPT, PROMPT_AT, PROMPT_CPS, PROMPT_SENT,
REJECT, REJECT_GROW, REJECT_SLAM, RESCAN_AT, RESCAN_LINE, RISKS_LINE, SCAN_CMD, SCENES, SHOT_LANGS, SHOT_SIZES, SHOTS_AT, SHOTS_ROW, SHOTS_STEP, scene, veil } from './script.js';
REJECT_DARK, REJECT_LAND, REJECT_PUSH, REJECT_STEP, REJECTS, STAMP, RESCAN_AT, RESCAN_LINE, RISKS_LINE, SCAN_CMD, SCENES, SHOT_LANGS, SHOT_SIZES, SHOTS_AT, SHOTS_ROW, SHOTS_STEP, scene, veil } from './script.js';
const q = new URLSearchParams(location.search);
const W = FORMAT.w, H = FORMAT.h;
@ -170,22 +170,29 @@ function pile(t) {
});
}
// ---------- 3 reject: one finding grows to fill the frame ----------
const findEl = $('#finding');
{
const m = REJECT.head.match(/^(BLOCKER) (\S+) · App Store · (.+)$/);
findEl.querySelector('.l1').innerHTML = `<span class="sev">${m[1]}</span><span class="gl">${esc(m[2])}</span><span>· App Store ·</span><span>${esc(m[3])}</span>`;
findEl.querySelector('.l2').innerHTML = esc(REJECT.at).replace(/^(at \S+)/, '<span class="loc">$1</span>');
}
// ---------- 3 reject: real scan findings land one per beat, then the REJECTED stamp ----------
const rejEl = $('#rejects'), stampEl = $('#stamp');
rejEl.innerHTML = REJECTS.map(f => {
const m = f.head.match(/^(\S+)\s+.*? · App Store · (.+)$/);
return `<div class="rj"><div class="rule"><span class="sev">${m[1]}</span><span>${esc(f.rule)}</span></div><div class="title">${esc(f.title)}</div><div class="real">${esc(m[2])}</div></div>`;
}).join('');
const rjEls = [...rejEl.children];
function reject(t) {
const s = scene('reject'); if (t < s.start - 1 || t > s.end + 1) return;
const r = t - s.start;
// the slam: in at once, overshooting scale down to 1 with a short shake; then a push onto the rule
const sp = prog(r, -LEAD, REJECT_SLAM.d + LEAD);
set(findEl, 'opacity', String(clamp(sp * 4)));
const z = lerp(REJECT_SLAM.from, 1, eoc(sp)) * lerp(REJECT_GROW[0], REJECT_GROW[1], ecam(prog(r, REJECT_SLAM.d, s.end - s.start - REJECT_SLAM.d)));
const shake = r < 0.45 && r > REJECT_SLAM.d ? Math.sin(r * 90) * 14 * (1 - (r - REJECT_SLAM.d) / (0.45 - REJECT_SLAM.d)) : 0;
set(findEl, 'transform', `translate(calc(-50% + ${shake.toFixed(1)}px), -50%) scale(${z.toFixed(4)})`);
const r = t - s.start, len = s.end - s.start;
const jolt = r >= STAMP.at && r < STAMP.at + 1 / 30 ? STAMP.jolt : 0;
const z = lerp(REJECT_PUSH[0], REJECT_PUSH[1], esin(prog(r, 0, len)));
set(rejEl, 'transform', `translate(calc(-50% + ${jolt}px), calc(-50% + ${jolt / 2}px)) scale(${z.toFixed(4)})`);
set(rejEl, 'filter', `brightness(${(1 - 0.45 * eio(prog(r, REJECT_DARK, len - REJECT_DARK))).toFixed(3)})`);
rjEls.forEach((el, i) => {
const lp = prog(r, i * REJECT_STEP - (i ? 0 : LEAD), REJECT_LAND.d + (i ? 0 : LEAD));
set(el, 'opacity', String(clamp(lp * 4)));
set(el, 'transform', `scale(${lerp(REJECT_LAND.from, 1, eoc(lp)).toFixed(4)})`);
});
const sp = prog(r, STAMP.at - STAMP.d, STAMP.d);
set(stampEl, 'opacity', String(r < STAMP.at - STAMP.d ? 0 : clamp(sp * 3)));
set(stampEl, 'filter', rejEl.style.filter);
set(stampEl, 'transform', `translate(calc(-50% + ${jolt}px), -50%) rotate(${STAMP.rot}deg) scale(${lerp(STAMP.from, 1, eoc(sp)).toFixed(4)})`);
}
// ---------- 4 ask: the request to the agent ----------
@ -419,6 +426,13 @@ function end(t) {
// the caret: solid while typing, then a steady blink (two per second, on the beat) through the hold
set(endCaret, 'opacity', r < END.full || Math.floor((r - END.full) * 2) % 2 === 0 ? '1' : '0');
}
/** The reject cards' boxes (and the stamp's) as drawn at time t, for the safe-margin check. */
function rejectLayout(t) {
seek(t);
const k = W / stage.getBoundingClientRect().width;
const box = el => { const b = el.getBoundingClientRect(); return { x: b.x * k, y: b.y * k, w: b.width * k, h: b.height * k }; };
return { t, cards: rjEls.filter(el => Number(el.style.opacity) > 0).map(box), stamp: Number(stampEl.style.opacity) > 0 ? box(stampEl) : null };
}
/** Layout data for the animatic checks: the end card's install line, as drawn at time t. */
function layout(t) {
seek(t);
@ -462,7 +476,7 @@ const ready = (async () => {
for (let i = 0; i < all.length; i += 8) await Promise.all(all.slice(i, i + 8).map(decode));
seek(0);
})();
window.teaser = { ready, seek, layout, DURATION, FPS, W, H, captionTimes };
window.teaser = { ready, seek, layout, rejectLayout, DURATION, FPS, W, H, captionTimes };
if (q.has('t')) ready.then(() => seek(Number(q.get('t'))));
if (q.has('play')) ready.then(() => {