reelkit-cli 0.8.0 → 0.10.2

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 (122) hide show
  1. package/README.md +13 -1
  2. package/package.json +49 -10
  3. package/skill/.claude-plugin/plugin.json +7 -0
  4. package/skill/SKILL.md +44 -19
  5. package/skill/THIRD_PARTY.md +37 -5
  6. package/skill/commands/launch-film.md +1 -1
  7. package/skill/hooks/hooks.json +1 -0
  8. package/skill/hooks/player.ts +278 -0
  9. package/skill/hooks/project.ts +227 -0
  10. package/skill/hooks/register.tsx +533 -0
  11. package/skill/hooks/timeline.ts +218 -0
  12. package/skill/reference/art-styles.md +70 -0
  13. package/skill/reference/backgrounds.md +1 -1
  14. package/skill/reference/brand-motion.md +62 -0
  15. package/skill/reference/clips.md +3 -3
  16. package/skill/reference/component-authoring.md +3 -1
  17. package/skill/reference/continuity.md +5 -5
  18. package/skill/reference/delivery-review.md +40 -0
  19. package/skill/reference/hebrew-rtl.md +3 -4
  20. package/skill/reference/higgsfield-production.md +70 -0
  21. package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +14 -8
  22. package/skill/reference/kit.md +7 -3
  23. package/skill/reference/launch-film.md +13 -9
  24. package/skill/reference/motion-design.md +26 -22
  25. package/skill/reference/sound-design.md +34 -12
  26. package/skill/reference/studio-editing.md +55 -0
  27. package/skill/reference/studio-pane.md +31 -0
  28. package/skill/reference/style-components.md +40 -0
  29. package/skill/reference/style-recipes.md +86 -0
  30. package/skill/reference/styles.md +17 -10
  31. package/skill/reference/three-d.md +8 -7
  32. package/skill/reference/voice-sync.md +1 -1
  33. package/skill/tests/studio.test.ts +111 -0
  34. package/skill/types/index.d.ts +65 -0
  35. package/src/commands/assets.ts +5 -1
  36. package/src/contract/index.ts +1 -1
  37. package/src/hyperframes/Root.tsx +1 -0
  38. package/src/hyperframes/fonts.ts +54 -0
  39. package/src/hyperframes/frame.tsx +46 -0
  40. package/src/hyperframes/host.tsx +38 -0
  41. package/src/{remotion → hyperframes}/kit/Assemble3D.tsx +1 -1
  42. package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
  43. package/src/{remotion → hyperframes}/kit/BrowserFrame.tsx +1 -1
  44. package/src/{remotion → hyperframes}/kit/Camera.tsx +1 -1
  45. package/src/{remotion → hyperframes}/kit/Captions.tsx +1 -1
  46. package/src/{remotion → hyperframes}/kit/Card3D.tsx +2 -2
  47. package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
  48. package/src/{remotion → hyperframes}/kit/ChapterFrame.tsx +1 -1
  49. package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
  50. package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
  51. package/src/{remotion → hyperframes}/kit/CounterRoll.tsx +1 -1
  52. package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
  53. package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
  54. package/src/{remotion → hyperframes}/kit/GlassPanel.tsx +1 -1
  55. package/src/{remotion → hyperframes}/kit/Grounds.tsx +1 -1
  56. package/src/{remotion → hyperframes}/kit/Headline.tsx +1 -1
  57. package/src/{remotion → hyperframes}/kit/Hero3D.tsx +1 -1
  58. package/src/{remotion → hyperframes}/kit/HudOverlay.tsx +1 -1
  59. package/src/{remotion → hyperframes}/kit/ImageLayers.tsx +1 -1
  60. package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
  61. package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
  62. package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
  63. package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
  64. package/src/{remotion → hyperframes}/kit/Music.tsx +1 -1
  65. package/src/{remotion → hyperframes}/kit/NamedCursor.tsx +1 -1
  66. package/src/{remotion → hyperframes}/kit/Orbit3D.tsx +1 -1
  67. package/src/{remotion → hyperframes}/kit/Particles3D.tsx +1 -1
  68. package/src/{remotion → hyperframes}/kit/Place.tsx +1 -1
  69. package/src/{remotion → hyperframes}/kit/PromptBox.tsx +1 -1
  70. package/src/{remotion → hyperframes}/kit/Scene3D.tsx +2 -2
  71. package/src/{remotion → hyperframes}/kit/SceneFrame.tsx +1 -1
  72. package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
  73. package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
  74. package/src/{remotion → hyperframes}/kit/SoundCues.tsx +1 -1
  75. package/src/{remotion → hyperframes}/kit/TerminalLog.tsx +1 -1
  76. package/src/{remotion → hyperframes}/kit/Text3D.tsx +1 -1
  77. package/src/{remotion → hyperframes}/kit/TextOnImage.tsx +1 -1
  78. package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
  79. package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
  80. package/src/{remotion → hyperframes}/kit/Warp3D.tsx +1 -1
  81. package/src/hyperframes/kit/brand-transform.ts +25 -0
  82. package/src/{remotion → hyperframes}/kit/docs.ts +6 -2
  83. package/src/{remotion → hyperframes}/kit/index.ts +3 -0
  84. package/src/{remotion → hyperframes}/kit/sound-cues.ts +1 -1
  85. package/src/{remotion → hyperframes}/kit/sound-kinds.ts +14 -1
  86. package/src/{remotion → hyperframes}/kit/theme.ts +42 -40
  87. package/src/{remotion → hyperframes}/kit/ui-math.ts +1 -1
  88. package/src/hyperframes/math.ts +62 -0
  89. package/src/hyperframes/three.tsx +10 -0
  90. package/src/project/chromakey.ts +1 -1
  91. package/src/project/layers.ts +1 -1
  92. package/src/project/serve.ts +2 -2
  93. package/src/render/component-preview.ts +11 -55
  94. package/src/render/render.ts +62 -64
  95. package/src/render/serve.ts +31 -0
  96. package/src/render/static-check.ts +15 -4
  97. package/src/render/validate.ts +4 -4
  98. package/src/render/word-check.ts +1 -1
  99. package/src/render/worker.ts +71 -0
  100. package/src/testing/fixtures.ts +1 -1
  101. package/src/remotion/Root.tsx +0 -31
  102. /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
  103. /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
  104. /package/src/{remotion → hyperframes}/kit/bg-math.ts +0 -0
  105. /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
  106. /package/src/{remotion → hyperframes}/kit/caption-groups.ts +0 -0
  107. /package/src/{remotion → hyperframes}/kit/caption-style.ts +0 -0
  108. /package/src/{remotion → hyperframes}/kit/image-layers-math.ts +0 -0
  109. /package/src/{remotion → hyperframes}/kit/inter-bold-typeface.ts +0 -0
  110. /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
  111. /package/src/{remotion → hyperframes}/kit/motion-math.ts +0 -0
  112. /package/src/{remotion → hyperframes}/kit/music-math.ts +0 -0
  113. /package/src/{remotion → hyperframes}/kit/quiet-three.ts +0 -0
  114. /package/src/{remotion → hyperframes}/kit/sample-text.ts +0 -0
  115. /package/src/{remotion → hyperframes}/kit/scene3d-context.ts +0 -0
  116. /package/src/{remotion → hyperframes}/kit/seeded.ts +0 -0
  117. /package/src/{remotion → hyperframes}/kit/three-fx-math.ts +0 -0
  118. /package/src/{remotion → hyperframes}/kit/three-math.ts +0 -0
  119. /package/src/{remotion → hyperframes}/kit/transition-math.ts +0 -0
  120. /package/src/{remotion → hyperframes}/kit/ui-theme.ts +0 -0
  121. /package/src/{remotion → hyperframes}/kit/word-anchor.ts +0 -0
  122. /package/src/{remotion → hyperframes}/types.ts +0 -0
