reelkit-cli 0.6.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +5 -3
  2. package/package.json +52 -9
  3. package/skill/SKILL.md +40 -19
  4. package/skill/THIRD_PARTY.md +104 -2
  5. package/skill/commands/launch-film.md +7 -0
  6. package/skill/reference/art-styles.md +70 -0
  7. package/skill/reference/asset-reuse.md +13 -2
  8. package/skill/reference/backgrounds.md +63 -0
  9. package/skill/reference/beat-sync.md +25 -19
  10. package/skill/reference/brand-motion.md +62 -0
  11. package/skill/reference/captions.md +11 -5
  12. package/skill/reference/clips.md +3 -3
  13. package/skill/reference/component-authoring.md +1 -1
  14. package/skill/reference/continuity.md +26 -7
  15. package/skill/reference/delivery-review.md +40 -0
  16. package/skill/reference/hebrew-rtl.md +3 -4
  17. package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +18 -11
  18. package/skill/reference/kit.md +144 -12
  19. package/skill/reference/launch-film.md +194 -0
  20. package/skill/reference/motion-design.md +18 -16
  21. package/skill/reference/scene-treatments.md +20 -0
  22. package/skill/reference/scriptwriting.md +4 -1
  23. package/skill/reference/sound-design.md +34 -12
  24. package/skill/reference/studio-editing.md +55 -0
  25. package/skill/reference/styles.md +9 -6
  26. package/skill/reference/three-d.md +135 -0
  27. package/skill/reference/voice-sync.md +108 -0
  28. package/src/agents.ts +23 -12
  29. package/src/api/client.ts +4 -1
  30. package/src/cli.ts +18 -7
  31. package/src/commands/assets.ts +314 -30
  32. package/src/commands/build.ts +148 -35
  33. package/src/commands/init.ts +1 -1
  34. package/src/commands/install.ts +1 -1
  35. package/src/commands/plan.ts +8 -5
  36. package/src/commands/ref.ts +5 -2
  37. package/src/contract/index.ts +5 -3
  38. package/src/hyperframes/Root.tsx +1 -0
  39. package/src/hyperframes/fonts.ts +54 -0
  40. package/src/hyperframes/frame.tsx +46 -0
  41. package/src/hyperframes/host.tsx +38 -0
  42. package/src/hyperframes/kit/Assemble3D.tsx +92 -0
  43. package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
  44. package/src/hyperframes/kit/BrowserFrame.tsx +83 -0
  45. package/src/{remotion → hyperframes}/kit/Camera.tsx +7 -5
  46. package/src/{remotion → hyperframes}/kit/Captions.tsx +34 -18
  47. package/src/hyperframes/kit/Card3D.tsx +211 -0
  48. package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
  49. package/src/hyperframes/kit/ChapterFrame.tsx +68 -0
  50. package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
  51. package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
  52. package/src/hyperframes/kit/CounterRoll.tsx +75 -0
  53. package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
  54. package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
  55. package/src/hyperframes/kit/GlassPanel.tsx +43 -0
  56. package/src/hyperframes/kit/Grounds.tsx +177 -0
  57. package/src/hyperframes/kit/Headline.tsx +97 -0
  58. package/src/hyperframes/kit/Hero3D.tsx +197 -0
  59. package/src/hyperframes/kit/HudOverlay.tsx +52 -0
  60. package/src/hyperframes/kit/ImageLayers.tsx +48 -0
  61. package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
  62. package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
  63. package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
  64. package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
  65. package/src/hyperframes/kit/Music.tsx +19 -0
  66. package/src/hyperframes/kit/NamedCursor.tsx +54 -0
  67. package/src/hyperframes/kit/Orbit3D.tsx +49 -0
  68. package/src/hyperframes/kit/Particles3D.tsx +74 -0
  69. package/src/hyperframes/kit/Place.tsx +12 -0
  70. package/src/hyperframes/kit/PromptBox.tsx +84 -0
  71. package/src/hyperframes/kit/Scene3D.tsx +70 -0
  72. package/src/hyperframes/kit/SceneFrame.tsx +96 -0
  73. package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
  74. package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
  75. package/src/hyperframes/kit/SoundCues.tsx +22 -0
  76. package/src/hyperframes/kit/TerminalLog.tsx +98 -0
  77. package/src/hyperframes/kit/Text3D.tsx +78 -0
  78. package/src/hyperframes/kit/TextOnImage.tsx +41 -0
  79. package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
  80. package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
  81. package/src/hyperframes/kit/Warp3D.tsx +59 -0
  82. package/src/hyperframes/kit/bg-math.ts +179 -0
  83. package/src/hyperframes/kit/brand-transform.ts +25 -0
  84. package/src/{remotion → hyperframes}/kit/caption-groups.ts +7 -3
  85. package/src/hyperframes/kit/caption-style.ts +45 -0
  86. package/src/hyperframes/kit/docs.ts +249 -0
  87. package/src/hyperframes/kit/image-layers-math.ts +115 -0
  88. package/src/hyperframes/kit/index.ts +74 -0
  89. package/src/hyperframes/kit/inter-bold-typeface.ts +3 -0
  90. package/src/{remotion → hyperframes}/kit/motion-math.ts +36 -2
  91. package/src/hyperframes/kit/music-math.ts +59 -0
  92. package/src/hyperframes/kit/quiet-three.ts +11 -0
  93. package/src/hyperframes/kit/sample-text.ts +55 -0
  94. package/src/hyperframes/kit/scene3d-context.ts +5 -0
  95. package/src/hyperframes/kit/seeded.ts +13 -0
  96. package/src/hyperframes/kit/sound-cues.ts +89 -0
  97. package/src/hyperframes/kit/sound-kinds.ts +135 -0
  98. package/src/{remotion → hyperframes}/kit/theme.ts +43 -39
  99. package/src/hyperframes/kit/three-fx-math.ts +192 -0
  100. package/src/hyperframes/kit/three-math.ts +145 -0
  101. package/src/hyperframes/kit/transition-math.ts +116 -0
  102. package/src/hyperframes/kit/ui-math.ts +145 -0
  103. package/src/hyperframes/kit/ui-theme.ts +25 -0
  104. package/src/hyperframes/kit/word-anchor.ts +107 -0
  105. package/src/hyperframes/math.ts +62 -0
  106. package/src/hyperframes/three.tsx +10 -0
  107. package/src/pipeline/beatsnap.ts +72 -0
  108. package/src/pipeline/review.ts +44 -10
  109. package/src/pipeline/schema.ts +51 -4
  110. package/src/pipeline/timing.ts +27 -1
  111. package/src/project/background.ts +33 -0
  112. package/src/project/chromakey.ts +1 -1
  113. package/src/project/layers.ts +60 -0
  114. package/src/project/manifest.ts +59 -14
  115. package/src/project/music.ts +19 -5
  116. package/src/project/project.ts +4 -1
  117. package/src/project/serve.ts +2 -2
  118. package/src/project/soundreport.ts +347 -0
  119. package/src/project/svgcheck.ts +21 -0
  120. package/src/render/component-preview.ts +11 -55
  121. package/src/render/contact-sheet.ts +39 -0
  122. package/src/render/continuity.ts +14 -4
  123. package/src/render/deps.ts +15 -3
  124. package/src/render/render.ts +62 -57
  125. package/src/render/serve.ts +31 -0
  126. package/src/render/sound-notes.ts +106 -0
  127. package/src/render/static-check.ts +15 -4
  128. package/src/render/validate.ts +4 -4
  129. package/src/render/word-check.ts +181 -0
  130. package/src/render/worker.ts +71 -0
  131. package/src/testing/conformance.ts +12 -0
  132. package/src/testing/fake-api.ts +4 -4
  133. package/src/testing/fixtures.ts +4 -1
  134. package/src/remotion/Root.tsx +0 -31
  135. package/src/remotion/kit/Music.tsx +0 -19
  136. package/src/remotion/kit/SceneFrame.tsx +0 -19
  137. package/src/remotion/kit/docs.ts +0 -124
  138. package/src/remotion/kit/index.ts +0 -29
  139. package/src/remotion/kit/music-math.ts +0 -42
  140. /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
  141. /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
  142. /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
  143. /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
  144. /package/src/{remotion → hyperframes}/types.ts +0 -0
@@ -1,11 +1,13 @@
1
1
  # The starter kit
2
2
 
3
+ Library interface components (widgets, banners, lists) come small by default: an interface piece fills at least 70% of the frame width, so scale it with a wrapper or its size prop; and one typeface for the film: pass the film font where a component takes one, and prefer components that do.
4
+
3
5
  ```
4
6
  Import from "reelkit/kit". Media props (src) take urls[key]; the kit components also accept the bare project path. For your own <Img>, <Audio> or <OffthreadVideo>, always pass urls[key]. Inside a SceneFrame, useCurrentFrame() starts at 0 for that scene.
5
7
 
6
- SceneFrame { from: number; durationInFrames: number; children }
7
- Wraps one scene. Places children on the timeline and fades them in and out.
8
- <SceneFrame from={s.startFrame} durationInFrames={s.durationFrames}>...</SceneFrame>
8
+ SceneFrame { from: number; durationInFrames: number; enter?: Transition; exit?: Transition; transitionFrames?: number; origin?: { x: number; y: number }; color?: string; shape?: "circle" | "bar" | "diagonal"; children }
9
+ Wraps one scene. Places children on the timeline and fades them in and out. enter and exit choose another transition: "fade" (the default), "cut", "zoom-in" (arrives from 1.25x and blurred, settling), "zoom-out", "zoom-through" (exit: scales up fast toward origin and blurs away; enter: the next scene arrives from small at the same point), "push-left" | "push-right" | "push-up" | "push-down", "whip-left" | "whip-right" (a fast slide with directional blur), "blur" and "turn" (a quarter turn in perspective about the vertical axis). Five more are hits meant for the beat: "color-push" (a flat field of color covers the frame as a "circle" growing from origin, a "bar" or a "diagonal" and uncovers the next scene: 10 frames), "rgb-whip" (a whip with the red and blue channels pulled apart and a smear: 6 frames), "flash" (white, or color, for 3 frames), "ring" (the scene closes into a ring of color that sweeps in to origin and the next scene opens out of one: 12 frames) and "flip" (a hard cut with ONE inverted frame). Their own lengths apply unless transitionFrames is set (a flash is never over 4 frames, a flip is always 1). The covers are fullest on the scene boundary, so changeFrame and cuesOnChanges put the hit on the boundary for color-push, flash, ring and flip and half the span before it for rgb-whip. A scene's exit and the next scene's enter are meant to be the same kind. transitionFrames is how long it takes (default 8, at most 20, never more than half the scene); origin is where a zoom aims, as fractions of the frame (default the centre). The exit plays in the last frames of its scene and the enter in the first frames of the next, so a transition is complete exactly at the scene boundary and the cuts stay on the beat.
10
+ <SceneFrame from={s.startFrame} durationInFrames={s.durationFrames} enter="zoom-through" exit="zoom-through" origin={{ x: 0.5, y: 0.4 }}>...</SceneFrame>
9
11
 
10
12
  TitleCard { text: string; subtitle?: string; color?: string; background?: string }
11
13
  Large centred title that springs in.
@@ -15,10 +17,10 @@ LowerThird { title: string; subtitle?: string; accent?: string }
15
17
  Name/label strip that slides in from the left, low on the screen.
16
18
  <LowerThird title="Step 1" subtitle="Connect your account" />
17
19
 
18
- Captions { words: WordTiming[]; mode?: "highlight" | "pop" | "karaoke"; highlight?: string; color?: string; perLine?: number; group?: "none" | "word" | "phrase"; uppercase?: boolean; bottom?: number; face?: string; rtl?: boolean }
19
- Word-timed captions near the bottom. Pass the scene's words from the manifest. Pick one mode for the whole video:
20
+ Captions { words: WordTiming[]; mode?: "highlight" | "pop" | "karaoke"; highlight?: string; color?: string; perLine?: number; group?: "none" | "word" | "phrase"; maxWords?: number; uppercase?: boolean; bottom?: number; face?: string; rtl?: boolean }
21
+ Word-timed captions near the bottom, on a dark rounded plate (about 58% opaque) in white so they read on any ground; the spoken word sits on a chip of highlight (the film's accent) with white or dark ink, whichever reads better on it (a karaoke fill is the accent mixed toward white until it reads on the plate). Pass the scene's words from the manifest. Pick one mode for the whole video:
20
22
  "highlight" (a line, spoken word coloured), "pop" (1-3 words popping in as spoken), "karaoke" (a line filling with colour).
21
- group decides which words share the screen: "word" is one word at a time, "phrase" groups words as they are spoken (a group ends at a sentence end, at a comma after three words, at a pause of 0.35 s, and never holds more than 6 words or about 32 characters), "none" draws nothing. Pass the plan's choice straight through: group={manifest.captions}. Without group, perLine counts the words as before.
23
+ group decides which words share the screen: "word" is one word at a time, "phrase" groups words as they are spoken (a group ends at a sentence end ". ? !" and never spans two sentences, at a comma after three words, at a pause of 0.35 s, and holds at most maxWords words, default 3, and about 32 characters), "none" draws nothing. Pass the plan's choice straight through: group={manifest.captions}. Without group and perLine the groups are 3 words, never one; only the plan's "word" shows one word at a time. perLine alone counts the words as before.
22
24
  bottom is the distance from the bottom edge as a fraction of the height (default 0.16).
23
25
  For Hebrew narration pass face={font("heebo")} and rtl.
24
26
  <Captions words={s.words} mode="pop" highlight={palette.hero} uppercase />
@@ -61,13 +63,118 @@ WordReveal { text: string; delay?: number; per?: number; highlight?: string; hig
61
63
  Headline that rises in word by word from behind a mask. highlight colours one word.
62
64
  <WordReveal text="Stretch first, phone second" highlight="first" highlightColor={palette.hero} style={{ fontFamily: fonts.display, fontWeight: 800, fontSize: width * 0.09, color: palette.ink }} />
63
65
 
66
+ Act the product out (a claim is performed in a small believable interface, not stated)
67
+ Which to reach for: a speed claim -> CounterRoll; an agent or a developer tool -> TerminalLog; an assistant -> PromptBox with a reply; a web product -> BrowserFrame with the user's own screenshot; collaboration -> NamedCursor over the screen it works on; a card, a ticket or a plan -> GlassPanel around your own content; a film's chapters -> ChapterFrame, once per scene, the same slots every time; the words that land the claim -> Headline. HudOverlay lifts any scene. Highlight at most three claims in 30 seconds and merely mention the rest.
68
+ One timing convention for all of them: every time prop (delay, per, startFrame-like props) is a frame counted from the component's own mount, that is the frame useCurrentFrame() gives where it stands (inside a SceneFrame, the scene's own frame). Every one has a companion function that takes the SAME props and returns its event frames in that clock, so a sound sits on the frame the picture settles: add the scene's startFrame (or pass it as offset to cuesFor, below). Size: every one takes width, a fraction of the frame's width (default 0.86), and fits a 9:16 phone frame without numbers; its text is at least 0.032 of the frame's width (about 35 px at 1080). Every one takes hero (the accent), font and, where it has a panel, theme: "dark" (default), "light" or "glass"; themeFor(bg) gives "light" or "dark" for a ground colour.
69
+
70
+ PromptBox { text: string; cps?: number (22); chips?: string[]; reply?: string; replyCps?: number (70); placeholder?: string; hero?; theme?; font?; width?: number (0.86); delay?: number (6); sendPause?: number (9); replyDelay?: number (12); y?: number (0.5) }
71
+ A rounded input with the question typed in at cps characters a second with a blinking caret, chips under it, a round send button that presses when the typing ends, then a reply that streams in below. Open a film with the user's own question typed into it when the product is an assistant or a tool. promptFrames(props, fps) gives { type: number[] (every character's frame), typedEnd, send, replyStart, reply: number[], end }; typedFrames(text, cps, fps, start?) gives the frame of each character.
72
+ <PromptBox text="Can you answer Anna's refund email?" chips={["Relay", "Inbox"]} reply="Done. A warm reply is ready for your review." hero={palette.hero} />
73
+
74
+ TerminalLog { lines: { kind: "cmd" | "out" | "ok" | "warn" | "edit"; text: string; add?: number; del?: number; wait?: number }[]; rate?: number (3 lines a second); title?: string; status?: string ("Working"); checklist?: string[]; checkEvery?: number (16); maxLines?: number (8); speed?: number (8); hero?; theme?; font? (monospace); width?; delay?: number (6); y? }
75
+ A window with a title bar, lines that stream in (cmd typed command, out plain, ok success, warn warning, edit a file changed with +add -del), an optional checklist whose items get struck through one by one, and a status line with an elapsed counter that becomes "Done". The newest line is at the bottom. terminalFrames(props, fps) gives { lines: number[], checks: number[], done }; lineFrames(lines, rate, fps, delay) gives the lines' frames alone.
76
+ <TerminalLog lines={[{ kind: "cmd", text: "relay triage inbox" }, { kind: "edit", text: "drafts/anna.md", add: 18, del: 2 }, { kind: "ok", text: "Replies drafted" }]} checklist={["Read", "Draft", "Queue"]} />
77
+
78
+ NamedCursor { points: { x: number; y: number; click?: boolean; hold?: number }[]; name?: string; color?: string; hero?; font?; delay?: number (4); moveFrames?: number (22); size?: number (0.055) }
79
+ An arrow with a coloured name pill (a teammate or an agent) that travels the points (fractions of the frame) on an eased arc, rests hold frames (10 on a click, 5 otherwise) and clicks with a ripple. Lay it over the screen it points at. clickFrames(points, { delay, moveFrames }) gives the click frames; cursorArrivals gives the arrival frames.
80
+ <NamedCursor name="Maya" points={[{ x: 0.2, y: 0.8 }, { x: 0.7, y: 0.45, click: true }]} />
81
+
82
+ GlassPanel { children; hero?; glow?: string; theme?; width? (0.86); height?: number (fraction of the frame, default as tall as the content); tilt?: number (degrees, 0); delay?: number (0); radius?: number (0.06); padding?: number (0.05); y? }
83
+ A slab for your own content: a near-black rounded panel (theme "light" for a pale one) with a hairline stroke lit along its top, a coloured glow pool behind it, an optional perspective tilt and an entrance that slides up and settles with a small overshoot. panelSettleFrame(delay, fps) is the frame it lands.
84
+ <GlassPanel tilt={8} glow={palette.hero}><div style={{ color: "#fff", fontSize: width * 0.06 }}>Refund request</div></GlassPanel>
85
+
86
+ ChapterFrame { index: number; total: number; color: string; ghost?: string; caption?: string; badges?: string[] (up to 3); hero?; ink?; font?; delay?: number (0); children }
87
+ The grammar of one chapter, as slots: "03 / 07" and a progress rule at the top, a large ghost word behind everything, children as the hero in the middle, up to three badges that pop in, one caption line at the bottom. color fills the frame and is the chapter's ground; ink is chosen to read on it. Use it once per scene with the same slots every time, or not at all. chapterFrames({ delay, badges }) gives { badges: number[] (the pops), progressDone }.
88
+ <ChapterFrame index={3} total={7} color="#7C3AED" ghost="Speed" badges={["Instant", "Private"]} caption="Replies in seconds" hero="#FFD166"><CounterRoll to={94} suffix="%" ink="#fff" hero="#FFD166" /></ChapterFrame>
89
+
90
+ Headline { text: string; keyword?: string; emphasis?: "color" | "highlight" | "underline" ("color"); hero?; ink?: string ("#f4f4f5"); font?; width? (0.86); maxSize?: number (0.2, of the width); delay?: number (0); per?: number (4); align?: "center" | "left"; y? }
91
+ Two to five words set heavy, entering word by word, with ONE keyword emphasised: the word in the hero colour, a block of hero that sweeps in behind it, or a bar that draws on under it. keyword must be one of the words (case and punctuation ignored) or it throws and lists the words. It breaks into lines of about 12 characters and is measured after layout, so it fills width and never leaves the frame. headlineFrames(text, { delay, per }) gives { words: number[], emphasisDone }.
92
+ <Headline text="Answer every email" keyword="every" emphasis="highlight" hero={palette.hero} />
93
+
94
+ CounterRoll { to: number; from?: number (0); frames?: number (45); delay?: number (0); prefix?; suffix?; label?; decimals?: number (0); group?: boolean (true); hero?; ink?; font?; width? (0.86); maxSize?: number (0.3); y? }
95
+ An odometer: the digits roll like wheels from from to to, easing out, and land exactly, in tabular digits. prefix and suffix and label take the hero colour. settleFrame({ delay, frames }) is the frame it lands (put the settle chime there); rollValue(frame, { from, to, frames, delay }) is its value.
96
+ <CounterRoll to={12480} prefix="$" label="saved this month" delay={8} />
97
+
98
+ HudOverlay { labels?: string[] (up to 4, top left, top right, bottom left, bottom right); timecode?: boolean (true); scanlines?: boolean (true); hero?; ink?; font?; inset?: number (0.05) }
99
+ Corner brackets, small monospace labels in the corners (your words), a running timecode from the frame and faint scan lines, over a scene. Labels are never below 0.024 of the frame's width. No events to time.
100
+ <HudOverlay labels={["relay v2", "live"]} />
101
+
102
+ BrowserFrame { src: string; device?: "browser" | "phone"; scroll?: number (0.7, of the window's height); frames?: number (90); delay?: number (0); tilt?: number (-12 browser, -16 phone); url?: string; theme?; hero?; font?; width? (0.86 browser, 0.5 phone); viewAspect?; y? }
103
+ A tilted browser window (or phone) carrying the user's own screenshot (src, a project path or urls[...]) that starts at the top and scrolls down by scroll, with a soft shadow and a sheen that sweeps once as it lands. CSS 3D. It never scrolls past the end of the picture. browserFrames({ delay }) gives { settled, sheenStart, sheenEnd }.
104
+ <BrowserFrame src={urls["assets/screens/inbox.png"]} scroll={0.8} url="relay.app" />
105
+
106
+ Images in layers (a picture's words are a layer OF the picture)
107
+ An image scene is built in layers: the picture at the back, the words in the middle, the subject cut out of the picture in front when it has one. Run reelkit assets layers --scene <id> (or --layers on assets gen image and assets pull --scene) for the one or two hero pictures: the subject is cut out as <name>.subject.png (about 1 second of the cutout quota each) and the manifest scene gets imageLayers { back, subject?, subjectBox?, textZone: { region, luminance, busy } }; without it the words still go in the calm zone, which is measured on this machine for free whenever a picture is registered.
108
+
109
+ ImageLayers { layers?: ImageLayersRecord; back?: string; subject?: string; depth?: number (0.4); focus?: "subject" | "back"; front?: ReactNode; children }
110
+ Draws back picture, then children (the middle layer: your words), then the subject, then front, so a headline can pass BEHIND the subject. layers is s.imageLayers; or give back and subject (urls[...]) yourself. Depth comes from the frame: the back drifts and grows slowly, the subject a little more and the other way (depth 0 moves them together, 1 is the most), the words between them at their own rate. focus softly blurs the other layer. Without a subject it is one flat picture with the words over it. Every layer covers the frame at every frame.
111
+ <ImageLayers layers={s.imageLayers}><TextOnImage layers={s.imageLayers}><Headline text="Meet Tom" /></TextOnImage></ImageLayers>
112
+
113
+ TextOnImage { layers?: ImageLayersRecord; zone?: "top" | "middle" | "bottom" | "left" | "right" | { x; y; w; h }; behindSubject?: boolean; ink?: string; scrim?: number; children }
114
+ The always-correct way to put words on a picture. It sets its children in the picture's calm zone (layers.textZone, or zone), picks dark or light ink from how light the zone is (a Headline inside takes it), and lays a soft scrim just behind the words, stronger the busier the zone, so they read on any picture. With a subject layer the words go BEHIND the subject when it covers 10 to 60 percent of the text block (the title behind the person) and in front of it when it would hide more than 60 percent. Never put words on a flat card beside the picture and never over its busiest part.
115
+
64
116
  Carry { keys: { frame: number; x: number; y: number; width: number; height: number; radius?: number; opacity?: number; rotate?: number }[]; lead?: number; stiffness?: number; damping?: number; children | (box) => children }
65
117
  One element held through several scenes. Place it once, beside the SceneFrames and above them, so it is still on screen when a scene changes. Each key says where the box must have arrived by an absolute composition frame: x, y are its centre and width, height are fractions of the frame (0 to 1); radius is the corner radius as a fraction of the frame width; rotate is in degrees. The move toward a key starts lead frames before the key's frame (default 12, or at the previous key's frame when the keys are closer) on a spring (default springs.smooth) and lands on the key's frame exactly; between moves the box holds. Before the first key nothing is drawn unless that key sets an opacity; after the last the box holds. Children fill the box; a function child gets { x, y, width, height, radius, opacity, rotate, widthPx, heightPx } for the current moment, so the content can change as the box does.
66
118
  <Carry keys={[{ frame: a.startFrame + 6, x: 0.5, y: 0.45, width: 0.8, height: 0.3 }, { frame: b.startFrame, x: 0.5, y: 0.5, width: 1, height: 1, radius: 0 }]}>{(box) => <Card compact={box.width < 0.5} />}</Carry>
67
119
 
68
- Camera { keys: { frame: number; x?: number; y?: number; zoom?: number; rotate?: number }[]; drift?: number; lead?: number; stiffness?: number; damping?: number; children }
69
- Moves the whole picture. Wrap all the scenes for one camera that never cuts, or wrap one scene's content. x, y are the point of the content, as fractions, that sits at the middle of the frame (default 0.5, 0.5); zoom 1 shows the content as it fits; rotate is in degrees. A key that leaves a value out keeps the one before it. The timing rule is the same as Carry's: the move toward a key starts lead frames before its frame and lands on it. drift is a very slow continuous push, as a fraction of the zoom per second (0.02 is two percent a second), so a held shot is never perfectly still.
120
+ Camera { keys: { frame: number; x?: number; y?: number; zoom?: number; rotate?: number }[]; drift?: number; punches?: { frame: number; x: number; y: number; amount?: number; frames?: number; hold?: number }[]; lead?: number; stiffness?: number; damping?: number; children }
121
+ Moves the whole picture. Wrap all the scenes for one camera that never cuts, or wrap one scene's content. x, y are the point of the content, as fractions, that sits at the middle of the frame (default 0.5, 0.5); zoom 1 shows the content as it fits; rotate is in degrees. A key that leaves a value out keeps the one before it. The timing rule is the same as Carry's: the move toward a key starts lead frames before its frame and lands on it. Frames are counted from the start of the scene when the Camera is inside a SceneFrame (frame 0 is the scene's first frame), and are the video's absolute frames when it wraps all the scenes. drift is a very slow continuous push, as a fraction of the zoom per second (0.02 is two percent a second), so a held shot is never perfectly still.
70
122
  <Camera keys={[{ frame: 0, zoom: 1 }, { frame: b.startFrame, x: 0.7, y: 0.4, zoom: 4 }]} drift={0.01}>{scenes}</Camera>
123
+ punches are quick emphasis pushes on top of the keys, for a word or a click: a zoom of 8 to 15 percent (amount, default 0.1) onto the point (x, y) over frames (default 5) with a tiny overshoot, held for hold frames (default 6), then eased back. The point stays still on screen. <Camera keys={[{ frame: 0 }]} punches={[{ frame: click, x: 0.62, y: 0.4, amount: 0.12 }]}>...</Camera>
124
+ zoomTo(frame, { x, y, width, height }) makes the key that pushes into an element until it fills the frame (the box is the element's centre and size as fractions of the frame): keys={[{ frame: 0 }, zoomTo(b.startFrame, { x: 0.5, y: 0.4, width: 0.3, height: 0.2 })]}.
125
+
126
+ Scene3D { camera?: { keys: { frame: number; x?: number; y?: number; z?: number; lookAt?: [number, number, number]; fov?: number }[]; lead?: number }; background?: string; lights?: boolean; mood?: "studio" | "night" | "sunset" | "neon"; children }
127
+ A three.js scene at the size of the frame, for real depth. Transparent unless background is set, so it lays over a 2D ground; a soft three-point light is placed for you (lights={false} to place your own); mood ("studio", "night", "sunset" or "neon") chooses the light colours, their intensities and the fog tint together instead, so lights are never hand-tuned. Put it inside a SceneFrame, with captions and small labels in 2D above it. camera.keys move the camera with the same timing as Camera (a spring that starts 12 frames before the key's frame and lands on it); the default camera is at z 6, looking at the origin, fov 50, and a key that leaves a value out keeps the one before. What it sees: at fov 50 the height is 0.93 times the distance to the object and the width that times width over height, so 3.1 units across at z 6 on 9:16 (4.7 at z 9), 5.6 on 1:1 and 9.9 on 16:9 (not "5.5"). Text3D and Orbit3D read the camera keys and size themselves to fit at the key where the camera is closest: leave their sizes out. visibleWidth(distance, aspect, fov?) and visibleHeight(distance, fov?) give the same numbers. No GL context is made unless a Scene3D is on screen. A 3D scene renders slower: use it only for the shots that need it. See reference/three-d.md.
128
+ <Scene3D camera={{ keys: [{ frame: 0, z: 7.5 }, { frame: 40, z: 6 }] }}><Text3D text="Reelkit" color={palette.hero} enter="rise" /></Scene3D>
129
+
130
+ BrandTransform3D { mode?: "turn" | "arc" | "launch"; from?: number (0); frames?: number (32); strength?: number (0 to 1, default 1); children }
131
+ A deterministic arrival for a branded object inside Scene3D: turn exposes its face, arc carries it on a curved path, launch approaches from depth. Wrap Text3D, Card3D or Hero3D; the final pose is neutral and holds exactly. Keep the supplied identity intact and allow a readable hold after landing. A transformation has no built-in audio. brandTransformFrames({ from, frames }) returns { start, settle } in the same local clock; cuesFor([start, settle], "transform", { offset: scene.startFrame, voice, sweepFrames }) places the transform-sweep role before the landing and transform-lock on it. Map both roles to pulled sound files. sweepFrames is the chosen sound's attack in frames (default 10). brandTransformAt(frame, fps, options) returns { visible, position, rotation, scale }; BRAND_TRANSFORMS lists the modes. See reference/brand-motion.md for when and why to choose each.
132
+ <BrandTransform3D mode="turn" from={6} frames={32}><Text3D text="Reelkit" fit={0.72} /></BrandTransform3D>
133
+
134
+ Text3D { text: string; fit?: number (0.8); size?: number; depth?: number; color?: string; position?: [x, y, z]; rotation?: [x, y, z] (degrees); enter?: "rise" | "turn" | "fly" | "none"; delay?: number }
135
+ Extruded text, centred on its position, inside a Scene3D. Latin letters, digits and common punctuation only (one bundled typeface, Inter Bold): Hebrew and other scripts stay 2D. Leave size out: the word is measured on its own letters and sized to fill fit (default 0.8) of the frame's width at the closest the Scene3D camera comes, so it is whole at every moment on any aspect; a line break in the text makes a second line, and the height is held to fit of the frame's height. A size you give wins (scene units, the height of a capital-ish line). A push-in starts the word smaller in proportion: z 7.5 to 6 is a gentle one. depth is how far it is extruded (default a quarter of the size). enter is how it arrives, starting delay frames into the scene; "none" is there from the start. Keep type that must be read facing the camera and held at least 1.5 seconds.
136
+ <Text3D text="Meet Reelkit" color="#ffffff" position={[0, 1, 0]} enter="turn" delay={6} />
137
+
138
+ Card3D { src?: string; color?: string; label?: string; icon?: string; accent?: string; font?: string; width?: number; height?: number; radius?: number; position?: [x, y, z]; rotation?: [x, y, z] (degrees); tilt?: number; float?: boolean | number }
139
+ A rounded plane in space that shows a picture (src, a project path or urls[...]; the height follows the picture) or a designed face, with a soft shadow behind it. The face is a gradient of color with a highlight and a rim in accent; label is drawn large on it in the film's font (font, default the kit's display face; ink is white on a dark colour, dark on a light one) and icon (a project path to an image or svg, drawn in its own colours) above the label. It is drawn on both sides, so a card seen from behind reads the right way round. Give every card in a ring a label or an icon, never an empty colour. tilt is the one tilt a card needs: a turn about the vertical axis in degrees with a slight lean. float makes it drift slowly and deterministically.
140
+ <Card3D src={urls[s.userAssetKeys[0]]} width={2.4} tilt={-16} float /> <Card3D color={palette.hero} width={1.2} height={1.6} label="Plan" icon="assets/icons/plan.svg" />
141
+
142
+ Orbit3D { radius?: number; fit?: number (0.85); speed?: number; tiltDeg?: number; billboard?: boolean; position?: [x, y, z]; children }
143
+ Arranges its children on a ring and turns it slowly (speed is degrees a frame, default 0.5), for a set of cards or logos. Leave radius out: the ring takes the radius at which its cards stand side by side and is scaled, radius and cards together, to lie inside fit (default 0.85) of the frame at the closest the Scene3D camera comes, however it has turned and on any aspect. A radius you give wins and nothing is scaled. Children face outward, so the one at the front faces the camera; billboard keeps all of them facing the camera. tiltDeg tips the ring toward the camera. Every card carries a label or an icon.
144
+ <Orbit3D tiltDeg={12}><Card3D color="#8B5CF6" width={1.2} height={1.6} label="Plan" /><Card3D color="#2563EB" width={1.2} height={1.6} label="Focus" /><Card3D color="#059669" width={1.2} height={1.6} label="Ship" /></Orbit3D>
145
+
146
+ Assemble3D { shape?: "grid" | "ring" | "sphere" | "wall" | "text" ("grid"); text?: string; targets?: [x, y, z][]; count?: number (400, at most 4000); order?: "y" | "x" | "radial" | "random" ("y"); from?: number (0); frames?: number (60); colors?: string[] (two or three); fit?: number (0.8); size?: number; spread?: number (1.6); seed?: number; position? }
147
+ Instanced blocks that fly from a seeded cloud into an arrangement, one after another, each on an arc with a turn that unwinds, then rest: the build-up beat. "text" puts the blocks inside the letters of text (a short Latin word); targets is any [x, y, z] list (centred and fitted). Every piece is home at from + frames. Fits the frame by default at the closest the camera comes. In a Scene3D. assembleProgress(frame, i, { ranks, from, frames }), assembledCount(frame, { ranks, from, frames }) (for a piece counter), assembleRanks(targets, order, seed) and assembleEnd({ from, frames }) (the frame of the lock sound: cuesFor([from, assembleEnd(...)], "assemble")) are exported.
148
+ <Scene3D><Assemble3D shape="text" text="Relay" count={900} order="x" colors={[palette.hero, palette.accent]} /></Scene3D>
149
+
150
+ Particles3D { from?: "sphere" | "torus" | "knot" | "galaxy" | "plane" | "text" ("sphere"); to? ("torus"); text?: string; count?: number (12000, at most 60000); morph?: number; morphFrames?: [number, number] ([20, 80]); colors?: [string, string]; drift?: number (0.012); fit?: number (0.8); size?: number (0.026); spin?: number (6, degrees a second); seed?: number; position? }
151
+ A cloud of glowing points that morphs between two shapes, an abstract idea made of light: additive, soft, a gentle seeded drift. The morph runs between morphFrames and holds either side, or give morph (0 to 1). Done on the CPU with ordinary points, so it is the same on every machine. In a Scene3D.
152
+ <Scene3D><Particles3D from="galaxy" to="text" text="Relay" morphFrames={[10, 60]} /></Scene3D>
153
+
154
+ Hero3D { kind?: "device" | "logo" | "box" | "coin" ("box"); src?: string; text?: string ("R"); hero?: string; rim1?: string; rim2?: string; floor?: "soft" | "mirror" | "none" ("soft"); spin?: number; hover?: number (0.5); fit?: number (0.7); aspect?: number; poses?: { frame: number; rotation?: [x, y, z] (degrees); position?: [x, y, z]; scale?: number }[]; lights?: boolean (true); position? }
155
+ A studio-lit hero object: physical material with a clearcoat in hero, a key light and two coloured rim lights (rim1, rim2, by default the hero colour toward blue and toward pink), a contact shadow or a faded mirror copy below. "device" is a rounded slab whose screen shows src (the user's screenshot; without it a designed screen), "logo" extrudes text (a word or one letter) in the kit's typeface, "coin" is a disc with the first letter of text on it. spin is degrees a second (12; a device sways by that many degrees instead of turning), hover is how much it bobs. poses re-pose the one object across scenes with the same timing as Camera (the move starts 12 frames before the key and lands on it); poseAt(keys, frame, fps) is exported. Fits the frame by default. In a Scene3D.
156
+ <Scene3D mood="studio"><Hero3D kind="device" src={urls["assets/screens/inbox.png"]} hero={palette.hero} poses={[{ frame: 0, rotation: [8, -28, 0] }, { frame: 70, rotation: [0, 0, 0], scale: 1.1 }]} /></Scene3D>
157
+
158
+ Screen3D { the props of Hero3D without kind and text; spin (5) }
159
+ A tilted screen in 3D carrying a screenshot (src) or a designed mock, with a bezel, a shadow and a sheen: the interface carried across scenes and re-posed with poses. It is Hero3D kind="device" with a small sway.
160
+ <Scene3D><Screen3D src={urls["assets/screens/inbox.png"]} poses={[{ frame: 0, rotation: [10, -30, 0] }, { frame: 80, rotation: [0, 0, 0] }]} /></Scene3D>
161
+
162
+ Warp3D { speed?: number | { frame: number; speed: number }[] (1); burst?: { frame: number; frames: number; peak?: number }; count?: number (500); colors?: [string, string]; width?: number (0.035); seed?: number; position? }
163
+ Speed streaks rushing out of a vanishing point, additive, in two colours: a ground for a hyperspace beat, or a 12 to 20 frame transition burst (burst: a rise and fall that is nothing outside it). The streaks are longer and quicker the higher speed is. In a Scene3D; it is drawn over everything else in it. warpSpeed(frame, speed, burst) is exported.
164
+ <Scene3D><Warp3D burst={{ frame: 20, frames: 16, peak: 4 }} /></Scene3D>
165
+
166
+ Grounds (the layer under everything; one for the whole film, see reference/backgrounds.md)
167
+ BgVideo { src; dim?: 0-0.8 (0.35); blur?: px (0); zoom?: number (0.04); tint?: string; loop?: boolean (true); playbackRate?: number (1); durationSec?: number }
168
+ A video as the ground, always muted. dim darkens it so type stays readable, tint lays a colour over at low opacity so the clip takes the film's accent, zoom is a slow push over the film. With loop and durationSec (manifest.background.durationSec), a film longer than the clip starts it again with a short cross-fade over the join. <BgVideo src={urls[manifest.background.key]} durationSec={manifest.background.durationSec} dim={0.4} tint={palette.hero} />
169
+ BgImage { src; dim?: 0.35; blur?: 0; tint?; zoom?: 0.06; drift?: "none" | "left" | "right" | "up" | "down"; fit?: "cover" }
170
+ One still picture as the ground, with a slow push and an optional slow pan. It always covers the frame: the pan and the blur never show an edge.
171
+ BgSequence { items: { src: string; from: number }[]; transition?: "fade" | "blur" | "wipe" | "zoom"; transitionFrames?: 18; align?: "center" | "start"; dim?; blur?; tint?; zoom?; drift? }
172
+ Grounds that change smoothly. items are in frame order, the first visible from frame 0 (out of order or on the same frame is an error). The change is centred on from unless align is "start". "blur" cross-fades through a blur peak, "wipe" is a soft-edged wipe, "zoom" pushes the old one in while the new one settles. Each item keeps its own slow zoom through the change. A video src is drawn too (played once, held on its last frame).
173
+ bgPerScene(manifest.scenes, srcs): { src, from }[] makes items that change exactly on scene starts (which are on the beat); a scene whose src is undefined keeps the picture before it.
174
+ <BgSequence items={bgPerScene(manifest.scenes, manifest.scenes.map((s) => s.background && urls[s.background.key]))} tint={palette.hero} dim={0.4} />
175
+ BgAurora { bg; hero; speed?; intensity? (0.6) } BgGrid { bg; hero; speed?; intensity? (0.6) } BgBeams { bg; hero; speed?; intensity? (0.6) } BgGrain { bg; hero; speed?; intensity? (0.6) }
176
+ Grounds drawn in code, with no media, cheap to render. BgAurora: two or three large soft colour fields drifting and crossing. BgGrid: a faint perspective grid receding to a horizon, with a glow there. BgBeams: wide soft diagonal light beams sweeping very slowly. BgGrain: a still ground with a slowly breathing vignette and film grain that steps by the frame number. hero is the accent; speed 1 is the default pace. intensity (0 to 1, default 0.6) is how strongly the ground lights the base; Grade and Vignette on top darken it, so raise it to 0.8 under them (reference/backgrounds.md).
177
+ <BgAurora bg={palette.bg} hero={palette.hero} />
71
178
 
72
179
  fonts { display: string; body: string }
73
180
  Loaded font families. Use fonts.display (weights 600-800) for headlines and numbers, fonts.body for supporting text. Never leave hero text on a default font.
@@ -88,7 +195,7 @@ font(name): string
88
195
  greatVibes (formal script), sacramento (monoline script), outfit (geometric sans 100, 300, 500, 900), syne (wide heavy 800),
89
196
  lilitaOne (heavy rounded), kaushanScript (bold brush script), yellowtail (flowing brush script), mrsSaintDelafield (signature script),
90
197
  permanentMarker (marker lettering), bungeeShade (outlined sign capitals), cinzel (Roman capitals 700, 900: epic),
91
- playfairDisplay (elegant serif 700, 900), orbitron (sci-fi 700, 900), rye (western), creepster (horror).
198
+ playfairDisplay (elegant serif 700, 900), jetbrainsMono (monospace 400, 700: terminals, labels), orbitron (sci-fi 700, 900), rye (western), creepster (horror).
92
199
  Hebrew text must use a Hebrew face; every face above is Latin only and Hebrew would fall back to a default font. Hebrew faces
93
200
  (all also cover Latin): heebo, rubik, notoSansHebrew (400, 700, 900: neutral sans), assistant (400, 700, 800), alef (400, 700),
94
201
  secularOne (strong headline), varelaRound (soft rounded), fredoka (playful rounded 500, 700), karantina (tall condensed 400, 700),
@@ -112,14 +219,34 @@ Sfx { src: string; at?: number; volume?: number }
112
219
  A pulled sound is levelled when it is pulled (the manifest's soundGain), and Sfx and Music apply that by themselves: the same volume sounds equally loud for every file. Nothing to pass.
113
220
  <Sfx src={urls["assets/lib/<id>/clip.mp3"]} at={10} volume={0.35} />
114
221
 
115
- Music { src: string; volume?: number; duckTo?: number }
116
- The video's one music track (pull it with: reelkit assets pull <id> --music). Place it once, outside the scenes. It sits at duckTo (default 0.12) while the voiceover is heard and comes up to volume (default 0.5) in gaps longer than 0.6 s, loops if the video is longer than the track, and fades out over the last second. It follows the manifest, so nothing else is passed.
222
+ SoundCues { cues: { at: number; sound: string; volume?: number }[]; sounds: Record<string, string>; volume?: number }
223
+ Plays many sound effects from one place, for a video scored with sound (a video with no voice). Place it once, outside the scenes. "at" is an absolute frame of the video, "sound" is a key into "sounds" (name -> link), a cue's own volume defaults to 0.35, and "volume" is a master level for all of them (default 1). It applies the manifest's levelling gain by itself, like Sfx. A cue whose sound is not in "sounds" stops the render with the names that exist.
224
+ <SoundCues sounds={{ click: urls["assets/lib/<id>/clip.mp3"], whoosh: urls["assets/lib/<id>/clip.mp3"] }} cues={[{ at: 0, sound: "whoosh" }, { at: 18, sound: "click", volume: 0.3 }]} />
225
+
226
+ cuesOnBeats(beatFrames, { from, to, every, sound, volume? }): { at, sound, volume? }[] cueBefore(frame, leadFrames): number
227
+ Helpers for SoundCues cues. cuesOnBeats gives a cue on every Nth beat from the frame "from" to the frame "to" (both included), starting with the first beat in that range. cueBefore gives the frame at which a sound leadFrames long must start to END on "frame" (a riser or a whoosh into a cut), never before frame 0.
228
+ const cues = [...cuesOnBeats(manifest.music?.beatFrames ?? [], { from: 0, to: 120, every: 2, sound: "tick", volume: 0.2 }), { at: cueBefore(s.startFrame, 12), sound: "whoosh" }];
229
+
230
+ cuesOnChanges(scenes, { impact?: { sound, volume? }, whoosh?: { sound, volume?, frames }, skip?: number[] }): { at, sound, volume? }[] changeFrame(scene): number cueOnCamera(frame, { punch?, punchFrames?, lead? }): number
231
+ Sounds for the scene changes, placed where the eye sees them. A SceneFrame transition is complete on the boundary but the picture changes most half the transition's frames before it (4 for the default 8, 3 for transitionFrames={6}; none for "cut"), and a hit on the boundary lands after the change. scenes are the manifest's scenes, each with the exit and transitionFrames its SceneFrame has: cuesOnChanges gives an impact on that peak and a whoosh (frames long, from the search line's duration times 30) that ENDS on it, for every change but the one after the last scene; skip lists scene indices to leave silent. changeFrame(scene) is the peak for one scene. A camera zoom, zoomTo or punch is a picture change too: cueOnCamera(frame) is the frame where the Camera move to the key at "frame" is fastest (about 9 frames before it); with punch: true, the fastest frame of a punch (just after its frame). The Camera counts frames from the scene's start when it is inside a SceneFrame, so add the scene's startFrame for an absolute cue.
232
+ const exits = [{ exit: "zoom-through" as const }, { exit: "whip-left" as const, transitionFrames: 6 }, {}]; const cues = cuesOnChanges(manifest.scenes.map((s, i) => ({ ...s, ...exits[i] })), { impact: { sound: "thud", volume: 0.55 }, whoosh: { sound: "whoosh", frames: 24, volume: 0.3 } });
233
+
234
+ cuesFor(events, kind, { offset?, fps?, voice?, volume?, seed?, sweepFrames?, riserFrames? }): { at, sound, volume }[] clearBefore(cues, frame, { seconds?, fps?, riserFrames? }): cues
235
+ Turns the event frames a component reports into SoundCues with a sensible sound role, level and density for the kind: "type" (promptFrames(...).type: a tick on every 2nd or 3rd character, at most 12 a second, a little different in level each, and a thock one frame after the last), "click" (clickFrames), "snap" (panelSettleFrame: a card or panel landing), "pop" (chapterFrames(...).badges), "count" ([start, settleFrame]: a train of ticks that speeds up and ends on a chime ON settleFrame), "stream" (terminalFrames(...).lines: one soft tick per line, at most 6 a second), "assemble" ([start, lockFrame]: a rising run that gets louder and a lock hit on lockFrame), "transform" ([start, settle]: transform-sweep begins sweepFrames before settle, never before start, and transform-lock lands on settle) and "resolve" ([frame] the logo or end card lands: a riser that ends on the chime, which is on the frame). offset is the scene's startFrame when the events are in the scene's own clock. voice: true gives the quieter levels for a film with a narrator (never over 0.3). Each cue's sound is a role: type-tick, type-return, click, snap, pop, count-tick, count-settle, stream, assemble-run, assemble-lock, transform-sweep, transform-lock, resolve-riser, resolve-chime. Map each role to a pulled file in sounds, searching with: type-tick "mouse click"; type-return "glass tap"; click "mouse click"; snap "soft impact"; pop "bubble pop"; count-tick "counter ticks"; count-settle "success chime"; stream "glass tap"; assemble-run "counter ticks"; assemble-lock "soft impact"; transform-sweep "soft ui whoosh"; transform-lock "soft impact"; resolve-riser "whoosh buildup"; resolve-chime "sparkle shimmer" (reelkit assets search "<words>" --kind sfx; the interface pack's names begin with sfx-ui-). clearBefore(cues, chimeFrame) removes every other cue in the second before the riser so the end card has a clear second of nothing; give the music a dip there (Music dips).
236
+ const p = promptFrames({ text, reply }, fps); const cues = [...cuesFor(p.type, "type", { offset: s.startFrame }), ...cuesFor([p.send], "click", { offset: s.startFrame }), ...cuesFor([settleFrame({ delay: 20 })], "snap", { offset: s.startFrame })];
237
+
238
+ Music { src: string; volume?: number; duckTo?: number; dips?: { from: number; to: number; volume: number }[] }
239
+ The video's one music track (pull it with: reelkit assets pull <id> --music). Place it once, outside the scenes. It sits at duckTo (default 0.18) while the voiceover is heard, steady across the 0.2 to 0.5 s between sentences, and comes up to volume (default 0.5) only in a real pause of 1.2 s or more (rising over 0.4 s and back down 0.25 s before the next word), so at the start before the first word and after the last, loops if the video is longer than the track, and fades out over the last second. It follows the manifest, so nothing else is passed. dips are rests in absolute frames: inside each the track holds at that volume (with a short ramp either side), so it can fall quiet before the big moment: dips={[{ from: reveal.startFrame - 20, to: reveal.startFrame, volume: 0.05 }]}.
117
240
  <Music src={urls[manifest.music.key]} />
118
241
 
119
242
  nearestBeat(beatFrames, frame): number | undefined nextBeat(beatFrames, frame): number | undefined beatPulse(beatFrames, frame, decayFrames = 10): number
120
243
  Beat helpers; beatFrames is manifest.music.beatFrames (composition frames). nearestBeat is the closest beat, nextBeat the first at or after the frame, beatPulse a number from 0 to 1 that is 1 on a beat and falls to 0 after it. Inside a scene, subtract s.startFrame from a beat to get a frame for that scene's own clock.
121
244
  const b = nextBeat(manifest.music?.beatFrames ?? [], s.startFrame + 20) ?? s.startFrame + 20; // <Entrance delay={b - s.startFrame}>
122
245
 
246
+ wordFrame(scene, word, { nth?, edge?, absolute?, fps? }): number wordFrames(scene, words[], opts?): number[] onWord(scene, word, { lead?, ...wordFrame options }): number onWordBeat(scene, word, manifest.music, { window?, lead?, ... }): number sceneById(manifest, id): scene
247
+ Put things on the spoken word. scene is a manifest scene (it has the real word times). wordFrame is the frame inside the scene (counted like useCurrentFrame() in its SceneFrame) at which the word starts; word ignores case and punctuation, nth (1-based) picks a repeated word, a phrase ("whole week") gives the first word's start, edge "end" gives where it ends, absolute adds the scene's startFrame. A word the scene does not say THROWS with the scene's words listed, so a typo fails the render instead of timing to frame 0. onWord is the frame to START an entrance (a delay, a from, an at) so it is seen landing on the word: wordFrame minus lead (default 3), never below 0. onWordBeat is the same but lets the entrance land on a beat when one lies within window frames (default 3) AFTER the word; otherwise the word wins, and it is never earlier than onWord: with a narrator the voice is the clock for anything that shows a word, the beat is for decoration. sceneById(manifest, "id") finds the scene.
248
+ const meet = sceneById(manifest, "meet"); <Entrance delay={onWord(meet, "tasks")}><Card /></Entrance> <Sequence from={onWord(meet, "builds")}><Card /></Sequence> <Sfx src={urls[SFX.pop]} at={wordFrame(meet, "energy") - 2} volume={0.25} />
249
+
123
250
  Voiceover { src: string; volume?: number }
124
251
  The scene's narration audio. One per narrated scene, inside its SceneFrame. Guard it with s.voiceoverKey so a scene without a recording still renders.
125
252
  {s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
@@ -129,7 +256,7 @@ Voiceover { src: string; volume?: number }
129
256
 
130
257
  ```tsx
