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:
parent
710c3a53d0
commit
614c32bcba
8 changed files with 391 additions and 99 deletions
|
|
@ -1,5 +1,5 @@
|
|||
// Teaser timeline (v6): the single source of truth for the picture (teaser.js) and the sound (music.js).
|
||||
// One idea per scene, 4-5 s each (about 55 s in all), one camera move per scene with a quick ease-out, short dissolves
|
||||
// Teaser timeline (v7): the single source of truth for the picture (teaser.js) and the sound (music.js).
|
||||
// One idea per scene, 4-5 s each (62 s in all), one camera move per scene with a quick ease-out, short dissolves
|
||||
// on the cuts (no whips, no rapid montages). Long terminal output streams faster instead of being cut. Every scene has
|
||||
// one plain-English caption at the top that is fully on screen for at least 3 s; the lower part shows the real
|
||||
// product: CLI output and desktop-app frames.
|
||||
|
|
@ -12,9 +12,11 @@
|
|||
// to the config, the same store double (before: nothing on the stores; after: both products on both)
|
||||
// forms the declarations proposal of `storebound scan` (tests/fixtures/scan/teaser) and the Data safety
|
||||
// answer count of `storebound check`
|
||||
// consoles `storebound console plan apple-app-privacy` in the init folder (offline)
|
||||
// rejections `storebound scan` of tests/fixtures/scan/teaser and, after the fixes, of teaser-fixed
|
||||
// The store text is examples/demo-game/store/shots/copy.json, the screenshots its real renders (render.ts copies
|
||||
// them), the desktop frames the app in fixture mode (desktop.ts). The plain reasons under the findings and the
|
||||
// them), the desktop frames the app in fixture mode (desktop.ts; its console plan panel shows the real plan of
|
||||
// `console plan apple-app-privacy --json`). The plain reasons under the findings and the
|
||||
// callouts are annotations, set apart from the terminal text.
|
||||
|
||||
export const FPS = 30;
|
||||
|
|
@ -22,7 +24,7 @@ export const FPS = 30;
|
|||
export const FORMAT = { w: 1920, h: 1080 };
|
||||
|
||||
/** Scene lengths in seconds (4-5 s each). The next scene dissolves in over DISSOLVE s centred on the cut. */
|
||||
const LENGTHS = [['hook', 4.5], ['init', 4.5], ['plan', 5], ['store', 5], ['products', 4.5], ['forms', 5], ['rejections', 5], ['portfolio', 4.5], ['charts', 4.5], ['catalog', 4.5], ['desktop', 4.5], ['submit', 4]];
|
||||
const LENGTHS = [['hook', 4.5], ['init', 4.5], ['plan', 5], ['store', 5], ['products', 4.5], ['forms', 5], ['consoles', 5], ['rejections', 5], ['portfolio', 4.5], ['charts', 4.5], ['catalog', 4.5], ['resubmit', 5], ['submit', 5]];
|
||||
export const SCENES = LENGTHS.reduce((a, [id, len]) => { const start = a.at(-1)?.end ?? 0; return [...a, { id, start, end: start + len }]; }, []);
|
||||
export const DURATION = SCENES.at(-1).end;
|
||||
export const DISSOLVE = 0.4;
|
||||
|
|
@ -39,13 +41,15 @@ export const CAMERA = {
|
|||
store: { from: { z: 1.02, x: 40, y: 0 }, to: { z: 1.02, x: -40, y: 0 } },
|
||||
products: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.06, x: 0, y: -10 } },
|
||||
forms: { from: { z: 1.02, x: 30, y: 0 }, to: { z: 1.02, x: -30, y: 0 } },
|
||||
// the terminal, then the app's plan panel dissolving in over it: one slow push over both
|
||||
consoles: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.03, x: 0, y: 4 } },
|
||||
rejections: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.05, x: 0, y: -12 } },
|
||||
// the desktop scenes: the push happens inside the app window (DESK)
|
||||
portfolio: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
charts: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
catalog: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
desktop: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
submit: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.05, x: 0, y: 0 } },
|
||||
resubmit: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
submit: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
};
|
||||
|
||||
/** Captions: one per scene, at the top. *word* = highlighted. Lines are set as written. */
|
||||
|
|
@ -56,11 +60,12 @@ export const CAPTIONS = [
|
|||
{ id: 'store', lines: ['Store text and screenshots.', '*Every language, every size.*'] },
|
||||
{ id: 'products', lines: ['In-app products on both stores,', 'priced in *175 countries.*'] },
|
||||
{ id: 'forms', lines: ['The forms, answered from your code.', 'It asks what it *can\'t know.*'] },
|
||||
{ id: 'consoles', lines: ['No API? *Your AI fills the console.*', 'It stops at Publish.'] },
|
||||
{ id: 'rejections', lines: ['It catches rejections *before Apple does.*'], hl: 'green' },
|
||||
{ id: 'portfolio', lines: ['Stop switching between two consoles.', 'Every app, both stores, *one window.*'] },
|
||||
{ id: 'charts', lines: ['Installs, conversion and crashes, *charted.*'] },
|
||||
{ id: 'catalog', lines: ['Products and prices on both stores, *side by side.*'] },
|
||||
{ id: 'desktop', lines: ['Both stores, one view. *Ready for review.*'] },
|
||||
{ id: 'resubmit', lines: ['Rejected? Fix it, *resubmit in one click.*'] },
|
||||
{ id: 'submit', lines: ['You press *Submit.* That\'s it.'] },
|
||||
];
|
||||
/** Caption fade: in over CAP_IN from CAP_DELAY after the scene starts; out over CAP_OUT, ending CAP_END before it ends. */
|
||||
|
|
@ -187,7 +192,31 @@ export const FORM_AT = 1.9, FORM_STEP = 0.14;
|
|||
export const DECL_TOTAL = 768;
|
||||
export const FORMS_PILL = { at: 3.0, text: 'Data safety answers' };
|
||||
|
||||
// ---------- 7 rejections: three findings with a plain reason each, then fixed ----------
|
||||
// ---------- 7 consoles: the console plan of a form without an API, then the app's plan panel ----------
|
||||
export const CONSOLE_CMD = 'storebound console plan apple-app-privacy';
|
||||
/** The command is typed (fast), then the output streams. */
|
||||
export const CONSOLE_TYPE_AT = 0.05, CONSOLE_CPS = 120;
|
||||
/** Real lines of the plan (a fresh `storebound init` folder); the other steps and answers left out. at = seconds into
|
||||
* the scene. final = the owner's button, highlighted. */
|
||||
export const CONSOLE_LINES = [
|
||||
{ at: 0.45, text: 'App Store Connect: App Privacy', head: true },
|
||||
{ at: 0.6, text: 'Steps:', head: true },
|
||||
{ at: 0.72, text: ' 1. Open App Store Connect > Apps > the app > App Privacy (sidebar).' },
|
||||
{ at: 0.84, text: ' 2. First time: click "Get Started" and answer the Data Collection question; then Next. [click: "Get Started"]' },
|
||||
{ at: 0.96, text: ' 39. Stop. Do NOT press Publish: tell the owner to review the summary on the App Privacy page and press Publish, then Publish again in the "Publish Your App Privacy Responses?" dialog (it puts the answers on the App Store product page). Afterwards the page shows "Published … ago by …".' },
|
||||
{ at: 1.14, text: 'Answer sheet:', head: true },
|
||||
{ at: 1.26, text: ' [ok] Data type: Contact Info > Email Address: Yes -> tick "Email Address" @ the Data Collection dialog (Edit next to Data Types): the data type list' },
|
||||
{ at: 1.38, text: ' [ask] Data type: User Content > Gameplay Content: (you decide) -> untick "Gameplay Content" @ the Data Collection dialog (Edit next to Data Types): the data type list' },
|
||||
{ at: 1.56, text: 'Never press: "Publish", "Submit", "Create", "Create app", "Send", "Release", "Definitions and Examples", "?", "More information"' },
|
||||
{ at: 1.72, text: "Final button (the owner's): Publish", final: true },
|
||||
];
|
||||
/** Steps and answer-sheet rows in the whole plan (the numbered and the [ok] / [ask] lines of the real output). */
|
||||
export const CONSOLE_STEPS = 40, CONSOLE_ANSWERS = 92;
|
||||
export const CONSOLES_PILL = { at: 1.95, text: `*${CONSOLE_STEPS}* steps · *${CONSOLE_ANSWERS}* answers · stops before *Publish*` };
|
||||
/** The terminal gives way to the app's plan panel (DESK.consoles) over this window, seconds into the scene. */
|
||||
export const CONSOLES_SWAP = [2.45, 0.5];
|
||||
|
||||
// ---------- 8 rejections: three findings with a plain reason each, then fixed ----------
|
||||
export const SCAN_CMD = 'storebound scan';
|
||||
export const RISKS_LINE = 'Rejection risks: 2 blocker, 3 high, 0 medium, 0 low';
|
||||
/** Three of the five findings (real text); why = a plain-English annotation, not CLI output. */
|
||||
|
|
@ -204,13 +233,22 @@ export const FIX_AT = [2.7, 2.95, 3.2];
|
|||
export const RESCAN_AT = 3.6;
|
||||
export const RESCAN_LINE = 'Rejection risks: 0 blocker, 0 high, 0 medium, 0 low';
|
||||
|
||||
// ---------- 8-11 desktop: the app as the one console for both stores ----------
|
||||
// ---------- 7, 9-13 desktop: the app as the one console for both stores ----------
|
||||
/** Real frames of the desktop app in fixture mode (desktop.ts), one entry per scene. groups: what the window shows, in
|
||||
* order; a group is one frame, or a frame plus a hover sweep (the pointer glides across a chart, the crosshair and
|
||||
* tooltip follow it day by day: one captured frame per hovered day, pointer positions in boxes.json). swap: the
|
||||
* dissolve from group 1 to group 2 (seconds into the scene). cam: one push, f = focus point (fraction of the 1280x800
|
||||
* frame), z = zoom. callouts: one per scene, on a box captured from the app (boxes.json). */
|
||||
* frame), z = zoom. callouts: on boxes captured from the app (boxes.json), below the box (above: true, above it).
|
||||
* show / hide: the window fades in / out ([at, seconds]); pointer: the mouse pointer glides onto a captured box and
|
||||
* rests there (it never clicks). */
|
||||
export const DESK = {
|
||||
// the console plan panel (App info > Console steps > App Privacy > Plan) with the real plan, at its final button
|
||||
consoles: {
|
||||
groups: [{ frame: 'console-plan' }],
|
||||
show: CONSOLES_SWAP,
|
||||
cam: { from: { f: [0.5, 0.56], z: 1.18 }, to: { f: [0.5, 0.56], z: 1.18 } },
|
||||
callouts: [{ at: 3.15, until: 5.3, target: 'cpFinal', text: 'Publish stays yours: it stops there', green: true }],
|
||||
},
|
||||
portfolio: {
|
||||
groups: [{ frame: 'portfolio-apps' }],
|
||||
cam: { from: { f: [0.5, 0.42], z: 1.0 }, to: { f: [0.6, 0.6], z: 1.14 } },
|
||||
|
|
@ -227,15 +265,28 @@ export const DESK = {
|
|||
cam: { from: { f: [0.55, 0.4], z: 1.0 }, to: { f: [0.72, 0.42], z: 1.14 } },
|
||||
callouts: [{ at: 1.2, until: 5, target: 'priceGap', text: 'Price gap between the stores, per country' }],
|
||||
},
|
||||
// v5's desktop scene, unchanged: the release timeline, then Store readiness (validate): no blockers
|
||||
desktop: {
|
||||
groups: [{ frame: 'releases' }, { frame: 'ready' }],
|
||||
swap: [2.1, 2.6],
|
||||
cam: { from: { f: [0.56, 0.42], z: 1.0 }, to: { f: [0.8, 0.4], z: 1.22 } },
|
||||
// Releases > Send for review, back from App Review (UNRESOLVED_ISSUES): the rejected item, build 52 picked, Resubmit
|
||||
resubmit: {
|
||||
groups: [{ frame: 'resubmit' }],
|
||||
cam: { from: { f: [0.45, 0.45], z: 1.0 }, to: { f: [0.3, 0.62], z: 1.18 } },
|
||||
callouts: [
|
||||
{ at: 0.7, until: 2.0, target: 'timeline', text: 'App Store + Google Play, one track' },
|
||||
{ at: 2.9, until: 5, target: 'readyEmpty', text: 'Validate: 0 blockers on both', green: true },
|
||||
{ at: 0.6, until: 2.5, target: 'unresolvedItem', text: 'Back from App Review: what was rejected' },
|
||||
{ at: 2.75, until: 5.3, target: 'resubmitBtn', text: 'Marks it resolved and resubmits' },
|
||||
],
|
||||
pointer: { at: 2.2, d: 0.8, target: 'resubmitBtn' },
|
||||
},
|
||||
// Submit for review: the dry-run plan with "Continue" (the in-app step before the system dialog), then the new
|
||||
// submission waiting for review; the window then gives way to the end card
|
||||
submit: {
|
||||
groups: [{ frame: 'submit-plan' }, { frame: 'submit-waiting' }],
|
||||
swap: [1.55, 1.95],
|
||||
hide: [3.0, 0.5],
|
||||
cam: { from: { f: [0.5, 0.45], z: 1.0 }, to: { f: [0.5, 0.48], z: 1.1 } },
|
||||
callouts: [
|
||||
{ at: 0.35, until: 1.5, target: 'oaPlan', text: 'Dry run first: nothing sent yet', above: true },
|
||||
{ at: 2.05, until: 3.0, target: 'waitingItem', text: 'Waiting for review', green: true },
|
||||
],
|
||||
pointer: { at: 0.3, d: 0.85, target: 'oaContinue', until: 1.55 },
|
||||
},
|
||||
};
|
||||
export const DESK_SCENES = Object.keys(DESK);
|
||||
|
|
@ -243,8 +294,9 @@ export const DESK_SCENES = Object.keys(DESK);
|
|||
export const DESK_FRAMES = [...new Set(Object.values(DESK).flatMap(d => d.groups.map(g => g.frame)))];
|
||||
export const DESK_SWEEPS = Object.values(DESK).flatMap(d => d.groups.filter(g => g.sweep).map(g => g.sweep.seq));
|
||||
|
||||
// ---------- 9 submit + end card ----------
|
||||
export const SUBMIT = { hover: 1.0, logoAt: 2.0, logoIn: 0.6 };
|
||||
// ---------- 13 submit: end card ----------
|
||||
/** The end card comes in at logoAt (seconds into the submit scene), as the app window fades out (DESK.submit.hide). */
|
||||
export const SUBMIT = { logoAt: 3.0, logoIn: 0.6 };
|
||||
export const URL = 'storebound.dev';
|
||||
export const TAGLINE = 'Open source · CLI · Desktop app';
|
||||
|
||||
|
|
@ -265,9 +317,10 @@ export function ticks() {
|
|||
...DECL_LINES.map((_, i) => ({ t: at('forms') + DECL_AT + i * DECL_STEP, kind: 'tiny' })),
|
||||
{ t: at('forms') + ASK_AT, kind: 'ask' },
|
||||
...DATA_SAFETY_ROWS.map((_, i) => ({ t: at('forms') + FORM_AT + i * FORM_STEP, kind: 'tiny' })),
|
||||
...CONSOLE_LINES.map(l => ({ t: at('consoles') + l.at, kind: l.final ? 'ask' : 'tiny' })),
|
||||
...FINDINGS.map(f => ({ t: at('rejections') + f.at, kind: 'low' })),
|
||||
...FIX_AT.map((t, i) => ({ t: at('rejections') + t, kind: 'good', step: i })),
|
||||
...DESK_SCENES.flatMap(id => DESK[id].callouts.map(c => ({ t: at(id) + c.at, kind: 'soft' }))),
|
||||
...DESK_SCENES.flatMap(id => DESK[id].callouts.map(c => (id === 'submit' && c.green ? { t: at(id) + c.at, kind: 'good', step: 2 } : { t: at(id) + c.at, kind: 'soft' }))),
|
||||
];
|
||||
}
|
||||
/** Seconds at which each typed character lands (soft key clicks). */
|
||||
|
|
@ -275,5 +328,6 @@ export function keyTimes() {
|
|||
const out = [];
|
||||
for (let i = 0; i < INIT_CMD.length; i++) out.push(scene('init').start + INIT_TYPE_AT + i / INIT_CPS);
|
||||
for (let i = 0; i < PROMPT.length; i++) out.push(scene('plan').start + PROMPT_AT + i / PROMPT_CPS);
|
||||
for (let i = 0; i < CONSOLE_CMD.length; i += 3) out.push(scene('consoles').start + CONSOLE_TYPE_AT + i / CONSOLE_CPS);
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue