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
@@ -5,9 +5,9 @@ description: Use before planning, to choose the video's look with the user - the
5
5
 
6
6
  # Choosing the look
7
7
 
8
- Pick one look for the whole video before writing the plan, and say it in the first scene's `notes`. A video that mixes looks reads as a template.
8
+ Choose a visual language that fits the material and record it in the first scene's `notes`. Brand films usually benefit from one system; a narration-driven mixed-media film can change worlds by meaning while retaining a thread of type, accent or object. Read `reference/studio-editing.md` when the approach is still open.
9
9
 
10
- If the user already described what they want, match it to a look and confirm in one sentence. Otherwise offer the catalogue below as a short numbered list with one line each, and include "your own: describe it, or send a picture or a link". Ask every open question in one message, each with a default, so the user can answer "defaults".
10
+ Use the user's chosen direction. Otherwise recommend a fitting look with a short reason; use the catalogue as technique references, not a mandatory menu. Offer alternatives when requested. Ask only about consequential missing information.
11
11
 
12
12
  ## Two modes
13
13
  Every look belongs to one mode. The mode decides which rules in `reference/motion-design.md` bend.
@@ -19,8 +19,8 @@ Every look belongs to one mode. The mode decides which rules in `reference/motio
19
19
 
20
20
  **Showreel** (openers, announcements, anything that must stop the scroll)
21
21
  - Hard cuts on the beat between full-colour backgrounds; type that fills the frame. Flat solid colour fields are right here.
22
- - The camera never stops: a slow push, a slight turn, a short shake on each hit.
23
- - No frame sits still longer than a third of a second, except the final hold.
22
+ - Camera movement and brief impact shakes can reinforce selected hits. Keep reading moments stable.
23
+ - Pace changes by meaning and musical phrasing, allowing deliberate holds rather than moving every frame.
24
24
  - At most three full-screen flashes in any second, and never a red strobe. This is a safety rule, not a taste rule.
25
25
 
26
26
  ## The catalogue
@@ -57,7 +57,7 @@ Each look is built from what the kit and the shared library already hold, so mos
57
57
  **When:** an opener or end card, from the user's own logo file. **How:** build toward the logo from its own shapes and colours and land on it exactly as supplied, held at least 1.5 seconds. **Forbidden:** redrawing, recolouring or stretching the logo; anyone else's logo or trademark.
58
58
 
59
59
  ### 11. Launch film (restrained)
60
- **When:** a product, app or feature launch for a tech, software or AI brand: the short film that opens a launch post. **How:** a clean off-white or near-black ground with one soft pool of the hero colour at an edge (search "glow backdrop"); one object in the middle of each shot and a lot of empty space around it. Open on a single line of small, calm type that comes into focus word by word (search "blur reveal"), then one idea per shot, each 1.5 to 2.5 seconds: a field being typed into (search "search bar", "prompt box"), the real screenshot on a card floating at a slight tilt (search "tilt card", "laptop mockup", "phone mockup"), tiles circling a short line (search "orbit"), a feature grid (search "bento"), a pointer that clicks through it (search "cursor click"), the one real number as the payoff (search "statistic counter"), and the logo small and centred to close (search "logo reveal", "brand splash"). Between shots the outgoing object leaves fast with a short directional blur and the next one arrives on a heavy spring; the camera pushes in 2 to 4 percent on every shot. Six to eight shots in 12 to 20 seconds, music without a voice or with very few words. This look opens small on purpose: the first object may fill only a fifth of the frame, as long as the push carries it larger. **Forbidden:** two objects competing in one shot, a busy or textured background, a second accent colour, an invented screen, a shot that holds still, and any brand's mark but the user's own.
60
+ **When:** a product, app or feature launch for a tech, software or AI brand: the short film that opens a launch post. **How:** a clean off-white or near-black ground with one soft pool of the hero colour at an edge (search "glow backdrop"); one object in the middle of each shot and a lot of empty space around it. Open on a single line of small, calm type that comes into focus word by word (search "blur reveal"), then one idea per shot, each 1.5 to 2.5 seconds: a field being typed into (search "search bar", "prompt box"), the real screenshot on a card floating at a slight tilt (search "tilt card", "laptop mockup", "phone mockup"), tiles circling a short line (search "orbit"), a feature grid (search "bento"), a pointer that clicks through it (search "cursor click"), the one real number as the payoff (search "statistic counter"), and the logo small and centred to close (search "logo reveal", "brand splash"). Between shots the outgoing object leaves fast with a short directional blur and the next one arrives on a heavy spring; the camera pushes in 2 to 4 percent on every shot. Six to eight shots in 12 to 20 seconds, music without a voice or with very few words. This look opens small on purpose: the first object may fill only a fifth of the frame, as long as the push carries it larger. **Forbidden:** two objects competing in one shot, a busy or textured background, a second accent colour, an invented screen, an unreadable hold, and a third-party mark unrelated to the material.
61
61
 
62
62
  ### 12. Social native (showreel)
63
63
  **When:** the video should feel like it was made inside the app it is posted to: a reply to a comment, a reaction, proof from the user's own account. **How:** the picture is footage or a generated image and the graphics are the platform's own furniture, used honestly: the comment being answered at the top (search "comment sticker"), a post or a story in its frame (search "post frame", "story frame"), the reel's own buttons over the picture (search "reel overlay"), a profile with a Follow button to close (search "profile card"), reactions floating up on the payoff (search "emoji reactions", "emoji pop"), and words punching in one at a time on the voice (search "punch words"). Emoji are allowed in this look, as reactions and stickers only. **Forbidden:** an invented comment, post, follower count or like count; a platform's logo when the video is not about that platform; emoji standing in for a logo or an icon.
@@ -65,8 +65,11 @@ Each look is built from what the kit and the shared library already hold, so mos
65
65
  ### 13. AI at work (restrained)
66
66
  **When:** an AI product, an assistant or an automation: the viewer should watch it think and deliver. **How:** near-black ground; the request typed into a glowing field and sent (search "prompt box", "assistant chat window"); the work shown as steps that spin and tick (search "agent steps") or as a placeholder that resolves into the result (search "shimmer skeleton"); the answer streaming in (search "streaming answer"); a calm orb when it listens or speaks (search "glow orb"). One glow, on the field or the orb, never both in one frame. Every answer on screen is the product's real output, supplied by the user. **Forbidden:** an answer the product did not give, a second glowing element, sparkles as decoration, text that streams faster than it can be read.
67
67
 
68
+ ## Fifteen art styles (14 to 28)
69
+ `reference/art-styles.md` continues this list with fifteen looks whose picture is drawn in code rather than built from interface parts, adapted from the mg-styles-15 films: 14 flat vector, 15 line art, 16 isometric, 17 soft 3D render, 18 hand-drawn cel, 19 collage cut-out, 20 liquid, 21 shape morph, 22 Bauhaus grid, 23 synthwave, 24 aurora glass, 25 variety captions, 26 sticker explainer, 27 pixel art, 28 HUD. Use them as technique references, and read that file when the brief needs a style in that family (a drawn explainer, a line-art logo, an isometric city, "80s", "pixel", "glass", a kinetic poster). Each card there says which motion rules the look is allowed to bend.
70
+
68
71
  ## Your own
69
- Ask what the message is, what happens on screen, the colours, the length and shape, and for an example if they have one. Choose the nearest look above as the technical base and write a four-line brief in the same shape (when, how, forbidden, structure). Show the brief with the plan at the checkpoint.
72
+ Use the supplied message, actions, colours, duration and format. Ask only for consequential missing details or a reference that the choice depends on. Choose the nearest look above as the technical base and write a four-line brief in the same shape (when, how, forbidden, structure). Show the brief with the plan at the checkpoint.
70
73
 
71
74
  ## What every look shares
72
75
  - The plan checkpoint lists the exact words that will appear on screen and when. Fixing a list takes a minute; fixing a finished animation takes an hour.
@@ -5,21 +5,22 @@ description: Use when a shot needs real depth - extruded type, a screen floating
5
5
 
6
6
  # 3D shots
7
7
 
8
- The kit has a 3D layer on three.js: `Scene3D`, `Text3D`, `Card3D` and `Orbit3D`, and five more elements for the moments in a film that earn them: `Assemble3D`, `Particles3D`, `Hero3D`, `Screen3D` and `Warp3D`. Everything is deterministic and driven by the frame. You import only from `reelkit/kit`; three itself is not importable from the composition.
8
+ The kit has a 3D layer on three.js: `Scene3D`, `Text3D`, `Card3D` and `Orbit3D`, and elements for the moments in a film that earn them: `Assemble3D`, `BrandTransform3D`, `Particles3D`, `Hero3D`, `Screen3D` and `Warp3D`. Everything is deterministic and driven by the frame. You import only from `reelkit/kit`; three itself is not importable from the composition.
9
9
 
10
10
  ## When 3D earns its place
11
11
 
12
12
  One hero moment in a film: the product's name landing as thick letters, a screen floating in space and turning to face the viewer, a ring of features that the camera pushes through. That is what the extra depth is for.
13
13
 
14
- It reads as a template when everything spins, when several objects each get their own 3D entrance, or when body text is extruded. Type that is read in a sentence stays 2D. Keep 3D to one or two shots in a film, and let the rest be flat and fast.
14
+ It reads as a template when everything spins, when several objects each get their own 3D entrance, or when body text is extruded. Type that is read in a sentence stays 2D. One or two 3D shots usually provide enough contrast. A requested brand film or showreel can use more when each has a distinct purpose; keep sentences and detailed screens readable.
15
15
 
16
16
  ## Which element earns its place
17
17
 
18
- One 3D moment per film is usually enough. Choose by what the moment has to say:
18
+ Choose by what the moment has to say. For a brand transformation and its matching sound, read `reference/brand-motion.md`:
19
19
 
20
20
  | The moment | Reach for | Why |
21
21
  |---|---|---|
22
22
  | A reveal: the name, a logo, a number of things arriving | `Assemble3D` (`shape="text"` for a word, `"grid"`, `"sphere"`, `"wall"`, `"ring"`) | Hundreds of blocks build into the thing; the piece counter (`assembledCount`) and the lock sound (`assembleEnd`) come with it |
23
+ | A brand or product arriving in its readable pose | `BrandTransform3D` (`mode="turn"`, `"arc"`, `"launch"`) | One deterministic arrival that holds neutral; `brandTransformFrames` gives the exact sound landing |
23
24
  | An abstract idea: intelligence, flow, a network, "everything in one place" | `Particles3D` (`from` and `to` shapes, or `"text"`) | Light that changes shape reads as thought, and costs no artwork |
24
25
  | The product itself | `Hero3D` (`kind="device"` with a screenshot, `"logo"`, `"coin"`, `"box"`) | A studio-lit object with clearcoat, two coloured rim lights and a soft floor |
25
26
  | An interface carried across scenes | `Screen3D` with `poses` | One screen, re-posed at each scene, instead of a new mock each time |
@@ -27,7 +28,7 @@ One 3D moment per film is usually enough. Choose by what the moment has to say:
27
28
 
28
29
  Set the mood once on the `Scene3D` (`mood="studio"`, `"night"`, `"sunset"` or `"neon"`): it chooses the light colours, their intensities and the fog tint together, so nobody tunes lights by hand. Everything fits a 9:16 frame by default (`fit`, the share of the width at the closest the camera comes), and everything is a pure function of the frame and a `seed`.
29
30
 
30
- Cost, measured on this machine (software GL, as every render here): one second (30 frames) of 9:16 at 1080 by 1920 took about 1.4 to 1.9 seconds to render for each of them at its default size: `Assemble3D` of 600 blocks 1.9 s, 700 blocks 1.7 s, a 900-block word 1.5 s; `Particles3D` of 12000 points 1.5 to 1.8 s; `Hero3D` and `Screen3D` 1.4 to 1.5 s; `Warp3D` of 500 streaks 1.6 s. That includes the start of the renderer, so a longer shot costs less per second; a 3D shot is not what makes a film slow, but keep `count` at what the picture needs (`Assemble3D` up to 4000 pieces, `Particles3D` up to 60000 points, both of which cost more in proportion).
31
+ HyperFrames captures 3D through software GL for consistent output. Render cost depends on the browser, resolution, geometry and other jobs on the machine; measure the actual shot instead of relying on timings from a previous renderer. Start with the count the picture needs and raise it only when the preview improves.
31
32
 
32
33
  ## How to layer it
33
34
 
@@ -47,7 +48,7 @@ So on a phone frame the camera sees about 3 units across at z 6, not 5.5; a card
47
48
 
48
49
  ## Sizes: leave them out
49
50
 
50
- `Text3D` without `size` is sized from the measured width of its own letters so that it fills `fit` of the frame's width (default 0.8) at the closest the camera comes to it. "Closest" is read from the `Scene3D` camera keys: the key where it sees the least. A word is therefore whole at every moment, on any aspect, and exactly as big as it can be. A line break (`\n`) makes a second line; its height is held to `fit` of the frame's height. Giving `size` turns this off and you are back to doing the arithmetic yourself.
51
+ `Text3D` without `size` is sized from the measured width of its own letters so that it fills `fit` of the frame's width (default 0.8) at the closest the camera comes to it. "Closest" is read from the `Scene3D` camera keys: the key where it sees the least. A word is therefore whole at every moment, on any aspect, and exactly as big as it can be. A line break makes a second line: write `text={"Hyper\nFrames"}` in JSX, since `text="Hyper\nFrames"` passes a literal backslash and n; its height is held to `fit` of the frame's height. Giving `size` turns this off and you are back to doing the arithmetic yourself.
51
52
 
52
53
  A push-in makes the word smaller at the start in proportion: from z 9 to z 6 it begins at two thirds of its final width. A start at about 25 percent further away than the end (z 7.5 to 6) reads as a gentle push; a larger one begins as a small word. Camera sideways moves (`x`) are not counted, so keep the word near the middle when you orbit.
53
54
 
@@ -83,7 +84,7 @@ A word and a ring of labelled cards on a phone frame. Nothing is sized by hand,
83
84
 
84
85
  ```tsx