@@ -0,0 +1,46 @@
1
+ import React, { createContext, useContext, useId } from "react";
2
+ export * from "./math";
3
+ export type VideoConfig = { width: number; height: number; fps: number; durationInFrames: number; id?: string };
4
+ export type FrameState = { frame: number; offset: number; duration: number; config: VideoConfig; loop?: number; collecting?: boolean };
5
+ export const FrameContext = createContext<FrameState>({ frame: 0, offset: 0, duration: 1, config: { width: 1080, height: 1920, fps: 30, durationInFrames: 1 } });
6
+ export const useCurrentFrame = () => useContext(FrameContext).frame;
7
+ export const useVideoConfig = () => { const s = useContext(FrameContext); return { ...s.config, durationInFrames: s.duration }; };
8
+ export const AbsoluteFill = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ style, ...props }, ref) => <div ref={ref} {...props} style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", ...style }} />);
9
+ export const Img = React.forwardRef<HTMLImageElement, React.ImgHTMLAttributes<HTMLImageElement>>((props, ref) => <img {...props} ref={ref} />);
10
+ export const Sequence: React.FC<{ from?: number; durationInFrames?: number; layout?: "none" | "absolute-fill"; children?: React.ReactNode; name?: string; style?: React.CSSProperties; premountFor?: number }> = ({ from = 0, durationInFrames, layout, children, style }) => {
11
+ const s = useContext(FrameContext), duration = Math.min(durationInFrames ?? s.duration - from, s.duration - from);
12
+ if (!s.collecting && (s.frame < from || s.frame >= from + duration)) return null;
13
+ const content = <FrameContext.Provider value={{ ...s, frame: s.frame - from, offset: s.offset + from, duration }}>{children}</FrameContext.Provider>;
14
+ return layout === "none" ? content : <AbsoluteFill style={style}>{content}</AbsoluteFill>;
15
+ };
16
+ export const Freeze: React.FC<{ frame: number; children?: React.ReactNode }> = ({ frame, children }) => { const s = useContext(FrameContext); return <FrameContext.Provider value={{ ...s, frame }}>{children}</FrameContext.Provider>; };
17
+ export const Loop: React.FC<{ durationInFrames: number; children?: React.ReactNode; times?: number; layout?: "none" | "absolute-fill" }> = ({ durationInFrames, children }) => {
18
+ const s = useContext(FrameContext); return <FrameContext.Provider value={{ ...s, frame: ((s.frame % durationInFrames) + durationInFrames) % durationInFrames, loop: durationInFrames }}>{children}</FrameContext.Provider>;
19
+ };
20
+ export type AudioRecord = { id: string; src: string; start: number; end: number; mediaStart: number; layer: number; volume: number; volumeKeyframes: { time: number; volume: number }[]; type: "audio" | "video"; loop: boolean };
21
+ export const audioRecords = new Map<string, AudioRecord>();
22
+ type MediaProps = { src: string; volume?: number | ((frame: number) => number); startFrom?: number; endAt?: number; trimBefore?: number; trimAfter?: number; playbackRate?: number; loopVolumeCurveBehavior?: "extend" | "repeat" };
23
+ function recordAudio(id: string, props: MediaProps & { loop?: boolean }, type: "audio" | "video", state: FrameState) {
24
+ const { config: { fps } } = state;
25
+ const volume = typeof props.volume === "function" ? props.volume(state.frame) : props.volume ?? 1;
26
+ const start = state.offset / fps, end = Math.min(state.config.durationInFrames, state.offset + state.duration) / fps;
27
+ let record = audioRecords.get(id);
28
+ if (!record) { record = { id, src: props.src, start, end, mediaStart: (props.trimBefore ?? props.startFrom ?? 0) / fps, layer: audioRecords.size, volume, volumeKeyframes: [], type, loop: props.loop ?? false }; audioRecords.set(id, record); }
29
+ const time = (state.frame + state.offset) / fps;
30
+ if (time >= start && time < end && typeof props.volume === "function") record.volumeKeyframes.push({ time, volume });
31
+ }
32
+ export const Audio: React.FC<MediaProps & { loop?: boolean }> = (props) => {
33
+ const s = useContext(FrameContext), id = useId(); recordAudio(id, props, "audio", s);
34
+ return <audio id={`hf-audio-${id.replace(/[^a-z0-9]/gi, "")}`} src={props.src} preload="none" data-start={s.offset / s.config.fps} data-duration={s.duration / s.config.fps} />;
35
+ };
36
+ export const OffthreadVideo: React.FC<Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "volume"> & MediaProps & { transparent?: boolean; pauseWhenBuffering?: boolean }> = ({ volume, startFrom, endAt, trimBefore, trimAfter, transparent, pauseWhenBuffering, playbackRate = 1, ...props }) => {
37
+ const s = useContext(FrameContext), id = useId();
38
+ if (!props.muted) recordAudio(id, { ...props, src: props.src!, volume, startFrom, trimBefore }, "video", s);
39
+ return <video {...props} muted playsInline preload="auto" data-hf-frame-video="true" data-hf-time={Math.max(0, s.frame / s.config.fps * playbackRate + (trimBefore ?? startFrom ?? 0) / s.config.fps)} data-hf-loop={props.loop || s.loop ? "true" : undefined} />;
40
+ };
41
+ export const Video = OffthreadVideo;
42
+ let nextHandle = 0;
43
+ const pending = new Map<number, { promise: Promise<void>; resolve: () => void }>();
44
+ export function delayRender(_label?: string): number { const id = ++nextHandle; let resolve!: () => void; const promise = new Promise<void>((r) => { resolve = r; }); pending.set(id, { promise, resolve }); return id; }
45
+ export function continueRender(handle: number): void { pending.get(handle)?.resolve(); pending.delete(handle); }
46
+ export async function waitForRender(): Promise<void> { while (pending.size) await Promise.all([...pending.values()].map((p) => p.promise)); }
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import type { HfProtocol } from "@hyperframes/engine";
3
+ import { createRoot } from "react-dom/client";
4
+ import { flushSync } from "react-dom";
5
+ import { audioRecords, FrameContext, waitForRender } from "./frame";
6
+ import { ManifestContext, UrlsContext } from "./kit/media";
7
+ import type { VideoProps } from "./types";
8
+
9
+ declare global { interface Window { __reelkitProps: VideoProps; __reelkitReady: Promise<void>; __reelkitAudio: typeof audioRecords; __hf?: HfProtocol; } }
10
+ async function settleMedia() {
11
+ await waitForRender();
12
+ await document.fonts.ready;
13
+ await Promise.all([...document.images].map(async (img) => { if (!img.complete) await new Promise<void>((resolve, reject) => { img.addEventListener("load", () => resolve(), { once: true }); img.addEventListener("error", () => reject(new Error(`Image failed: ${img.src}`)), { once: true }); }); if (img.naturalWidth === 0) throw new Error(`Image failed: ${img.src}`); await img.decode(); }));
14
+ await Promise.all([...document.querySelectorAll<HTMLVideoElement>("video[data-hf-frame-video]")].map(async (video) => {
15
+ if (video.readyState < 1) await new Promise<void>((resolve, reject) => { video.addEventListener("loadedmetadata", () => resolve(), { once: true }); video.addEventListener("error", () => reject(new Error(`Video failed: ${video.src}`)), { once: true }); });
16
+ const time = Number(video.dataset.hfTime), duration = video.duration;
17
+ const target = video.dataset.hfLoop ? time % duration : Math.min(time, Math.max(0, duration - 0.00001));
18
+ if (video.readyState < 2 || Math.abs(video.currentTime - target) > 0.00001) await new Promise<void>((resolve, reject) => { video.addEventListener("seeked", () => resolve(), { once: true }); video.addEventListener("error", () => reject(new Error(`Video seek failed: ${video.src}`)), { once: true }); video.currentTime = target; });
19
+ video.pause();
20
+ }));
21
+ await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
22
+ }
23
+ export function mountVideo(Video: React.ComponentType<VideoProps>, previewProps?: VideoProps) {
24
+ const props = previewProps ?? window.__reelkitProps, m = props.manifest;
25
+ const stage = document.getElementById("stage")!;
26
+ stage.style.width = `${m.width}px`; stage.style.height = `${m.height}px`;
27
+ stage.dataset.compositionId = "Video"; stage.dataset.noTimeline = "true"; stage.dataset.duration = String(m.totalFrames / m.fps);
28
+ stage.dataset.width = String(m.width); stage.dataset.height = String(m.height);
29
+ const root = createRoot(stage);
30
+ window.__reelkitAudio = audioRecords;
31
+ const seek = (time: number) => {
32
+ const frame = Math.min(m.totalFrames - 1, Math.max(0, Math.round(time * m.fps)));
33
+ flushSync(() => root.render(<FrameContext.Provider value={{ frame, offset: 0, duration: m.totalFrames, config: { width: m.width, height: m.height, fps: m.fps, durationInFrames: m.totalFrames } }}><UrlsContext.Provider value={props.urls}><ManifestContext.Provider value={m}><Video {...props} /></ManifestContext.Provider></UrlsContext.Provider></FrameContext.Provider>));
34
+ window.__reelkitReady = settleMedia();
35
+ };
36
+ window.__hf = { duration: m.totalFrames / m.fps, seek };
37
+ seek(0);
38
+ }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Color, Object3D, type InstancedMesh } from "three";
4
4
  import { CameraKeysContext } from "./scene3d-context";
