reelkit-cli 0.8.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 (110) hide show
  1. package/README.md +3 -1
  2. package/package.json +49 -10
  3. package/skill/SKILL.md +29 -19
  4. package/skill/THIRD_PARTY.md +3 -3
  5. package/skill/commands/launch-film.md +1 -1
  6. package/skill/reference/art-styles.md +70 -0
  7. package/skill/reference/backgrounds.md +1 -1
  8. package/skill/reference/brand-motion.md +62 -0
  9. package/skill/reference/clips.md +3 -3
  10. package/skill/reference/component-authoring.md +1 -1
  11. package/skill/reference/continuity.md +5 -5
  12. package/skill/reference/delivery-review.md +40 -0
  13. package/skill/reference/hebrew-rtl.md +3 -4
  14. package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +14 -8
  15. package/skill/reference/kit.md +7 -3
  16. package/skill/reference/launch-film.md +13 -9
  17. package/skill/reference/motion-design.md +18 -16
  18. package/skill/reference/sound-design.md +34 -12
  19. package/skill/reference/studio-editing.md +55 -0
  20. package/skill/reference/styles.md +9 -6
  21. package/skill/reference/three-d.md +8 -7
  22. package/skill/reference/voice-sync.md +1 -1
  23. package/src/commands/assets.ts +5 -1
  24. package/src/contract/index.ts +1 -1
  25. package/src/hyperframes/Root.tsx +1 -0
  26. package/src/hyperframes/fonts.ts +54 -0
  27. package/src/hyperframes/frame.tsx +46 -0
  28. package/src/hyperframes/host.tsx +38 -0
  29. package/src/{remotion → hyperframes}/kit/Assemble3D.tsx +1 -1
  30. package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
  31. package/src/{remotion → hyperframes}/kit/BrowserFrame.tsx +1 -1
  32. package/src/{remotion → hyperframes}/kit/Camera.tsx +1 -1
  33. package/src/{remotion → hyperframes}/kit/Captions.tsx +1 -1
  34. package/src/{remotion → hyperframes}/kit/Card3D.tsx +2 -2
  35. package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
  36. package/src/{remotion → hyperframes}/kit/ChapterFrame.tsx +1 -1
  37. package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
  38. package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
  39. package/src/{remotion → hyperframes}/kit/CounterRoll.tsx +1 -1
  40. package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
  41. package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
  42. package/src/{remotion → hyperframes}/kit/GlassPanel.tsx +1 -1
  43. package/src/{remotion → hyperframes}/kit/Grounds.tsx +1 -1
  44. package/src/{remotion → hyperframes}/kit/Headline.tsx +1 -1
  45. package/src/{remotion → hyperframes}/kit/Hero3D.tsx +1 -1
  46. package/src/{remotion → hyperframes}/kit/HudOverlay.tsx +1 -1
  47. package/src/{remotion → hyperframes}/kit/ImageLayers.tsx +1 -1
  48. package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
  49. package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
  50. package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
  51. package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
  52. package/src/{remotion → hyperframes}/kit/Music.tsx +1 -1
  53. package/src/{remotion → hyperframes}/kit/NamedCursor.tsx +1 -1
  54. package/src/{remotion → hyperframes}/kit/Orbit3D.tsx +1 -1
  55. package/src/{remotion → hyperframes}/kit/Particles3D.tsx +1 -1
  56. package/src/{remotion → hyperframes}/kit/Place.tsx +1 -1
  57. package/src/{remotion → hyperframes}/kit/PromptBox.tsx +1 -1
  58. package/src/{remotion → hyperframes}/kit/Scene3D.tsx +2 -2
  59. package/src/{remotion → hyperframes}/kit/SceneFrame.tsx +1 -1
  60. package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
  61. package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
  62. package/src/{remotion → hyperframes}/kit/SoundCues.tsx +1 -1
  63. package/src/{remotion → hyperframes}/kit/TerminalLog.tsx +1 -1
  64. package/src/{remotion → hyperframes}/kit/Text3D.tsx +1 -1
  65. package/src/{remotion → hyperframes}/kit/TextOnImage.tsx +1 -1
  66. package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
  67. package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
  68. package/src/{remotion → hyperframes}/kit/Warp3D.tsx +1 -1
  69. package/src/hyperframes/kit/brand-transform.ts +25 -0
  70. package/src/{remotion → hyperframes}/kit/docs.ts +6 -2
  71. package/src/{remotion → hyperframes}/kit/index.ts +3 -0
  72. package/src/{remotion → hyperframes}/kit/sound-cues.ts +1 -1
  73. package/src/{remotion → hyperframes}/kit/sound-kinds.ts +14 -1
  74. package/src/{remotion → hyperframes}/kit/theme.ts +42 -40
  75. package/src/{remotion → hyperframes}/kit/ui-math.ts +1 -1
  76. package/src/hyperframes/math.ts +62 -0
  77. package/src/hyperframes/three.tsx +10 -0
  78. package/src/project/chromakey.ts +1 -1
  79. package/src/project/layers.ts +1 -1
  80. package/src/project/serve.ts +2 -2
  81. package/src/render/component-preview.ts +11 -55
  82. package/src/render/render.ts +62 -64
  83. package/src/render/serve.ts +31 -0
  84. package/src/render/static-check.ts +15 -4
  85. package/src/render/validate.ts +4 -4
  86. package/src/render/word-check.ts +1 -1
  87. package/src/render/worker.ts +71 -0
  88. package/src/testing/fixtures.ts +1 -1
  89. package/src/remotion/Root.tsx +0 -31
  90. /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
  91. /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
  92. /package/src/{remotion → hyperframes}/kit/bg-math.ts +0 -0
  93. /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
  94. /package/src/{remotion → hyperframes}/kit/caption-groups.ts +0 -0
  95. /package/src/{remotion → hyperframes}/kit/caption-style.ts +0 -0
  96. /package/src/{remotion → hyperframes}/kit/image-layers-math.ts +0 -0
  97. /package/src/{remotion → hyperframes}/kit/inter-bold-typeface.ts +0 -0
  98. /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
  99. /package/src/{remotion → hyperframes}/kit/motion-math.ts +0 -0
  100. /package/src/{remotion → hyperframes}/kit/music-math.ts +0 -0
  101. /package/src/{remotion → hyperframes}/kit/quiet-three.ts +0 -0
  102. /package/src/{remotion → hyperframes}/kit/sample-text.ts +0 -0
  103. /package/src/{remotion → hyperframes}/kit/scene3d-context.ts +0 -0
  104. /package/src/{remotion → hyperframes}/kit/seeded.ts +0 -0
  105. /package/src/{remotion → hyperframes}/kit/three-fx-math.ts +0 -0
  106. /package/src/{remotion → hyperframes}/kit/three-math.ts +0 -0
  107. /package/src/{remotion → hyperframes}/kit/transition-math.ts +0 -0
  108. /package/src/{remotion → hyperframes}/kit/ui-theme.ts +0 -0
  109. /package/src/{remotion → hyperframes}/kit/word-anchor.ts +0 -0
  110. /package/src/{remotion → hyperframes}/types.ts +0 -0
@@ -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))]);
@@ -1,43 +1,44 @@
1
- import { loadFont as loadBody } from "@remotion/google-fonts/Inter";
2
- import { loadFont as loadDisplay } from "@remotion/google-fonts/Sora";
3
- import { loadFont as loadArchivoBlack } from "@remotion/google-fonts/ArchivoBlack";
4
- import { loadFont as loadBebasNeue } from "@remotion/google-fonts/BebasNeue";
5
- import { loadFont as loadGreatVibes } from "@remotion/google-fonts/GreatVibes";
6
- import { loadFont as loadJetBrainsMono } from "@remotion/google-fonts/JetBrainsMono";
7
- import { loadFont as loadKanit } from "@remotion/google-fonts/Kanit";
8
- import { loadFont as loadModak } from "@remotion/google-fonts/Modak";
9
- import { loadFont as loadMontserrat } from "@remotion/google-fonts/Montserrat";
10
- import { loadFont as loadRoboto } from "@remotion/google-fonts/Roboto";
11
- import { loadFont as loadRobotoSlab } from "@remotion/google-fonts/RobotoSlab";
12
- import { loadFont as loadSacramento } from "@remotion/google-fonts/Sacramento";
13
- import { loadFont as loadUnbounded } from "@remotion/google-fonts/Unbounded";
14
- import { loadFont as loadBungeeShade } from "@remotion/google-fonts/BungeeShade";
15
- import { loadFont as loadCinzel } from "@remotion/google-fonts/Cinzel";
16
- import { loadFont as loadCreepster } from "@remotion/google-fonts/Creepster";
17
- import { loadFont as loadHeebo } from "@remotion/google-fonts/Heebo";
18
- import { loadFont as loadKaushanScript } from "@remotion/google-fonts/KaushanScript";
19
- import { loadFont as loadLilitaOne } from "@remotion/google-fonts/LilitaOne";
20
- import { loadFont as loadMrsSaintDelafield } from "@remotion/google-fonts/MrsSaintDelafield";
21
- import { loadFont as loadOrbitron } from "@remotion/google-fonts/Orbitron";
22
- import { loadFont as loadOutfit } from "@remotion/google-fonts/Outfit";
23
- import { loadFont as loadPermanentMarker } from "@remotion/google-fonts/PermanentMarker";
24
- import { loadFont as loadPlayfairDisplay } from "@remotion/google-fonts/PlayfairDisplay";
25
- import { loadFont as loadRubik } from "@remotion/google-fonts/Rubik";
26
- import { loadFont as loadRye } from "@remotion/google-fonts/Rye";
27
- import { loadFont as loadSyne } from "@remotion/google-fonts/Syne";
28
- import { loadFont as loadYellowtail } from "@remotion/google-fonts/Yellowtail";
29
- import { loadFont as loadAlef } from "@remotion/google-fonts/Alef";
30
- import { loadFont as loadAmaticSC } from "@remotion/google-fonts/AmaticSC";
31
- import { loadFont as loadAssistant } from "@remotion/google-fonts/Assistant";
32
- import { loadFont as loadDavidLibre } from "@remotion/google-fonts/DavidLibre";
33
- import { loadFont as loadFrankRuhlLibre } from "@remotion/google-fonts/FrankRuhlLibre";
34
- import { loadFont as loadFredoka } from "@remotion/google-fonts/Fredoka";
35
- import { loadFont as loadKarantina } from "@remotion/google-fonts/Karantina";
36
- import { loadFont as loadNotoSansHebrew } from "@remotion/google-fonts/NotoSansHebrew";
37
- import { loadFont as loadSecularOne } from "@remotion/google-fonts/SecularOne";
38
- import { loadFont as loadSuezOne } from "@remotion/google-fonts/SuezOne";
39
- import { loadFont as loadVarelaRound } from "@remotion/google-fonts/VarelaRound";
40
- import { Easing } from "remotion";
1
+ import { loadFont } from "../fonts";
2
+ const loadBody = (_style: string, _options: unknown) => loadFont("Inter", "inter");
3
+ const loadDisplay = (_style: string, _options: unknown) => loadFont("Sora", "sora");
4
+ const loadArchivoBlack = (_style: string, _options: unknown) => loadFont("Archivo Black", "archivo-black");
5
+ const loadBebasNeue = (_style: string, _options: unknown) => loadFont("Bebas Neue", "bebas-neue");
6
+ const loadGreatVibes = (_style: string, _options: unknown) => loadFont("Great Vibes", "great-vibes");
7
+ const loadJetBrainsMono = (_style: string, _options: unknown) => loadFont("JetBrains Mono", "jetbrains-mono");
8
+ const loadKanit = (_style: string, _options: unknown) => loadFont("Kanit", "kanit");
9
+ const loadModak = (_style: string, _options: unknown) => loadFont("Modak", "modak");
10
+ const loadMontserrat = (_style: string, _options: unknown) => loadFont("Montserrat", "montserrat");
11
+ const loadRoboto = (_style: string, _options: unknown) => loadFont("Roboto", "roboto");
12
+ const loadRobotoSlab = (_style: string, _options: unknown) => loadFont("Roboto Slab", "roboto-slab");
13
+ const loadSacramento = (_style: string, _options: unknown) => loadFont("Sacramento", "sacramento");
14
+ const loadUnbounded = (_style: string, _options: unknown) => loadFont("Unbounded", "unbounded");
15
+ const loadBungeeShade = (_style: string, _options: unknown) => loadFont("Bungee Shade", "bungee-shade");
16
+ const loadCinzel = (_style: string, _options: unknown) => loadFont("Cinzel", "cinzel");
17
+ const loadCreepster = (_style: string, _options: unknown) => loadFont("Creepster", "creepster");
18
+ const loadHeebo = (_style: string, _options: unknown) => loadFont("Heebo", "heebo");
19
+ const loadKaushanScript = (_style: string, _options: unknown) => loadFont("Kaushan Script", "kaushan-script");
20
+ const loadLilitaOne = (_style: string, _options: unknown) => loadFont("Lilita One", "lilita-one");
21
+ const loadMrsSaintDelafield = (_style: string, _options: unknown) => loadFont("Mrs Saint Delafield", "mrs-saint-delafield");
22
+ const loadOrbitron = (_style: string, _options: unknown) => loadFont("Orbitron", "orbitron");
23
+ const loadOutfit = (_style: string, _options: unknown) => loadFont("Outfit", "outfit");
24
+ const loadPermanentMarker = (_style: string, _options: unknown) => loadFont("Permanent Marker", "permanent-marker");
25
+ const loadPlayfairDisplay = (_style: string, _options: unknown) => loadFont("Playfair Display", "playfair-display");
26
+ const loadRubik = (_style: string, _options: unknown) => loadFont("Rubik", "rubik");
27
+ const loadRye = (_style: string, _options: unknown) => loadFont("Rye", "rye");
28
+ const loadSyne = (_style: string, _options: unknown) => loadFont("Syne", "syne");
29
+ const loadYellowtail = (_style: string, _options: unknown) => loadFont("Yellowtail", "yellowtail");
30
+ const loadAlef = (_style: string, _options: unknown) => loadFont("Alef", "alef");
31
+ const loadAmaticSC = (_style: string, _options: unknown) => loadFont("Amatic SC", "amatic-sc");
32
+ const loadAssistant = (_style: string, _options: unknown) => loadFont("Assistant", "assistant");
33
+ const loadDavidLibre = (_style: string, _options: unknown) => loadFont("David Libre", "david-libre");
34
+ const loadFrankRuhlLibre = (_style: string, _options: unknown) => loadFont("Frank Ruhl Libre", "frank-ruhl-libre");
35
+ const loadFredoka = (_style: string, _options: unknown) => loadFont("Fredoka", "fredoka");
36
+ const loadKarantina = (_style: string, _options: unknown) => loadFont("Karantina", "karantina");
37
+ const loadNotoSansHebrew = (_style: string, _options: unknown) => loadFont("Noto Sans Hebrew", "noto-sans-hebrew");
38
+ const loadSecularOne = (_style: string, _options: unknown) => loadFont("Secular One", "secular-one");
39
+ const loadSuezOne = (_style: string, _options: unknown) => loadFont("Suez One", "suez-one");
40
+ const loadVarelaRound = (_style: string, _options: unknown) => loadFont("Varela Round", "varela-round");
41
+ import { Easing } from "../frame";
41
42
 