85
86
  import React from "react";
86
- import { AbsoluteFill } from "remotion";
87
+ import { AbsoluteFill } from "reelkit/frame";
87
88
  import { Card3D, Orbit3D, Scene3D, SceneFrame, Text3D, palettes } from "reelkit/kit";
88
89
  import type { VideoProps } from "reelkit/kit";
89
90
 
@@ -114,7 +115,7 @@ The name arrives as blocks that fly out of a cloud and settle into the letters,
114
115
 
115
116
  ```tsx
116
117
  import React from "react";
117
- import { AbsoluteFill } from "remotion";
118
+ import { AbsoluteFill } from "reelkit/frame";
118
119
  import { Assemble3D, Scene3D, SceneFrame, palettes } from "reelkit/kit";
119
120
  import type { VideoProps } from "reelkit/kit";
120
121
 
@@ -52,7 +52,7 @@ The composition. The number finishes counting as "thousand" is said, and each ro
52
52
 
53
53
  ```tsx
54
54
  import React from "react";
55
- import { AbsoluteFill, Sequence, useVideoConfig } from "remotion";
55
+ import { AbsoluteFill, Sequence, useVideoConfig } from "reelkit/frame";
56
56
  import { Captions, Counter, Entrance, Music, onWord, SceneFrame, sceneById, Sfx, Voiceover, wordFrame } from "reelkit/kit";
