teaser v9 animatic: 41 s story cut tight (hard cuts on the beat, 2-4 s shots, frame 0 moving, every shot opens on its subject, captions <= 6 words held >= 2 s, end card with npx storebound init held 4 s); render.ts --animatic (960x540 draft with sound, contact sheet, shot list, end-card layout checks); tests for the v9 timing rules
This commit is contained in:
parent
7ac028421a
commit
c608b9f63e
6 changed files with 326 additions and 207 deletions
|
|
@ -1,18 +1,18 @@
|
|||
// Teaser timeline (v8, a story): the single source of truth for the picture (teaser.js) and the sound (music.js).
|
||||
// 58 s, 16:9. Little text: one short line per scene at most, big and calm; the music carries the arc
|
||||
// (calm -> tension -> near silence -> a driving build -> stop -> resolve). Every cut lands on a half-second beat.
|
||||
// Teaser timeline (v9, the story cut tight): the single source of truth for the picture (teaser.js) and the sound
|
||||
// (music.js). 41 s, 16:9, 120 BPM: every cut lands on a beat (a whole half second), every shot does one thing in
|
||||
// 2-4 s and cuts when that thing completes. Frame 0 already moves; nothing holds still longer than 0.8 s; the last
|
||||
// 4 s are the end card with `npx storebound init`.
|
||||
//
|
||||
// game 0-5 the demo game's real screens, a gentle drift "The game is done."
|
||||
// pile 5-11 real store work piles up: App Privacy answers, Data safety CSV, "Then come the stores."
|
||||
// screenshot sizes, price sheets
|
||||
// reject 11-15 one real scan finding (5.1.1(v)) grows to fill the frame, it darkens "One missing line, and you start over."
|
||||
// ask 15-19 near silence: one request typed to the AI agent "So I handed it all to my AI."
|
||||
// builds..consoles 19-37 the montage, one word at the edge: Builds. / Screenshots, every size. / Products, 175
|
||||
// countries. / Forms. / Consoles.
|
||||
// scan 37-42 `storebound scan`: each finding turns green, the re-scan is clean "Nothing left for a reviewer to catch."
|
||||
// button 42-48 the desktop app's Send for review, the pointer resting on Submit; the music stops "One button is yours."
|
||||
// waiting 48-53 the click, then "Waiting for review" (the app's own state); the music resolves
|
||||
// end 53-58 the logo, "From an empty folder to Submit.", "Open source · storebound.dev"
|
||||
// game 0-3 cold open: the demo game's real screens rush past, a push in "Your game is done."
|
||||
// pile 3-7 a wall of real store work lands, one card per beat "Then come the stores."
|
||||
// reject 7-9.5 the real scan finding 5.1.1(v) slams in, the picture darkens "One line. Rejected."
|
||||
// ask 9.5-12 one request typed to the AI agent, sent on the beat "So my AI did it."
|
||||
// builds..consoles 12-27 the montage, 3 s each, one word at the edge, each result lands: Builds. /
|
||||
// Screenshots, every size. / Products, 175 countries. / Forms. / Consoles.
|
||||
// scan 27-31 `storebound scan`: one finding turns green per beat, the clean re-scan "Nothing left to reject."
|
||||
// button 31-33 the desktop app's Send for review, the camera pushes onto Submit "You press Submit."
|
||||
// waiting 33-36 the click, then the app's own "Waiting for review" (same line)
|
||||
// end 36-41 the logo, `npx storebound init` typed out, storebound.dev (4 s hold)
|
||||
//
|
||||
// Every CLI line shown is real output; tests/teaser.test.ts runs each command and checks every line:
|
||||
// pile `storebound console plan apple-app-privacy` (answer sheet, steps), `storebound play data-safety --dry`
|
||||
|
|
@ -31,74 +31,91 @@ export const FPS = 30;
|
|||
/** 16:9 only: the master; render.ts also makes the 1280x720 web version from it. */
|
||||
export const FORMAT = { w: 1920, h: 1080 };
|
||||
|
||||
/** Scenes: id, length (s), the transition into it (s, centred on the cut; short = a cut on the beat). */
|
||||
/** Shots: id, length (s), the transition into it (s; 0 = a hard cut on the beat). */
|
||||
const LENGTHS = [
|
||||
['game', 5, 0], ['pile', 6, 0.6], ['reject', 4, 0.5], ['ask', 4, 0.8],
|
||||
['builds', 4, 0.1], ['shots', 4, 0.1], ['products', 3.5, 0.1], ['forms', 3.5, 0.1], ['consoles', 3, 0.1],
|
||||
['scan', 5, 0.1], ['button', 6, 0.5], ['waiting', 5, 0.1], ['end', 5, 0.8],
|
||||
['game', 3, 0], ['pile', 4, 0], ['reject', 2.5, 0], ['ask', 2.5, 0],
|
||||
['builds', 3, 0], ['shots', 3, 0], ['products', 3, 0], ['forms', 3, 0], ['consoles', 3, 0],
|
||||
['scan', 4, 0], ['button', 2, 0], ['waiting', 3, 0], ['end', 5, 0],
|
||||
];
|
||||
export const SCENES = LENGTHS.reduce((a, [id, len, x]) => { const start = a.at(-1)?.end ?? 0; return [...a, { id, start, end: start + len, x }]; }, []);
|
||||
export const DURATION = SCENES.at(-1).end;
|
||||
export const scene = id => SCENES.find(s => s.id === id);
|
||||
/** The montage: one word or short phrase at the edge, cuts on the beat. */
|
||||
export const MONTAGE = ['builds', 'shots', 'products', 'forms', 'consoles'];
|
||||
|
||||
/** One camera move per scene: from -> to (z = zoom, x / y = pan in px), a slow ease (story) or a quick push (montage). */
|
||||
export const CAMERA = {
|
||||
game: { from: { z: 1.0, x: 40, y: 10 }, to: { z: 1.07, x: -40, y: -6 } },
|
||||
pile: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.12, x: 0, y: 10 } },
|
||||
reject: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
ask: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.04, x: 0, y: 0 } },
|
||||
builds: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.06, x: -20, y: -10 } },
|
||||
shots: { from: { z: 1.04, x: 30, y: 0 }, to: { z: 1.0, x: -30, y: 0 } },
|
||||
products: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.06, x: 20, y: -10 } },
|
||||
forms: { from: { z: 1.04, x: -20, y: 0 }, to: { z: 1.0, x: 20, y: 0 } },
|
||||
consoles: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
scan: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.05, x: 0, y: -10 } },
|
||||
button: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
waiting: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
end: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.03, x: 0, y: 0 } },
|
||||
/** Why each shot cuts where it does (the shot list, render.ts --animatic). */
|
||||
export const SHOT_NOTES = {
|
||||
game: 'the demo game\'s four real screens rush past, push in; cut as the push lands',
|
||||
pile: 'App Privacy, Data safety CSV, screenshot sizes, prices: one real card per beat; cut when the wall is full',
|
||||
reject: 'the real BLOCKER 5.1.1(v) finding slams in, push onto it, the picture darkens; cut when it fills the frame',
|
||||
ask: 'one request typed to the AI agent; cut on the send',
|
||||
builds: '`apple build --dry`, `play build --dry`: build 52 / versionCode 52; cut on "build planned"',
|
||||
shots: 'the demo game\'s renders: 7 store sizes x 4 languages fill in column by column; cut when the grid is full',
|
||||
products: '`play products --dry`, `catalog diff`: 175 region prices, 0 mismatches; cut on the 0',
|
||||
forms: 'scan\'s declarations proposal ticks the Data safety rows, the count runs to 768; cut when it lands',
|
||||
consoles: '`console plan apple-app-privacy`, then the app\'s plan panel, the owner\'s Publish ringed; cut on the ring',
|
||||
scan: '`storebound scan`: 5 findings turn green one per beat, the clean re-scan; cut on 0 blockers',
|
||||
button: 'the desktop app\'s Send for review, the camera pushes onto Submit, the pointer arrives; cut on arrival',
|
||||
waiting: 'the click, then the app\'s own "Waiting for review", ringed; cut when it settles',
|
||||
end: 'logo, `npx storebound init` typed out big, storebound.dev; holds 4 s (caret blink, slow push)',
|
||||
};
|
||||
|
||||
/** Captions: at most one short line per scene. top = the story line, big and centred at the top; edge = the montage
|
||||
* word, bottom left. at / until: seconds into the scene (fade in from `at`, gone at `until`). *word* = highlighted. */
|
||||
/** One camera move per shot: from -> to (z = zoom, x / y = pan in px), a quick push that settles at 80 % of the shot. */
|
||||
export const CAMERA = {
|
||||
game: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.14, x: 0, y: 30 } },
|
||||
pile: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.1, x: 0, y: 10 } },
|
||||
reject: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
ask: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.12, x: -60, y: 0 } },
|
||||
builds: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.1, x: 40, y: -40 } },
|
||||
shots: { from: { z: 0.98, x: 0, y: 0 }, to: { z: 1.08, x: 0, y: 0 } },
|
||||
products: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.1, x: 60, y: -50 } },
|
||||
forms: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.07, x: 30, y: 0 } },
|
||||
consoles: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
scan: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.08, x: 0, y: -20 } },
|
||||
button: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
waiting: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.0, x: 0, y: 0 } },
|
||||
end: { from: { z: 1.0, x: 0, y: 0 }, to: { z: 1.05, x: 0, y: 0 } },
|
||||
};
|
||||
|
||||
/** Captions: at most six words, each fully on screen for at least 2 s. top = the story line, big and centred at the
|
||||
* top; edge = the montage word, bottom left. at / until: absolute seconds. *word* = highlighted. */
|
||||
export const CAPTIONS = [
|
||||
{ id: 'game', pos: 'top', text: 'The game is *done.*', at: 0.6, until: 4.85 },
|
||||
{ id: 'pile', pos: 'top', text: 'Then come *the stores.*', at: 0.5, until: 5.9 },
|
||||
{ id: 'reject', pos: 'top', text: 'One missing line, and you *start over.*', at: 0.35, until: 3.95, hl: 'red' },
|
||||
{ id: 'ask', pos: 'top', text: 'So I handed it all *to my AI.*', at: 0.35, until: 3.95 },
|
||||
{ id: 'builds', pos: 'edge', text: 'Builds.', at: 0.05, until: 3.95 },
|
||||
{ id: 'shots', pos: 'edge', text: 'Screenshots, every size.', at: 0.05, until: 3.95 },
|
||||
{ id: 'products', pos: 'edge', text: 'Products, 175 countries.', at: 0.05, until: 3.45 },
|
||||
{ id: 'forms', pos: 'edge', text: 'Forms.', at: 0.05, until: 3.45 },
|
||||
{ id: 'consoles', pos: 'edge', text: 'Consoles.', at: 0.05, until: 2.95 },
|
||||
{ id: 'scan', pos: 'top', text: 'Nothing left for a reviewer *to catch.*', at: 1.5, until: 4.95, hl: 'green' },
|
||||
{ id: 'button', pos: 'top', text: 'One button is *yours.*', at: 0.9, until: 5.9 },
|
||||
{ id: 'game', pos: 'top', text: 'Your game is *done.*', at: 0.1, until: 2.95 },
|
||||
{ id: 'pile', pos: 'top', text: 'Then come *the stores.*', at: 3.05, until: 6.95 },
|
||||
{ id: 'reject', pos: 'top', text: 'One line. *Rejected.*', at: 7.05, until: 9.45, hl: 'red' },
|
||||
{ id: 'ask', pos: 'top', text: 'So my AI *did it.*', at: 9.55, until: 11.95 },
|
||||
{ id: 'builds', pos: 'edge', text: 'Builds.', at: 12.05, until: 14.95 },
|
||||
{ id: 'shots', pos: 'edge', text: 'Screenshots, every size.', at: 15.05, until: 17.95 },
|
||||
{ id: 'products', pos: 'edge', text: 'Products, 175 countries.', at: 18.05, until: 20.95 },
|
||||
{ id: 'forms', pos: 'edge', text: 'Forms.', at: 21.05, until: 23.95 },
|
||||
{ id: 'consoles', pos: 'edge', text: 'Consoles.', at: 24.05, until: 26.95 },
|
||||
{ id: 'scan', pos: 'top', text: 'Nothing left to *reject.*', at: 28.6, until: 30.95, hl: 'green' },
|
||||
{ id: 'button', pos: 'top', text: 'You press *Submit.*', at: 31.1, until: 35.95 },
|
||||
];
|
||||
/** Fades (s): the story lines slow and soft, the montage words quick. */
|
||||
export const CAP_FADE = { top: [0.5, 0.4], edge: [0.15, 0.15] };
|
||||
/** When each caption is fully on screen (tests: at least 2.5 s, inside its scene). */
|
||||
/** Fades (s): quick, a cut is a cut. */
|
||||
export const CAP_FADE = { top: [0.12, 0.08], edge: [0.1, 0.08] };
|
||||
/** When each caption is fully on screen. */
|
||||
export function captionTimes() {
|
||||
return CAPTIONS.map(c => {
|
||||
const s = scene(c.id), [fin, fout] = CAP_FADE[c.pos];
|
||||
const at = s.start + c.at, from = at + fin, to = s.start + c.until - fout, gone = s.start + c.until;
|
||||
return { id: c.id, text: c.text.replace(/\*/g, ''), pos: c.pos, at, from, to, gone, hold: to - from };
|
||||
const [fin, fout] = CAP_FADE[c.pos];
|
||||
const from = c.at + fin, to = c.until - fout;
|
||||
return { id: c.id, text: c.text.replace(/\*/g, ''), pos: c.pos, at: c.at, from, to, gone: c.until, hold: to - from };
|
||||
});
|
||||
}
|
||||
/** The dark veil over the picture (0-1) at time t: it falls over the rejection, holds through the request, lifts on
|
||||
* the first beat of the montage. */
|
||||
/** The dark veil over the picture (0-1) at time t: it falls with the rejection, holds through the request, gone on the
|
||||
* first beat of the montage. */
|
||||
export function veil(t) {
|
||||
const r = scene('reject'), a = scene('ask');
|
||||
if (t < r.start + 0.4) return 0;
|
||||
if (t < r.end) return 0.72 * Math.min(1, (t - r.start - 0.4) / 3.2);
|
||||
if (t < a.end - 0.05) return 0.72 - 0.12 * Math.min(1, (t - a.start) / 1.5);
|
||||
if (t < r.start + 0.15) return 0;
|
||||
if (t < r.end) return 0.7 * Math.min(1, (t - r.start - 0.15) / 1.6);
|
||||
if (t < a.end) return 0.6;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------- 1 game: the demo game's real screens ----------
|
||||
/** examples/demo-game/store/shots/frames/en/phone/<id>.png (the game's own captures), left to right. */
|
||||
export const GAME_SCREENS = ['play', 'combo', 'map', 'stars'];
|
||||
export const GAME_AT = 0.15, GAME_STEP = 0.22;
|
||||
/** The cold open: the screens rush past (px/s) while the camera pushes in; already moving on frame 0. */
|
||||
export const GAME_AT = 0, GAME_STEP = 0, GAME_SPEED = 520;
|
||||
|
||||
// ---------- 2 pile: the store work piles up ----------
|
||||
/** Real lines (see the header). */
|
||||
|
|
@ -157,16 +174,15 @@ export const PRICE_CMD = 'prices ppp --offline --dry --store apple --only com.ex
|
|||
/** The cards that drop onto the pile, faster and faster. at = seconds into the scene; x / y = the card's centre
|
||||
* (px), w = width, rot = degrees. kind: what it shows. */
|
||||
export const PILE = [
|
||||
{ at: 0.25, kind: 'privacy', x: 700, y: 560, w: 1000, rot: -3 },
|
||||
{ at: 1.25, kind: 'csv', x: 1220, y: 600, w: 980, rot: 2.5 },
|
||||
{ at: 2.15, kind: 'shots', x: 860, y: 640, w: 900, rot: -1.5 },
|
||||
{ at: 2.95, kind: 'prices', x: 1180, y: 560, w: 760, rot: 4 },
|
||||
{ at: 3.6, kind: 'steps', x: 760, y: 690, w: 1040, rot: 2 },
|
||||
{ at: 4.15, kind: 'csv', x: 1060, y: 610, w: 1000, rot: -4.5, from: 7 },
|
||||
{ at: 4.6, kind: 'shots', x: 940, y: 560, w: 940, rot: 3.5, lang: 'ja' },
|
||||
{ at: 5.0, kind: 'prices', x: 820, y: 640, w: 800, rot: -2.5, from: 11 },
|
||||
{ at: 5.35, kind: 'privacy', x: 1040, y: 600, w: 1060, rot: 1, from: 6 },
|
||||
];
|
||||
{ at: 0.0, kind: 'privacy', x: 640, y: 540, w: 1000, rot: -3 },
|
||||
{ at: 0.5, kind: 'csv', x: 1260, y: 600, w: 980, rot: 2.5 },
|
||||
{ at: 1.0, kind: 'shots', x: 820, y: 680, w: 900, rot: -1.5 },
|
||||
{ at: 1.5, kind: 'prices', x: 1240, y: 520, w: 760, rot: 4 },
|
||||
{ at: 2.0, kind: 'steps', x: 760, y: 700, w: 1040, rot: 2 },
|
||||
{ at: 2.5, kind: 'csv', x: 1100, y: 620, w: 1000, rot: -4.5, from: 7 },
|
||||
{ at: 3.0, kind: 'shots', x: 940, y: 560, w: 940, rot: 3.5, lang: 'ja' },
|
||||
{ at: 3.5, kind: 'prices', x: 980, y: 640, w: 820, rot: -2.5, from: 11 },
|
||||
]
|
||||
/** The card headers (what each card is; no claims). */
|
||||
export const PILE_TITLES = { privacy: 'App Privacy · answer sheet', steps: 'App Privacy · console steps', csv: `Data safety · ${DECL_TOTAL} rows`, shots: 'Screenshots', prices: 'Prices · every region' };
|
||||
|
||||
|
|
@ -176,13 +192,14 @@ export const REJECT = {
|
|||
head: 'BLOCKER 5.1.1(v) · App Store · Accounts can be created but no in-app account deletion was found',
|
||||
at: 'at Planner/LoginView.swift:11 Button("Create account") { Task { try await AccountApi.signUp() } }',
|
||||
};
|
||||
export const REJECT_GROW = [0.6, 1.12];
|
||||
/** The slam (scale from -> 1 in `slam` s), then a push onto the rule number. */
|
||||
export const REJECT_GROW = [1.0, 1.28], REJECT_SLAM = { from: 1.7, d: 0.16 };
|
||||
|
||||
// ---------- 4 ask: the request to the agent ----------
|
||||
export const PROMPT = 'Get this game ready for review on both stores.';
|
||||
export const PROMPT_AT = 0.7, PROMPT_CPS = 24;
|
||||
export const PROMPT_AT = 0.1, PROMPT_CPS = 31;
|
||||
/** Enter: the request is sent (a soft accent), seconds into the scene. */
|
||||
export const PROMPT_SENT = 3.05;
|
||||
export const PROMPT_SENT = 2.0;
|
||||
|
||||
// ---------- 5 montage ----------
|
||||
/** builds: real lines of `apple build --dry` and `play build --dry` (clock prefix and "(0s)" left out). */
|
||||
|
|
@ -202,18 +219,18 @@ export const SHOT_SIZES = [
|
|||
{ id: 'play', label: 'Play phone' }, { id: 'tab7', label: 'Play 7″' }, { id: 'tab10', label: 'Play 10″' }, { id: 'feature', label: 'Feature graphic' },
|
||||
];
|
||||
export const SHOT_LANGS = ['en', 'tr', 'ja', 'ar'];
|
||||
export const SHOTS_AT = 0.1, SHOTS_STEP = 0.2;
|
||||
export const SHOTS_AT = 0.05, SHOTS_STEP = 0.3, SHOTS_ROW = 0.1;
|
||||
/** products: real output (clock prefix and the "(0s)" timer left out). */
|
||||
export const PRODUCT_LINES = [
|
||||
{ at: 0.05, text: '$ storebound play products --dry', cmd: true },
|
||||
{ at: 0.3, text: '[play] 1/2 com.example.app.no_ads: would create · 174 regions · 2 listings (dry)' },
|
||||
{ at: 0.48, text: '[play] 2/2 com.example.app.gems_100: would create · 174 regions · 2 listings (dry)' },
|
||||
{ at: 0.8, text: '$ storebound catalog diff', cmd: true },
|
||||
{ at: 1.05, text: 'ok [apple] com.example.app.gems_100: consumable, 175 region prices, READY_TO_SUBMIT' },
|
||||
{ at: 1.2, text: 'ok [play] com.example.app.gems_100: consumable, 174 region prices, ACTIVE' },
|
||||
{ at: 1.35, text: 'ok [apple] com.example.app.no_ads: non_consumable, 175 region prices, READY_TO_SUBMIT' },
|
||||
{ at: 1.5, text: 'ok [play] com.example.app.no_ads: non_consumable, 174 region prices, ACTIVE' },
|
||||
{ at: 1.75, text: '[catalog] 2 catalog items: 0 mismatch(es), 0 warning(s), 0 price difference(s) between the stores' },
|
||||
{ at: 0.07, text: '$ storebound play products --dry', cmd: true },
|
||||
{ at: 0.41, text: '[play] 1/2 com.example.app.no_ads: would create · 174 regions · 2 listings (dry)' },
|
||||
{ at: 0.65, text: '[play] 2/2 com.example.app.gems_100: would create · 174 regions · 2 listings (dry)' },
|
||||
{ at: 1.08, text: '$ storebound catalog diff', cmd: true },
|
||||
{ at: 1.42, text: 'ok [apple] com.example.app.gems_100: consumable, 175 region prices, READY_TO_SUBMIT' },
|
||||
{ at: 1.62, text: 'ok [play] com.example.app.gems_100: consumable, 174 region prices, ACTIVE' },
|
||||
{ at: 1.82, text: 'ok [apple] com.example.app.no_ads: non_consumable, 175 region prices, READY_TO_SUBMIT' },
|
||||
{ at: 2.03, text: 'ok [play] com.example.app.no_ads: non_consumable, 174 region prices, ACTIVE' },
|
||||
{ at: 2.36, text: '[catalog] 2 catalog items: 0 mismatch(es), 0 warning(s), 0 price difference(s) between the stores' },
|
||||
];
|
||||
/** forms: the declarations proposal (scan of the teaser fixture), then the Data safety rows it fills. */
|
||||
export const DECL_LINES = [
|
||||
|
|
@ -253,10 +270,10 @@ export const FINDINGS = [
|
|||
'HIGH 3.1.1 · App Store · Restorable purchases (non-consumables or subscriptions) but no Restore Purchases path was found',
|
||||
'HIGH 5.1.2(i) · App Store · Tracking-capable code without App Tracking Transparency (Google AdMob, IDFA)',
|
||||
];
|
||||
export const FINDINGS_AT = 0.4, FINDINGS_STEP = 0.12;
|
||||
export const FINDINGS_AT = 0.2, FINDINGS_STEP = 0.06;
|
||||
/** Each finding turns green (fixed) on a beat, then the re-scan of the fixed copy (tests/fixtures/scan/teaser-fixed). */
|
||||
export const FIX_AT = [1.0, 1.5, 2.0, 2.5, 3.0];
|
||||
export const RESCAN_AT = 3.45;
|
||||
export const FIX_AT = [0.5, 1.0, 1.5, 2.0, 2.5];
|
||||
export const RESCAN_AT = 3.0;
|
||||
export const RESCAN_LINE = 'Rejection risks: 0 blocker, 0 high, 0 medium, 0 low';
|
||||
|
||||
// ---------- desktop: real frames of the app in fixture mode (desktop.ts) ----------
|
||||
|
|
@ -269,21 +286,21 @@ export const DESK = {
|
|||
groups: [{ frame: 'console-plan' }],
|
||||
show: CONSOLES_SWAP,
|
||||
cam: { from: { f: [0.5, 0.56], z: 1.12 }, to: { f: [0.5, 0.58], z: 1.22 } },
|
||||
rings: [{ at: 1.6, until: 3.2, target: 'cpFinal', green: true }],
|
||||
rings: [{ at: 1.5, until: 3.4, target: 'cpFinal', green: true }],
|
||||
},
|
||||
// Releases > Send for review: build 52 picked, the one button left; the camera settles on it, the pointer rests
|
||||
button: {
|
||||
groups: [{ frame: 'submit-ready' }],
|
||||
cam: { from: { f: [0.45, 0.5], z: 1.0 }, to: { f: [0.3, 0.68], z: 1.55 } },
|
||||
rings: [{ at: 3.2, until: 6.3, target: 'submitBtn' }],
|
||||
pointer: { at: 1.9, d: 1.1, target: 'submitBtn' },
|
||||
rings: [{ at: 1.3, until: 2.6, target: 'submitBtn' }],
|
||||
pointer: { at: 0.4, d: 0.9, target: 'submitBtn' },
|
||||
},
|
||||
// the press, then the app's own "Waiting for review" state
|
||||
waiting: {
|
||||
groups: [{ frame: 'submit-ready' }, { frame: 'submit-waiting' }],
|
||||
swap: [0.45, 0.9],
|
||||
cam: { from: { f: [0.3, 0.68], z: 1.55 }, to: { f: [0.4, 0.4], z: 1.3 } },
|
||||
rings: [{ at: 1.2, until: 5.3, target: 'waitingItem', green: true }],
|
||||
rings: [{ at: 1.0, until: 3.4, target: 'waitingItem', green: true }],
|
||||
pointer: { at: -1, d: 0.01, target: 'submitBtn', click: 0.2, until: 0.6 },
|
||||
},
|
||||
};
|
||||
|
|
@ -293,11 +310,13 @@ export const DESK_FRAMES = [...new Set(Object.values(DESK).flatMap(d => d.groups
|
|||
/** The click on Submit (absolute seconds): the music's resolve starts here. */
|
||||
export const CLICK_T = scene('waiting').start + DESK.waiting.pointer.click;
|
||||
|
||||
// ---------- 9 end card ----------
|
||||
/** Seconds into the end scene at which each line comes in. */
|
||||
export const END = { markAt: 0.25, wordAt: 0.5, lineAt: 0.9, urlAt: 1.25, d: 0.6 };
|
||||
export const END_LINE = 'From an empty folder to Submit.';
|
||||
export const END_URL = 'Open source · storebound.dev';
|
||||
// ---------- 9 end card: the last thing on screen ----------
|
||||
/** Seconds into the end shot: the logo and word slam in, the install line is typed, then the site. Everything is on
|
||||
* screen by `full`; the rest (at least 4 s) holds with a blinking caret and a slow push. */
|
||||
export const END_CMD = 'npx storebound init';
|
||||
export const END_URL = 'storebound.dev';
|
||||
export const END = { markAt: 0, wordAt: 0.06, cmdAt: 0.15, cps: 40, urlAt: 0.7, d: 0.2 };
|
||||
END.full = Math.max(END.cmdAt + END_CMD.length / END.cps, END.urlAt + END.d);
|
||||
|
||||
// ---------- sound cues (music.js) ----------
|
||||
/** Cuts with a sound accent (every scene change but the seamless button -> waiting one). */
|
||||
|
|
@ -321,5 +340,6 @@ export function ticks() {
|
|||
export function keyTimes() {
|
||||
const out = [];
|
||||
for (let i = 0; i < PROMPT.length; i++) if (PROMPT[i] !== ' ' || i % 2) out.push(scene('ask').start + PROMPT_AT + i / PROMPT_CPS);
|
||||
for (let i = 0; i < END_CMD.length; i++) out.push(scene('end').start + END.cmdAt + i / END.cps);
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue