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
@@ -0,0 +1,249 @@
1
+ export const KIT_DOCS = `
2
+ 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.
3
+
4
+ SceneFrame { from: number; durationInFrames: number; enter?: Transition; exit?: Transition; transitionFrames?: number; origin?: { x: number; y: number }; color?: string; shape?: "circle" | "bar" | "diagonal"; children }
5
+ 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.
6
+ <SceneFrame from={s.startFrame} durationInFrames={s.durationFrames} enter="zoom-through" exit="zoom-through" origin={{ x: 0.5, y: 0.4 }}>...</SceneFrame>
7
+
8
+ TitleCard { text: string; subtitle?: string; color?: string; background?: string }
9
+ Large centred title that springs in.
10
+ <TitleCard text="Ship faster" subtitle="in three steps" background="#101020" />
11
+
12
+ LowerThird { title: string; subtitle?: string; accent?: string }
13
+ Name/label strip that slides in from the left, low on the screen.
14
+ <LowerThird title="Step 1" subtitle="Connect your account" />
15
+
16
+ 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 }
17
+ 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:
18
+ "highlight" (a line, spoken word coloured), "pop" (1-3 words popping in as spoken), "karaoke" (a line filling with colour).
19
+ 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.
20
+ bottom is the distance from the bottom edge as a fraction of the height (default 0.16).
21
+ For Hebrew narration pass face={font("heebo")} and rtl.
22
+ <Captions words={s.words} mode="pop" highlight={palette.hero} uppercase />
23
+
24
+ Counter { from?: number; to: number; durationFrames?: number; prefix?: string; suffix?: string; color?: string }
25
+ Centred number that counts up.
26
+ <Counter to={10000} suffix="+" durationFrames={45} />
27
+
28
+ KenBurnsImage { src: string; zoom?: number; direction?: "in" | "out" }
29
+ Full-frame image with a slow zoom and pan. src must come from urls[...]. Alternate direction between consecutive image scenes.
30
+ <KenBurnsImage src={urls[s.imageKey]} />
31
+
32
+ FootageLayer { src: string; muted?: boolean; dim?: number }
33
+ Full-frame user footage. Place it once, outside the scenes, as the bottom layer. dim (0-1) darkens it for legibility.
34
+ <FootageLayer src={urls[manifest.footageKey]} dim={0.25} />
35
+
36
+ ClipLayer { src: string; muted?: boolean; dim?: number }
37
+ Full-frame generated or library video clip as a scene's picture (treatment "clip"). Place it inside the scene's SceneFrame as the bottom layer, so it starts with the scene. When the scene is longer than the clip it holds the last frame; it never loops. dim (0-1) darkens it for legibility.
38
+ <ClipLayer src={urls[s.clipKey]} dim={0.2} />
39
+
40
+ KeyedClip { src: string; x?: number; y?: number; scale?: number; muted?: boolean }
41
+ A green-screen clip with the green keyed out (the transparent .webm, s.clipKeyedKey). Lay it over your own background or screenshot, below the captions. x and y place its bottom centre as fractions of the frame (default x 0.5, y 1: bottom centre); scale is its width as a fraction of the frame width (default 1). Holds its last frame when the scene is longer.
42
+ <KeyedClip src={urls[s.clipKeyedKey]} x={0.5} y={1} scale={0.8} />
43
+
44
+ BgMesh { bg: string; hero: string; accent?: string }
45
+ The bottom layer of the video: the base colour with two slow-drifting soft colour fields. Use it instead of a flat background.
46
+ <BgMesh bg={palette.bg} hero={palette.hero} accent={palette.accent} />
47
+
48
+ Grade { color: string; strength?: number } Grain { opacity?: number; blend?: "multiply" | "overlay" } Vignette { strength?: number }
49
+ Finishing layers, placed once at the very top of the video in this order: Grade, Grain, Vignette.
50
+ Grade tints everything toward the hero colour so images and graphics read as one look (strength 0.10-0.15 on light themes, 0.18-0.25 on dark).
51
+ Grain uses blend "multiply" on light themes and "overlay" on dark ones.
52
+ <Grade color={palette.hero} /><Grain blend="overlay" /><Vignette />
53
+
54
+ Entrance { delay?: number; exitAt?: number; rise?: number; breathe?: boolean; style?; children }
55
+ The standard way to bring anything in: fade + rise + scale on a spring, then a slow idle breathe. delay and exitAt are frames from the scene start; exitAt adds a faster exit.
56
+ <Entrance delay={8} exitAt={s.durationFrames - 14}><Card /></Entrance>
57
+
58
+ WordReveal { text: string; delay?: number; per?: number; highlight?: string; highlightColor?: string; style? }
59
+ Headline that rises in word by word from behind a mask. highlight colours one word.
60
+ <WordReveal text="Stretch first, phone second" highlight="first" highlightColor={palette.hero} style={{ fontFamily: fonts.display, fontWeight: 800, fontSize: width * 0.09, color: palette.ink }} />
61
+
62
+ Act the product out (a claim is performed in a small believable interface, not stated)
63
+ 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.
64
+ 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.
65
+
66
+ 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) }
67
+ 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.
68
+ <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} />
69
+
70
+ 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? }
71
+ 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.
72
+ <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"]} />
73
+
74
+ 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) }
75
+ 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.
76
+ <NamedCursor name="Maya" points={[{ x: 0.2, y: 0.8 }, { x: 0.7, y: 0.45, click: true }]} />
77
+
78
+ 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? }
79
+ 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.
80
+ <GlassPanel tilt={8} glow={palette.hero}><div style={{ color: "#fff", fontSize: width * 0.06 }}>Refund request</div></GlassPanel>
81
+
82
+ ChapterFrame { index: number; total: number; color: string; ghost?: string; caption?: string; badges?: string[] (up to 3); hero?; ink?; font?; delay?: number (0); children }
83
+ 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 }.
84
+ <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>
85
+
86
+ 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? }
87
+ 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 }.
88
+ <Headline text="Answer every email" keyword="every" emphasis="highlight" hero={palette.hero} />
89
+
90
+ 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? }
91
+ 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.
92
+ <CounterRoll to={12480} prefix="$" label="saved this month" delay={8} />
93
+
94
+ 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) }
95
+ 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.
96
+ <HudOverlay labels={["relay v2", "live"]} />
97
+
98
+ 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? }
99
+ 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 }.
100
+ <BrowserFrame src={urls["assets/screens/inbox.png"]} scroll={0.8} url="relay.app" />
101
+
102
+ Images in layers (a picture's words are a layer OF the picture)
103
+ 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.
104
+
105
+ ImageLayers { layers?: ImageLayersRecord; back?: string; subject?: string; depth?: number (0.4); focus?: "subject" | "back"; front?: ReactNode; children }
106
+ 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.
107
+ <ImageLayers layers={s.imageLayers}><TextOnImage layers={s.imageLayers}><Headline text="Meet Tom" /></TextOnImage></ImageLayers>
108
+
109
+ TextOnImage { layers?: ImageLayersRecord; zone?: "top" | "middle" | "bottom" | "left" | "right" | { x; y; w; h }; behindSubject?: boolean; ink?: string; scrim?: number; children }
110
+ 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.
111
+
112
+ 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 }
113
+ 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.
114
+ <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>
115
+
116
+ 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 }
117
+ 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.
118
+ <Camera keys={[{ frame: 0, zoom: 1 }, { frame: b.startFrame, x: 0.7, y: 0.4, zoom: 4 }]} drift={0.01}>{scenes}</Camera>
119
+ 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>
120
+ 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 })]}.
121
+
122
+ 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 }
123
+ 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.
124
+ <Scene3D camera={{ keys: [{ frame: 0, z: 7.5 }, { frame: 40, z: 6 }] }}><Text3D text="Reelkit" color={palette.hero} enter="rise" /></Scene3D>
125
+
126
+ BrandTransform3D { mode?: "turn" | "arc" | "launch"; from?: number (0); frames?: number (32); strength?: number (0 to 1, default 1); children }
127
+ 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.
128
+ <BrandTransform3D mode="turn" from={6} frames={32}><Text3D text="Reelkit" fit={0.72} /></BrandTransform3D>
129
+
130
+ 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 }
131
+ 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.
132
+ <Text3D text="Meet Reelkit" color="#ffffff" position={[0, 1, 0]} enter="turn" delay={6} />
133
+
134
+ 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 }
135
+ 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.
136
+ <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" />
137
+
138
+ Orbit3D { radius?: number; fit?: number (0.85); speed?: number; tiltDeg?: number; billboard?: boolean; position?: [x, y, z]; children }
139
+ 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.
140
+ <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>
141
+
142
+ 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? }
143
+ 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.
144
+ <Scene3D><Assemble3D shape="text" text="Relay" count={900} order="x" colors={[palette.hero, palette.accent]} /></Scene3D>
145
+
146
+ 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? }
147
+ 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.
148
+ <Scene3D><Particles3D from="galaxy" to="text" text="Relay" morphFrames={[10, 60]} /></Scene3D>
149
+
150
+ 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? }
151
+ 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.
152
+ <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>
153
+
154
+ Screen3D { the props of Hero3D without kind and text; spin (5) }
155
+ 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.
156
+ <Scene3D><Screen3D src={urls["assets/screens/inbox.png"]} poses={[{ frame: 0, rotation: [10, -30, 0] }, { frame: 80, rotation: [0, 0, 0] }]} /></Scene3D>
157
+
158
+ 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? }
159
+ 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.
160
+ <Scene3D><Warp3D burst={{ frame: 20, frames: 16, peak: 4 }} /></Scene3D>
161
+
162
+ Grounds (the layer under everything; one for the whole film, see reference/backgrounds.md)
163
+ 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 }
164
+ 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} />
165
+ BgImage { src; dim?: 0.35; blur?: 0; tint?; zoom?: 0.06; drift?: "none" | "left" | "right" | "up" | "down"; fit?: "cover" }
166
+ 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.
167
+ BgSequence { items: { src: string; from: number }[]; transition?: "fade" | "blur" | "wipe" | "zoom"; transitionFrames?: 18; align?: "center" | "start"; dim?; blur?; tint?; zoom?; drift? }
168
+ 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).
169
+ 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.
170
+ <BgSequence items={bgPerScene(manifest.scenes, manifest.scenes.map((s) => s.background && urls[s.background.key]))} tint={palette.hero} dim={0.4} />
171
+ 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) }
172
+ 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).
173
+ <BgAurora bg={palette.bg} hero={palette.hero} />
174
+
175
+ fonts { display: string; body: string }
176
+ 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.
177
+
178
+ Icon { name: IconName; size: number; color?: string } brandColor(name): string
179
+ A platform's real logo mark, or a plain interface glyph. size is in pixels. color "brand" uses the platform's own colour.
180
+ Brands: instagram, tiktok, youtube, x, facebook, whatsapp, telegram, twitch, discord, spotify, snapchat, pinterest, messenger, threads,
181
+ reddit, github, gmail, soundcloud, behance, vimeo, dribbble, imessage, apple, googlemessages, signal, googlecalendar, stripe, paypal,
182
+ applepay, shopify, notion, zoom. Glyphs: message, mail, bell, phone, calendar, cart, heart, star, user, play.
183
+ Use Icon for any platform logo. Never draw a logo yourself, never use a letter or an emoji in its place, and only show a
184
+ platform's mark when the video really refers to that platform.
185
+ <Icon name="instagram" size={width * 0.06} color="brand" />
186
+
187
+ font(name): string
188
+ Loads one extra typeface and returns its family name. Call it once at the top of a file: const heavy = font("montserrat").
189
+ montserrat (300, 800, 900), montserratItalic (800, 900), kanit (600), roboto (300, 500, 700, 900), robotoSlab (700),
190
+ bebasNeue (tall condensed caps), archivoBlack (very heavy), unbounded (wide heavy display, 800, 900), modak (chunky rounded),
191
+ greatVibes (formal script), sacramento (monoline script), outfit (geometric sans 100, 300, 500, 900), syne (wide heavy 800),
192
+ lilitaOne (heavy rounded), kaushanScript (bold brush script), yellowtail (flowing brush script), mrsSaintDelafield (signature script),
193
+ permanentMarker (marker lettering), bungeeShade (outlined sign capitals), cinzel (Roman capitals 700, 900: epic),
194
+ playfairDisplay (elegant serif 700, 900), jetbrainsMono (monospace 400, 700: terminals, labels), orbitron (sci-fi 700, 900), rye (western), creepster (horror).
195
+ Hebrew text must use a Hebrew face; every face above is Latin only and Hebrew would fall back to a default font. Hebrew faces
196
+ (all also cover Latin): heebo, rubik, notoSansHebrew (400, 700, 900: neutral sans), assistant (400, 700, 800), alef (400, 700),
197
+ secularOne (strong headline), varelaRound (soft rounded), fredoka (playful rounded 500, 700), karantina (tall condensed 400, 700),
198
+ suezOne (heavy serif headline), frankRuhlLibre (book serif 400, 700, 900), davidLibre (traditional serif 400, 700),
199
+ amaticSC (hand-lettered 400, 700).
200
+ Use at most two typefaces in one video.
201
+
202
+ springs { snappy, smooth, heavy } ease { out, in, inOut }
203
+ Spring configs for spring({ frame, fps, config: springs.smooth }) and easings for interpolate(..., { easing: ease.out, extrapolateLeft: "clamp", extrapolateRight: "clamp" }).
204
+
205
+ palettes { darkTech, warmEditorial, warmPremium, cleanLight } each is a Palette { bg, ink, hero, accent, dim }
206
+ Proven starting palettes. Pick one that suits the topic, or define your own Palette object. Declare it once at the top of Video.tsx and take every colour from it.
207
+
208
+ ScreenOverlay { src: string; durationSec?: number; opacity?: number }
209
+ Lays a shared overlay clip (film marks, HUD frames) over the video with a screen blend. Pull one with: reelkit assets search "<description>" --kind overlay, then reelkit assets pull <id>; the clip's durationSec is in its entry in assets/library.json.
210
+ Place it once, above the scenes and below Grade. Works best on dark themes.
211
+ <ScreenOverlay src={urls["assets/lib/<id>/clip.mp4"]} durationSec={5} opacity={0.5} />
212
+
213
+ Sfx { src: string; at?: number; volume?: number }
214
+ Plays one sound effect from the shared library, starting at the frame given by "at", counted from the start of the enclosing scene. Pull one with: reelkit assets search "<description>" --kind sfx, then reelkit assets pull <id>.
215
+ 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.
216
+ <Sfx src={urls["assets/lib/<id>/clip.mp3"]} at={10} volume={0.35} />
217
+
218
+ SoundCues { cues: { at: number; sound: string; volume?: number }[]; sounds: Record<string, string>; volume?: number }
219
+ 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.
220
+ <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 }]} />
221
+
222
+ cuesOnBeats(beatFrames, { from, to, every, sound, volume? }): { at, sound, volume? }[] cueBefore(frame, leadFrames): number
223
+ 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.
224
+ const cues = [...cuesOnBeats(manifest.music?.beatFrames ?? [], { from: 0, to: 120, every: 2, sound: "tick", volume: 0.2 }), { at: cueBefore(s.startFrame, 12), sound: "whoosh" }];
225
+
226
+ cuesOnChanges(scenes, { impact?: { sound, volume? }, whoosh?: { sound, volume?, frames }, skip?: number[] }): { at, sound, volume? }[] changeFrame(scene): number cueOnCamera(frame, { punch?, punchFrames?, lead? }): number
227
+ 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.
228
+ 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 } });
229
+
230
+ cuesFor(events, kind, { offset?, fps?, voice?, volume?, seed?, sweepFrames?, riserFrames? }): { at, sound, volume }[] clearBefore(cues, frame, { seconds?, fps?, riserFrames? }): cues
231
+ 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).
232
+ 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 })];
233
+
234
+ Music { src: string; volume?: number; duckTo?: number; dips?: { from: number; to: number; volume: number }[] }
235
+ 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 }]}.
236
+ <Music src={urls[manifest.music.key]} />
237
+
238
+ nearestBeat(beatFrames, frame): number | undefined nextBeat(beatFrames, frame): number | undefined beatPulse(beatFrames, frame, decayFrames = 10): number
239
+ 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.
240
+ const b = nextBeat(manifest.music?.beatFrames ?? [], s.startFrame + 20) ?? s.startFrame + 20; // <Entrance delay={b - s.startFrame}>
241
+
242
+ 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
243
+ 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.
244
+ 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} />
245
+
246
+ Voiceover { src: string; volume?: number }
247
+ 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.
248
+ {s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
249
+ `.trim();
@@ -0,0 +1,115 @@
1
+ // The arithmetic behind ImageLayers and TextOnImage, and the measuring the CLI does on a picture. No React and no files here: everything works on small grids
2
+ // of grey values or alpha values, so it is exact and can be tested with synthetic pictures.
3
+
4
+ export type Region = "top" | "middle" | "bottom" | "left" | "right";
5
+ export const REGIONS: readonly Region[] = ["top", "middle", "bottom", "left", "right"];
6
+ export type Rect = { x: number; y: number; w: number; h: number };
7
+ // Where text can sit, measured on the picture: the calmest large region, how light it is (0 black to 1 white) and how busy it is (0 flat to 1 very detailed).
8
+ export type TextZone = { region: Region; luminance: number; busy: number };
9
+ export type ImageLayersRecord = { back: string; subject?: string; subjectBox?: Rect; textZone: TextZone };
10
+
11
+ // The part of the frame each region is, as fractions: the thirds from the top and the halves from the left, which is where a block of type is set.
12
+ export const REGION_RECTS: Record<Region, Rect> = {
13
+ top: { x: 0, y: 0, w: 1, h: 1 / 3 }, middle: { x: 0, y: 1 / 3, w: 1, h: 1 / 3 }, bottom: { x: 0, y: 2 / 3, w: 1, h: 1 / 3 },
14
+ left: { x: 0, y: 0, w: 0.5, h: 1 }, right: { x: 0.5, y: 0, w: 0.5, h: 1 },
15
+ };
16
+ // The rectangle type is set in for a region: inset a little from its edges so it never touches the frame.
17
+ export function zoneRect(region: Region): Rect {
18
+ const r = REGION_RECTS[region], px = 0.06, py = 0.04;
19
+ const x = r.x + (r.x === 0 ? px : 0.02), w = r.w - (r.x === 0 ? px : 0.02) - (r.x + r.w >= 1 ? px : 0.02);
20
+ return { x, y: r.y + py, w, h: r.h - 2 * py };
21
+ }
22
+
23
+ // How detailed a region is: the mean size of the step between neighbouring grey values, over the cells inside it, on a 0..1 scale of 255. A flat sky is near 0.
24
+ function energy(grid: ArrayLike<number>, w: number, h: number, r: Rect): { energy: number; mean: number } {
25
+ const x0 = Math.floor(r.x * w), x1 = Math.max(x0 + 1, Math.ceil((r.x + r.w) * w)), y0 = Math.floor(r.y * h), y1 = Math.max(y0 + 1, Math.ceil((r.y + r.h) * h));
26
+ let e = 0, n = 0, sum = 0, cells = 0;
27
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) {
28
+ const v = grid[y * w + x]!;
29
+ sum += v; cells++;
30
+ if (x + 1 < x1) { e += Math.abs(v - grid[y * w + x + 1]!); n++; }
31
+ if (y + 1 < y1) { e += Math.abs(v - grid[(y + 1) * w + x]!); n++; }
32
+ }
33
+ return { energy: n ? e / n / 255 : 0, mean: cells ? sum / cells / 255 : 0 };
34
+ }
35
+
36
+ // The calmest large region of a picture, from a greyscale grid (values 0..255, row by row), and how light it is. `exclude` is a box (a subject) whose cells count as busy,
37
+ // so text is not offered a place behind a subject that is in the way of all the calm. Ties go to the region listed first of: bottom, top, middle, left, right.
38
+ export function textZoneOf(grey: ArrayLike<number>, w: number, h: number, subject?: Rect): TextZone {
39
+ const order: Region[] = ["bottom", "top", "middle", "left", "right"];
40
+ let best: { region: Region; e: number; mean: number } | undefined;
41
+ for (const region of order) {
42
+ const m = energy(grey, w, h, REGION_RECTS[region]);
43
+ // The halves are tall and narrow: a region that holds the subject is not calm, whatever its pixels say.
44
+ const e = m.energy + (subject ? overlapOf(zoneRect(region), subject) * 0.5 : 0);
45
+ if (!best || e < best.e - 1e-9) best = { region, e, mean: m.mean };
46
+ }
47
+ // 0.12 of the grey step per cell is about as busy as a picture gets in practice.
48
+ return { region: best!.region, luminance: Math.round(best!.mean * 1000) / 1000, busy: Math.round(Math.min(1, best!.e / 0.12) * 1000) / 1000 };
49
+ }
50
+
51
+ // The box that holds the opaque part of a subject, as fractions of the picture, from a grid of alpha values (0..255); undefined when nothing is opaque.
52
+ // Cells above `threshold` (default 40) count. A few stray cells do not stretch it: a row or a column must hold at least 3 percent of a side (two cells at least).
53
+ export function subjectBoxOf(alpha: ArrayLike<number>, w: number, h: number, threshold = 40): Rect | undefined {
54
+ const rows = new Array<number>(h).fill(0), cols = new Array<number>(w).fill(0);
55
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (alpha[y * w + x]! > threshold) { rows[y]!++; cols[x]!++; }
56
+ const keepRow = (n: number) => n >= Math.max(2, w * 0.03), keepCol = (n: number) => n >= Math.max(2, h * 0.03);
57
+ const ys = rows.map((n, i) => (keepRow(n) ? i : -1)).filter((i) => i >= 0), xs = cols.map((n, i) => (keepCol(n) ? i : -1)).filter((i) => i >= 0);
58
+ if (!ys.length || !xs.length) return undefined;
59
+ const r = (n: number) => Math.round(n * 1000) / 1000;
60
+ return { x: r(xs[0]! / w), y: r(ys[0]! / h), w: r((xs[xs.length - 1]! + 1 - xs[0]!) / w), h: r((ys[ys.length - 1]! + 1 - ys[0]!) / h) };
61
+ }
62
+
63
+ // The share of the picture that is opaque, 0..1.
64
+ export function coverageOf(alpha: ArrayLike<number>, threshold = 40): number {
65
+ let n = 0;
66
+ for (let i = 0; i < alpha.length; i++) if (alpha[i]! > threshold) n++;
67
+ return alpha.length ? n / alpha.length : 0;
68
+ }
69
+
70
+ export const MIN_SUBJECT = 0.03, MAX_SUBJECT = 0.85;
71
+ // Whether a subject layer is worth keeping: below 3 percent of the picture it is a speck, above 85 percent it is the whole picture and nothing can sit between.
72
+ export function coverageVerdict(coverage: number): { ok: boolean; note?: string } {
73
+ const pct = Math.round(coverage * 1000) / 10;
74
+ if (coverage < MIN_SUBJECT) return { ok: false, note: `The picture has no clear subject (the cut-out covers ${pct}% of it, under ${MIN_SUBJECT * 100}%), so it stays one flat image. Words still go in its calm zone with TextOnImage.` };
75
+ if (coverage > MAX_SUBJECT) return { ok: false, note: `The picture has no clear subject (the cut-out covers ${pct}% of it, over ${MAX_SUBJECT * 100}%), so it stays one flat image. Words still go in its calm zone with TextOnImage.` };
76
+ return { ok: true };
77
+ }
78
+
79
+ // The share of `a` that lies inside `b`, 0..1.
80
+ export function overlapOf(a: Rect, b: Rect): number {
81
+ const w = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)), h = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
82
+ return a.w * a.h > 0 ? (w * h) / (a.w * a.h) : 0;
83
+ }
84
+
85
+ // Behind the subject or in front of it: the words go behind it when the subject's box covers 10 to 60 percent of the text block (the title behind the person), in front
86
+ // when it would hide more than 60 percent of it, and in front when there is no overlap to speak of (nothing is hidden, so nothing is behind).
87
+ export function placementOf(text: Rect, subject: Rect | undefined, behindSubject?: boolean): { behind: boolean; overlap: number } {
88
+ const overlap = subject ? overlapOf(text, subject) : 0;
89
+ if (!subject) return { behind: false, overlap };
90
+ const auto = overlap >= 0.1 && overlap <= 0.6;
91
+ // An explicit choice stands unless it would hide most of the text.
92
+ return { behind: behindSubject === undefined ? auto : behindSubject && overlap <= 0.6, overlap };
93
+ }
94
+
95
+ // The ink for a zone of a given luminance, and the soft scrim laid behind the text: dark ink on a light zone and white on a dark one, the scrim the opposite colour,
96
+ // stronger the busier the zone is (0.2 on a flat one, 0.65 on a very busy one).
97
+ export function inkFor(zone: { luminance: number; busy: number }): { ink: string; scrim: string; strength: number } {
98
+ const light = zone.luminance > 0.55;
99
+ return { ink: light ? "#14141c" : "#ffffff", scrim: light ? "255,255,255" : "0,0,0", strength: Math.round((0.2 + 0.45 * Math.min(1, Math.max(0, zone.busy))) * 1000) / 1000 };
100
+ }
101
+
102
+ // How far each layer of an ImageLayers has moved by `frame`. The back drifts and grows slowly; the subject does a little more, the other way (`depth` 0 to 1 is how
103
+ // much more); the words between them at their own rate. Scale is relative to the cover fit, tx and ty are fractions of the frame. Every layer is scaled enough to
104
+ // cover the frame at every moment: the shift never exceeds half of what the scale adds, so no edge is ever shown.
105
+ export type LayerMove = { scale: number; tx: number; ty: number };
106
+ export function layerMoves(frame: number, fps: number, depth = 0.4): { back: LayerMove; text: LayerMove; subject: LayerMove } {
107
+ const d = Math.min(1, Math.max(0, depth)), t = Math.max(0, frame) / fps;
108
+ // The back and the subject grow together (the subject is the same picture, so a different growth would show a second, larger copy of it); what differs is the drift
109
+ // sideways and a little up and down: the back goes one way and the subject the other, by `depth` times more. At depth 0 they are the same move.
110
+ const scale = 1.08 + 0.012 * t, room = ((scale - 1) / 2) * 0.9;
111
+ const clamp = (v: number) => Math.max(-room, Math.min(room, v));
112
+ const back: LayerMove = { scale, tx: clamp(-0.001 * t), ty: clamp(0.0005 * t) };
113
+ const subject: LayerMove = { scale, tx: clamp(back.tx + 0.004 * d * t), ty: clamp(back.ty - 0.002 * d * t) };
114
+ return { back, text: { scale: 1 + 0.006 * (1 + d) * t, tx: 0, ty: 0 }, subject };
115
+ }
@@ -0,0 +1,74 @@
1
+ export { Camera } from "./Camera";
2
+ export { Captions } from "./Captions";
3
+ export { BgAurora, BgBeams, BgGrain, BgGrid, BgImage, BgSequence, BgVideo } from "./Grounds";
4
+ export { bgPerScene } from "./bg-math";
5
+ export type { SequenceItem } from "./bg-math";
6
+ export { Carry } from "./Carry";
7
+ export type { CarryBox } from "./Carry";
8
+ export { ClipLayer } from "./ClipLayer";
9
+ export { Card3D } from "./Card3D";
10
+ export { Assemble3D } from "./Assemble3D";
11
+ export { BrandTransform3D } from "./BrandTransform3D";
12
+ export { BRAND_TRANSFORMS, brandTransformAt, brandTransformFrames } from "./brand-transform";
13
+ export type { BrandTransform, BrandTransformTiming } from "./brand-transform";
14
+ export { Hero3D, Screen3D } from "./Hero3D";
15
+ export { Particles3D } from "./Particles3D";
16
+ export { Warp3D } from "./Warp3D";
17
+ export { assembleEnd, assembleProgress, assembledCount, assembleRanks, moodLights, poseAt, shapeSlots, warpSpeed } from "./three-fx-math";
18
+ export type { AssembleOrder, AssembleShape, Mood, ParticleShape, PoseKey, SpeedKey } from "./three-fx-math";
19
+ export { Counter } from "./Counter";
20
+ export { BrowserFrame } from "./BrowserFrame";
21
+ export { ChapterFrame } from "./ChapterFrame";
22
+ export { CounterRoll } from "./CounterRoll";
23
+ export { GlassPanel } from "./GlassPanel";
24
+ export { Headline } from "./Headline";
25
+ export { ImageLayers } from "./ImageLayers";
26
+ export { TextOnImage } from "./TextOnImage";
27
+ export type { ImageLayersRecord, Region as TextRegion } from "./image-layers-math";
28
+ export { HudOverlay } from "./HudOverlay";
29
+ export { NamedCursor } from "./NamedCursor";
30
+ export { PromptBox } from "./PromptBox";
31
+ export { TerminalLog } from "./TerminalLog";
32
+ export { browserFrames, chapterFrames, clickFrames, headlineFrames, lineFrames, panelSettleFrame, promptFrames, rollValue, settleFrame, terminalFrames, typedFrames } from "./ui-math";
33
+ export type { CursorPoint, LineKind, TerminalLine } from "./ui-math";
34
+ export { themeFor } from "./ui-theme";
35
+ export type { UiTheme } from "./ui-theme";
36
+ export { Entrance, WordReveal } from "./Entrance";
37
+ export { FootageLayer } from "./FootageLayer";
38
+ export { brandColor, Icon } from "./Icon";
39
+ export type { GlyphName, IconName } from "./Icon";
40
+ export type { BrandName } from "./brand-icons";
41
+ export { KenBurnsImage } from "./KenBurnsImage";
42
+ export { KeyedClip } from "./KeyedClip";
43
+ export { BgMesh, Grade, Grain, Vignette } from "./Layers";
44
+ export { LowerThird } from "./LowerThird";
45
+ export { SceneFrame } from "./SceneFrame";
46
+ export type { SceneTransition } from "./SceneFrame";
47
+ export type { TransitionKind } from "./transition-math";
48
+ export { zoomTo } from "./motion-math";
49
+ export { ScreenOverlay } from "./ScreenOverlay";
50
+ export { useMedia } from "./media";
51
+ export { Music } from "./Music";
52
+ export { Orbit3D } from "./Orbit3D";
53
+ export { Scene3D } from "./Scene3D";
54
+ export { Text3D } from "./Text3D";
55
+ export type { Camera3DKey } from "./three-math";
56
+ export { beatPulse, nearestBeat, nextBeat } from "./beat";
57
+ export { onWord, onWordBeat, sceneById, wordFrame, wordFrames } from "./word-anchor";
58
+ export type { OnWordBeatOptions, OnWordOptions, WordFrameOptions, WordScene } from "./word-anchor";
59
+ export { Sfx } from "./Sfx";
60
+ export { SoundCues } from "./SoundCues";
61
+ export { changeFrame, cueBefore, cueOnCamera, cuesOnBeats, cuesOnChanges } from "./sound-cues";
62
+ export type { SoundCue } from "./sound-cues";
63
+ export { CUE_KINDS, CUE_TABLE, clearBefore, cuesFor } from "./sound-kinds";
64
+ export type { CueKind, CueOptions } from "./sound-kinds";
65
+ export { hash01, mulberry32 } from "./seeded";
66
+ export { TitleCard } from "./TitleCard";
67
+ export { Voiceover } from "./Voiceover";
68
+ export type { VideoProps } from "../types";
69
+ export { ease, font, fonts, palettes, springs } from "./theme";
70
+ export type { CaptionMode } from "./Captions";
71
+ export type { CaptionGroup } from "./caption-groups";
72
+ export type { BoxKey, CameraKey, Punch } from "./motion-math";
73
+ export type { KitFont, Palette } from "./theme";
74
+ export { luminance, mixHex, nearestVisible, visibleHeight, visibleWidth } from "./three-math";