42
43
  // Loaded once for every video. Hero text must never fall back to a system default font.
43
44
  const display = loadDisplay("normal", { weights: ["600", "700", "800"], subsets: ["latin"] });
@@ -98,6 +99,7 @@ const loaders = {
98
99
  amaticSC: () => loadAmaticSC("normal", { weights: ["400", "700"], ...hebrew }), // hand-lettered capitals
99
100
  };
100
101
  export type KitFont = keyof typeof loaders;
102
+ export const KIT_FONTS = Object.keys(loaders) as KitFont[];
101
103
  const loaded = new Map<KitFont, string>();
102
104
  export function font(name: KitFont): string {
103
105
  let family = loaded.get(name);
@@ -2,7 +2,7 @@
2
2
  // mount: the frame useCurrentFrame() gives where the component stands (inside a SceneFrame, the scene's own frame; inside a Sequence, the sequence's).
3
3
  // Each component has a companion function here that takes the SAME props and returns the frames of its events, in that same clock, so a sound can be
4
4
  // placed on them: add the scene's startFrame (or pass it as `offset` to cuesFor) to make them absolute.
5
- import { measureSpring } from "remotion";
5
+ import { measureSpring } from "../frame";
6
6
 
7
7
  export const clamp01 = (n: number) => (n > 0 ? (n < 1 ? n : 1) : 0);
8
8
  export const outCubic = (t: number) => 1 - (1 - clamp01(t)) ** 3;
@@ -0,0 +1,62 @@
1
+ // Seekable animation math. These functions have no browser or React dependency.
2
+ export type SpringConfig = { damping?: number; stiffness?: number; mass?: number; overshootClamping?: boolean };
3
+ export function spring({ frame, fps, config = {}, from = 0, to = 1, delay = 0, durationInFrames, durationRestThreshold = 0.005, reverse = false }: { frame: number; fps: number; config?: SpringConfig; from?: number; to?: number; delay?: number; durationInFrames?: number; durationRestThreshold?: number; reverse?: boolean }): number {
4
+ const length = durationInFrames ?? measureSpring({ fps, config, threshold: durationRestThreshold });
5
+ let f = frame - delay;
6
+ if (reverse) f = length - f;
7
+ if (f <= 0) return from;
8
+ if (durationInFrames && f >= durationInFrames) return to;
9
+ const t = f / fps * (durationInFrames ? measureSpring({ fps, config, threshold: durationRestThreshold }) / durationInFrames : 1);
10
+ const m = config.mass ?? 1, k = config.stiffness ?? 100, c = config.damping ?? 10;
11
+ const w = Math.sqrt(k / m), z = c / (2 * Math.sqrt(k * m));
12
+ let p: number;
13
+ if (z < 1) { const wd = w * Math.sqrt(1 - z * z); p = 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + z * w / wd * Math.sin(wd * t)); }
14
+ else if (z === 1) p = 1 - (1 + w * t) * Math.exp(-w * t);
15
+ else { const r = Math.sqrt(z * z - 1), a = -w * (z - r), b = -w * (z + r); p = 1 - (b * Math.exp(a * t) - a * Math.exp(b * t)) / (b - a); }
16
+ return from + (to - from) * (config.overshootClamping ? Math.min(1, p) : p);
17
+ }
18
+ export function measureSpring({ fps, config = {}, threshold = 0.005 }: { fps: number; config?: SpringConfig; threshold?: number }): number {
19
+ const m = config.mass ?? 1, k = config.stiffness ?? 100, c = config.damping ?? 10;
20
+ const w = Math.sqrt(k / m), z = c / (2 * Math.sqrt(k * m));
21
+ // Require a full second inside the rest band so an oscillation's zero crossing cannot end the spring.
22
+ let last = 0;
23
+ for (let f = 0; f < fps * 60; f++) {
24
+ const t = f / fps;
25
+ let error: number;
26
+ if (z < 1) { const wd = w * Math.sqrt(1 - z * z); error = Math.exp(-z * w * t) * (Math.cos(wd * t) + z * w / wd * Math.sin(wd * t)); }
27
+ else if (z === 1) error = (1 + w * t) * Math.exp(-w * t);
28
+ else { const r = Math.sqrt(z * z - 1), a = -w * (z - r), b = -w * (z + r); error = (b * Math.exp(a * t) - a * Math.exp(b * t)) / (b - a); }
29
+ if (Math.abs(error) > threshold) last = f;
30
+ if (f - last >= fps) return last + 1;
31
+ }
32
+ throw new Error("Spring did not settle within 60 seconds.");
33
+ }
34
+ type Extrapolate = "extend" | "clamp" | "identity" | "wrap";
35
+ export function interpolate(value: number, input: readonly number[], output: readonly number[], options: { easing?: (t: number) => number; extrapolateLeft?: Extrapolate; extrapolateRight?: Extrapolate } = {}): number {
36
+ if (input.length !== output.length || input.length < 2 || input.some((n, i) => i > 0 && n <= input[i - 1]!)) throw new Error("Interpolation ranges must have equal length and increasing input values.");
37
+ const lo = input[0]!, hi = input[input.length - 1]!;
38
+ if (value < lo || value > hi) { const mode = value < lo ? options.extrapolateLeft : options.extrapolateRight; if (mode === "identity") return value; if (mode === "clamp") value = Math.min(hi, Math.max(lo, value)); if (mode === "wrap") value = lo + ((value - lo) % (hi - lo) + hi - lo) % (hi - lo); }
39
+ let i = 0; while (i < input.length - 2 && value > input[i + 1]!) i++;
40
+ const t = (value - input[i]!) / (input[i + 1]! - input[i]!);
41
+ return output[i]! + (output[i + 1]! - output[i]!) * (options.easing ?? Easing.linear)(t);
42
+ }
43
+ export const Easing = {
44
+ linear: (t: number) => t, quad: (t: number) => t * t, cubic: (t: number) => t ** 3,
45
+ sin: (t: number) => 1 - Math.cos(t * Math.PI / 2), exp: (t: number) => t === 0 ? 0 : 2 ** (10 * (t - 1)),
46
+ in: (fn: (t: number) => number) => fn,
47
+ out: (fn: (t: number) => number) => (t: number) => 1 - fn(1 - t),
48
+ inOut: (fn: (t: number) => number) => (t: number) => t < 0.5 ? fn(t * 2) / 2 : 1 - fn((1 - t) * 2) / 2,
49
+ bezier: (x1: number, y1: number, x2: number, y2: number) => (t: number) => {
50
+ if (t <= 0 || t >= 1) return t;
51
+ const curve = (u: number, a: number, b: number) => 3 * (1 - u) ** 2 * u * a + 3 * (1 - u) * u * u * b + u ** 3;
52
+ let lo = 0, hi = 1;
53
+ for (let i = 0; i < 40; i++) { const u = (lo + hi) / 2; if (curve(u, x1, x2) < t) lo = u; else hi = u; }
54
+ return curve((lo + hi) / 2, y1, y2);
55
+ },
56
+ };
57
+ export function random(seed: string | number): number {
58
+ let h = 2166136261;
59
+ for (const c of String(seed)) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
60
+ h ^= h >>> 16; h = Math.imul(h, 0x7feb352d); h ^= h >>> 15; h = Math.imul(h, 0x846ca68b);
61
+ return ((h ^ h >>> 16) >>> 0) / 4294967296;
62
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { Canvas, useThree, type CanvasProps } from "@react-three/fiber";
3
+ import { FrameContext, useCurrentFrame, delayRender, continueRender } from "./frame";
4
+ const Draw: React.FC = () => { const f = useCurrentFrame(), s = useThree(); React.useLayoutEffect(() => { s.invalidate(); }, [f, s]); return null; };
5
+ export const ThreeCanvas: React.FC<CanvasProps & { width: number; height: number }> = ({ width, height, children, style, ...props }) => {
6
+ const state = React.useContext(FrameContext);
7
+ const [handle] = React.useState(() => delayRender("3D canvas"));
8
+ React.useEffect(() => () => continueRender(handle), [handle]);
9
+ return <Canvas {...props} onCreated={() => continueRender(handle)} frameloop="demand" gl={{ preserveDrawingBuffer: true, alpha: true }} style={{ width, height, ...style }}><FrameContext.Provider value={state}>{children}<Draw /></FrameContext.Provider></Canvas>;
10
+ };
@@ -41,7 +41,7 @@ async function cornerGreen(input: string): Promise<{ colour: string; saturated:
41
41
  return { colour: `0x${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`, saturated: g - Math.max(r, b) >= 60 };
42
42
  }
43
43
 
44
- // Turns the green of a green-screen clip into transparency: VP9 with an alpha channel, which Remotion plays with `transparent`.
44
+ // Turns the green of a green-screen clip into transparency: VP9 with an alpha channel, which HyperFrames plays with `transparent`.
45
45
  // The key is on the green found in the frame's corners, with some give for noise and uneven light; the despill pulls the green glow
46
46
  // off the subject's edges. The original is left alone.
47
47
  export async function keyGreen(input: string, output: string, opts: KeyOptions = {}): Promise<void> {
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { coverageOf, subjectBoxOf, textZoneOf, type ImageLayersRecord, type Rect, type TextZone } from "../remotion/kit/image-layers-math";
6
+ import { coverageOf, subjectBoxOf, textZoneOf, type ImageLayersRecord, type Rect, type TextZone } from "../hyperframes/kit/image-layers-math";
7
7
 
8
8
  const run = promisify(execFile);
9
9
  const GRID = 48;
@@ -12,9 +12,9 @@ const TYPES: Record<string, string> = {
12
12
 
13
13
  export type MediaServer = { url(rel: string): string; close(): Promise<void> };
14
14
 
15
- // Remotion loads media over HTTP, so the project's media is served on a local port while a build runs.
15
+ // HyperFrames loads media over HTTP, so the project's media is served on a local port while a build runs.
16
16
  // Only files under <project>/assets/ are served, never .json, and only under a random per-run path prefix: access-control-allow-origin
17
- // has to be * for Remotion's renderer, so the unguessable prefix is what keeps other web pages from reading the files.
17
+ // has to be * for HyperFrames's renderer, so the unguessable prefix is what keeps other web pages from reading the files.
18
18
  export async function serveDir(root: string): Promise<MediaServer> {
19
19
  const assets = join(resolve(root), "assets");
20
20
  const prefix = randomBytes(16).toString("hex");
@@ -1,65 +1,21 @@
1
- import { execFile } from "node:child_process";
2
- import { mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
2
  import { tmpdir } from "node:os";
4
3
  import { dirname, join } from "node:path";
5
- import { promisify } from "node:util";
6
- import { renderMedia, selectComposition } from "@remotion/renderer";
7
- import { bundleProject, disposeBundle } from "./render";
8
-
9
- const run = promisify(execFile);
10
- const NAME = /^[A-Z][A-Za-z0-9]*$/;
11
-
4
+ import { bundleProject, disposeBundle, renderVideo } from "./render";
5
+ import { migrateFrameImports } from "./static-check";
12
6
  export const PREVIEW = { width: 960, height: 600, fps: 30, frames: 105 } as const;
13
-
14
- // The generated entry: the kit's default dark background with the example filling the frame. Importing the kit also loads
15
- // its fonts, which is what makes text render in the kit fonts. `name` is checked against NAME and `example` starts with `<name`,
16
- // because both are interpolated into this code; the example is trusted seed text from the owner's own repository.
17
- const entrySource = (name: string, example: string) => `import React from "react";
18
- import { AbsoluteFill, Composition, registerRoot } from "remotion";
19
- import { palettes } from "reelkit/kit";
20
- import { ${name} } from "./${name}";
21
-
22
- void ${name};
23
- const Preview: React.FC = () => (
24
- <AbsoluteFill style={{ background: palettes.darkTech.bg }}>
25
- ${example}
26
- </AbsoluteFill>
27
- );
28
-
29
- registerRoot(() => (
30
- <Composition id="Preview" component={Preview} durationInFrames={${PREVIEW.frames}} fps={${PREVIEW.fps}} width={${PREVIEW.width}} height={${PREVIEW.height}} />
31
- ));
32
- `;
33
-
34
- // Renders one component with its example to a short silent MP4 (960x600, 30 fps, 3.5 s, H.264, yuv420p, CRF 28).
35
- // Everything it creates besides outPath (the temp folder and the bundle) is removed again, also when it fails.
7
+ const NAME = /^[A-Z][A-Za-z0-9]*$/;
36
8
  export async function renderComponentPreview(input: { name: string; source: string; example: string }, outPath: string): Promise<void> {
37
9
  const { name, source, example } = input;
38
10
  if (!NAME.test(name)) throw new Error(`Component name "${name}" must be PascalCase letters and digits.`);
39
11
  if (!example.trimStart().startsWith(`<${name}`)) throw new Error(`The example for ${name} must start with <${name}.`);
40
-
41
- const work = await mkdtemp(join(tmpdir(), "rk-cmp-preview-"));
42
- let serveUrl: string | undefined;
12
+ const work = await mkdtemp(join(tmpdir(), "rk-cmp-preview-")); let bundle: string | undefined;
13
+ const props = { manifest: { width: PREVIEW.width, height: PREVIEW.height, fps: PREVIEW.fps, totalFrames: PREVIEW.frames, scenes: [] }, urls: {} };
43
14
  try {
44
- await writeFile(join(work, `${name}.tsx`), source);
15
+ await writeFile(join(work, `${name}.tsx`), migrateFrameImports(source));
45
16
  const entry = join(work, "index.tsx");
46
- await writeFile(entry, entrySource(name, example.trim()));
47
- serveUrl = await bundleProject(entry);
48
- const composition = await selectComposition({ serveUrl, id: "Preview" });
49
- const rendered = join(work, "rendered.mp4");
50
- await renderMedia({
51
- composition, serveUrl, codec: "h264", outputLocation: rendered, crf: 28, pixelFormat: "yuv420p", colorSpace: "bt709", muted: true, logLevel: "error",
52
- });
53
- // The last pass guarantees no audio stream and the index at the front, whatever Remotion wrote; the video is copied, not re-encoded.
54
- const finished = join(work, "final.mp4");
55
- await run("ffmpeg", ["-y", "-v", "error", "-i", rendered, "-an", "-movflags", "+faststart", "-c:v", "copy", finished]);
56
- await mkdir(dirname(outPath), { recursive: true });
57
- await rename(finished, outPath).catch(async () => {
58
- // A different file system than the temp folder: copy instead.
59
- await run("ffmpeg", ["-y", "-v", "error", "-i", finished, "-c", "copy", "-movflags", "+faststart", outPath]);
60
- });
61
- } finally {
62
- if (serveUrl) await disposeBundle(serveUrl).catch(() => {});
63
- await rm(work, { recursive: true, force: true });
64
- }
17
+ await writeFile(entry, `import React from "react";\nimport { AbsoluteFill } from "reelkit/frame";\nimport { mountVideo } from "reelkit/host";\nimport { palettes } from "reelkit/kit";\nimport { ${name} } from "./${name}";\nmountVideo(() => <AbsoluteFill style={{background: palettes.darkTech.bg}}>${example.trim()}</AbsoluteFill>);\n`);
18
+ bundle = await bundleProject(entry); await mkdir(dirname(outPath), { recursive: true });
19
+ await renderVideo(bundle, props, outPath, { silent: true, crf: 28 });
20
+ } finally { if (bundle) await disposeBundle(bundle).catch(() => {}); await rm(work, { recursive: true, force: true }); }
65
21
  }