@lalalic/markcut 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +39 -15
- package/README.md +7 -3
- package/SKILL.md +26 -172
- package/docs/edit-mode.md +1 -1
- package/docs/label-mode.md +6 -4
- package/docs/markdown-descriptive.md +46 -1
- package/docs/system-prompt-edit.md +16 -0
- package/package.json +1 -1
- package/src/config.mjs +8 -3
- package/src/descriptive/compiler.test.ts +42 -42
- package/src/descriptive/compiler.ts +41 -52
- package/src/descriptive/markdown.ts +8 -4
- package/src/descriptive/resolve.test.ts +14 -7
- package/src/descriptive/resolve.ts +148 -20
- package/src/entry.tsx +8 -2
- package/src/player/browser.tsx +178 -54
- package/src/player/bundle/player.js +1168 -566
- package/src/player/components/EditControls.tsx +92 -0
- package/src/player/components/HeaderBar.tsx +60 -0
- package/src/player/components/LabelControls.tsx +367 -0
- package/src/player/components/SceneThumbnails.tsx +87 -0
- package/src/player/components/VariantBar.tsx +39 -0
- package/src/player/components/index.ts +5 -0
- package/src/player/pipeline.mjs +130 -66
- package/src/player/pipeline.ts +3 -1
- package/src/player/server-shared.mjs +5 -7
- package/src/player/server.mjs +194 -187
- package/src/render/cli.mjs +66 -13
- package/src/schema/index.ts +20 -23
- package/src/types/Audio.tsx +25 -33
- package/src/types/Component.tsx +18 -24
- package/src/types/Effect.tsx +31 -39
- package/src/types/Image.tsx +23 -30
- package/src/types/Include.tsx +70 -76
- package/src/types/Map.tsx +48 -44
- package/src/types/Rhythm.tsx +19 -27
- package/src/types/Video.tsx +40 -47
- package/src/utils/component-import-map.ts +93 -0
- package/src/utils/index.ts +23 -10
- package/src/vision/cli.mjs +6 -6
- package/templates/courseware/TEMPLATE.md +317 -0
- package/templates/courseware/agents/reviewer.md +84 -0
- package/templates/courseware/prompts/fix.md +30 -0
- package/templates/courseware/prompts/outline.md +53 -0
- package/templates/courseware/prompts/scene.md +64 -0
- package/tests/fixtures/audio.json +4 -2
- package/tests/fixtures/basic.json +2 -1
- package/tests/fixtures/component-all.json +4 -2
- package/tests/fixtures/components.json +4 -2
- package/tests/fixtures/effects.json +12 -6
- package/tests/fixtures/full.json +8 -4
- package/tests/fixtures/map.json +2 -1
- package/tests/fixtures/md/dialogue.md +12 -0
- package/tests/fixtures/md/edge-cases.md +9 -0
- package/tests/fixtures/scenes.json +4 -2
- package/tests/fixtures/subtitle.json +6 -3
- package/tests/fixtures/subvideo.json +11 -6
- package/tests/fixtures/templates/courseware.md +61 -4
- package/tests/fixtures/tween-visual.json +2 -6
- package/tests/fixtures/video-series.json +6 -14
- package/tests/md-descriptive.test.ts +170 -0
- package/tests/render.test.ts +32 -16
- package/tests/schema.test.ts +6 -3
- package/tests/server.test.ts +9 -6
- package/tests/utils.ts +4 -4
- package/docs/dynamic-components.md +0 -191
- package/docs/templates.md +0 -52
- package/scripts/artlist-dl.mjs +0 -190
- package/src/player/label-server.mjs +0 -599
package/src/render/cli.mjs
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* (TTS/STT/durations)
|
|
8
8
|
*/
|
|
9
9
|
import { execSync, spawn } from "node:child_process";
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { resolve, dirname, join, extname } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -111,11 +112,42 @@ export function parseArgs(argv) {
|
|
|
111
112
|
*
|
|
112
113
|
* Use --verbose to see every frame line (original behavior).
|
|
113
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* Stage local absolute-path assets (TTS audio, TTI/TTV media, subtitles from
|
|
117
|
+
* .markcut/) into ROOT/public/.render-assets/ and rewrite srcs to relative
|
|
118
|
+
* paths, so `npx remotion render` (publicDir = ROOT/public) can serve them.
|
|
119
|
+
* The preview server handles these paths via multi-root serving; the render
|
|
120
|
+
* CLI needs them staged into the one public dir Remotion knows about.
|
|
121
|
+
*/
|
|
122
|
+
function stageLocalAssets(tree) {
|
|
123
|
+
const assetsDir = join(ROOT, "public", ".render-assets");
|
|
124
|
+
const seen = new Map();
|
|
125
|
+
const walk = (node) => {
|
|
126
|
+
if (!node || typeof node !== "object") return;
|
|
127
|
+
if (typeof node.src === "string" && node.src.startsWith("/") && !node.src.startsWith("//") && existsSync(node.src)) {
|
|
128
|
+
let staged = seen.get(node.src);
|
|
129
|
+
if (!staged) {
|
|
130
|
+
const name = createHash("md5").update(node.src).digest("hex").slice(0, 12) + extname(node.src);
|
|
131
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
132
|
+
copyFileSync(node.src, join(assetsDir, name));
|
|
133
|
+
staged = `.render-assets/${name}`;
|
|
134
|
+
seen.set(node.src, staged);
|
|
135
|
+
}
|
|
136
|
+
node.src = staged;
|
|
137
|
+
}
|
|
138
|
+
for (const value of Object.values(node)) {
|
|
139
|
+
if (value && typeof value === "object") walk(value);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
walk(tree);
|
|
143
|
+
return tree;
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
function renderOne(streamTree, aspect, outputPath, verbose) {
|
|
115
147
|
const dims = ASPECTS[aspect];
|
|
116
148
|
if (!dims) throw new Error(`Unknown aspect: ${aspect}`);
|
|
117
149
|
|
|
118
|
-
const adapted = { ...streamTree, width: dims.width, height: dims.height };
|
|
150
|
+
const adapted = stageLocalAssets(JSON.parse(JSON.stringify({ ...streamTree, width: dims.width, height: dims.height })));
|
|
119
151
|
const tmpProps = join(ROOT, ".tmp", `render-${aspect}.json`);
|
|
120
152
|
mkdirSync(dirname(tmpProps), { recursive: true });
|
|
121
153
|
writeFileSync(tmpProps, JSON.stringify({ root: adapted }));
|
|
@@ -211,21 +243,19 @@ async function main() {
|
|
|
211
243
|
}
|
|
212
244
|
|
|
213
245
|
if (args.label || args.edit) {
|
|
214
|
-
const playerServer =
|
|
215
|
-
? join(__dirname, "..", "player", "label-server.mjs")
|
|
216
|
-
: join(__dirname, "..", "player", "server.mjs");
|
|
246
|
+
const playerServer = join(__dirname, "..", "player", "server.mjs");
|
|
217
247
|
if (!existsSync(playerServer)) {
|
|
218
248
|
console.error("Player server not found at", playerServer);
|
|
219
249
|
process.exit(1);
|
|
220
250
|
}
|
|
221
|
-
const
|
|
251
|
+
const labelFlag = args.label ? "--label" : "";
|
|
222
252
|
const editFlag = args.edit ? "--edit" : "";
|
|
223
253
|
const portFlag = `--port=${args.port || 3001}`;
|
|
224
254
|
const fileFlag = args.file || join(ROOT, "video.json");
|
|
225
255
|
const port = args.port || 3001;
|
|
226
256
|
// Pass variant flags to the server
|
|
227
257
|
const variantFlags = args.variant.map(v => `--variant=${v}`);
|
|
228
|
-
const serverArgs = [playerServer, resolve(fileFlag),
|
|
258
|
+
const serverArgs = [playerServer, resolve(fileFlag), labelFlag, editFlag, portFlag, ...variantFlags].filter(Boolean);
|
|
229
259
|
const child = spawn("node", serverArgs, { cwd: ROOT, stdio: ["ignore", "pipe", "inherit"] });
|
|
230
260
|
let serverReady = false;
|
|
231
261
|
let stdoutBuffer = "";
|
|
@@ -235,7 +265,7 @@ async function main() {
|
|
|
235
265
|
process.stdout.write(chunk);
|
|
236
266
|
if (!serverReady && !args.noBrowser) {
|
|
237
267
|
stdoutBuffer += chunk.toString();
|
|
238
|
-
if (stdoutBuffer.includes("Player ready")
|
|
268
|
+
if (stdoutBuffer.includes("Player ready")) {
|
|
239
269
|
serverReady = true;
|
|
240
270
|
|
|
241
271
|
try {
|
|
@@ -262,10 +292,12 @@ async function main() {
|
|
|
262
292
|
|
|
263
293
|
if (args.command === "render") {
|
|
264
294
|
let streamTree;
|
|
295
|
+
let rawInput = "";
|
|
265
296
|
|
|
266
297
|
if (args.file) {
|
|
267
298
|
const filePath = resolve(args.file);
|
|
268
299
|
const raw = readFileSync(filePath, "utf-8");
|
|
300
|
+
rawInput = raw;
|
|
269
301
|
|
|
270
302
|
// Helper: all generated artifacts live under .markcut/generated/
|
|
271
303
|
function generatedDir(filePath, sub) {
|
|
@@ -285,14 +317,14 @@ async function main() {
|
|
|
285
317
|
// Helper: resolve variants from the parsed descriptive root
|
|
286
318
|
async function resolveWithVariants(descriptive, options) {
|
|
287
319
|
const { resolveVariantOverrides, compileDescriptiveRoot, resolveAll } = await import("../player/pipeline.mjs");
|
|
288
|
-
const {
|
|
320
|
+
const { parseMarkdownVariants } = await import("../player/pipeline.mjs");
|
|
289
321
|
|
|
290
322
|
let resolved = descriptive;
|
|
291
323
|
|
|
292
324
|
// If variants are specified, apply variant-prefixed overrides
|
|
293
325
|
if (args.variant && args.variant.length > 0) {
|
|
294
326
|
// Also apply root config overrides from variant sections
|
|
295
|
-
const parsed =
|
|
327
|
+
const parsed = parseMarkdownVariants(raw);
|
|
296
328
|
const variantRoot = parsed.variants.get(args.variant[0]);
|
|
297
329
|
if (variantRoot) {
|
|
298
330
|
// Merge variant root config into base (tts, stt, width, height, etc.)
|
|
@@ -308,9 +340,9 @@ async function main() {
|
|
|
308
340
|
}
|
|
309
341
|
|
|
310
342
|
if (filePath.endsWith(".md")) {
|
|
311
|
-
const {
|
|
343
|
+
const { parseMarkdownVariants } = await import("../player/pipeline.mjs");
|
|
312
344
|
const fileDir = dirname(filePath);
|
|
313
|
-
const parsed =
|
|
345
|
+
const parsed = parseMarkdownVariants(raw);
|
|
314
346
|
streamTree = await resolveWithVariants(parsed.base, {
|
|
315
347
|
baseDir: fileDir,
|
|
316
348
|
scriptOutputDir: generatedDir(filePath, "tts"),
|
|
@@ -340,6 +372,27 @@ async function main() {
|
|
|
340
372
|
process.exit(1);
|
|
341
373
|
}
|
|
342
374
|
|
|
375
|
+
// Bundle the ```js imports``` component block (the preview server does this
|
|
376
|
+
// post-compile; the render CLI must do it too, otherwise jsx components are
|
|
377
|
+
// unregistered and slides render as unstyled raw text).
|
|
378
|
+
if (!streamTree.imports) {
|
|
379
|
+
const fenceMatch = rawInput.match(/^(```|~~~)\s*js imports\s*\n([\s\S]*?)^\1\s*$/m);
|
|
380
|
+
const rawSource = fenceMatch ? fenceMatch[2] : (streamTree.importsBlock ?? null);
|
|
381
|
+
if (rawSource && rawSource.trim()) {
|
|
382
|
+
const { parseImportsBlock, extractDependencySpecs } = await import("../player/pipeline.mjs");
|
|
383
|
+
const { bundleFromEntries } = await import("../player/bundler.mjs");
|
|
384
|
+
const entries = parseImportsBlock(rawSource);
|
|
385
|
+
const extraSpecs = extractDependencySpecs(rawSource);
|
|
386
|
+
const bundleDir = join(ROOT, "public", ".render-assets");
|
|
387
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
388
|
+
const bundle = await bundleFromEntries(entries, extraSpecs, rawSource, bundleDir);
|
|
389
|
+
if (bundle.url) {
|
|
390
|
+
streamTree.imports = ".render-assets/" + bundle.url.split("/").pop();
|
|
391
|
+
console.log(` \u2705 components \u2192 ${bundle.exports.join(", ")}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
343
396
|
const output = args.output ? resolve(args.output) : join(ROOT, "out", "video.mp4");
|
|
344
397
|
await renderOne(streamTree, "16x9", output, args.verbose);
|
|
345
398
|
|
package/src/schema/index.ts
CHANGED
|
@@ -6,25 +6,21 @@ import { uid } from "../utils/index";
|
|
|
6
6
|
* Render-only — no prompts, no async transforms, no providers.
|
|
7
7
|
*
|
|
8
8
|
* A stream tree is rooted at `root`. Folders contain children.
|
|
9
|
-
* Leaf streams (video/audio/image/component)
|
|
10
|
-
* describing when they appear
|
|
9
|
+
* Leaf streams (video/audio/image/component) carry timing on their base
|
|
10
|
+
* fields (start/end/duration/startFrom/endAt) describing when they appear
|
|
11
|
+
* on the timeline.
|
|
11
12
|
*
|
|
12
13
|
* Subtitles are a root-level overlay: root.subtitle points to a VTT file
|
|
13
14
|
* that covers the full video duration. Generated by the TTS pipeline.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
effectId: z.string().optional(),
|
|
24
|
-
style: z.string().optional().describe("inline css"),
|
|
25
|
-
volume: z.number().min(0).max(1).optional(),
|
|
26
|
-
});
|
|
27
|
-
export type Action = z.infer<typeof action>;
|
|
17
|
+
/**
|
|
18
|
+
* Leaf timing lives directly on BaseShape (start/end/duration/startFrom/endAt).
|
|
19
|
+
* The old `actions[]` array has been removed — every leaf occupies exactly one
|
|
20
|
+
* span on its parent's timeline. `end` is the source of truth for duration;
|
|
21
|
+
* `duration` is a normalized convenience (end = start + duration when end is
|
|
22
|
+
* absent). `loop`/`volume`/`style` moved onto the stream type itself.
|
|
23
|
+
*/
|
|
28
24
|
|
|
29
25
|
export const eventSpec = z.object({
|
|
30
26
|
when: z.string().describe("'start' | 'end' | '50%' | '2.5s'"),
|
|
@@ -39,6 +35,11 @@ const BaseShape = {
|
|
|
39
35
|
style: z.string().optional().describe("inline css"),
|
|
40
36
|
visible: z.boolean().default(true),
|
|
41
37
|
isBackground: z.boolean().optional(),
|
|
38
|
+
start: z.number().min(0).default(0).describe("seconds, relative to parent timeline (0 inside series parents)"),
|
|
39
|
+
end: z.number().min(0).optional().describe("seconds, relative to parent timeline — source of truth for duration"),
|
|
40
|
+
duration: z.number().min(0).optional().describe("convenience; normalized to end = start + duration when end is absent"),
|
|
41
|
+
startFrom: z.number().min(0).optional().describe("trim seconds from source start (video/audio)"),
|
|
42
|
+
endAt: z.number().min(0).optional().describe("trim seconds at source end (video/audio)"),
|
|
42
43
|
durationInSeconds: z.number().optional().describe("set by engine; do not edit by hand"),
|
|
43
44
|
on: eventSpec.optional().describe("event that fires at a specific frame, mutating registered component state"),
|
|
44
45
|
};
|
|
@@ -102,7 +103,7 @@ export const video = base.extend({
|
|
|
102
103
|
playbackRate: z.number().optional(),
|
|
103
104
|
width: z.number().default(1080),
|
|
104
105
|
height: z.number().default(1920),
|
|
105
|
-
|
|
106
|
+
loop: z.number().int().min(1).optional().describe(">1 = loop count"),
|
|
106
107
|
});
|
|
107
108
|
export type Video = z.infer<typeof video>;
|
|
108
109
|
|
|
@@ -111,7 +112,8 @@ export const audio = base.extend({
|
|
|
111
112
|
src: z.string().optional(),
|
|
112
113
|
volume: z.number().min(0).max(1).default(1),
|
|
113
114
|
foreground: z.boolean().optional().describe("ducks parent video audio while playing"),
|
|
114
|
-
|
|
115
|
+
loop: z.number().int().min(1).optional().describe(">1 = loop count"),
|
|
116
|
+
speaker: z.string().optional().describe("speaker name for multi-turn dialogue; set by resolveDialogue"),
|
|
115
117
|
});
|
|
116
118
|
export type Audio = z.infer<typeof audio>;
|
|
117
119
|
|
|
@@ -119,7 +121,6 @@ export const image = base.extend({
|
|
|
119
121
|
type: z.literal("image").default("image"),
|
|
120
122
|
src: z.string().optional(),
|
|
121
123
|
fit: z.enum(["contain", "cover", "fill"]).default("contain"),
|
|
122
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
123
124
|
});
|
|
124
125
|
export type Image = z.infer<typeof image>;
|
|
125
126
|
|
|
@@ -134,7 +135,6 @@ export const component = base.extend({
|
|
|
134
135
|
type: z.literal("component").default("component"),
|
|
135
136
|
jsx: z.string().describe("usage JSX expression compiled at runtime; tag names resolved from imports"),
|
|
136
137
|
data: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("extra variables (e.g. from ~~~md source code fences) available in JSX scope"),
|
|
137
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
138
138
|
});
|
|
139
139
|
export type Component = z.infer<typeof component>;
|
|
140
140
|
|
|
@@ -153,7 +153,6 @@ export const effect = base.extend({
|
|
|
153
153
|
.optional()
|
|
154
154
|
.describe('inline keyframes: { "0": { opacity: "0" }, "100": { opacity: "1" } }'),
|
|
155
155
|
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []),
|
|
156
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
157
156
|
});
|
|
158
157
|
export type Effect = z.infer<typeof effect>;
|
|
159
158
|
|
|
@@ -166,7 +165,6 @@ export const rhythm = base.extend({
|
|
|
166
165
|
volume: z.number().min(0).max(1).default(1),
|
|
167
166
|
spots: z.array(z.number()).optional().describe("pre-computed beat timestamps in seconds"),
|
|
168
167
|
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []),
|
|
169
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
170
168
|
});
|
|
171
169
|
export type Rhythm = z.infer<typeof rhythm>;
|
|
172
170
|
|
|
@@ -177,7 +175,7 @@ export type Rhythm = z.infer<typeof rhythm>;
|
|
|
177
175
|
// - A scene-based video.json (has `meta` and `scenes`)
|
|
178
176
|
//
|
|
179
177
|
// Usage:
|
|
180
|
-
// { type: "include", src: "./path/to/video.json",
|
|
178
|
+
// { type: "include", src: "./path/to/video.json", start: 0, end: 5 }
|
|
181
179
|
//
|
|
182
180
|
// Falls back to inline `children` if `src` is not set (legacy behavior).
|
|
183
181
|
// ---------------------------------------------------------------------------
|
|
@@ -186,7 +184,6 @@ export const include = base.extend({
|
|
|
186
184
|
src: z.string().optional().describe("path or URL to video JSON file (stream tree or scene-based)"),
|
|
187
185
|
volume: z.number().min(0).max(1).default(1),
|
|
188
186
|
children: z.array(z.lazy((): z.ZodTypeAny => stream)).default(() => []),
|
|
189
|
-
actions: z.array(action).min(1).default(() => [action.parse({})]),
|
|
190
187
|
imports: z.string().optional().describe("component bundle URL for sub-video (set by engine)"),
|
|
191
188
|
});
|
|
192
189
|
export type Include = z.infer<typeof include>;
|
|
@@ -234,7 +231,7 @@ export const mapStream = base.extend({
|
|
|
234
231
|
mapType: z.enum(["roadmap", "satellite", "hybrid", "terrain"]).default("roadmap").describe("Google Maps style"),
|
|
235
232
|
travelMode: z.enum(["DRIVING", "WALKING", "BICYCLING", "TRANSIT"]).default("DRIVING").describe("Directions API travel mode"),
|
|
236
233
|
routeMarker: z.string().default("🚗").describe("emoji/character for the animated traveling marker"),
|
|
237
|
-
|
|
234
|
+
googleMapsApiKey: z.string().optional().describe("injected by compiler from GOOGLE_MAPS_API_KEY env var"),
|
|
238
235
|
});
|
|
239
236
|
export type MapStream = z.infer<typeof mapStream>;
|
|
240
237
|
|
package/src/types/Audio.tsx
CHANGED
|
@@ -13,44 +13,36 @@ export function AudioLeaf({ stream }: { stream: Audio }) {
|
|
|
13
13
|
const { fps } = useVideoConfig();
|
|
14
14
|
const environment = useRemotionEnvironment();
|
|
15
15
|
const ctx = React.useContext(AudioContext);
|
|
16
|
-
const
|
|
16
|
+
const start = stream.start ?? 0;
|
|
17
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
18
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
17
19
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
18
20
|
if (!stream.src) return null;
|
|
19
21
|
if (environment.isStudio) return null;
|
|
20
22
|
|
|
21
23
|
const resolvedSrc = resolveAudioSrc(stream.src);
|
|
22
|
-
|
|
24
|
+
const startFrom = stream.startFrom ?? 0;
|
|
25
|
+
const endAt = stream.endAt ?? totalDur;
|
|
26
|
+
const volume = stream.volume ?? 1;
|
|
27
|
+
const playbackRate = stream.loop ? 1 : toPlaybackRate((endAt - startFrom) / (end - start));
|
|
23
28
|
return (
|
|
24
|
-
|
|
25
|
-
{stream.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
src={resolvedSrc}
|
|
43
|
-
startFrom={Math.floor(startFrom * fps)}
|
|
44
|
-
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
45
|
-
muted={volume === 0 || !!ctx?.foreground}
|
|
46
|
-
volume={volume}
|
|
47
|
-
loop={(a.loop ?? 1) > 1}
|
|
48
|
-
playbackRate={playbackRate}
|
|
49
|
-
showInTimeline={false}
|
|
50
|
-
/>
|
|
51
|
-
</Sequence>
|
|
52
|
-
);
|
|
53
|
-
})}
|
|
54
|
-
</>
|
|
29
|
+
<Sequence
|
|
30
|
+
name={stream.src ?? "audio"}
|
|
31
|
+
durationInFrames={Math.max(1, Math.floor(fps * (end - start)))}
|
|
32
|
+
from={Math.floor(fps * start)}
|
|
33
|
+
layout="none"
|
|
34
|
+
showInTimeline={false}
|
|
35
|
+
>
|
|
36
|
+
<RemotionAudio
|
|
37
|
+
src={resolvedSrc}
|
|
38
|
+
startFrom={Math.floor(startFrom * fps)}
|
|
39
|
+
endAt={Math.floor(startFrom * fps) + Math.floor(((endAt - startFrom) * fps) / playbackRate)}
|
|
40
|
+
muted={volume === 0 || !!ctx?.foreground}
|
|
41
|
+
volume={volume}
|
|
42
|
+
loop={(stream.loop ?? 1) > 1}
|
|
43
|
+
playbackRate={playbackRate}
|
|
44
|
+
showInTimeline={false}
|
|
45
|
+
/>
|
|
46
|
+
</Sequence>
|
|
55
47
|
);
|
|
56
48
|
}
|
package/src/types/Component.tsx
CHANGED
|
@@ -74,31 +74,25 @@ export function ComponentLeaf({ stream }: { stream: Component }) {
|
|
|
74
74
|
|
|
75
75
|
if (!stream.jsx) return null;
|
|
76
76
|
|
|
77
|
+
const start = stream.start ?? 0;
|
|
78
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
79
|
+
const durFrames = Math.max(1, Math.floor(fps * (end - start)));
|
|
80
|
+
|
|
77
81
|
return (
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
components={components}
|
|
93
|
-
data={bindings}
|
|
94
|
-
action={a}
|
|
95
|
-
durFrames={durFrames}
|
|
96
|
-
on={stream.on}
|
|
97
|
-
/>
|
|
98
|
-
</Sequence>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
101
|
-
</>
|
|
82
|
+
<Sequence
|
|
83
|
+
durationInFrames={durFrames}
|
|
84
|
+
from={Math.floor(fps * start)}
|
|
85
|
+
layout="none"
|
|
86
|
+
>
|
|
87
|
+
<EventAwareComponent
|
|
88
|
+
jsx={stream.jsx}
|
|
89
|
+
components={components}
|
|
90
|
+
data={bindings}
|
|
91
|
+
action={{ start, end }}
|
|
92
|
+
durFrames={durFrames}
|
|
93
|
+
on={stream.on}
|
|
94
|
+
/>
|
|
95
|
+
</Sequence>
|
|
102
96
|
);
|
|
103
97
|
}
|
|
104
98
|
|
package/src/types/Effect.tsx
CHANGED
|
@@ -23,57 +23,49 @@ export function EffectWrapper({
|
|
|
23
23
|
}) {
|
|
24
24
|
const frame = useCurrentFrame();
|
|
25
25
|
const { fps, width, height } = useVideoConfig();
|
|
26
|
-
const
|
|
26
|
+
const startSec = stream.start ?? 0;
|
|
27
|
+
const endSec = stream.end ?? startSec + (stream.duration ?? 1);
|
|
28
|
+
const totalDur = stream.durationInSeconds ?? endSec;
|
|
27
29
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
28
30
|
|
|
29
|
-
const actions = stream.actions ?? [];
|
|
30
|
-
|
|
31
31
|
const styles = React.useMemo(() => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const end = Math.ceil(action.end * fps);
|
|
37
|
-
const durationInFrames = end - start;
|
|
38
|
-
if (durationInFrames <= 0) continue;
|
|
32
|
+
const start = Math.ceil(startSec * fps);
|
|
33
|
+
const end = Math.ceil(endSec * fps);
|
|
34
|
+
const durationInFrames = end - start;
|
|
35
|
+
if (durationInFrames <= 0) return [] as Record<string, string>[];
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const animation = stream.animation;
|
|
38
|
+
const timingFn = stream.animationTimingFunction;
|
|
39
|
+
const iterCount = stream.animationIterationCount ?? 1;
|
|
40
|
+
const style = (cssJS(stream.style) ?? {}) as Record<string, string>;
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
42
|
+
// Handle iteration count: loop the animation within the span
|
|
43
|
+
let currentFrame = frame;
|
|
44
|
+
if (iterCount > 0 && durationInFrames > 0) {
|
|
45
|
+
const iteration = Math.floor((frame - start) / durationInFrames);
|
|
46
|
+
if (iteration < iterCount) {
|
|
47
|
+
currentFrame = start + ((frame - start) % durationInFrames);
|
|
52
48
|
}
|
|
49
|
+
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (animation) {
|
|
58
|
-
const config = resolveAnimation(animation, stream.customKeyframes);
|
|
59
|
-
if (config) {
|
|
60
|
-
const animStyle = interpolateKeyframes(config, actionFrame, {
|
|
61
|
-
fps,
|
|
62
|
-
durationInSeconds: durationInFrames / fps,
|
|
63
|
-
timingFunction: timingFn,
|
|
64
|
-
});
|
|
65
|
-
if (animStyle) Object.assign(style, animStyle);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
51
|
+
if (currentFrame >= start && currentFrame < end) {
|
|
52
|
+
const actionFrame = currentFrame - start;
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
if (animation) {
|
|
55
|
+
const config = resolveAnimation(animation, stream.customKeyframes);
|
|
56
|
+
if (config) {
|
|
57
|
+
const animStyle = interpolateKeyframes(config, actionFrame, {
|
|
58
|
+
fps,
|
|
59
|
+
durationInSeconds: durationInFrames / fps,
|
|
60
|
+
timingFunction: timingFn,
|
|
61
|
+
});
|
|
62
|
+
if (animStyle) Object.assign(style, animStyle);
|
|
71
63
|
}
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
return
|
|
76
|
-
}, [frame, fps,
|
|
67
|
+
return Object.keys(style).length > 0 ? [style] : [];
|
|
68
|
+
}, [frame, fps, startSec, endSec, stream.animation, stream.animationTimingFunction, stream.animationIterationCount, stream.customKeyframes, stream.style]);
|
|
77
69
|
|
|
78
70
|
if (styles.length === 0) return <>{children}</>;
|
|
79
71
|
|
package/src/types/Image.tsx
CHANGED
|
@@ -12,40 +12,33 @@ function resolveImageSrc(src: string): string {
|
|
|
12
12
|
|
|
13
13
|
export function ImageLeaf({ stream }: { stream: Image }) {
|
|
14
14
|
const { fps } = useVideoConfig();
|
|
15
|
-
const
|
|
15
|
+
const start = stream.start ?? 0;
|
|
16
|
+
const end = stream.end ?? start + (stream.duration ?? 1);
|
|
17
|
+
const totalDur = stream.durationInSeconds ?? end;
|
|
16
18
|
useFrameEvents(stream.on, Math.max(1, Math.floor(totalDur * fps)));
|
|
17
19
|
if (!stream.src) return null;
|
|
18
20
|
const resolvedSrc = resolveImageSrc(stream.src);
|
|
19
21
|
|
|
20
22
|
return (
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
e.stopPropagation();
|
|
42
|
-
return false;
|
|
43
|
-
}}
|
|
44
|
-
/>
|
|
45
|
-
</FrameSyncStyle>
|
|
46
|
-
</Sequence>
|
|
47
|
-
);
|
|
48
|
-
})}
|
|
49
|
-
</>
|
|
23
|
+
<Sequence
|
|
24
|
+
durationInFrames={Math.max(1, Math.floor(fps * (end - start)))}
|
|
25
|
+
from={Math.floor(fps * start)}
|
|
26
|
+
layout="none"
|
|
27
|
+
>
|
|
28
|
+
<FrameSyncStyle style={cssJS(stream.style)}>
|
|
29
|
+
<Img
|
|
30
|
+
src={resolvedSrc}
|
|
31
|
+
style={{
|
|
32
|
+
width: "100%",
|
|
33
|
+
height: "100%",
|
|
34
|
+
objectFit: stream.fit,
|
|
35
|
+
}}
|
|
36
|
+
onDragStart={(e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
return false;
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
</FrameSyncStyle>
|
|
42
|
+
</Sequence>
|
|
50
43
|
);
|
|
51
44
|
}
|