57
57
  import type { VideoProps } from "reelkit/kit";
58
58
 
@@ -9,13 +9,14 @@ import { GreenTooDullError, keyGreen } from "../project/chromakey";
9
9
  import { buildManifest, sentenceGaps, voiceoverStale, type ClipRecord, type Voiceover } from "../project/manifest";
10
10
  import { loadPlan } from "./plan";
11
11
  import { FILE_NAME } from "../render/validate";
12
+ import { migrateFrameImports } from "../render/static-check";
12
13
  import { levelOf } from "../project/loudness";
13
14
  import { beatReport, measureMusic, type MusicRecord } from "../project/music";
14
15
  import { audioDuration } from "../project/refmeasure";
15
16
  import { BACKGROUND_CLIP_SENTENCE, BACKGROUND_IMAGE_SENTENCE, isVideoPath, lookFromNotes, readBackground, setBackground } from "../project/background";
16
17
  import { probeFile } from "../project/probe";
17
18
  import { frameWithAlpha, measureTextZone, stillToVideo, tempDir, type LayerEntry } from "../project/layers";
18
- import { coverageVerdict } from "../remotion/kit/image-layers-math";
19
+ import { coverageVerdict } from "../hyperframes/kit/image-layers-math";
19
20
  import { svgProblem } from "../project/svgcheck";
