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.
- package/README.md +5 -3
- package/package.json +52 -9
- package/skill/SKILL.md +40 -19
- package/skill/THIRD_PARTY.md +104 -2
- package/skill/commands/launch-film.md +7 -0
- package/skill/reference/art-styles.md +70 -0
- package/skill/reference/asset-reuse.md +13 -2
- package/skill/reference/backgrounds.md +63 -0
- package/skill/reference/beat-sync.md +25 -19
- package/skill/reference/brand-motion.md +62 -0
- package/skill/reference/captions.md +11 -5
- package/skill/reference/clips.md +3 -3
- package/skill/reference/component-authoring.md +1 -1
- package/skill/reference/continuity.md +26 -7
- package/skill/reference/delivery-review.md +40 -0
- package/skill/reference/hebrew-rtl.md +3 -4
- package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +18 -11
- package/skill/reference/kit.md +144 -12
- package/skill/reference/launch-film.md +194 -0
- package/skill/reference/motion-design.md +18 -16
- package/skill/reference/scene-treatments.md +20 -0
- package/skill/reference/scriptwriting.md +4 -1
- package/skill/reference/sound-design.md +34 -12
- package/skill/reference/studio-editing.md +55 -0
- package/skill/reference/styles.md +9 -6
- package/skill/reference/three-d.md +135 -0
- package/skill/reference/voice-sync.md +108 -0
- package/src/agents.ts +23 -12
- package/src/api/client.ts +4 -1
- package/src/cli.ts +18 -7
- package/src/commands/assets.ts +314 -30
- package/src/commands/build.ts +148 -35
- package/src/commands/init.ts +1 -1
- package/src/commands/install.ts +1 -1
- package/src/commands/plan.ts +8 -5
- package/src/commands/ref.ts +5 -2
- package/src/contract/index.ts +5 -3
- package/src/hyperframes/Root.tsx +1 -0
- package/src/hyperframes/fonts.ts +54 -0
- package/src/hyperframes/frame.tsx +46 -0
- package/src/hyperframes/host.tsx +38 -0
- package/src/hyperframes/kit/Assemble3D.tsx +92 -0
- package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
- package/src/hyperframes/kit/BrowserFrame.tsx +83 -0
- package/src/{remotion → hyperframes}/kit/Camera.tsx +7 -5
- package/src/{remotion → hyperframes}/kit/Captions.tsx +34 -18
- package/src/hyperframes/kit/Card3D.tsx +211 -0
- package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
- package/src/hyperframes/kit/ChapterFrame.tsx +68 -0
- package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
- package/src/hyperframes/kit/CounterRoll.tsx +75 -0
- package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
- package/src/hyperframes/kit/GlassPanel.tsx +43 -0
- package/src/hyperframes/kit/Grounds.tsx +177 -0
- package/src/hyperframes/kit/Headline.tsx +97 -0
- package/src/hyperframes/kit/Hero3D.tsx +197 -0
- package/src/hyperframes/kit/HudOverlay.tsx +52 -0
- package/src/hyperframes/kit/ImageLayers.tsx +48 -0
- package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
- package/src/hyperframes/kit/Music.tsx +19 -0
- package/src/hyperframes/kit/NamedCursor.tsx +54 -0
- package/src/hyperframes/kit/Orbit3D.tsx +49 -0
- package/src/hyperframes/kit/Particles3D.tsx +74 -0
- package/src/hyperframes/kit/Place.tsx +12 -0
- package/src/hyperframes/kit/PromptBox.tsx +84 -0
- package/src/hyperframes/kit/Scene3D.tsx +70 -0
- package/src/hyperframes/kit/SceneFrame.tsx +96 -0
- package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
- package/src/hyperframes/kit/SoundCues.tsx +22 -0
- package/src/hyperframes/kit/TerminalLog.tsx +98 -0
- package/src/hyperframes/kit/Text3D.tsx +78 -0
- package/src/hyperframes/kit/TextOnImage.tsx +41 -0
- package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
- package/src/hyperframes/kit/Warp3D.tsx +59 -0
- package/src/hyperframes/kit/bg-math.ts +179 -0
- package/src/hyperframes/kit/brand-transform.ts +25 -0
- package/src/{remotion → hyperframes}/kit/caption-groups.ts +7 -3
- package/src/hyperframes/kit/caption-style.ts +45 -0
- package/src/hyperframes/kit/docs.ts +249 -0
- package/src/hyperframes/kit/image-layers-math.ts +115 -0
- package/src/hyperframes/kit/index.ts +74 -0
- package/src/hyperframes/kit/inter-bold-typeface.ts +3 -0
- package/src/{remotion → hyperframes}/kit/motion-math.ts +36 -2
- package/src/hyperframes/kit/music-math.ts +59 -0
- package/src/hyperframes/kit/quiet-three.ts +11 -0
- package/src/hyperframes/kit/sample-text.ts +55 -0
- package/src/hyperframes/kit/scene3d-context.ts +5 -0
- package/src/hyperframes/kit/seeded.ts +13 -0
- package/src/hyperframes/kit/sound-cues.ts +89 -0
- package/src/hyperframes/kit/sound-kinds.ts +135 -0
- package/src/{remotion → hyperframes}/kit/theme.ts +43 -39
- package/src/hyperframes/kit/three-fx-math.ts +192 -0
- package/src/hyperframes/kit/three-math.ts +145 -0
- package/src/hyperframes/kit/transition-math.ts +116 -0
- package/src/hyperframes/kit/ui-math.ts +145 -0
- package/src/hyperframes/kit/ui-theme.ts +25 -0
- package/src/hyperframes/kit/word-anchor.ts +107 -0
- package/src/hyperframes/math.ts +62 -0
- package/src/hyperframes/three.tsx +10 -0
- package/src/pipeline/beatsnap.ts +72 -0
- package/src/pipeline/review.ts +44 -10
- package/src/pipeline/schema.ts +51 -4
- package/src/pipeline/timing.ts +27 -1
- package/src/project/background.ts +33 -0
- package/src/project/chromakey.ts +1 -1
- package/src/project/layers.ts +60 -0
- package/src/project/manifest.ts +59 -14
- package/src/project/music.ts +19 -5
- package/src/project/project.ts +4 -1
- package/src/project/serve.ts +2 -2
- package/src/project/soundreport.ts +347 -0
- package/src/project/svgcheck.ts +21 -0
- package/src/render/component-preview.ts +11 -55
- package/src/render/contact-sheet.ts +39 -0
- package/src/render/continuity.ts +14 -4
- package/src/render/deps.ts +15 -3
- package/src/render/render.ts +62 -57
- package/src/render/serve.ts +31 -0
- package/src/render/sound-notes.ts +106 -0
- package/src/render/static-check.ts +15 -4
- package/src/render/validate.ts +4 -4
- package/src/render/word-check.ts +181 -0
- package/src/render/worker.ts +71 -0
- package/src/testing/conformance.ts +12 -0
- package/src/testing/fake-api.ts +4 -4
- package/src/testing/fixtures.ts +4 -1
- package/src/remotion/Root.tsx +0 -31
- package/src/remotion/kit/Music.tsx +0 -19
- package/src/remotion/kit/SceneFrame.tsx +0 -19
- package/src/remotion/kit/docs.ts +0 -124
- package/src/remotion/kit/index.ts +0 -29
- package/src/remotion/kit/music-math.ts +0 -42
- /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
- /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
- /package/src/{remotion → hyperframes}/types.ts +0 -0
package/src/testing/fake-api.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { MAX_UPLOAD_BYTES, routes, type ErrorCode, type LibraryItem, type RouteName, type Voice } from "../contract";
|
|
9
9
|
import { staticCheck } from "../render/validate";
|
|
10
|
-
import { PIXEL_PNG, silentWav } from "./fixtures";
|
|
10
|
+
import { FAKE_SVG, PIXEL_PNG, silentWav } from "./fixtures";
|
|
11
11
|
|
|
12
12
|
type Blob = { filename: string; contentType: string; bytes: Uint8Array };
|
|
13
13
|
// owner is the uploader's user id; a seeded item has none. An upload is not an item for anyone until it is committed.
|
|
@@ -212,15 +212,15 @@ export async function startFakeApi(opts: { voiceoverCharLimit?: number; imageLim
|
|
|
212
212
|
const durationSec = list.length * WORD_SEC;
|
|
213
213
|
return { url: fileUrl({ filename: "vo.wav", contentType: "audio/wav", bytes: silentWav(durationSec) }), ext: "wav", contentType: "audio/wav", durationSec, words: list, chars: text.length };
|
|
214
214
|
},
|
|
215
|
-
images: ({ prompt, aspect, shareable, tags }, { userId }) => {
|
|
215
|
+
images: ({ prompt, aspect, shareable, tags, format }, { userId }) => {
|
|
216
216
|
const m = meter(userId);
|
|
217
217
|
if (m.images + 1 > limits.images) throw new Fail(429, "quota_exceeded", `Image quota used up. It resets on ${resetDate().slice(0, 10)}.`);
|
|
218
218
|
m.images++;
|
|
219
219
|
usage.images++;
|
|
220
220
|
const id = nextId("image");
|
|
221
|
-
const file = { filename: "image.png", contentType: "image/png", bytes: PIXEL_PNG };
|
|
221
|
+
const file = format === "svg" ? { filename: "image.svg", contentType: "image/svg+xml", bytes: new TextEncoder().encode(FAKE_SVG) } : { filename: "image.png", contentType: "image/png", bytes: PIXEL_PNG };
|
|
222
222
|
if (shareable) store.set(id, { item: { id, kind: "image", title: cut(prompt, 60), description: prompt, tags, meta: { aspect }, visibility: "review" }, file, filename: file.filename, contentType: file.contentType, owner: userId, committed: true });
|
|
223
|
-
return { url: fileUrl(file), ext: "
|
|
223
|
+
return { url: fileUrl(file), ext: format === "svg" ? "svg" : "png", contentType: file.contentType, ...(shareable ? { libraryId: id } : {}) };
|
|
224
224
|
},
|
|
225
225
|
clipStart: (input, { userId }) => {
|
|
226
226
|
if (opts.noClipProvider) throw new Fail(400, "invalid_request", "Clip generation is not available on this server yet.");
|
package/src/testing/fixtures.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const PIXEL_PNG = new Uint8Array(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAE
|
|
|
12
12
|
|
|
13
13
|
// A known-good composition, used as the worked example for composition authors.
|
|
14
14
|
export const FIXTURE_COMPOSITION = `import React from "react";
|
|
15
|
-
import { AbsoluteFill, useVideoConfig } from "
|
|
15
|
+
import { AbsoluteFill, useVideoConfig } from "reelkit/frame";
|
|
16
16
|
import { BgMesh, Captions, FootageLayer, fonts, Grade, Grain, KenBurnsImage, palettes, SceneFrame, Vignette, Voiceover, WordReveal } from "reelkit/kit";
|
|
17
17
|
import type { VideoProps } from "reelkit/kit";
|
|
18
18
|
|
|
@@ -42,3 +42,6 @@ export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
|
|
|
42
42
|
);
|
|
43
43
|
};
|
|
44
44
|
`;
|
|
45
|
+
|
|
46
|
+
// A tiny valid vector graphic, what the fake API returns for format "svg".
|
|
47
|
+
export const FAKE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#e02020"/><rect x="30" y="30" width="40" height="40" fill="#2040e0"/></svg>';
|
package/src/remotion/Root.tsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Composition } from "remotion";
|
|
3
|
-
import { ManifestContext, UrlsContext } from "./kit/media";
|
|
4
|
-
import type { VideoProps } from "./types";
|
|
5
|
-
|
|
6
|
-
const EMPTY: VideoProps = { manifest: { fps: 30, width: 1080, height: 1920, totalFrames: 1, scenes: [] }, urls: {} };
|
|
7
|
-
|
|
8
|
-
export const makeRoot = (Video: React.FC<VideoProps>): React.FC => {
|
|
9
|
-
const WithMedia: React.FC<VideoProps> = (props) => (
|
|
10
|
-
<UrlsContext.Provider value={props.urls}>
|
|
11
|
-
<ManifestContext.Provider value={props.manifest}>
|
|
12
|
-
<Video {...props} />
|
|
13
|
-
</ManifestContext.Provider>
|
|
14
|
-
</UrlsContext.Provider>
|
|
15
|
-
);
|
|
16
|
-
return () => (
|
|
17
|
-
<Composition
|
|
18
|
-
id="Video"
|
|
19
|
-
component={WithMedia as unknown as React.FC<Record<string, unknown>>}
|
|
20
|
-
durationInFrames={1}
|
|
21
|
-
fps={30}
|
|
22
|
-
width={1080}
|
|
23
|
-
height={1920}
|
|
24
|
-
defaultProps={EMPTY as unknown as Record<string, unknown>}
|
|
25
|
-
calculateMetadata={({ props }) => {
|
|
26
|
-
const m = (props as unknown as VideoProps).manifest;
|
|
27
|
-
return { durationInFrames: m.totalFrames, fps: m.fps, width: m.width, height: m.height };
|
|
28
|
-
}}
|
|
29
|
-
/>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Audio, useVideoConfig } from "remotion";
|
|
3
|
-
import { musicVolume, speechSpans, type SpeechScene } from "./music-math";
|
|
4
|
-
import { useGain, useManifest, useMedia } from "./media";
|
|
5
|
-
|
|
6
|
-
// The video's music track. Place it once, outside the scenes. It sits at `duckTo` while the voiceover is heard and comes up to `volume`
|
|
7
|
-
// in the gaps, loops when the video is longer than the track, and fades out over the last second. `scenes` defaults to the manifest's.
|
|
8
|
-
export const Music: React.FC<{ src: string; volume?: number; duckTo?: number; scenes?: SpeechScene[] }> = ({ src, volume = 0.5, duckTo = 0.12, scenes }) => {
|
|
9
|
-
const { fps, durationInFrames } = useVideoConfig();
|
|
10
|
-
const manifest = useManifest();
|
|
11
|
-
const gain = useGain(src);
|
|
12
|
-
const spans = React.useMemo(() => speechSpans(scenes ?? manifest?.scenes ?? [], fps), [scenes, manifest, fps]);
|
|
13
|
-
return (
|
|
14
|
-
<Audio
|
|
15
|
-
src={useMedia(src)} loop loopVolumeCurveBehavior="extend"
|
|
16
|
-
volume={(f) => musicVolume(f, { fps, totalFrames: durationInFrames, volume, duckTo, spans, gain })}
|
|
17
|
-
/>
|
|
18
|
-
);
|
|
19
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { AbsoluteFill, interpolate, Sequence, useCurrentFrame } from "remotion";
|
|
3
|
-
|
|
4
|
-
const Fade: React.FC<{ durationInFrames: number; children: React.ReactNode }> = ({ durationInFrames, children }) => {
|
|
5
|
-
const frame = useCurrentFrame();
|
|
6
|
-
const edge = Math.min(8, Math.floor(durationInFrames / 2));
|
|
7
|
-
const opacity = interpolate(frame, [0, edge, durationInFrames - edge, durationInFrames], [0, 1, 1, 0], {
|
|
8
|
-
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
9
|
-
});
|
|
10
|
-
return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const SceneFrame: React.FC<{ from: number; durationInFrames: number; children: React.ReactNode }> = ({
|
|
14
|
-
from, durationInFrames, children,
|
|
15
|
-
}) => (
|
|
16
|
-
<Sequence from={from} durationInFrames={durationInFrames}>
|
|
17
|
-
<Fade durationInFrames={durationInFrames}>{children}</Fade>
|
|
18
|
-
</Sequence>
|
|
19
|
-
);
|
package/src/remotion/kit/docs.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
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; children }
|
|
5
|
-
Wraps one scene. Places children on the timeline and fades them in and out.
|
|
6
|
-
<SceneFrame from={s.startFrame} durationInFrames={s.durationFrames}>...</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"; uppercase?: boolean; bottom?: number; face?: string; rtl?: boolean }
|
|
17
|
-
Word-timed captions near the bottom. 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, 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.
|
|
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
|
-
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 }
|
|
63
|
-
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.
|
|
64
|
-
<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>
|
|
65
|
-
|
|
66
|
-
Camera { keys: { frame: number; x?: number; y?: number; zoom?: number; rotate?: number }[]; drift?: number; lead?: number; stiffness?: number; damping?: number; children }
|
|
67
|
-
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.
|
|
68
|
-
<Camera keys={[{ frame: 0, zoom: 1 }, { frame: b.startFrame, x: 0.7, y: 0.4, zoom: 4 }]} drift={0.01}>{scenes}</Camera>
|
|
69
|
-
|
|
70
|
-
fonts { display: string; body: string }
|
|
71
|
-
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.
|
|
72
|
-
|
|
73
|
-
Icon { name: IconName; size: number; color?: string } brandColor(name): string
|
|
74
|
-
A platform's real logo mark, or a plain interface glyph. size is in pixels. color "brand" uses the platform's own colour.
|
|
75
|
-
Brands: instagram, tiktok, youtube, x, facebook, whatsapp, telegram, twitch, discord, spotify, snapchat, pinterest, messenger, threads,
|
|
76
|
-
reddit, github, gmail, soundcloud, behance, vimeo, dribbble, imessage, apple, googlemessages, signal, googlecalendar, stripe, paypal,
|
|
77
|
-
applepay, shopify, notion, zoom. Glyphs: message, mail, bell, phone, calendar, cart, heart, star, user, play.
|
|
78
|
-
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
|
|
79
|
-
platform's mark when the video really refers to that platform.
|
|
80
|
-
<Icon name="instagram" size={width * 0.06} color="brand" />
|
|
81
|
-
|
|
82
|
-
font(name): string
|
|
83
|
-
Loads one extra typeface and returns its family name. Call it once at the top of a file: const heavy = font("montserrat").
|
|
84
|
-
montserrat (300, 800, 900), montserratItalic (800, 900), kanit (600), roboto (300, 500, 700, 900), robotoSlab (700),
|
|
85
|
-
bebasNeue (tall condensed caps), archivoBlack (very heavy), unbounded (wide heavy display, 800, 900), modak (chunky rounded),
|
|
86
|
-
greatVibes (formal script), sacramento (monoline script), outfit (geometric sans 100, 300, 500, 900), syne (wide heavy 800),
|
|
87
|
-
lilitaOne (heavy rounded), kaushanScript (bold brush script), yellowtail (flowing brush script), mrsSaintDelafield (signature script),
|
|
88
|
-
permanentMarker (marker lettering), bungeeShade (outlined sign capitals), cinzel (Roman capitals 700, 900: epic),
|
|
89
|
-
playfairDisplay (elegant serif 700, 900), orbitron (sci-fi 700, 900), rye (western), creepster (horror).
|
|
90
|
-
Hebrew text must use a Hebrew face; every face above is Latin only and Hebrew would fall back to a default font. Hebrew faces
|
|
91
|
-
(all also cover Latin): heebo, rubik, notoSansHebrew (400, 700, 900: neutral sans), assistant (400, 700, 800), alef (400, 700),
|
|
92
|
-
secularOne (strong headline), varelaRound (soft rounded), fredoka (playful rounded 500, 700), karantina (tall condensed 400, 700),
|
|
93
|
-
suezOne (heavy serif headline), frankRuhlLibre (book serif 400, 700, 900), davidLibre (traditional serif 400, 700),
|
|
94
|
-
amaticSC (hand-lettered 400, 700).
|
|
95
|
-
Use at most two typefaces in one video.
|
|
96
|
-
|
|
97
|
-
springs { snappy, smooth, heavy } ease { out, in, inOut }
|
|
98
|
-
Spring configs for spring({ frame, fps, config: springs.smooth }) and easings for interpolate(..., { easing: ease.out, extrapolateLeft: "clamp", extrapolateRight: "clamp" }).
|
|
99
|
-
|
|
100
|
-
palettes { darkTech, warmEditorial, warmPremium, cleanLight } each is a Palette { bg, ink, hero, accent, dim }
|
|
101
|
-
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.
|
|
102
|
-
|
|
103
|
-
ScreenOverlay { src: string; durationSec?: number; opacity?: number }
|
|
104
|
-
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.
|
|
105
|
-
Place it once, above the scenes and below Grade. Works best on dark themes.
|
|
106
|
-
<ScreenOverlay src={urls["assets/lib/<id>/clip.mp4"]} durationSec={5} opacity={0.5} />
|
|
107
|
-
|
|
108
|
-
Sfx { src: string; at?: number; volume?: number }
|
|
109
|
-
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>.
|
|
110
|
-
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.
|
|
111
|
-
<Sfx src={urls["assets/lib/<id>/clip.mp3"]} at={10} volume={0.35} />
|
|
112
|
-
|
|
113
|
-
Music { src: string; volume?: number; duckTo?: number }
|
|
114
|
-
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.
|
|
115
|
-
<Music src={urls[manifest.music.key]} />
|
|
116
|
-
|
|
117
|
-
nearestBeat(beatFrames, frame): number | undefined nextBeat(beatFrames, frame): number | undefined beatPulse(beatFrames, frame, decayFrames = 10): number
|
|
118
|
-
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.
|
|
119
|
-
const b = nextBeat(manifest.music?.beatFrames ?? [], s.startFrame + 20) ?? s.startFrame + 20; // <Entrance delay={b - s.startFrame}>
|
|
120
|
-
|
|
121
|
-
Voiceover { src: string; volume?: number }
|
|
122
|
-
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.
|
|
123
|
-
{s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
|
|
124
|
-
`.trim();
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export { Camera } from "./Camera";
|
|
2
|
-
export { Captions } from "./Captions";
|
|
3
|
-
export { Carry } from "./Carry";
|
|
4
|
-
export type { CarryBox } from "./Carry";
|
|
5
|
-
export { ClipLayer } from "./ClipLayer";
|
|
6
|
-
export { Counter } from "./Counter";
|
|
7
|
-
export { Entrance, WordReveal } from "./Entrance";
|
|
8
|
-
export { FootageLayer } from "./FootageLayer";
|
|
9
|
-
export { brandColor, Icon } from "./Icon";
|
|
10
|
-
export type { GlyphName, IconName } from "./Icon";
|
|
11
|
-
export type { BrandName } from "./brand-icons";
|
|
12
|
-
export { KenBurnsImage } from "./KenBurnsImage";
|
|
13
|
-
export { KeyedClip } from "./KeyedClip";
|
|
14
|
-
export { BgMesh, Grade, Grain, Vignette } from "./Layers";
|
|
15
|
-
export { LowerThird } from "./LowerThird";
|
|
16
|
-
export { SceneFrame } from "./SceneFrame";
|
|
17
|
-
export { ScreenOverlay } from "./ScreenOverlay";
|
|
18
|
-
export { useMedia } from "./media";
|
|
19
|
-
export { Music } from "./Music";
|
|
20
|
-
export { beatPulse, nearestBeat, nextBeat } from "./beat";
|
|
21
|
-
export { Sfx } from "./Sfx";
|
|
22
|
-
export { TitleCard } from "./TitleCard";
|
|
23
|
-
export { Voiceover } from "./Voiceover";
|
|
24
|
-
export type { VideoProps } from "../types";
|
|
25
|
-
export { ease, font, fonts, palettes, springs } from "./theme";
|
|
26
|
-
export type { CaptionMode } from "./Captions";
|
|
27
|
-
export type { CaptionGroup } from "./caption-groups";
|
|
28
|
-
export type { BoxKey, CameraKey } from "./motion-math";
|
|
29
|
-
export type { KitFont, Palette } from "./theme";
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { WordTiming } from "../../pipeline/schema";
|
|
2
|
-
|
|
3
|
-
export const DUCK_PAD_SEC = 0.25;
|
|
4
|
-
export const DUCK_RAMP_SEC = 0.2;
|
|
5
|
-
// A gap between words at least this long lets the music come back up; a shorter one keeps it down so it does not pump.
|
|
6
|
-
export const DUCK_GAP_SEC = 0.6;
|
|
7
|
-
export const FADE_OUT_SEC = 1;
|
|
8
|
-
|
|
9
|
-
export type SpeechScene = { startFrame: number; words: WordTiming[] };
|
|
10
|
-
|
|
11
|
-
// The stretches of the video, in seconds, during which a voice is heard (with the padding around each word); words closer than
|
|
12
|
-
// DUCK_GAP_SEC are one stretch.
|
|
13
|
-
export function speechSpans(scenes: SpeechScene[], fps: number): [number, number][] {
|
|
14
|
-
const words = scenes.flatMap((s) => s.words.map((w) => [s.startFrame / fps + w.startSec, s.startFrame / fps + w.endSec] as const)).sort((a, b) => a[0] - b[0]);
|
|
15
|
-
const spans: [number, number][] = [];
|
|
16
|
-
for (const [start, end] of words) {
|
|
17
|
-
const last = spans[spans.length - 1];
|
|
18
|
-
if (last && start - (last[1] - DUCK_PAD_SEC) <= DUCK_GAP_SEC) last[1] = Math.max(last[1], end + DUCK_PAD_SEC);
|
|
19
|
-
else spans.push([start - DUCK_PAD_SEC, end + DUCK_PAD_SEC]);
|
|
20
|
-
}
|
|
21
|
-
return spans;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// How far down the music is at `t` seconds, 0 (not at all) to 1 (fully): full inside a stretch, ramping over DUCK_RAMP_SEC just outside its edges.
|
|
25
|
-
function duckAmount(spans: [number, number][], t: number): number {
|
|
26
|
-
let amount = 0;
|
|
27
|
-
for (const [a, b] of spans) {
|
|
28
|
-
const edge = Math.min((t - a) / DUCK_RAMP_SEC + 1, (b - t) / DUCK_RAMP_SEC + 1);
|
|
29
|
-
amount = Math.max(amount, Math.max(0, Math.min(1, edge)));
|
|
30
|
-
}
|
|
31
|
-
return amount;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// The music's volume at a composition frame: `volume` in gaps, `duckTo` under a voice, and a fade to nothing over the last second.
|
|
35
|
-
// `gain` is the track's levelling (linear). Looping needs no handling here: the frame is the composition's, not the track's.
|
|
36
|
-
export function musicVolume(frame: number, o: { fps: number; totalFrames: number; volume: number; duckTo: number; spans: [number, number][]; gain?: number }): number {
|
|
37
|
-
const t = frame / o.fps;
|
|
38
|
-
const duck = duckAmount(o.spans, t);
|
|
39
|
-
const base = o.volume + (Math.min(o.duckTo, o.volume) - o.volume) * duck;
|
|
40
|
-
const fade = Math.max(0, Math.min(1, (o.totalFrames - frame) / (FADE_OUT_SEC * o.fps)));
|
|
41
|
-
return base * fade * (o.gain ?? 1);
|
|
42
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|