@democraft/remotion 0.1.0-beta.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.
@@ -0,0 +1,684 @@
1
+ import {
2
+ createProductDemoVideoProps
3
+ } from "./chunk-RINGSQ6X.js";
4
+
5
+ // src/stage.ts
6
+ import React from "react";
7
+ import { Img, OffthreadVideo, interpolate, staticFile } from "remotion";
8
+
9
+ // src/utils.ts
10
+ function active(track, frame) {
11
+ return frame >= track.fromFrame && frame < track.fromFrame + track.durationInFrames;
12
+ }
13
+ function lerp(from, to, progress) {
14
+ return from + (to - from) * progress;
15
+ }
16
+ function smoothstep(value) {
17
+ return value * value * (3 - 2 * value);
18
+ }
19
+
20
+ // src/camera.ts
21
+ function makeCamera(scale, focusX, focusY) {
22
+ return {
23
+ scale,
24
+ focusX,
25
+ focusY,
26
+ translateX: 720 / scale - focusX,
27
+ translateY: 450 / scale - focusY
28
+ };
29
+ }
30
+ function identityCamera() {
31
+ return makeCamera(1, 720, 450);
32
+ }
33
+ function cameraStateAt(timeline, frame) {
34
+ const tracks = [...timeline.camera].sort((a, b) => a.fromFrame - b.fromFrame);
35
+ let index = -1;
36
+ for (let cursor = 0; cursor < tracks.length; cursor += 1) {
37
+ if (frame >= tracks[cursor].fromFrame) index = cursor;
38
+ }
39
+ if (index === -1) return identityCamera();
40
+ const track = tracks[index];
41
+ const previous = index > 0 ? cameraTarget(tracks[index - 1]) : identityCamera();
42
+ const next = cameraTarget(track);
43
+ const progress = smoothstep(
44
+ Math.min(
45
+ 1,
46
+ Math.max(0, (frame - track.fromFrame) / track.durationInFrames)
47
+ )
48
+ );
49
+ return makeCamera(
50
+ lerp(previous.scale, next.scale, progress),
51
+ lerp(previous.focusX, next.focusX, progress),
52
+ lerp(previous.focusY, next.focusY, progress)
53
+ );
54
+ }
55
+ function cameraTarget(track) {
56
+ if (!track?.boundingBox || track.kind === "establish")
57
+ return identityCamera();
58
+ const padding = 88;
59
+ const scale = Math.min(
60
+ 1.32,
61
+ Math.max(
62
+ 1,
63
+ Math.min(
64
+ 1440 / (track.boundingBox.width + padding * 2),
65
+ 900 / (track.boundingBox.height + padding * 2)
66
+ )
67
+ )
68
+ );
69
+ const focusX = track.boundingBox.x + track.boundingBox.width / 2;
70
+ const focusY = track.boundingBox.y + track.boundingBox.height / 2;
71
+ return makeCamera(scale, focusX, focusY);
72
+ }
73
+ function cameraTransform(camera, stage) {
74
+ const scale = stage.scale * camera.scale;
75
+ return `matrix(${scale}, 0, 0, ${scale}, ${stage.x + camera.translateX * scale}, ${stage.y + camera.translateY * scale})`;
76
+ }
77
+
78
+ // src/stage.ts
79
+ var DEFAULT_CAPTURE = { width: 1920, height: 1080 };
80
+ function stageLayout(width, height, capture = DEFAULT_CAPTURE) {
81
+ const scale = Math.min(width / capture.width, height / capture.height);
82
+ return {
83
+ scale,
84
+ x: (width - capture.width * scale) / 2,
85
+ y: (height - capture.height * scale) / 2
86
+ };
87
+ }
88
+ function transformedBox(box, camera, stage) {
89
+ const scale = stage.scale * camera.scale;
90
+ return {
91
+ x: stage.x + (box.x + camera.translateX) * scale,
92
+ y: stage.y + (box.y + camera.translateY) * scale,
93
+ width: box.width * scale,
94
+ height: box.height * scale
95
+ };
96
+ }
97
+ function imageStyle(opacity, capture = DEFAULT_CAPTURE) {
98
+ const dsf = capture.deviceScaleFactor ?? 1;
99
+ return {
100
+ position: "absolute",
101
+ top: 0,
102
+ left: 0,
103
+ width: capture.width * dsf,
104
+ height: capture.height * dsf,
105
+ // Host styles such as Tailwind's `img { max-width: 100% }` must not clamp
106
+ // the native DPR-sized bitmap before the compensating transform runs.
107
+ maxWidth: "none",
108
+ maxHeight: "none",
109
+ transform: `scale(${1 / dsf})`,
110
+ transformOrigin: "0 0",
111
+ objectFit: "contain",
112
+ opacity
113
+ };
114
+ }
115
+ function currentStep(steps, frame) {
116
+ return [...steps].reverse().find((step) => frame >= step.fromFrame);
117
+ }
118
+ function firstScreenshot(sources) {
119
+ return Object.values(sources)[0];
120
+ }
121
+ function previousRenderStep(steps, step) {
122
+ const index = step ? steps.findIndex((item) => item.stepId === step.stepId) : -1;
123
+ return index > 0 ? steps[index - 1] : void 0;
124
+ }
125
+ function nextRenderStep(steps, step) {
126
+ const index = step ? steps.findIndex((item) => item.stepId === step.stepId) : -1;
127
+ return index >= 0 && index < steps.length - 1 ? steps[index + 1] : void 0;
128
+ }
129
+ function screenshotForStep(sources, step) {
130
+ return step ? sources[step.stepId] : firstScreenshot(sources);
131
+ }
132
+ function stageMediaState(steps, sources, frame) {
133
+ const step = currentStep(steps, frame);
134
+ if (!step) {
135
+ return {
136
+ currentSrc: firstScreenshot(sources),
137
+ currentOpacity: 1,
138
+ previousOpacity: 0
139
+ };
140
+ }
141
+ if (step.kind !== "timeline.transition" || step.durationInFrames <= 1) {
142
+ return {
143
+ currentSrc: screenshotForStep(sources, step),
144
+ currentOpacity: 1,
145
+ previousOpacity: 0
146
+ };
147
+ }
148
+ const progress = interpolate(
149
+ frame,
150
+ [step.fromFrame, step.fromFrame + step.durationInFrames],
151
+ [0, 1],
152
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
153
+ );
154
+ return {
155
+ currentSrc: screenshotForStep(sources, nextRenderStep(steps, step) ?? step),
156
+ currentOpacity: progress,
157
+ previousSrc: screenshotForStep(sources, previousRenderStep(steps, step)),
158
+ previousOpacity: 1 - progress
159
+ };
160
+ }
161
+ function Backdrop() {
162
+ return React.createElement("div", {
163
+ style: {
164
+ position: "absolute",
165
+ inset: 0,
166
+ background: "linear-gradient(135deg, #10131a 0%, #18202b 52%, #0d1118 100%)"
167
+ }
168
+ });
169
+ }
170
+ function StageMedia({
171
+ camera,
172
+ capture = DEFAULT_CAPTURE,
173
+ frame,
174
+ recordingSrc,
175
+ screenshotSrcByStepId,
176
+ stage,
177
+ steps
178
+ }) {
179
+ const media = stageMediaState(steps, screenshotSrcByStepId, frame);
180
+ return React.createElement(
181
+ "div",
182
+ {
183
+ style: {
184
+ position: "absolute",
185
+ left: stage.x,
186
+ top: stage.y,
187
+ width: capture.width * stage.scale,
188
+ height: capture.height * stage.scale,
189
+ borderRadius: 18,
190
+ boxShadow: "0 28px 90px rgba(0,0,0,.35)",
191
+ overflow: "hidden"
192
+ }
193
+ },
194
+ React.createElement(
195
+ "div",
196
+ {
197
+ style: {
198
+ position: "absolute",
199
+ width: capture.width,
200
+ height: capture.height,
201
+ transform: cameraTransform(camera, { ...stage, x: 0, y: 0 }),
202
+ transformOrigin: "0 0"
203
+ }
204
+ },
205
+ recordingSrc ? React.createElement(OffthreadVideo, {
206
+ muted: true,
207
+ src: staticFile(recordingSrc),
208
+ style: imageStyle(1, capture)
209
+ }) : null,
210
+ recordingSrc ? null : media.previousSrc && media.previousOpacity > 0 ? React.createElement(Img, {
211
+ src: media.previousSrc,
212
+ style: imageStyle(media.previousOpacity, capture)
213
+ }) : null,
214
+ !recordingSrc && media.currentSrc ? React.createElement(Img, {
215
+ src: media.currentSrc,
216
+ style: imageStyle(media.currentOpacity, capture)
217
+ }) : null
218
+ )
219
+ );
220
+ }
221
+
222
+ // src/overlays.ts
223
+ import React2 from "react";
224
+ import { interpolate as interpolate3, Sequence } from "remotion";
225
+
226
+ // src/components/remocn/soft-blur-in.tsx
227
+ import { Easing, interpolate as interpolate2, useCurrentFrame } from "remotion";
228
+ import { jsx } from "react/jsx-runtime";
229
+ function SoftBlurIn({
230
+ text,
231
+ blur = 12,
232
+ fontSize = 72,
233
+ color = "#171717",
234
+ fontWeight = 600,
235
+ speed = 1,
236
+ className
237
+ }) {
238
+ const frame = useCurrentFrame() * speed;
239
+ const chars = Array.from(text);
240
+ const charDurationFrames = 27;
241
+ const staggerFrames = 1;
242
+ return /* @__PURE__ */ jsx(
243
+ "div",
244
+ {
245
+ style: {
246
+ position: "absolute",
247
+ inset: 0,
248
+ display: "flex",
249
+ alignItems: "center",
250
+ justifyContent: "center",
251
+ background: "transparent"
252
+ },
253
+ children: /* @__PURE__ */ jsx(
254
+ "span",
255
+ {
256
+ className,
257
+ style: {
258
+ fontSize,
259
+ fontWeight,
260
+ color,
261
+ letterSpacing: "-0.05em",
262
+ fontFamily: "var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif"
263
+ },
264
+ children: chars.map((char, i) => {
265
+ const local = frame - i * staggerFrames;
266
+ const easing = Easing.bezier(0.22, 1, 0.36, 1);
267
+ const opacity = interpolate2(local, [0, charDurationFrames], [0, 1], {
268
+ extrapolateLeft: "clamp",
269
+ extrapolateRight: "clamp",
270
+ easing
271
+ });
272
+ const y = interpolate2(local, [0, charDurationFrames], [16, 0], {
273
+ extrapolateLeft: "clamp",
274
+ extrapolateRight: "clamp",
275
+ easing
276
+ });
277
+ const blurAmount = interpolate2(
278
+ local,
279
+ [0, charDurationFrames],
280
+ [blur, 0],
281
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing }
282
+ );
283
+ return /* @__PURE__ */ jsx(
284
+ "span",
285
+ {
286
+ style: {
287
+ display: "inline-block",
288
+ whiteSpace: "pre",
289
+ backfaceVisibility: "hidden",
290
+ transformOrigin: "50% 55%",
291
+ opacity,
292
+ transform: `translateY(${y}px)`,
293
+ filter: `blur(${blurAmount}px)`
294
+ },
295
+ children: char
296
+ },
297
+ i
298
+ );
299
+ })
300
+ }
301
+ )
302
+ }
303
+ );
304
+ }
305
+
306
+ // src/overlays.ts
307
+ function overlayOpacity(overlay, frame) {
308
+ return interpolate3(
309
+ frame,
310
+ [
311
+ overlay.fromFrame,
312
+ overlay.fromFrame + 12,
313
+ overlay.fromFrame + overlay.durationInFrames - 12,
314
+ overlay.fromFrame + overlay.durationInFrames
315
+ ],
316
+ [0, 1, 1, 0],
317
+ { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
318
+ );
319
+ }
320
+ function calloutStyle(box, opacity, backgroundColor) {
321
+ return {
322
+ position: "absolute",
323
+ left: Math.min(box.x + box.width + 24, 1920 - 460),
324
+ top: Math.max(42, box.y),
325
+ width: 380,
326
+ padding: 22,
327
+ borderRadius: 14,
328
+ backgroundColor,
329
+ color: "white",
330
+ boxShadow: "0 24px 70px rgba(0,0,0,.32)",
331
+ opacity
332
+ };
333
+ }
334
+ function OverlayLayer({
335
+ camera,
336
+ frame,
337
+ registry,
338
+ stage,
339
+ timeline
340
+ }) {
341
+ return React2.createElement(
342
+ React2.Fragment,
343
+ null,
344
+ ...timeline.overlays.filter((overlay) => active(overlay, frame)).map((overlay) => {
345
+ if (overlay.kind === "caption") {
346
+ const opacity2 = overlayOpacity(overlay, frame);
347
+ const Component2 = resolveCaptionComponent(
348
+ registry,
349
+ overlay.renderer
350
+ );
351
+ return React2.createElement(Component2, {
352
+ key: overlay.id,
353
+ opacity: opacity2,
354
+ overlay
355
+ });
356
+ }
357
+ if (overlay.kind === "visual") {
358
+ const Component2 = resolveVisualComponent(registry, overlay.visual);
359
+ return React2.createElement(
360
+ Sequence,
361
+ {
362
+ key: overlay.id,
363
+ from: overlay.fromFrame,
364
+ durationInFrames: overlay.durationInFrames,
365
+ name: overlay.visual
366
+ },
367
+ React2.createElement(Component2, overlay.props)
368
+ );
369
+ }
370
+ const opacity = overlayOpacity(overlay, frame);
371
+ const rawBox = overlay.boundingBox ?? {
372
+ x: 40,
373
+ y: 40,
374
+ width: 0,
375
+ height: 0
376
+ };
377
+ const box = transformedBox(rawBox, camera, stage);
378
+ const Component = resolveCalloutComponent(registry, overlay.renderer);
379
+ return React2.createElement(Component, {
380
+ box,
381
+ key: overlay.id,
382
+ opacity,
383
+ overlay
384
+ });
385
+ })
386
+ );
387
+ }
388
+ function resolveCaptionComponent(registry, renderer) {
389
+ if (!renderer) return registry.captions["motion.caption"] ?? Caption;
390
+ const component = registry.captions[renderer];
391
+ if (component) return component;
392
+ throw unknownRenderer("caption", renderer, Object.keys(registry.captions));
393
+ }
394
+ function resolveCalloutComponent(registry, renderer) {
395
+ if (!renderer) return registry.callouts["motion.callout"] ?? Callout;
396
+ const component = registry.callouts[renderer];
397
+ if (component) return component;
398
+ throw unknownRenderer("callout", renderer, Object.keys(registry.callouts));
399
+ }
400
+ function resolveVisualComponent(registry, visual) {
401
+ const component = registry.visuals[visual];
402
+ if (component) return component;
403
+ throw unknownRenderer("visual", visual, Object.keys(registry.visuals));
404
+ }
405
+ function unknownRenderer(kind, renderer, registered) {
406
+ return new Error(
407
+ `Unknown ${kind} renderer "${renderer}". Registered renderers: ${registered.sort().join(", ") || "none"}.`
408
+ );
409
+ }
410
+ function Caption({ opacity, overlay }) {
411
+ return React2.createElement(
412
+ "div",
413
+ {
414
+ style: {
415
+ position: "absolute",
416
+ left: "50%",
417
+ bottom: 74,
418
+ transform: "translateX(-50%)",
419
+ maxWidth: 980,
420
+ padding: "18px 26px",
421
+ borderRadius: 14,
422
+ backgroundColor: "rgba(10,14,20,.84)",
423
+ color: "white",
424
+ font: "500 34px Inter, system-ui, sans-serif",
425
+ opacity
426
+ }
427
+ },
428
+ overlay.text
429
+ );
430
+ }
431
+ function KineticCaption({ opacity, overlay }) {
432
+ return React2.createElement(
433
+ "div",
434
+ {
435
+ style: {
436
+ position: "absolute",
437
+ left: "50%",
438
+ bottom: 96,
439
+ transform: `translateX(-50%) scale(${0.96 + opacity * 0.04})`,
440
+ width: 980,
441
+ maxWidth: 980,
442
+ height: 76,
443
+ padding: "16px 24px",
444
+ borderRadius: 16,
445
+ backgroundColor: "rgba(10,14,20,.72)",
446
+ color: "white",
447
+ font: "800 38px Inter, system-ui, sans-serif",
448
+ letterSpacing: 0,
449
+ opacity,
450
+ textShadow: "0 18px 50px rgba(0,0,0,.42)"
451
+ }
452
+ },
453
+ React2.createElement(SoftBlurIn, {
454
+ blur: 10,
455
+ color: "white",
456
+ fontSize: 38,
457
+ fontWeight: 800,
458
+ speed: 1.2,
459
+ text: overlay.text
460
+ })
461
+ );
462
+ }
463
+ function Callout({ box, opacity, overlay }) {
464
+ return React2.createElement(
465
+ "div",
466
+ {
467
+ style: calloutStyle(box, opacity, "rgba(10,14,20,.88)")
468
+ },
469
+ React2.createElement(
470
+ "strong",
471
+ { style: { display: "block", color: "#79e3c7", fontSize: 26 } },
472
+ overlay.title
473
+ ),
474
+ overlay.description ? React2.createElement(
475
+ "p",
476
+ { style: { margin: "10px 0 0", fontSize: 21, lineHeight: 1.45 } },
477
+ overlay.description
478
+ ) : null
479
+ );
480
+ }
481
+ function GlassCallout({
482
+ box,
483
+ opacity,
484
+ overlay
485
+ }) {
486
+ return React2.createElement(
487
+ "div",
488
+ {
489
+ style: {
490
+ ...calloutStyle(box, opacity, "rgba(255,255,255,.86)"),
491
+ color: "#121722",
492
+ fontFamily: "Inter, system-ui, sans-serif"
493
+ }
494
+ },
495
+ React2.createElement(
496
+ "strong",
497
+ { style: { display: "block", color: "#185eaa", fontSize: 26 } },
498
+ overlay.title
499
+ ),
500
+ overlay.description ? React2.createElement(
501
+ "p",
502
+ { style: { margin: "10px 0 0", fontSize: 21, lineHeight: 1.45 } },
503
+ overlay.description
504
+ ) : null
505
+ );
506
+ }
507
+ var defaultVisualRegistry = {
508
+ captions: {
509
+ "motion.caption": Caption,
510
+ "remocn.kinetic-title": KineticCaption
511
+ },
512
+ callouts: {
513
+ "motion.callout": Callout,
514
+ "remocn.glass-callout": GlassCallout
515
+ },
516
+ visuals: {}
517
+ };
518
+
519
+ // src/composition.ts
520
+ import React4 from "react";
521
+ import { AbsoluteFill, useCurrentFrame as useCurrentFrame2, useVideoConfig } from "remotion";
522
+
523
+ // src/cursor.ts
524
+ import React3 from "react";
525
+ import { interpolate as interpolate4 } from "remotion";
526
+ var CLICK_PULSE_FRAMES = 28;
527
+ function TargetAndCursorLayer({
528
+ camera,
529
+ frame,
530
+ stage,
531
+ timeline
532
+ }) {
533
+ return React3.createElement(
534
+ "div",
535
+ {
536
+ style: {
537
+ position: "absolute",
538
+ width: 1440,
539
+ height: 900,
540
+ transform: cameraTransform(camera, stage),
541
+ transformOrigin: "0 0"
542
+ }
543
+ },
544
+ ...timeline.cursor.filter((track) => track.point && frame >= track.fromFrame).map(
545
+ (track) => React3.createElement(ClickRipple, {
546
+ key: track.id,
547
+ track,
548
+ frame
549
+ })
550
+ )
551
+ );
552
+ }
553
+ function ClickRipple({
554
+ track,
555
+ frame
556
+ }) {
557
+ if (!track.point) return null;
558
+ const local = frame - track.fromFrame;
559
+ if (local >= CLICK_PULSE_FRAMES) return null;
560
+ const progress = local / CLICK_PULSE_FRAMES;
561
+ const dotOpacity = interpolate4(progress, [0, 0.2, 0.7, 1], [0, 1, 1, 0], {
562
+ extrapolateLeft: "clamp",
563
+ extrapolateRight: "clamp"
564
+ });
565
+ const ringScale = interpolate4(progress, [0, 1], [0.4, 2.2], {
566
+ extrapolateRight: "clamp"
567
+ });
568
+ const ringOpacity = interpolate4(progress, [0, 0.3, 1], [0, 0.55, 0], {
569
+ extrapolateLeft: "clamp",
570
+ extrapolateRight: "clamp"
571
+ });
572
+ const size = 26;
573
+ return React3.createElement(
574
+ "div",
575
+ {
576
+ style: {
577
+ position: "absolute",
578
+ left: track.point.x,
579
+ top: track.point.y,
580
+ width: 0,
581
+ height: 0
582
+ }
583
+ },
584
+ React3.createElement("div", {
585
+ style: {
586
+ position: "absolute",
587
+ left: -size / 2,
588
+ top: -size / 2,
589
+ width: size,
590
+ height: size,
591
+ borderRadius: 999,
592
+ backgroundColor: "#79e3c7",
593
+ opacity: dotOpacity,
594
+ boxShadow: "0 0 0 6px rgba(121,227,199,.25)"
595
+ }
596
+ }),
597
+ React3.createElement("div", {
598
+ style: {
599
+ position: "absolute",
600
+ left: -size / 2,
601
+ top: -size / 2,
602
+ width: size,
603
+ height: size,
604
+ borderRadius: 999,
605
+ border: "3px solid #79e3c7",
606
+ transform: `scale(${ringScale})`,
607
+ opacity: ringOpacity
608
+ }
609
+ })
610
+ );
611
+ }
612
+
613
+ // src/composition.ts
614
+ var defaultProductDemoProps = createProductDemoVideoProps({
615
+ manifest: {
616
+ schemaVersion: "1",
617
+ demoId: "demo",
618
+ steps: [],
619
+ diagnostics: []
620
+ },
621
+ timeline: {
622
+ schemaVersion: "1",
623
+ demoId: "demo",
624
+ fps: 60,
625
+ durationInFrames: 1,
626
+ scenes: [],
627
+ camera: [],
628
+ cursor: [],
629
+ overlays: []
630
+ },
631
+ screenshotSrcByStepId: {}
632
+ });
633
+ function ProductDemoVideo(props) {
634
+ const frame = useCurrentFrame2();
635
+ const { width, height } = useVideoConfig();
636
+ const capture = props.manifest.capture ? {
637
+ width: props.manifest.capture.width,
638
+ height: props.manifest.capture.height,
639
+ deviceScaleFactor: props.manifest.capture.deviceScaleFactor
640
+ } : void 0;
641
+ const stage = stageLayout(width, height, capture);
642
+ const camera = cameraStateAt(props.timeline, frame);
643
+ const steps = props.timeline.scenes.flatMap((scene) => scene.steps);
644
+ const registry = props.registry ?? defaultVisualRegistry;
645
+ return React4.createElement(
646
+ AbsoluteFill,
647
+ { style: { backgroundColor: "#10131a" } },
648
+ React4.createElement(Backdrop),
649
+ React4.createElement(StageMedia, {
650
+ camera,
651
+ capture,
652
+ frame,
653
+ recordingSrc: props.recordingSrc,
654
+ screenshotSrcByStepId: props.screenshotSrcByStepId,
655
+ stage,
656
+ steps
657
+ }),
658
+ React4.createElement(TargetAndCursorLayer, {
659
+ camera,
660
+ frame,
661
+ stage,
662
+ timeline: props.timeline
663
+ }),
664
+ React4.createElement(OverlayLayer, {
665
+ camera,
666
+ frame,
667
+ registry,
668
+ stage,
669
+ timeline: props.timeline
670
+ })
671
+ );
672
+ }
673
+
674
+ export {
675
+ stageLayout,
676
+ OverlayLayer,
677
+ Caption,
678
+ KineticCaption,
679
+ Callout,
680
+ GlassCallout,
681
+ defaultVisualRegistry,
682
+ defaultProductDemoProps,
683
+ ProductDemoVideo
684
+ };