20
21
  import { FILES, isPrivateProject, openProject, type Project } from "../project/project";
21
22
 
@@ -296,6 +297,9 @@ export async function assetsPull(ctx: Ctx, id: string, opts: { scene?: string; f
296
297
  const path = `src/${pulled.filename}`;
297
298
  if (project.exists(path) && !opts.force) return { ok: false, summary: `${path} already exists. Use --force to replace it with the library version.` };
298
299
  await download(pulled.url, project.path(path));
300
+ const source = readFileSync(project.path(path), "utf8");
301
+ const migrated = migrateFrameImports(source);
302
+ if (migrated !== source) writeFileSync(project.path(path), migrated);
299
303
  recordPull(project, id, { path, kind: "component", title: pulled.item.title, meta: pulled.item.meta });
300
304
  const name = pulled.filename.replace(/\.tsx$/, "");
301
305
  const example = typeof pulled.item.meta.example === "string" ? `\nExample: ${pulled.item.meta.example}` : "";
@@ -14,7 +14,7 @@
14
14
  // - A library item `id` (pull, commit) matches `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`; a `deviceCode` is 1 to 200 characters of plain text.
15
15
  // - A `voiceId` is 1 to 64 letters and digits and must be one of the voices the server lists (`voices`), otherwise 400 invalid_request.
16
16
  // - A user's component is uploaded for review through the same two calls as any other file (`kind: "component"`). The server checks its source
17
- // at commit with the same static check a pulled component gets (only react, remotion and reelkit/kit imports, no network, no globals that reach
17
+ // at commit with the same static check a pulled component gets (only react, reelkit/frame and reelkit/kit imports, no network, no globals that reach
18
18
  // outside the video) and refuses a source that fails it with 400 `invalid_request` and the first problem. It stays in `review`, is never found
19
19
  // by search or the public listing, and is published only by the owner. A component upload is: `contentType` `text/plain`, `bytes` up to 65,536,
20
20
  // `shareable` true, `filename` matching `^[A-Z][A-Za-z0-9]*\.tsx$`, a `description` of at least 20 characters, and `meta.example` a string of at most
@@ -0,0 +1 @@
1
+ export { mountVideo, mountVideo as makeRoot } from "./host";
@@ -0,0 +1,54 @@
1
+ import { delayRender, continueRender } from "./frame";
2
+ const loaded = new Set<string>();
3
+ export function loadFont(family: string, slug: string) {
4
+ if (typeof document !== "undefined" && !loaded.has(slug)) {
5
+ loaded.add(slug);
6
+ const handle = delayRender(`font ${family}`);
7
+ const link = document.createElement("link");
8
+ link.onload = () => continueRender(handle);
9
+ link.onerror = () => { continueRender(handle); throw new Error(`Font stylesheet failed: ${slug}`); };
10
+ link.rel = "stylesheet"; link.href = `./fonts/${slug}.css`; document.head.appendChild(link);
11
+ }
12
+ return { fontFamily: `"${family}"` };
13
+ }
14
+ export const fontFamilies = {
15
+ "Inter": "inter",
16
+ "Sora": "sora",
17
+ "ArchivoBlack": "archivo-black",
18
+ "BebasNeue": "bebas-neue",
19
+ "GreatVibes": "great-vibes",
20
+ "JetBrainsMono": "jetbrains-mono",
21
+ "Kanit": "kanit",
22
+ "Modak": "modak",
23
+ "Montserrat": "montserrat",
24
+ "Roboto": "roboto",
25
+ "RobotoSlab": "roboto-slab",
26
+ "Sacramento": "sacramento",
27
+ "Unbounded": "unbounded",
28
+ "BungeeShade": "bungee-shade",
29
+ "Cinzel": "cinzel",
30
+ "Creepster": "creepster",
31
+ "Heebo": "heebo",
32
+ "KaushanScript": "kaushan-script",
33
+ "LilitaOne": "lilita-one",
34
+ "MrsSaintDelafield": "mrs-saint-delafield",
35
+ "Orbitron": "orbitron",
36
+ "Outfit": "outfit",
37
+ "PermanentMarker": "permanent-marker",
38
+ "PlayfairDisplay": "playfair-display",
39
+ "Rubik": "rubik",
40
+ "Rye": "rye",
41
+ "Syne": "syne",
42
+ "Yellowtail": "yellowtail",
43
+ "Alef": "alef",
44
+ "AmaticSC": "amatic-sc",
45
+ "Assistant": "assistant",
46
+ "DavidLibre": "david-libre",
47
+ "FrankRuhlLibre": "frank-ruhl-libre",
48
+ "Fredoka": "fredoka",
49
+ "Karantina": "karantina",
50
+ "NotoSansHebrew": "noto-sans-hebrew",
51
+ "SecularOne": "secular-one",
52
+ "SuezOne": "suez-one",
53
+ "VarelaRound": "varela-round"
54
+ } as const;
@@ -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 = {