131
258
  import React from "react";
132
- import { AbsoluteFill, useVideoConfig } from "remotion";
259
+ import { AbsoluteFill, useVideoConfig } from "reelkit/frame";
133
260
  import { BgMesh, Captions, FootageLayer, fonts, Grade, Grain, KenBurnsImage, palettes, SceneFrame, Vignette, Voiceover, WordReveal } from "reelkit/kit";
134
261
  import type { VideoProps } from "reelkit/kit";
135
262
 
@@ -159,3 +286,8 @@ export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
159
286
  );
160
287
  };
161
288
  ```
289
+
290
+ ## What the checks can see
291
+
292
+ - `reelkit check` counts sounds only where the code places them literally. When the cues are computed (a loop, `cuesOnBeats`, a helper, a variable that is not a plain list) its sound-count notes stay silent, so the absence of a note proves nothing. For a film with no voice, the sound report after `reelkit render` is the real count.
293
+ - `reelkit preview` prints the continuity and beat lines. It prints no rhythm line: how even the scene lengths are is `reelkit plan check`'s.
@@ -0,0 +1,194 @@
1
+ ---
2
+ name: launch-film
3
+ description: Use when the video is a product, feature or brand reveal with no narrator - a short launch film carried by music and sound effects alone: the plan with voice none, the shape of the film, the pictures, and a full section on scoring it with sound.
4
+ ---
5
+
6
+ # A launch film with no voice
7
+
8
+ The short films that studios post when a product launches have no narrator. The words are on the screen, and the sound (a music track and a great many small effects) does the work a voice would. This file is how to make one. Read `reference/continuity.md` too: the shots must grow out of each other.
9
+
10
+ ## When to choose it
11
+
12
+ Choose it for a product, a feature or a brand reveal where nobody needs to explain anything out loud. If the user wants a person to talk, or a story told in sentences, it is the wrong tool: write a narrated video instead. Use the requested format; ask about narration only if that decision is still consequential and unclear. Skip the voice and caption steps.
13
+
14
+ ## What good ones measured
15
+
16
+ Four previously measured launch films of 15 to 55 seconds provide examples of one energetic style. Their ranges are context, not targets for every brand or edit; a calm reveal can use fewer sounds and longer holds.
17
+
18
+ - No speech at all. Loudness between -14.1 and -14.8 LUFS in every one.
19
+ - 2.0 to 3.0 sound hits a second over the whole film (music beats and effects together), 1.5 to 2.8 of them low, and a whoosh-like swell every one to two seconds (0.5 to 1.3 a second).
20
+ - A hit lands within a tenth of a second of 55 to 75 percent of the big changes in the picture.
21
+ - Sound starts in the first 0.05 seconds in three of the four. The first big change in the picture comes between 0.7 and 3.8 seconds.
22
+ - Shots average 2.2 to 5.2 seconds; the shortest is 0.4 to 1.0 and the longest 6.5 to 12.7. The lengths vary by 0.64 to 0.77 (spread over average). The picture is still for 11 to 35 percent of the time.
23
+ - A third to a half of the cuts land on the music's beat. The tempo, where there was a clear beat, was 107 to 145 BPM.
24
+ - The look: one calm ground (near-white with a soft colour wash, near-black, or a warm photographic set), one accent colour, the product's logo tile at the start or the end, and a line of type under the object rather than over it.
25
+
26
+ ## The plan
27
+
28
+ Write `plan.json` with `"voice": "none"`. Every scene then has `seconds` (a number from 0.3 to 15) and `narration` may be an empty string; `voiceId` is not needed, and `captions` is `none` unless you set it. Up to 16 scenes are allowed in this mode.
29
+
30
+ - A common short launch film is 15 to 45 seconds with 6 to 14 shots; adapt to the requested duration and reading time.
31
+ - Lengths vary at least fourfold: one or two shots under a second, one long hold, the rest in between.
32
+ - On-screen text carries every word the viewer will read, so each scene has `onScreenText` or `notes` saying what is on screen (`reelkit plan check` flags a scene with neither).
33
+ - The product's logo comes first or last.
34
+ - One calm ground and one accent colour for the whole film; write both into the first scene's `notes` with the look from `reference/styles.md` ("Launch film").
35
+
36
+ `reelkit plan check` prints `Length: 24.5s over 9 scenes.` and notes a film under 10 or over 60 seconds, and one with no scene under a second. `reelkit assets voiceover` says there is nothing to record. Build the composition from `manifest.scenes` as usual: no scene has a `voiceoverKey` and `s.words` is empty, so place no `<Voiceover>` and no `<Captions>`.
37
+
38
+ ## The shape
39
+
40
+ 1. Open on motion and sound at once, in the first frame.
41
+ 2. One line of type that sets up the product.
42
+ 3. The product's interface, as an object in space.
43
+ 4. Three to five feature beats, each with a pointer or a change that has a visible cause.
44
+ 5. A quick burst of short shots.
45
+ 6. A rest: one calm shot, and near silence before the biggest moment.
46
+ 7. The name and one action.
47
+
48
+ ## Structure and briefing
49
+
50
+ What the strongest short launch films have in common, in rules a plan can follow:
51
+
52
+ - **Act each claim out.** A claim is performed in a small believable interface, never stated: a speed claim is a `CounterRoll`, an agent or tool a `TerminalLog`, an assistant a `PromptBox` with a reply, a web product a `BrowserFrame` with the user's own screenshot, collaboration a `NamedCursor`. Choose which claims are highlighted and which are merely mentioned: at most three highlighted in 30 seconds.
53
+ - **One grammar.** Either every scene is a `ChapterFrame` (the same slots: "03 / 07", a ghost word, one hero, up to three badges, one caption, a progress rule) or none is. Write a colour script into the plan notes: one accent colour, and for each scene whether its ground is dark or light, so the grounds alternate on purpose.
54
+ - **One hero object carried across cuts**, changing pose or scale: a `Hero3D` or `Screen3D` with `poses`, a `GlassPanel`, or a `Carry`.
55
+ - **Type with a voice.** A headline is two to five words with one emphasised keyword (`Headline`); never plain centred fade-in lines.
56
+ - **Open on the question.** When the product is an assistant or a tool, the first scene is the user's own question typed into a `PromptBox`. Always end on an end card: the wordmark, one line, one call to action, with a clear second before it (`clearBefore`).
57
+ - **Ask for the real material before writing**: screenshots, the real numbers, the logo, the exact product name; and use it. No third-party brand marks.
58
+ - **Length.** 15 to 40 seconds; state it in the plan. Over 60 seconds the same effect repeats.
59
+ - **Review.** Inspect the preview for phone readability, demonstrated claims, planned grounds and the hero object where it belongs. Fix observed problems and recheck changed shots. Finish with `reference/delivery-review.md`.
60
+
61
+ ## The pictures
62
+
63
+ - Use the user's real screens (`reelkit assets upload`), never an invented interface. Put each in a device or browser frame from the library (`reelkit assets search "device frame" --kind component`), tilted in space and settling as it arrives.
64
+ - Type goes under or beside the object, with one coloured word. Keep each line to a few words.
65
+ - Use `Carry` and `Camera` from `reference/continuity.md` so that each shot grows out of the one before: the screen that settled is the card that the next shot opens on.
66
+ - A pointer that clicks, a card that lands, a tile that opens: every change on screen has a cause you can see, and the cause has a sound.
67
+
68
+ ## Sound
69
+
70
+ With no voice the sound is half of the film. Plan it as you plan the picture.
71
+
72
+ 1. **Choose the track first and let the beat set the cuts.** Search for 107 to 145 BPM (`reelkit assets search "driving clean product launch, 120 BPM" --kind music`), pull it with `reelkit assets pull <id> --music`, and read `reference/beat-sync.md`. The CLI holds each scene to the next beat, so every cut lands on the beat, a little more than the films did. Let the effects fill the places between.
73
+ 2. **Sound from the first frame.** A cue at frame 0 and the music running from frame 0. `reelkit check` notes a film with nothing placed in the first half second.
74
+ 3. **Score meaningful changes on the frame where the eye sees them.** A press clicks, a card arriving pops, a move whooshes, a scene change lands on a low hit, and a reveal is led in by a riser that ends on it (`cueBefore`). Two things the measurement showed:
75
+ - A `SceneFrame` transition is complete on the boundary, but the picture changes most in the middle of it: for a zoom-through, blur, push or whip exit that is half the transition's frames before the boundary (4 frames for the default 8, 3 for `transitionFrames={6}`). A hit on the boundary frame lands after the change and does not register against it. Use `cuesOnChanges(scenes, { impact, whoosh })`: it puts the impact on the visual peak and the whoosh so that it ENDS there. Give each scene the same `exit` and `transitionFrames` you give its `SceneFrame` (one array for both, so they cannot disagree).
76
+ - A camera zoom, `zoomTo` or punch is a picture change too and needs its own hit. `cueOnCamera(frame)` gives the frame where a `Camera` key's move is fastest (about 9 frames before the key's frame, because the move starts 12 frames early); `cueOnCamera(frame, { punch: true })` gives it for a punch (just after its frame). Inside a `SceneFrame` the Camera counts frames from the start of the scene: add the scene's `startFrame` for an absolute cue.
77
+ 4. **Density.** An energetic burst may use frequent effects; a restrained film may need only the main actions and reveal. Allow deliberate silence. The measured 2 to 3 hits a second describes examples, not a quality threshold. `reelkit check` counts what the code places and says when it is under one per two seconds; that is a count of the code, not a listening test. Its sound-count notes stay silent when the cues are computed (built by a loop, a helper such as `cuesOnBeats`, or a variable that is not a plain list), so their absence proves nothing: use the render's sound report to see what the film really has.
78
+ 5. **One family of sounds.** Take them from the interface pack in `reference/sound-design.md`, and mix families only when one is missing what you need. For the music, see "Background music" there.
79
+ 6. **Levels.** For a film with no voice these are starting levels for the example style, rather than universal overrides of `reference/sound-design.md` and the narrated range in `reference/beat-sync.md`:
80
+
81
+ | What | `volume` |
82
+ |---|---|
83
+ | Music | 0.4 (0.5 at most) |
84
+ | Effects: pops, taps, clicks, chimes, ticks | 0.35 to 0.5 |
85
+ | Impacts and drops on a scene change | 0.5 to 0.6 |
86
+ | Swells: whooshes that rise, risers | 0.25 to 0.35 |
87
+
88
+ Every pulled sound is levelled on the way in (the interface effects got a gain of 0.7 to 0.8), and `reelkit render` then masters the whole film to -14 LUFS, so only the balance between these matters. At 0.2 to 0.3 an effect sits 6 dB or more under the music, which the tester of 0.8.0 found too quiet to register against the picture; a swell above about 0.35 hides the impact that ends it (the impact no longer rises out of what came before, and the report counts a change with no hit: on a test film, whooshes at 0.2 left 5 of 6 changes with a hit and at 0.35 or more only 2 of 6). Read the report and audition the mix when possible; change levels for an audible balance or timing problem, not merely to increase a measured count.
89
+ - **Which sounds swell.** The library's own "UI" pack (`sfx-ui-soft-whoosh`, `sfx-ui-fast-whip`) is made of short bursts that start at full level and fall away: they are hits to a listener and to the report, not swells. A swell that carries rises for half a second or more and cuts off on the change. Search for these words: `reelkit assets search "reverse whoosh" --kind sfx` (the reverse whooshes of the Quantum Motion pack, 1.7 to 2.3 seconds, with energy between 2 and 10 kHz), `"whoosh sweep"` and `"wind sweep"` for longer air, and `"riser building up"` or `"riser tension"` for a lead into a reveal. Their length is at the end of the search line: give it to `cueBefore` or `cuesOnChanges` (`whoosh: { frames }`), minus the tail after the peak. Measured on a test film, seven reverse whooshes at 0.2 gave six swells where they stand, while the film's UI whooshes gave three at the same places.
90
+ 7. **Silence.** Before the biggest moment, drop the effects, or the music, for a beat or two. The hit that follows then lands. Give `Music` a `dips` entry for the rest, in absolute frames: `dips={[{ from: reveal.startFrame - 24, to: reveal.startFrame, volume: 0.05 }]}` brings the track down for the last moments before the reveal and back up after it.
91
+
92
+ Place all the cues once, outside the scenes, with `SoundCues`; use `cuesOnBeats` for a pulse on the beat, `cuesOnChanges` for the sounds of the scene changes, `cueOnCamera` for a camera move or punch, and `cueBefore` for a sound that must end on a frame (give it the length of the sound in frames).
93
+
94
+ ```tsx
95
+ import React from "react";
96
+ import { AbsoluteFill } from "reelkit/frame";
97
+ import { Music, SceneFrame, SoundCues, cueBefore, cuesOnBeats, cuesOnChanges } from "reelkit/kit";
98
+ import type { VideoProps } from "reelkit/kit";
99
+
100
+ export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
101
+ const beats = manifest.music?.beatFrames ?? [];
102
+ const [, product, , burst, reveal] = manifest.scenes;
103
+ const sounds = {
104
+ whoosh: urls["assets/lib/<whoosh id>/clip.mp3"],
105
+ click: urls["assets/lib/<click id>/clip.mp3"],
106
+ pop: urls["assets/lib/<pop id>/clip.mp3"],
107
+ thud: urls["assets/lib/<impact id>/clip.mp3"],
108
+ riser: urls["assets/lib/<riser id>/clip.mp3"],
109
+ };
110
+ // The same exit and transitionFrames as the SceneFrames below, so the cues and the picture agree.
111
+ const exits = [{ exit: "zoom-through" as const }, {}, { exit: "whip-left" as const, transitionFrames: 6 }, {}, {}];
112
+ const cues = [
113
+ { at: 0, sound: "whoosh", volume: 0.3 },
114
+ { at: product.startFrame + 6, sound: "pop", volume: 0.4 },
115
+ ...cuesOnBeats(beats, { from: burst.startFrame, to: reveal.startFrame, every: 1, sound: "click", volume: 0.4 }),
116
+ // An impact on the visual peak of every scene change and a whoosh that ends on it. frames is the whoosh's length: its duration from the search line times 30.
117
+ ...cuesOnChanges(manifest.scenes.map((s, i) => ({ ...s, ...exits[i] })), { impact: { sound: "thud", volume: 0.55 }, whoosh: { sound: "whoosh", frames: 24, volume: 0.3 } }),
118
+ // The riser's length in frames is its duration from the search line (`(0.8s)`), times 30. Do not assume a length.
119
+ { at: cueBefore(reveal.startFrame, 24), sound: "riser", volume: 0.3 },
120
+ ];
121
+ return (
122
+ <AbsoluteFill>
123
+ {manifest.music ? <Music src={urls[manifest.music.key]} volume={0.5} dips={[{ from: reveal.startFrame - 24, to: reveal.startFrame, volume: 0.05 }]} /> : null}
124
+ {manifest.scenes.map((s) => (
125
+ <SceneFrame key={s.id} from={s.startFrame} durationInFrames={s.durationFrames} {...exits[manifest.scenes.indexOf(s)]}>
126
+ {null}
127
+ </SceneFrame>
128
+ ))}
129
+ <SoundCues cues={cues} sounds={sounds} />
130
+ </AbsoluteFill>
131
+ );
132
+ };
133
+ ```
134
+
135
+ The riser in this example is 0.8 seconds long, which is 24 frames, so `cueBefore(reveal.startFrame, 24)` starts it 24 frames before the reveal and it ends on it. The whooshes of `cuesOnChanges` are 24 frames too. Take that length from the pulled file: `reelkit assets search` prints each sound's duration at the end of its line (for example `(0.8s)`), so a 1.5-second riser needs 45 frames. The whoosh at frame 0 opens the film with sound; the dip gives the reveal its rest.
136
+
137
+ For the one hero moment (the name landing, a screen floating in space) the kit has a 3D layer: see `reference/three-d.md`.
138
+
139
+ For the ground behind the film, an animated ground or a dimmed video is an option: see `reference/backgrounds.md`.
140
+
141
+ ## Size and typeface of library interface pieces
142
+
143
+ Library components (widgets, banners, lists) come small by default, sized for a page and not a phone. An interface piece fills at least 70 percent of the frame's width: scale it with a wrapper (`transform: scale(...)` with a `transformOrigin`) or with its `size` prop if it has one. One typeface for the film: pass the film's font (`font("outfit")`, for example) to every component that takes one, and prefer the components that do. `Camera` frames: a `Camera` inside a `SceneFrame` counts frames from the start of that scene (frame 0 is the scene's first frame, because a `SceneFrame` is a Sequence), so `zoomTo(44, ...)` is 44 frames into the scene; a `Camera` outside every `SceneFrame`, around all of them, counts the video's absolute frames.
144
+
145
+ ## No screenshots
146
+
147
+ When the user has no real screens to show, build the interface pieces from library components and plain cards, inside a device or card frame, and mark nothing as the user's real app: no real-looking logo, no invented numbers presented as true. The library's components have different defaults for colour and typeface, so pass the film's one accent colour and one typeface to EVERY library component through its props, or the film will show three accents and three typefaces. With real screens, use them instead (see "The pictures").
148
+
149
+ ## Sound for interface events
150
+
151
+ A product surface (`PromptBox`, `TerminalLog`, `NamedCursor`, `GlassPanel`, `ChapterFrame`, `CounterRoll`) is performed on screen, so its sounds are small and exact. Every such component has a companion function that returns the frames of its events (`promptFrames`, `terminalFrames`, `clickFrames`, `panelSettleFrame`, `chapterFrames`, `settleFrame`), and `cuesFor(events, kind, { offset })` turns those frames into cues. The rule: the sound sits on the frame the picture settles, the frame the companion function gives, not on the beat grid and not on the start of the move. Add the scene's `startFrame` as `offset`; the frames come out in the scene's own clock.
152
+
153
+ | Event | `cuesFor` kind and roles | Search words for the file | Length | Level without / under a voice | How many per 10 s |
154
+ |---|---|---|---|---|---|
155
+ | Typed text | `type`: `type-tick`, then `type-return` one frame after the last character | `mouse click`, `glass tap` | 0.2 s, 0.5 s | 0.2 and 0.4 / 0.1 and 0.2 | 6 to 12 ticks in 1 to 3 runs; never over 12 a second |
156
+ | Cursor click, send button | `click`: `click` | `mouse click` | 0.2 s | 0.4 / 0.22 | 1 to 3 |
157
+ | Card or panel lands | `snap`: `snap` | `soft impact` | 0.7 s | 0.45 / 0.25 | 2 to 5 |
158
+ | Badge or chip appears | `pop`: `pop` | `bubble pop` | 0.5 s | 0.4 / 0.22 | 3 to 6 |
159
+ | Number counting | `count`: `count-tick` train, `count-settle` on `settleFrame` | `counter ticks`, `success chime` | 0.1 s each, 1 s | 0.2 and 0.45 / 0.1 and 0.25 | 0 to 2 trains |
160
+ | Terminal lines | `stream`: `stream`, one per line | `glass tap` | 0.5 s | 0.18 / 0.1 | 5 to 15; never over 6 a second |
161
+ | Pieces assemble | `assemble`: `assemble-run` rising, `assemble-lock` on the last piece | `counter ticks`, `soft impact` | 0.1 s each, 0.7 s | 0.25 rising to 0.5, and 0.55 / 0.12 and 0.3 | 0 to 1 per film |
162
+ | Logo or end card | `resolve`: `resolve-riser` ending on `resolve-chime` | `whoosh buildup`, `sparkle shimmer` | 1.2 s, 1.5 s | 0.35 and 0.55 / 0.2 and 0.3 | once, at the end |
163
+
164
+ The names are the roles: map each to a pulled file in the `sounds` object of `SoundCues` (the interface pack's files are named `sfx-ui-...`: `sfx-ui-key-clicks`, `sfx-ui-glass-tap`, `sfx-ui-bubble-pop`, `sfx-ui-soft-impact`, `sfx-ui-count-ticks`, `sfx-ui-success-chime`, `sfx-ui-sparkle-shimmer`; search first with `reelkit assets search "<words>" --kind sfx` and read each file's length). A film with a narrator passes `voice: true` for the quieter column: nothing is ever over 0.3 while the voice speaks. Scene changes keep `cuesOnChanges`; a `color-push`, `flash`, `ring` or `flip` change peaks on the boundary frame itself, and `changeFrame` knows it.
165
+
166
+ Give the end card a clear second: `clearBefore(cues, chimeFrame)` removes every other cue in the second before the riser, and a `Music` dip over the same span leaves the riser alone with the picture. `cuesFor` is checked by tests: counts, the caps above, no two cues of one role on one frame, and the same cues every time.
167
+
168
+ ```tsx
169
+ const q = promptFrames({ text: question, reply }, manifest.fps);
170
+ const cues = [
171
+ ...cuesFor(q.type, "type", { offset: ask.startFrame }),
172
+ ...cuesFor([q.send], "click", { offset: ask.startFrame }),
173
+ ...cuesFor([settleFrame({ delay: 8, frames: 40 })], "count", { offset: stat.startFrame }),
174
+ ];
175
+ ```
176
+
177
+ ## Read the sound report
178
+
179
+ `reelkit render` measures the finished file and prints one line for a film with no voice; `reelkit sound [file]` prints it again for `out/video.mp4` or any file, and `reelkit sound --detail` lists every event the counts were made from: each picture change (time, frame, strength) with the nearest hit and its distance, each swell (start, peak, length) and the first sound; with `--json` the same as arrays. It is a measurement of the audio and the picture, not a judgement of how the film sounds: listen to it as well. What the numbers mean:
180
+
181
+ - **Hits a second** (`hitsPerSecond`): sudden rises in the loudness (8 dB or more over the sound 48 milliseconds before, whatever else the film holds), over the whole sound. Targets: 2 to 3. Low: add clicks, pops and a thud on the scene changes. High: some moves can stay silent.
182
+ - **Swells** (`swellsPerSecond`): a rise of the band between 0.5 and 10 kHz, the air of a whoosh or a riser, of 4 dB or more above its own level over the second and a half around it, building over at least a tenth of a second. The level is local, so one loud riser does not hide the quiet whooshes around it, and steady music gives none. Targets: one every 1 to 2 seconds, so 0.5 to 1.3 a second. Low: put a whoosh that rises on each move and a riser before each reveal (a burst that starts at full level is a hit, not a swell; see "Which sounds swell" above). The targets come from the four measured films, with the detector the owner used then; this one counts about the same sounds, but treat the target as a direction.
183
+ - **Picture changes with a hit** (`changesWithHit` of `pictureChanges`): how many of the big changes in the picture have a hit within a tenth of a second. A transition is a span of frames, not one frame: the hit is measured to the nearest point of the span that changes (the run of frames above the change threshold around the peak), so a hit on the boundary at the end of a five-frame transition counts. Camera zooms and punches are picture changes too. The films had 55 to 75 percent. Low: a hit on each change that matters, on the visual peak (`cuesOnChanges`, `cueOnCamera`); `--detail` shows which changes have none and how far the nearest hit is.
184
+ - **First sound** (`firstSoundSec`): the films began within 0.05 seconds. Later: add a cue at frame 0.
185
+
186
+ The same numbers are in the render's `data.sound`. A measurement that cannot be made never fails the render.
187
+
188
+ ## Check before you show it
189
+
190
+ `reelkit preview` shows two frames per scene and prints a continuity line and a beat line (how many scene changes carry something across and land on the beat). It prints no rhythm line: the rhythm of the scene lengths is `reelkit plan check`'s. Look for a shot that holds still for long (the films were still for 11 to 35 percent of the time and no more), a shot whose type is over the object, and a second accent colour. Then show the user the frames, as in the main steps.
191
+
192
+ ## Brand reveals and transformations
193
+
194
+ For a brand-led shot, consult `reference/brand-motion.md`. Choose `BrandTransform3D` turn, arc or launch by the idea it connects; use the supplied identity, finish in a readable neutral pose, and place the sweep and lock from `brandTransformFrames` with `cuesFor(..., "transform")`. Several 3D shots are appropriate for a requested brand film when they each have a different purpose. Map the `transform-sweep` and `transform-lock` roles to the chosen whoosh and impact files. Treat the sound report as evidence to investigate, not a quota to fill with extra hits. Existing approval to edit or render carries through to the requested revision.
@@ -32,19 +32,19 @@ description: Use when deciding how a scene looks and moves - layout, typography,
32
32
  - Reveal Latin text with a masked rise (translate up from behind an overflow-hidden box), word by word, about 2 frames apart. Avoid plain opacity fades for headlines. Hebrew never uses a mask: its words enter whole (see `reference/hebrew-rtl.md`).
33
33
 
34
34
  ## Motion
35
- - **Springs, never linear moves and never cartoon bounce.** Use `spring()` with a damping high enough for a tiny overshoot at most. Presets (stiffness / damping): snappy UI 320 / 30, default containers 170 / 26, heavy type and logos 120 / 24.
35
+ - **Match the motion to the material.** Springs suit responsive UI and weighty arrivals; restrained eased motion suits calm or premium shots. Bounce, squash and overshoot suit playful scenes when deliberate. Useful spring starting points (stiffness / damping): snappy UI 320 / 30, containers 170 / 26, heavy type and logos 120 / 24.
36
36
  - A big move gets a small anticipation first: a few frames of pull the other way.
37
37
  - Exits are faster than entrances. An entrance takes about 0.1 to 0.25 seconds.
38
- - Linear motion is only for movement with no start or end inside the shot (a slow camera turn, a continuous scroll). Every entrance, exit and change of state is a spring.
38
+ - Linear motion suits steady scrolls or continuous travel; entrances and stops need an intentional easing or spring unless the chosen effect is a hard cut.
39
39
  - **The camera is a group.** Put the whole scene inside one wrapper and move that: a slow push of 2 to 4 percent across a shot, on a softer spring than the content. Never scale text up by enlarging a container that holds it as an image; set the size so it stays sharp.
40
- - **Work on a grid.** Place every event on a fixed grid (half a second is a good default) or on the spoken words. Something happens on every grid step.
41
- - **One thing moves at a time.** Stagger related items by 4 to 8 frames rather than moving everything at once.
40
+ - **Choose the clock.** Use spoken triggers for narrated meaning and music beats for rhythmic decoration. Do not invent an event to fill every grid step.
41
+ - Stage attention. Stagger a list when each item matters; move related objects together when their shared action explains the idea.
42
42
  - **Pace to speech.** One visible change per spoken beat, roughly every 0.4 to 1.2 seconds. Use the word timings in `s.words` to bring an element in on the frame its word starts, never before.
43
- - Something new should happen every 2 to 4 seconds, and something must move within the first half second of the video. No dead stretches.
43
+ - Make the premise clear early. Change the picture when attention or meaning needs it; a useful pause, close-up or readable hold is not dead time.
44
44
  - **Rhythm is hit, hold, build.** A fast move, then a hold of about half a second where nothing new enters, then the next move. Constant motion reads amateur; contrast reads expensive. During a hold, settled elements keep their slow breathe.
45
- - **Nothing sits perfectly frozen for more than about a second.** After an element settles, keep a very slow drift or scale (1 to 2 percent over the scene) alive so the frame breathes.
45
+ - A held pose can remain still. Add a slow drift only when it supports the look and keeps text sharp; do not force it onto footage or a reading-heavy shot.
46
46
  - Let an element finish animating and stay readable for at least 1.5 seconds before it leaves. A label or phrase of more than one word stays at least 1.3 seconds; the closing title or message is held at least 1.5 seconds before the last frame. If the plan's length cannot fit that, lengthen the scene and tell the user.
47
- - Counters, progress bars and drawn lines finish before the scene's midpoint.
47
+ - Counters and progress bars land on the event they explain, such as the spoken result or task completion; leave a readable hold afterward.
48
48
  - Text that swaps inside a container that is also changing size needs its own exit and enter timing, or old and new text overlap.
49
49
  - **Whatever a scene adds, the scene removes.** Every overlay, label and effect leaves when its moment ends. A leftover from an earlier scene is the most common thing a review finds.
50
50
  - **Show exactly what is being said.** Each visual belongs to its own sentence. A picture that fits only roughly is worse than plain type, and the same picture or clip never appears twice.
@@ -52,8 +52,8 @@ description: Use when deciding how a scene looks and moves - layout, typography,
52
52
  ## Legibility over images and footage
53
53
  - Text over an image or footage needs help: a dim layer of 0.2 to 0.45 on the media, or a solid plate behind the text, plus a soft shadow.
54
54
  - In footage mode keep overlays to one graphic at a time, clear of the subject.
55
- - Talking to a still camera gets dull fast. Give each sentence one small punch-in on its most important word: 10 to 15 percent, on a spring, centred on the speaker's face, landing on the frame the word starts and resetting at the next sentence. Give the last sentence a slow 3 percent push instead. Do not add this over footage that already has zooms of its own (screen recordings from tools that zoom automatically): two zooms on top of each other make people dizzy.
56
- - Never let more than about 3 seconds of speech pass in footage with nothing new on screen.
55
+ - Use a punch-in on selected emphasis or to improve framing; calm personal speech may need none. Keep the face framed and avoid repeated resets. Do not add a second zoom over footage that already zooms.
56
+ - Add an insert where it clarifies the speech or demonstrates evidence, not merely because a timer elapsed.
57
57
  - A punch-in softens 1080 footage. If the user can still choose, suggest filming in 4K when zooms are planned.
58
58
 
59
59
  ## Truth on screen
@@ -62,7 +62,7 @@ description: Use when deciding how a scene looks and moves - layout, typography,
62
62
  - Do not invent product screens or features. A recreated interface that is not the user's real one is illustrative and should look generic.
63
63
 
64
64
  ## Determinism
65
- - Never use `Math.random()` or the clock. A frame must render the same every time. Use `random(seed)` from remotion for any variation.
65
+ - Never use `Math.random()` or the clock. A frame must render the same every time. Use `random(seed)` from reelkit/frame for any variation.
66
66
  - Everything is a function of `useCurrentFrame()`. No CSS transitions or animations, no timers.
67
67
 
68
68
  ## Shape of a 30 second video
@@ -86,15 +86,13 @@ Hook (first 1.5 s: the boldest visual and claim) → context (one line, one visu
86
86
  - Compute heavy geometry once, outside the frame function, and only position it per frame.
87
87
  - Grain or paper texture is one static layer above everything, outside the camera group. Inside a zooming group it is slow and the grain swells into blotches.
88
88
 
89
- ## Banned, because they read as AI-made or cheap
90
- These hold unless the look chosen in `reference/styles.md` names an exception (a showreel look uses flat colour fields; the neon look uses glow on its hero element and lines).
91
-
92
- Flat solid backgrounds, opacity-only fades, everything entering at once, rainbow or multi-stop gradients on text and UI, particles and sparkles, glows on interface chrome, emoji as graphics, bouncy easing, gratuitous 3D flips, a centred title on a plain gradient, mixed icon styles, lorem ipsum, and anything that looks like a stock template.
89
+ ## Techniques need a purpose
90
+ A flat field, fade, particles, glow, comic bounce or 3D transformation can be right for the chosen idea. Avoid combining them as generic decoration. Keep identity, hierarchy and readability intact. Placeholder copy, false product results and unrelated logos are defects regardless of style. Consult `reference/styles.md` and `reference/brand-motion.md` for techniques that fit the message.
93
91
 
94
92
  ## The studio test
95
93
  Before showing anything, every answer is yes:
96
94
  1. There is one clear idea at each moment, and the eye knows where to look.
97
- 2. Every move starts and ends on a spring, with a small anticipation before a big one.
95
+ 2. Motion starts, lands and holds intentionally, with the timing and weight the material needs.
98
96
  3. Nothing is frozen longer than the look allows, and nothing shakes without a reason.
99
97
  4. Every word reads at phone size, in the right order, uncut and not touching its neighbour.
100
98
  5. Every colour comes from the palette that was agreed; no shade crept in on the way.
@@ -102,6 +100,10 @@ Before showing anything, every answer is yes:
102
100
  7. It looks like a studio made it, not like a template or a slide deck.
103
101
 
104
102
  ## Check your own frames
105
- Go round at least twice: preview, fix, preview again. The first pass finds what is broken; the second finds what is merely weak. If the review shows that something in the approved plan cannot work (an ending too small in the frame), make the smallest change that fixes it and tell the user what changed and why; a different scene or different words goes back for approval.
103
+ Preview and fix observable defects; repeat after a real change or unresolved problem. If an approved shot does not fit, make the smallest layout or timing correction and explain it. A substantial change of message or concept needs user direction when it was not already delegated. Use `reference/delivery-review.md` to verify the export.
106
104
 
107
105
  Run `reelkit preview`, then look at both frames of every scene in `out/preview/` (early at 30%, late at 90%) as a harsh director would. Hunt for: text overlapping during a swap, text cut by the frame edge, type too small for a phone, a frame with nothing in it, and anything that looks like a template. Fix what is real, then check again.
106
+
107
+ ## Choosing a transformation
108
+
109
+ Change the shot when the idea changes or the same object must take a new role. Name the relationship first: question to answer, parts to whole, product to result, or result to brand. Keep the agreed palette and the actual identity through the change. Use `reference/brand-motion.md` to choose a 2D carry, clear cut or branded 3D arrival and its sound. A deliberate readable hold can remain still; do not add drift or a flip just to meet a motion quota.