5
5
  import { hash01 } from "./seeded";
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
+ import { brandTransformAt, type BrandTransform, type BrandTransformTiming } from "./brand-transform";
4
+
5
+ // A branded object turning into view, arriving on an arc, or launching from depth. Use inside Scene3D around a Card3D, Hero3D or Text3D.
6
+ // The final pose holds exactly: the real logo or product can be read without a continuous spin. Sound stays outside Scene3D; brandTransformFrames gives its landing frame.
7
+ export const BrandTransform3D: React.FC<BrandTransformTiming & { mode?: BrandTransform; strength?: number; children?: React.ReactNode }> = ({ children, ...options }) => {
8
+ const frame = useCurrentFrame(), { fps } = useVideoConfig();
9
+ const pose = brandTransformAt(frame, fps, options);
10
+ if (!pose.visible) return null;
11
+ return <group position={pose.position} rotation={pose.rotation} scale={pose.scale}>{children}</group>;
12
+ };
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { continueRender, delayRender, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { continueRender, delayRender, interpolate, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Place } from "./Place";
4
4
  import { useMedia } from "./media";
5
5
  import { fonts } from "./theme";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { applyPunches, cameraAt, cameraTransform, type CameraKey, type Punch } from "./motion-math";
4
4
 
5
5
  // Moves the whole picture. Wrap all the scenes in it for one camera that never cuts, or wrap one scene's content.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import type { WordTiming } from "../../pipeline/schema";
4
4
  import { captionGroups, DEFAULT_GROUP_WORDS, groupAt, type CaptionGroup } from "./caption-groups";
5
5
  import { inkOnAccent, legibleAccent, PLATE_COLOR } from "./caption-style";
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { useLoader } from "@react-three/fiber";
4
4
  import { CanvasTexture, FrontSide, Shape, ShapeGeometry, SRGBColorSpace, TextureLoader, type Texture } from "three";
5
- import { delayRender, continueRender } from "remotion";
5
+ import { delayRender, continueRender } from "../frame";
6
6
  import { useMedia } from "./media";
7
7
  import { fonts } from "./theme";
8
8
  import { degToRad, floatAt, inkOn, mixHex, type Vec3 } from "./three-math";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { boxAt, type Box, type BoxKey } from "./motion-math";
4
4
 
5
5
  // What a function child is given: the box now, as fractions of the frame, and its size in pixels.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { font as loadFont, fonts, springs } from "./theme";
4
4
  import { inkOn, mixHex } from "./three-math";
5
5
  import { chapterFrames, clamp01 } from "./ui-math";
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { useMedia } from "./media";
3
- import { AbsoluteFill, OffthreadVideo } from "remotion";
3
+ import { AbsoluteFill, OffthreadVideo } from "../frame";
4
4
 
5
5
  // A generated or library video clip as a scene's picture. Place it inside the scene's SceneFrame, as the bottom layer, so it starts with the scene.
6
6
  // When the scene runs longer than the clip, the clip holds its last frame (it does not loop: a looped person or animal jumps back and shows the seam).
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { fonts } from "./theme";
3
- import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
3
+ import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from "../frame";
4
4
 
5
5
  export const Counter: React.FC<{ from?: number; to: number; durationFrames?: number; prefix?: string; suffix?: string; color?: string }> = ({
6
6
  from = 0, to, durationFrames = 30, prefix = "", suffix = "", color = "#ffffff",
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { interpolate, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { interpolate, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Place } from "./Place";
4
4
  import { fonts } from "./theme";
5
5
  import { DEFAULT_HERO } from "./ui-theme";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { interpolate, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { ease, springs } from "./theme";
4
4
 
5
5
  // The workhorse entrance: fade + rise + scale together, with an optional faster exit and a slow idle "breathe".
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { useMedia } from "./media";
3
- import { AbsoluteFill, OffthreadVideo } from "remotion";
3
+ import { AbsoluteFill, OffthreadVideo } from "../frame";
4
4
 
5
5
  export const FootageLayer: React.FC<{ src: string; muted?: boolean; dim?: number }> = ({ src, muted = true, dim = 0 }) => (
6
6
  <AbsoluteFill>
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Place } from "./Place";
4
4
  import { DEFAULT_HERO, rgba, uiColors, type UiTheme } from "./ui-theme";
5
5
  import { PANEL_SPRING } from "./ui-math";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, Img, OffthreadVideo, Sequence, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, Img, OffthreadVideo, Sequence, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { auroraBlobs, beamStates, breath, checkItems, grainSeed, gridOffset, gridRows, groundStrength, hueShift, mixHex, plateView, withAlpha, sequenceLayers, videoLayers, type Drift, type SequenceItem, type SequenceTransition } from "./bg-math";
4
4
  import { useMedia } from "./media";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { continueRender, delayRender, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { continueRender, delayRender, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Place } from "./Place";
4
4
  import { fonts, springs } from "./theme";
5
5
  import { inkOn } from "./three-math";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { continueRender, delayRender, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { continueRender, delayRender, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { useThree } from "@react-three/fiber";
4
4
  import { CanvasTexture, DoubleSide, ExtrudeGeometry, Shape, ShapeGeometry, SRGBColorSpace, TextureLoader, type BufferGeometry, type Texture } from "three";
5
5
  import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { font as loadFont } from "./theme";
4
4
  import { rgba } from "./ui-theme";
5
5
  import { timecode } from "./ui-math";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { useMedia } from "./media";
4
4
  import { layerMoves, type ImageLayersRecord } from "./image-layers-math";
5
5
 
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { useMedia } from "./media";
3
3
  import { ease } from "./theme";
4
- import { AbsoluteFill, Img, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
4
+ import { AbsoluteFill, Img, interpolate, useCurrentFrame, useVideoConfig } from "../frame";
5
5
 
6
6
  // direction "out" starts zoomed and pulls back; alternate between consecutive image scenes.
7
7
  export const KenBurnsImage: React.FC<{ src: string; zoom?: number; direction?: "in" | "out" }> = ({ src, zoom = 1.1, direction = "in" }) => {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { useMedia } from "./media";
3
- import { AbsoluteFill, OffthreadVideo } from "remotion";
3
+ import { AbsoluteFill, OffthreadVideo } from "../frame";
4
4
 
5
5
  // A green-screen clip with its green keyed out (the transparent .webm that `--green` makes), laid over whatever is beneath it.
6
6
  // x and y are where the clip's bottom-centre sits, as fractions of the frame (x 0 is the left edge, y 1 the bottom edge); scale is the clip's
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "../frame";
3
3
 
4
4
  // The bottom layer of every scene: the base colour with two slow-drifting soft colour fields. Never a flat fill.
5
5
  export const BgMesh: React.FC<{ bg: string; hero: string; accent?: string }> = ({ bg, hero, accent = hero }) => {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { fonts } from "./theme";
3
- import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
3
+ import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "../frame";
4
4
 
5
5
  export const LowerThird: React.FC<{ title: string; subtitle?: string; accent?: string }> = ({ title, subtitle, accent = "#5b8cff" }) => {
6
6
  const frame = useCurrentFrame();
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Audio, useVideoConfig } from "remotion";
2
+ import { Audio, useVideoConfig } from "../frame";
3
3
  import { DEFAULT_DUCK_TO, DEFAULT_MUSIC_VOLUME, musicVolume, speechSpans, type MusicDip, type SpeechScene } from "./music-math";
4
4
  import { useGain, useManifest, useMedia } from "./media";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { fonts } from "./theme";
4
4
  import { inkOn } from "./three-math";
5
5
  import { DEFAULT_HERO, rgba } from "./ui-theme";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { CameraKeysContext } from "./scene3d-context";
4
4
  import { naturalRingRadius, ringFitScale, ringPosition, type Vec3 } from "./three-math";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { AdditiveBlending, BufferAttribute, CanvasTexture, Color, SRGBColorSpace } from "three";
4
4
  import { CameraKeysContext } from "./scene3d-context";
5
5
  import { textPoints } from "./sample-text";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, useVideoConfig } from "../frame";
3
3
 
4
4
  // Centres a product-surface component in the frame, at fraction `y` of the height (0.5 is the middle). Used inside the components themselves.
5
5
  export const Place: React.FC<{ y?: number; children: React.ReactNode }> = ({ y = 0.5, children }) => {
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { interpolate, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Place } from "./Place";
4
4
  import { fonts, springs } from "./theme";
5
5
  import { DEFAULT_HERO, rgba, uiColors, type UiTheme } from "./ui-theme";
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
3
- import { ThreeCanvas } from "@remotion/three";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
+ import { ThreeCanvas } from "../three";
4
4
  import { useThree } from "@react-three/fiber";
5
5
  import { ManifestContext, UrlsContext } from "./media";
6
6
  import { camera3DAt, type Camera3DKey } from "./three-math";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { transitionAt, transitionCss, type TransitionKind } from "./transition-math";
4
4
 
5
5
  export type SceneTransition = {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { useMedia } from "./media";
3
- import { AbsoluteFill, Loop, OffthreadVideo, useVideoConfig } from "remotion";
3
+ import { AbsoluteFill, Loop, OffthreadVideo, useVideoConfig } from "../frame";
4
4
 
5
5
  // Lays a white-on-black overlay clip (film marks, HUD frames) over the video. The "screen" blend drops the black.
6
6
  // Pass durationSec from the overlay's record to loop it for the whole video; without it the clip plays once.
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { useGain, useMedia } from "./media";
3
- import { Audio, Sequence } from "remotion";
3
+ import { Audio, Sequence } from "../frame";
4
4
 
5
5
  // Plays one sound effect starting at a frame. at is counted from the start of the enclosing scene, or of the video
6
6
  // when placed outside the scenes. Keep volume well under the voiceover.
@@ -1,5 +1,5 @@
1
1
  import React, { useContext } from "react";
2
- import { Audio, Sequence } from "remotion";
2
+ import { Audio, Sequence } from "../frame";
3
3
  import { gainFor, ManifestContext, UrlsContext } from "./media";
4
4
  import { resolveCues, type SoundCue } from "./sound-cues";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { interpolate, spring, useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { Place } from "./Place";
4
4
  import { font as loadFont, fonts, springs } from "./theme";
5
5
  import { DEFAULT_HERO, rgba, uiColors, type UiTheme } from "./ui-theme";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
4
4
  import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
5
5
  import { interBoldTypeface } from "./inter-bold-typeface";
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { createPortal } from "react-dom";
3
- import { AbsoluteFill, useVideoConfig } from "remotion";
3
+ import { AbsoluteFill, useVideoConfig } from "../frame";
4
4
  import { ImageLayersContext, TextInkContext } from "./ImageLayers";
5
5
  import { inkFor, placementOf, REGIONS, zoneRect, type ImageLayersRecord, type Rect, type Region } from "./image-layers-math";
6
6
 
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { fonts } from "./theme";
3
- import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
3
+ import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "../frame";
4
4
 
5
5
  export const TitleCard: React.FC<{ text: string; subtitle?: string; color?: string; background?: string }> = ({
6
6
  text, subtitle, color = "#ffffff", background = "transparent",
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
2
  import { useMedia } from "./media";
3
- import { Audio } from "remotion";
3
+ import { Audio } from "../frame";
4
4
 
5
5
  export const Voiceover: React.FC<{ src: string; volume?: number }> = ({ src, volume = 1 }) => <Audio src={useMedia(src)} volume={volume} />;
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { useCurrentFrame, useVideoConfig } from "remotion";
2
+ import { useCurrentFrame, useVideoConfig } from "../frame";
3
3
  import { AdditiveBlending, BufferAttribute, Color, DoubleSide, type BufferGeometry } from "three";
4
4
  import { warpSpeed, warpStreaks, warpTravel, type SpeedKey } from "./three-fx-math";
5
5
  import type { Vec3 } from "./three-math";
@@ -0,0 +1,25 @@
1
+ import { spring } from "../math";
2
+ import type { Vec3 } from "./three-math";
3
+
4
+ export const BRAND_TRANSFORMS = ["turn", "arc", "launch"] as const;
5
+ export type BrandTransform = (typeof BRAND_TRANSFORMS)[number];
6
+ export type BrandTransformTiming = { from?: number; frames?: number };
7
+ export function brandTransformFrames({ from = 0, frames = 32 }: BrandTransformTiming = {}) {
8
+ const start = Math.max(0, Math.round(from));
9
+ return { start, settle: start + Math.max(2, Math.round(frames)) };
10
+ }
11
+
12
+ // A pose for any frame, including a seek straight to the hold. The final pose is exactly neutral.
13
+ export function brandTransformAt(frame: number, fps: number, options: BrandTransformTiming & { mode?: BrandTransform; strength?: number } = {}): { visible: boolean; position: Vec3; rotation: Vec3; scale: number } {
14
+ const { start, settle } = brandTransformFrames(options);
15
+ const strength = Math.max(0, Math.min(1, options.strength ?? 1));
16
+ const p = spring({ frame: frame - start, fps, durationInFrames: settle - start, config: { stiffness: 120, damping: 24, overshootClamping: true } });
17
+ if (p >= 1) return { visible: true, position: [0, 0, 0], rotation: [0, 0, 0], scale: 1 };
18
+ const rest = (1 - p) * strength;
19
+ if (options.mode === "arc") {
20
+ const angle = (1 - p) * Math.PI * 0.75;
21
+ return { visible: frame >= start, position: [Math.sin(angle) * 0.65 * strength, Math.sin(angle / 2) * 0.22 * strength, -rest * 1.2], rotation: [rest * 0.06, -rest * 0.95, rest * 0.04], scale: 1 - rest * 0.28 };
22
+ }
23
+ if (options.mode === "launch") return { visible: frame >= start, position: [0, -rest * 0.28, -rest * 3], rotation: [0, rest * 0.2, rest * 0.08], scale: 1 - rest * 0.55 };
24
+ return { visible: frame >= start, position: [-rest * 0.35, -rest * 0.12, -rest * 0.5], rotation: [rest * 0.1, -rest * 1.2, rest * 0.03], scale: 1 - rest * 0.2 };
25
+ }
@@ -123,6 +123,10 @@ Scene3D { camera?: { keys: { frame: number; x?: number; y?: number; z?: number;
123
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
124
  <Scene3D camera={{ keys: [{ frame: 0, z: 7.5 }, { frame: 40, z: 6 }] }}><Text3D text="Reelkit" color={palette.hero} enter="rise" /></Scene3D>
125
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
+
126
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 }
127
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.
128
132
  <Text3D text="Meet Reelkit" color="#ffffff" position={[0, 1, 0]} enter="turn" delay={6} />
@@ -223,8 +227,8 @@ cuesOnChanges(scenes, { impact?: { sound, volume? }, whoosh?: { sound, volume?,
223
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.
224
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 } });
225
229
 
226
- cuesFor(events, kind, { offset?, fps?, voice?, volume?, seed?, riserFrames? }): { at, sound, volume }[] clearBefore(cues, frame, { seconds?, fps?, riserFrames? }): cues
227
- 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) 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, 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"; 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).
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).
228
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 })];
229
233
 
230
234
  Music { src: string; volume?: number; duckTo?: number; dips?: { from: number; to: number; volume: number }[] }
@@ -8,6 +8,9 @@ export type { CarryBox } from "./Carry";
8
8
  export { ClipLayer } from "./ClipLayer";
9
9
  export { Card3D } from "./Card3D";
10
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";
11
14
  export { Hero3D, Screen3D } from "./Hero3D";
12
15
  export { Particles3D } from "./Particles3D";
13
16
  export { Warp3D } from "./Warp3D";
@@ -1,4 +1,4 @@
1
- // The pure parts of SoundCues: nothing here needs React or Remotion, so it can be tested and used in a composition's own timing code.
1
+ // The pure parts of SoundCues: nothing here needs React or HyperFrames, so it can be tested and used in a composition's own timing code.
2
2
  import { travel } from "./motion-math";
3
3
  import { PEAKS_ON_BOUNDARY, transitionSpan, type TransitionKind } from "./transition-math";
4
4
  import { punchEnvelope, type Punch } from "./motion-math";
@@ -4,7 +4,7 @@
4
4
  import { hash01 } from "./seeded";
5
5
  import type { SoundCue } from "./sound-cues";
6
6
 
7
- export const CUE_KINDS = ["type", "click", "snap", "pop", "count", "stream", "assemble", "resolve"] as const;
7
+ export const CUE_KINDS = ["type", "click", "snap", "pop", "count", "stream", "assemble", "transform", "resolve"] as const;
8
8
  export type CueKind = (typeof CUE_KINDS)[number];
9
9
 
10
10
  // `volume` is [without a narrator, under a narrator]: films with no voice use 0.35 to 0.6 for the main hits and less for small ticks; under a voice nothing is over 0.3.
@@ -32,6 +32,11 @@ export const CUE_TABLE: Record<CueKind, { about: string; per10s: string; roles:
32
32
  per10s: "0 to 1",
33
33
  roles: { "assemble-run": { search: "counter ticks", seconds: 0.1, volume: [0.25, 0.12] }, "assemble-lock": { search: "soft impact", seconds: 0.7, volume: [0.55, 0.3] } },
34
34
  },
35
+ transform: {
36
+ about: "a brand or product moving into its readable pose: one sweep into the landing, then one lock hit",
37
+ per10s: "0 to 2, on changes that explain the product or reveal the brand",
38
+ roles: { "transform-sweep": { search: "soft ui whoosh", seconds: 0.5, volume: [0.3, 0.16] }, "transform-lock": { search: "soft impact", seconds: 0.7, volume: [0.45, 0.25] } },
39
+ },
35
40
  resolve: {
36
41
  about: "the logo or the end card: a riser that ends on a chime, with a clear second of nothing before the riser",
37
42
  per10s: "1 per film, at the end",
@@ -54,6 +59,8 @@ export type CueOptions = {
54
59
  seed?: number;
55
60
  // "resolve" only: how long the riser is in frames (default 36: it ends on the chime).
56
61
  riserFrames?: number;
62
+ // "transform" only: frames from the sweep's start to its audible landing (default 10). Measure the chosen sound's attack.
63
+ sweepFrames?: number;
57
64
  };
58
65
 
59
66
  const level = (kind: CueKind, role: string, o: CueOptions) => CUE_TABLE[kind].roles[role]!.volume[o.voice ? 1 : 0] * (o.volume ?? 1);
@@ -74,6 +81,7 @@ const spaced = (frames: number[], gap: number): number[] => { const out: number[
74
81
  // "count": [startFrame, settleFrame] (rollStart and settleFrame of CounterRoll): a speeding tick train, then the chime ON settleFrame
75
82
  // "stream": the frame of every line (terminalFrames(...).lines): one soft tick per line, at most 6 a second
76
83
  // "assemble": [startFrame, lockFrame] (assembleProgress start and the frame the last piece lands): a rising run, then the lock ON lockFrame
84
+ // "transform": [startFrame, settleFrame]: a sweep leading by sweepFrames, then a lock ON settleFrame
77
85
  // "resolve": [frame] the frame the logo lands: the riser ends on it and the chime is ON it (use clearBefore to keep the second before the riser silent)
78
86
  export function cuesFor(events: readonly number[], kind: CueKind, o: CueOptions = {}): SoundCue[] {
79
87
  if (!CUE_KINDS.includes(kind)) throw new Error(`cuesFor: unknown kind "${kind}". The kinds are: ${CUE_KINDS.join(", ")}.`);
@@ -107,6 +115,11 @@ export function cuesFor(events: readonly number[], kind: CueKind, o: CueOptions
107
115
  const run = frames.map((fr, i) => cue(fr, role[0]!, kind === "assemble" ? base * (0.5 + 1.0 * (i / Math.max(1, frames.length - 1))) : base * (0.85 + 0.3 * hash01(i, seed))));
108
116
  return unique([...run, cue(end, role[1]!, level(kind, role[1]!, o))]);
109
117
  }
118
+ case "transform": {
119
+ const start = f[0]!, end = f[f.length - 1]!;
120
+ const sweep = Math.max(0, Math.round(o.sweepFrames ?? 10));
121
+ return unique([cue(Math.max(start, end - sweep), "transform-sweep", level("transform", "transform-sweep", o)), cue(end, "transform-lock", level("transform", "transform-lock", o))]);
122
+ }
110
123
  case "resolve": {
111
124
  const end = f[0]!, rise = o.riserFrames ?? 36;
112
125
  return unique([cue(Math.max(0, end - rise), "resolve-riser", level("resolve", "resolve-riser", o)), cue(end, "resolve-chime", level("resolve", "resolve-chime", o))]);