teaser v7: 62 s, two new scenes (consoles: the real console plan of apple-app-privacy, then the app's plan panel stopping at Publish; resubmit: back from App Review, Resubmit), submit renewed as the app's own send-for-review (dry-run plan with Continue, then Waiting for review); v6's desktop scene folded into it; console plan lines checked against real output, cuts on the beat, image decodes batched with retries

This commit is contained in:
CRTkafa 2026-10-13 03:49:39 +08:00
parent 710c3a53d0
commit 614c32bcba
8 changed files with 391 additions and 99 deletions

View file

@ -1,6 +1,7 @@
// Teaser picture (v6): every element is set from the time t alone (window.teaser.seek(t)), so frames render
// Teaser picture (v7): every element is set from the time t alone (window.teaser.seek(t)), so frames render
// deterministically, in any order. 16:9 only (1920x1080). URL: &t=12.5 holds a frame, &play=1 previews in real time.
import { APP_NAME, ASK_AT, CAMERA, CAP_DELAY, CAP_END, CAP_IN, CAP_OUT, CAPTIONS, captionTimes, COPY, COPY_AT, COPY_FILE, COPY_STEP,
import { APP_NAME, ASK_AT, CAMERA, CAP_DELAY, CAP_END, CAP_IN, CAP_OUT, CAPTIONS, captionTimes, CONSOLE_CMD, CONSOLE_CPS, CONSOLE_LINES,
CONSOLE_TYPE_AT, CONSOLES_PILL, CONSOLES_SWAP, COPY, COPY_AT, COPY_FILE, COPY_STEP,
DATA_SAFETY_ROWS, DECL_ASK, DECL_AT, DECL_LINES, DECL_STEP, DECL_TOTAL, DESK, DESK_SCENES, DISSOLVE, DURATION, FINDINGS, FIX_AT, FOLDER,
FORM_AT, FORM_STEP, FORMAT, FORMS_PILL, FPS, INIT_CMD, INIT_CPS, INIT_LINES, INIT_OUT_AT, INIT_STEP, INIT_TYPE_AT,
MAIL, MAIL_IN, PLAN_AT, PLAN_CMD, PLAN_CMD_AT, PLAN_LINES, PLAN_PILL, PLAN_STEP, PRODUCT_LINES, PRODUCTS_PILL, PROMPT, PROMPT_AT, PROMPT_CPS,
@ -264,7 +265,34 @@ function forms(t) {
pillAt(formsPill, r, FORMS_PILL.at);
}
// ---------- 7 rejections ----------
// ---------- 7 consoles: the console plan in the terminal, then the app's plan panel (DESK.consoles) ----------
const cpBody = $('#cpterm .body'), cpPill = $('#cp-pill'), cpTerm = $('#cpterm');
const cpLine = l => {
if (l.final) return `<span class="glow"></span><span class="k">${esc(l.text.replace(/: Publish$/, ':'))}</span> <span class="w">Publish</span>`;
if (l.head) return `<span class="num">${esc(l.text)}</span>`;
if (/^Never press:/.test(l.text)) return esc(l.text).replace(/^(Never press:)/, '<span class="bad">$1</span>');
return esc(l.text).replace(/^(\s+\d+\.)/, '<span class="tag">$1</span>').replace(/^(\s+)\[ok\]/, '$1<span class="okk">[ok]</span>')
.replace(/^(\s+)\[ask\]/, '$1<span class="warnk">[ask]</span>').replace(/(\[click: [^\]]+\])/, '<span class="loc">$1</span>');
};
cpBody.innerHTML = `<span class="ln" data-k="c"></span>${CONSOLE_LINES.map(l => `<span class="ln${l.final ? ' fin' : ''}">${cpLine(l)}</span>`).join('')}`;
const cpLns = [...cpBody.querySelectorAll('.ln')], cpGlow = $('#cpterm .fin .glow');
cpPill.innerHTML = bold(CONSOLES_PILL.text);
function consoles(t) {
const s = scene('consoles'); if (t < s.start - 1 || t > s.end + 1) return;
const r = t - s.start;
const out = 1 - eio(prog(r, CONSOLES_SWAP[0], CONSOLES_SWAP[1]));
appear(cpTerm, r, 0.0, 0.35, 14);
set(cpTerm, 'opacity', String(eoc(prog(r, 0, 0.35)) * out));
const n = clamp(Math.floor((r - CONSOLE_TYPE_AT) * CONSOLE_CPS), 0, CONSOLE_CMD.length);
html(cpLns[0], `<span class="ps">$</span> <span class="cmd">${esc(CONSOLE_CMD.slice(0, n))}</span>${n < CONSOLE_CMD.length ? '<span class="caret"></span>' : ''}`);
set(cpLns[0], 'opacity', '1');
cpLns.slice(1).forEach((el, i) => appear(el, r, CONSOLE_LINES[i].at, 0.25, 6));
set(cpGlow, 'opacity', String(eoc(prog(r, CONSOLE_LINES.at(-1).at + 0.15, 0.4))));
pillAt(cpPill, r, CONSOLES_PILL.at);
set(cpPill, 'opacity', String(eoc(prog(r, CONSOLES_PILL.at, 0.4)) * out));
}
// ---------- 8 rejections ----------
const scanBody = $('#scanterm .body');
scanBody.innerHTML = `<span class="ln"><span class="ps">$</span> <span class="cmd">${esc(SCAN_CMD)}</span></span>`
+ `<span class="ln num risk">${esc(RISKS_LINE).replace(/(2 blocker, 3 high)/, '<span class="bad">$1</span>')}</span>`
@ -292,7 +320,7 @@ function rejections(t) {
appear(scanLns[3], r, RESCAN_AT + 0.35, 0.3, 6);
}
// ---------- 8-11 desktop: real frames in a window, one push per scene, one callout, hover sweeps on the charts ----------
// ---------- 7, 9-13 desktop: real frames in a window, one push per scene, callouts, hover sweeps, a resting pointer ----------
const BOXES = await fetch('out/desktop/boxes.json').then(r => { if (!r.ok) throw new Error('out/desktop/boxes.json missing (bun media/teaser/desktop.ts)'); return r.json(); });
const WIN = { x: 370, y: 236, w: 1180, bar: 40 };
const VIEW = { w: WIN.w, h: WIN.w * 800 / 1280 };
@ -307,14 +335,17 @@ const sweepOf = g => {
const desks = DESK_SCENES.map(id => {
const d = DESK[id], cam = $(`#s-${id} > .cam`);
const groups = d.groups.map(g => ({ g, sweep: sweepOf(g) }));
cam.innerHTML = `<div class="win" style="left:${WIN.x}px;top:${WIN.y}px;width:${WIN.w}px;height:${WIN.h}px">
// the window lives in the scene's .dhost (the consoles scene has its terminal next to it), else in the camera
const host = cam.querySelector('.dhost') ?? cam.appendChild(Object.assign(document.createElement('div'), { className: 'dhost' }));
host.innerHTML = `<div class="win" style="left:${WIN.x}px;top:${WIN.y}px;width:${WIN.w}px;height:${WIN.h}px">
<div class="bar"><i></i><i></i><i></i><span class="tt">storebound</span></div>
<div class="view"><div class="cam2">${groups.map((x, gi) => `<div class="grp" data-g="${gi}"><img src="out/desktop/${x.g.frame}.jpg" alt="">${(x.sweep ?? []).map(f => `<img src="out/desktop/${f.file}.jpg" alt="" style="opacity:0">`).join('')}</div>`).join('')}</div></div></div>
${d.callouts.map(c => `<div class="ring${c.green ? ' green' : ''}"></div><div class="callout${c.green ? ' green' : ''}">${esc(c.text)}</div>`).join('')}
${groups.some(x => x.sweep) ? '<span class="dcur" aria-hidden="true"><span class="pi i-cursor"></span></span>' : ''}`;
const grpEls = [...cam.querySelectorAll('.grp')];
return { id, d, groups, cam2: cam.querySelector('.cam2'), grpEls, imgs: grpEls.map(g => [...g.querySelectorAll('img')]),
rings: [...cam.querySelectorAll('.ring')], callouts: [...cam.querySelectorAll('.callout')], cur: cam.querySelector('.dcur') };
${groups.some(x => x.sweep) || d.pointer ? '<span class="dcur" aria-hidden="true"><span class="pi i-cursor"></span></span>' : ''}`;
if (d.pointer && !BOXES[d.pointer.target]) throw new Error(`boxes.json: no "${d.pointer.target}" (bun media/teaser/desktop.ts)`);
const grpEls = [...host.querySelectorAll('.grp')];
return { id, d, host, groups, cam2: host.querySelector('.cam2'), grpEls, imgs: grpEls.map(g => [...g.querySelectorAll('img')]),
rings: [...host.querySelectorAll('.ring')], callouts: [...host.querySelectorAll('.callout')], cur: host.querySelector('.dcur') };
});
function deskView(dk, r) {
const s = scene(dk.id), len = s.end - s.start;
@ -337,6 +368,10 @@ function sweepAt(sw, f, r) {
function deskScene(dk, t) {
const s = scene(dk.id); if (t < s.start - 1 || t > s.end + 1) return;
const r = t - s.start, d = dk.d;
// the window fades in (show) and out (hide), with its callouts and pointer
const shown = (d.show ? eio(prog(r, d.show[0], d.show[1])) : 1) * (d.hide ? 1 - eio(prog(r, d.hide[0], d.hide[1])) : 1);
set(dk.host, 'opacity', String(shown));
set(dk.host, 'visibility', shown > 0 ? 'visible' : 'hidden');
const c = deskView(dk, r);
set(dk.cam2, 'transform', `translate(${c.ox.toFixed(2)}px, ${c.oy.toFixed(2)}px) scale(${c.S.toFixed(5)})`);
const sw = d.swap ? eio(prog(r, d.swap[0], d.swap[1] - d.swap[0])) : 0;
@ -351,6 +386,14 @@ function deskScene(dk, t) {
const op = prog(r, x.g.sweep.at - 0.35, 0.3) * vis;
if (op > 0 && (!cur || op > cur.op)) cur = { ...st, op };
});
// a resting pointer: glides in from below right onto its target and stays there (it never clicks)
if (d.pointer) {
const pt = d.pointer, b = BOXES[pt.target];
const tx = b.x + b.w * 0.62, ty = b.y + b.h * 0.64;
const p = eio(prog(r, pt.at, pt.d));
const op = prog(r, pt.at - 0.25, 0.25) * (pt.until ? 1 - prog(r, pt.until - 0.1, 0.25) : 1);
if (op > 0) cur = { px: lerp(Math.min(tx + 0.16, 0.97), tx, p), py: lerp(Math.min(ty + 0.2, 0.97), ty, p), op };
}
if (dk.cur) {
set(dk.cur, 'opacity', String(cur ? cur.op : 0));
if (cur) {
@ -369,7 +412,7 @@ function deskScene(dk, t) {
set(ring, 'width', `${(x1 - x0).toFixed(1)}px`); set(ring, 'height', `${(y1 - y0).toFixed(1)}px`);
const cw = call.offsetWidth;
const cx = clamp((x0 + x1) / 2, 60 + cw / 2, W - 60 - cw / 2);
const below = y1 + 90 < WIN.y + WIN.h;
const below = !co.above && y1 + 90 < WIN.y + WIN.h;
set(call, 'opacity', String(op));
set(call, 'left', `${cx.toFixed(1)}px`);
set(call, 'top', `${(below ? y1 + 24 : y0 - 24).toFixed(1)}px`);
@ -378,25 +421,14 @@ function deskScene(dk, t) {
}
const desktop = t => desks.forEach(dk => deskScene(dk, t));
// ---------- 9 submit + end card ----------
const sbtn = $('#sbtn'), shov = $('#sbtn .hov'), brand = $('#brand'), cursorEl = $('#cursor');
// ---------- 13 submit: the end card (the app window is DESK.submit; it fades out as the card comes in) ----------
const brand = $('#brand');
brand.querySelector('.url').textContent = URL;
brand.querySelector('.plat').textContent = TAGLINE;
function submit(t) {
const s = scene('submit');
const r = t - s.start;
if (t < s.start - 1) { set(cursorEl, 'opacity', '0'); return; }
const hov = prog(r, SUBMIT.hover, 0.25);
const out = prog(r, SUBMIT.logoAt, 0.6);
set(sbtn, 'opacity', String(eoc(prog(r, 0.0, 0.4)) * (1 - eio(out))));
set(sbtn, 'transform', `translate(-50%, 0) scale(${(lerp(0.96, 1, eoc(prog(r, 0, 0.5))) * (1 + 0.02 * hov)).toFixed(4)})`);
set(shov, 'opacity', String(hov));
// the cursor glides in and rests on the button: it never clicks
const btn = sbtn.getBoundingClientRect();
const tx = btn.left + btn.width * 0.64, ty = btn.top + btn.height * 0.66;
const p = eio(prog(r, 0.3, SUBMIT.hover - 0.3));
set(cursorEl, 'opacity', String(prog(r, 0.3, 0.3) * (1 - eio(out))));
set(cursorEl, 'transform', `translate(${(lerp(W * 0.86, tx, p) - 10).toFixed(1)}px, ${(lerp(H * 0.95, ty, p) - 10).toFixed(1)}px)`);
if (t < s.start - 1) return;
[...brand.children].forEach((el, i) => {
const q2 = prog(r, SUBMIT.logoAt + 0.2 + i * 0.18, SUBMIT.logoIn);
set(el, 'opacity', String(eoc(q2)));
@ -413,6 +445,7 @@ function seek(t) {
store(t);
products(t);
forms(t);
consoles(t);
rejections(t);
desktop(t);
submit(t);
@ -423,7 +456,17 @@ const ready = (async () => {
await document.fonts.ready;
const imgs = [...document.images];
const bgs = [...document.querySelectorAll('.tile')].map(el => Object.assign(new Image(), { src: el.style.backgroundImage.replace(/^url\("?|"?\)$/g, '') }));
await Promise.all([...imgs, ...bgs].map(im => im.decode().catch(() => { throw new Error(`image failed: ${im.src}`); })));
// a few at a time, each retried: Chrome refuses some of ~90 parallel 2560x1600 decodes under load (EncodingError)
const all = [...imgs, ...bgs];
const decode = async im => {
for (let i = 0; ; i++) {
try { await im.decode(); return; } catch (e) {
if (i >= 3) throw new Error(`image failed: ${im.src} (${e})`);
await new Promise(r => setTimeout(r, 200 * (i + 1)));
}
}
};
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, DURATION, FPS, W, H, captionTimes };