agentreel 0.3.4 → 0.4.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/README.md +49 -23
- package/bin/agentreel.mjs +263 -36
- package/package.json +1 -1
- package/scripts/browser_demo.py +10 -4
- package/scripts/cli_demo.py +31 -6
- package/src/CastVideo.tsx +187 -215
- package/src/Root.tsx +18 -7
- package/src/types.ts +1 -0
package/src/CastVideo.tsx
CHANGED
|
@@ -20,19 +20,18 @@ const TERM_BG = "#282a36";
|
|
|
20
20
|
const TITLE_BAR = "#1e1f29";
|
|
21
21
|
const CURSOR_COLOR = "#f8f8f2";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const TRANSITION_DUR = 0.5;
|
|
27
|
-
const END_DUR = 3.5;
|
|
23
|
+
// Mode-specific timing (synced with Root.tsx calculateMetadata)
|
|
24
|
+
const REEL_TIMING = { title: 2.5, termHighlight: 4.5, browserHighlight: 7.0, transition: 0.5, end: 3.5 };
|
|
25
|
+
const DEMO_TIMING = { title: 2.0, termHighlight: 12.0, browserHighlight: 10.0, transition: 0.4, end: 3.0 };
|
|
28
26
|
|
|
29
27
|
const VIEWPORT_W = 1280;
|
|
30
28
|
const VIEWPORT_H = 800;
|
|
31
29
|
const VIDEO_AREA_W = 880;
|
|
32
30
|
const VIDEO_AREA_H = 550; // 880 * 10/16
|
|
33
31
|
|
|
34
|
-
function getHighlightDuration(h: Highlight): number {
|
|
35
|
-
|
|
32
|
+
function getHighlightDuration(h: Highlight, mode: "reel" | "demo" = "reel"): number {
|
|
33
|
+
const t = mode === "demo" ? DEMO_TIMING : REEL_TIMING;
|
|
34
|
+
return h.videoSrc ? t.browserHighlight : t.termHighlight;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
const SANS =
|
|
@@ -87,17 +86,21 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
87
86
|
endText,
|
|
88
87
|
endUrl,
|
|
89
88
|
gradient,
|
|
89
|
+
mode: rawMode,
|
|
90
90
|
}) => {
|
|
91
91
|
const frame = useCurrentFrame();
|
|
92
92
|
const { fps, durationInFrames } = useVideoConfig();
|
|
93
|
+
const mode = rawMode || "reel";
|
|
94
|
+
const isDemo = mode === "demo";
|
|
95
|
+
const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
|
|
93
96
|
const g = gradient || ["#0f0f1a", "#1a0f2e"];
|
|
94
97
|
|
|
95
|
-
const titleFrames = Math.round(
|
|
96
|
-
const endFrames = Math.round(
|
|
98
|
+
const titleFrames = Math.round(timing.title * fps);
|
|
99
|
+
const endFrames = Math.round(timing.end * fps);
|
|
97
100
|
|
|
98
101
|
// Compute per-highlight durations and cumulative offsets
|
|
99
102
|
const hlDurations = highlights.map((h) =>
|
|
100
|
-
Math.round(getHighlightDuration(h) * fps)
|
|
103
|
+
Math.round(getHighlightDuration(h, mode) * fps)
|
|
101
104
|
);
|
|
102
105
|
const hlOffsets: number[] = [];
|
|
103
106
|
let cumulative = 0;
|
|
@@ -107,9 +110,11 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
// Animated gradient — hue rotates slowly over time
|
|
110
|
-
const gradAngle =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
const gradAngle = isDemo
|
|
114
|
+
? 145 // static angle for demo
|
|
115
|
+
: interpolate(frame, [0, durationInFrames], [125, 200], {
|
|
116
|
+
extrapolateRight: "clamp",
|
|
117
|
+
});
|
|
113
118
|
|
|
114
119
|
return (
|
|
115
120
|
<AbsoluteFill
|
|
@@ -118,17 +123,18 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
118
123
|
backgroundSize: "200% 200%",
|
|
119
124
|
}}
|
|
120
125
|
>
|
|
121
|
-
{/* Subtle animated glow blobs
|
|
122
|
-
<AnimatedBackground frame={frame} duration={durationInFrames} />
|
|
126
|
+
{/* Subtle animated glow blobs — reel only */}
|
|
127
|
+
{!isDemo && <AnimatedBackground frame={frame} duration={durationInFrames} />}
|
|
123
128
|
|
|
124
|
-
|
|
129
|
+
{/* Music — reel only */}
|
|
130
|
+
{!isDemo && <MusicTrack />}
|
|
125
131
|
|
|
126
132
|
<Sequence durationInFrames={titleFrames}>
|
|
127
|
-
<TitleCard title={title} subtitle={subtitle} />
|
|
133
|
+
<TitleCard title={title} subtitle={subtitle} isDemo={isDemo} />
|
|
128
134
|
</Sequence>
|
|
129
135
|
|
|
130
136
|
{highlights.map((h, i) => {
|
|
131
|
-
const dur = getHighlightDuration(h);
|
|
137
|
+
const dur = getHighlightDuration(h, mode);
|
|
132
138
|
return (
|
|
133
139
|
<Sequence
|
|
134
140
|
key={i}
|
|
@@ -142,6 +148,7 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
142
148
|
total={highlights.length}
|
|
143
149
|
transition={TRANSITIONS[i % TRANSITIONS.length]}
|
|
144
150
|
durationSec={dur}
|
|
151
|
+
isDemo={isDemo}
|
|
145
152
|
/>
|
|
146
153
|
) : (
|
|
147
154
|
<HighlightClip
|
|
@@ -150,6 +157,7 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
150
157
|
total={highlights.length}
|
|
151
158
|
transition={TRANSITIONS[i % TRANSITIONS.length]}
|
|
152
159
|
durationSec={dur}
|
|
160
|
+
isDemo={isDemo}
|
|
153
161
|
/>
|
|
154
162
|
)}
|
|
155
163
|
</Sequence>
|
|
@@ -160,7 +168,7 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
160
168
|
from={titleFrames + cumulative}
|
|
161
169
|
durationInFrames={endFrames}
|
|
162
170
|
>
|
|
163
|
-
<EndCard text={endText || title} url={endUrl} />
|
|
171
|
+
<EndCard text={endText || title} url={endUrl} isDemo={isDemo} />
|
|
164
172
|
</Sequence>
|
|
165
173
|
</AbsoluteFill>
|
|
166
174
|
);
|
|
@@ -408,12 +416,14 @@ const TextOverlay: React.FC<{ text: string; durationSec: number }> = ({
|
|
|
408
416
|
|
|
409
417
|
// ─── Title Card ───────────────────────────────────────────
|
|
410
418
|
|
|
411
|
-
const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
|
|
419
|
+
const TitleCard: React.FC<{ title: string; subtitle?: string; isDemo?: boolean }> = ({
|
|
412
420
|
title,
|
|
413
421
|
subtitle,
|
|
422
|
+
isDemo,
|
|
414
423
|
}) => {
|
|
415
424
|
const frame = useCurrentFrame();
|
|
416
425
|
const { fps } = useVideoConfig();
|
|
426
|
+
const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
|
|
417
427
|
|
|
418
428
|
const titleSpring = spring({ fps, frame, config: { damping: 14 } });
|
|
419
429
|
const subSpring = spring({
|
|
@@ -423,11 +433,11 @@ const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
|
|
|
423
433
|
});
|
|
424
434
|
const fadeOut = interpolate(
|
|
425
435
|
frame,
|
|
426
|
-
[fps * (
|
|
436
|
+
[fps * (timing.title - timing.transition), fps * timing.title],
|
|
427
437
|
[1, 0],
|
|
428
438
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
429
439
|
);
|
|
430
|
-
const titleZoom = interpolate(frame, [0, fps *
|
|
440
|
+
const titleZoom = isDemo ? 1 : interpolate(frame, [0, fps * timing.title], [1, 1.08], {
|
|
431
441
|
extrapolateLeft: "clamp",
|
|
432
442
|
extrapolateRight: "clamp",
|
|
433
443
|
});
|
|
@@ -449,7 +459,7 @@ const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
|
|
|
449
459
|
<div
|
|
450
460
|
style={{
|
|
451
461
|
fontFamily: SANS,
|
|
452
|
-
fontSize: 76,
|
|
462
|
+
fontSize: isDemo ? 56 : 76,
|
|
453
463
|
fontWeight: 800,
|
|
454
464
|
color: WHITE,
|
|
455
465
|
letterSpacing: -3,
|
|
@@ -463,7 +473,7 @@ const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
|
|
|
463
473
|
transform: `translateY(${interpolate(subSpring, [0, 1], [20, 0])}px)`,
|
|
464
474
|
opacity: subSpring,
|
|
465
475
|
fontFamily: SANS,
|
|
466
|
-
fontSize: 28,
|
|
476
|
+
fontSize: isDemo ? 24 : 28,
|
|
467
477
|
color: DIM,
|
|
468
478
|
marginTop: 16,
|
|
469
479
|
letterSpacing: 1,
|
|
@@ -485,82 +495,79 @@ const HighlightClip: React.FC<{
|
|
|
485
495
|
total: number;
|
|
486
496
|
transition: TransitionStyle;
|
|
487
497
|
durationSec: number;
|
|
488
|
-
|
|
498
|
+
isDemo?: boolean;
|
|
499
|
+
}> = ({ highlight, index, total, transition, durationSec, isDemo }) => {
|
|
489
500
|
const frame = useCurrentFrame();
|
|
490
|
-
const { fps } = useVideoConfig();
|
|
501
|
+
const { fps, width } = useVideoConfig();
|
|
502
|
+
const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
|
|
491
503
|
|
|
492
|
-
// Entry animation —
|
|
504
|
+
// Entry animation — simpler in demo mode
|
|
493
505
|
const enterSpring = spring({
|
|
494
506
|
fps,
|
|
495
507
|
frame,
|
|
496
|
-
config: { damping: 18, stiffness: 80 },
|
|
508
|
+
config: isDemo ? { damping: 22, stiffness: 120 } : { damping: 18, stiffness: 80 },
|
|
497
509
|
});
|
|
498
|
-
const entry =
|
|
510
|
+
const entry = isDemo
|
|
511
|
+
? { scale: 1, x: 0, y: 0 } // no transform in demo
|
|
512
|
+
: getEntryTransform(transition, enterSpring);
|
|
499
513
|
|
|
500
514
|
// Fade transitions
|
|
501
|
-
const fadeIn = interpolate(frame, [0, fps *
|
|
515
|
+
const fadeIn = interpolate(frame, [0, fps * timing.transition], [0, 1], {
|
|
502
516
|
extrapolateLeft: "clamp",
|
|
503
517
|
extrapolateRight: "clamp",
|
|
504
518
|
});
|
|
505
519
|
const fadeOut = interpolate(
|
|
506
520
|
frame,
|
|
507
|
-
[fps * (durationSec -
|
|
521
|
+
[fps * (durationSec - timing.transition), fps * durationSec],
|
|
508
522
|
[1, 0],
|
|
509
523
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
510
524
|
);
|
|
511
525
|
const opacity = Math.min(fadeIn, fadeOut);
|
|
512
526
|
|
|
513
|
-
// Zoom
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
[1, 1.12],
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
easing: Easing.
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
{
|
|
529
|
-
extrapolateLeft: "clamp",
|
|
530
|
-
extrapolateRight: "clamp",
|
|
531
|
-
easing: Easing.inOut(Easing.cubic),
|
|
532
|
-
}
|
|
533
|
-
);
|
|
534
|
-
const zoom = frame < fps * 2.5 ? zoomIn : zoomOut;
|
|
527
|
+
// Zoom/pan — reel only
|
|
528
|
+
let zoom = 1;
|
|
529
|
+
let panY = 0;
|
|
530
|
+
if (!isDemo) {
|
|
531
|
+
const zoomIn = interpolate(frame, [fps * 0.8, fps * 2.0], [1, 1.12], {
|
|
532
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic),
|
|
533
|
+
});
|
|
534
|
+
const zoomOut = interpolate(frame, [fps * 2.5, fps * (durationSec - 0.5)], [1.12, 1.02], {
|
|
535
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic),
|
|
536
|
+
});
|
|
537
|
+
zoom = frame < fps * 2.5 ? zoomIn : zoomOut;
|
|
538
|
+
panY = interpolate(frame, [fps * 0.8, fps * 2.0, fps * (durationSec - 1.0)], [0, -15, 5], {
|
|
539
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
535
542
|
|
|
536
|
-
//
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
[fps * 0.8, fps * 2.0, fps * (durationSec - 1.0)],
|
|
540
|
-
[0, -15, 5],
|
|
541
|
-
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
542
|
-
);
|
|
543
|
+
// Line timing — faster reveal in demo (more lines to show)
|
|
544
|
+
const lineDelay = isDemo ? fps * 0.08 : fps * 0.15;
|
|
545
|
+
const firstLineFrame = isDemo ? fps * 0.25 : fps * 0.35;
|
|
543
546
|
|
|
544
|
-
|
|
545
|
-
const lineDelay = fps * 0.15;
|
|
546
|
-
const firstLineFrame = fps * 0.35;
|
|
547
|
+
const lines = highlight.lines || [];
|
|
547
548
|
|
|
548
549
|
// Cursor tracking
|
|
549
|
-
const lastVisibleLineIdx =
|
|
550
|
+
const lastVisibleLineIdx = lines.findIndex((_, i) => {
|
|
550
551
|
return frame < firstLineFrame + (i + 1) * lineDelay;
|
|
551
552
|
});
|
|
552
553
|
const cursorLineIdx =
|
|
553
554
|
lastVisibleLineIdx === -1
|
|
554
|
-
?
|
|
555
|
+
? lines.length - 1
|
|
555
556
|
: Math.max(0, lastVisibleLineIdx - 1);
|
|
556
557
|
|
|
558
|
+
// Terminal sizing — wider in demo mode (landscape)
|
|
559
|
+
const termWidth = isDemo ? width - 160 : 820;
|
|
560
|
+
const termFontSize = isDemo ? 14 : 16;
|
|
561
|
+
const termMinHeight = isDemo ? 500 : 280;
|
|
562
|
+
const termPadding = isDemo ? "16px 20px" : "20px 24px";
|
|
563
|
+
|
|
557
564
|
return (
|
|
558
565
|
<AbsoluteFill style={{ opacity }}>
|
|
559
|
-
{/*
|
|
566
|
+
{/* Chapter label */}
|
|
560
567
|
<div
|
|
561
568
|
style={{
|
|
562
569
|
position: "absolute",
|
|
563
|
-
top: 45,
|
|
570
|
+
top: isDemo ? 24 : 45,
|
|
564
571
|
left: 0,
|
|
565
572
|
width: "100%",
|
|
566
573
|
textAlign: "center",
|
|
@@ -570,36 +577,31 @@ const HighlightClip: React.FC<{
|
|
|
570
577
|
<span
|
|
571
578
|
style={{
|
|
572
579
|
fontFamily: MONO,
|
|
573
|
-
fontSize: 13,
|
|
580
|
+
fontSize: isDemo ? 15 : 13,
|
|
574
581
|
color: ACCENT,
|
|
575
|
-
letterSpacing: 4,
|
|
582
|
+
letterSpacing: isDemo ? 3 : 4,
|
|
576
583
|
textTransform: "uppercase",
|
|
577
|
-
opacity: interpolate(enterSpring, [0, 1], [0, 0.6]),
|
|
584
|
+
opacity: interpolate(enterSpring, [0, 1], [0, isDemo ? 0.8 : 0.6]),
|
|
578
585
|
}}
|
|
579
586
|
>
|
|
580
587
|
{highlight.label}
|
|
581
588
|
</span>
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
i === index ? ACCENT : "rgba(255,255,255,0.12)",
|
|
599
|
-
}}
|
|
600
|
-
/>
|
|
601
|
-
))}
|
|
602
|
-
</div>
|
|
589
|
+
{/* Progress dots — reel only */}
|
|
590
|
+
{!isDemo && (
|
|
591
|
+
<div style={{ marginTop: 10, display: "flex", justifyContent: "center", gap: 8 }}>
|
|
592
|
+
{Array.from({ length: total }).map((_, i) => (
|
|
593
|
+
<div
|
|
594
|
+
key={i}
|
|
595
|
+
style={{
|
|
596
|
+
width: i === index ? 24 : 8,
|
|
597
|
+
height: 6,
|
|
598
|
+
borderRadius: 3,
|
|
599
|
+
backgroundColor: i === index ? ACCENT : "rgba(255,255,255,0.12)",
|
|
600
|
+
}}
|
|
601
|
+
/>
|
|
602
|
+
))}
|
|
603
|
+
</div>
|
|
604
|
+
)}
|
|
603
605
|
</div>
|
|
604
606
|
|
|
605
607
|
{/* Terminal window */}
|
|
@@ -607,20 +609,23 @@ const HighlightClip: React.FC<{
|
|
|
607
609
|
style={{
|
|
608
610
|
justifyContent: "center",
|
|
609
611
|
alignItems: "center",
|
|
610
|
-
padding: 40,
|
|
611
|
-
paddingTop: 100,
|
|
612
|
-
paddingBottom: 100,
|
|
612
|
+
padding: isDemo ? 20 : 40,
|
|
613
|
+
paddingTop: isDemo ? 60 : 100,
|
|
614
|
+
paddingBottom: isDemo ? 40 : 100,
|
|
613
615
|
}}
|
|
614
616
|
>
|
|
615
617
|
<div
|
|
616
618
|
style={{
|
|
617
|
-
transform:
|
|
619
|
+
transform: isDemo
|
|
620
|
+
? `scale(${enterSpring})`
|
|
621
|
+
: `scale(${entry.scale * zoom}) translate(${entry.x}px, ${entry.y + panY}px)`,
|
|
618
622
|
transformOrigin: "center center",
|
|
619
|
-
width:
|
|
620
|
-
borderRadius: 14,
|
|
623
|
+
width: termWidth,
|
|
624
|
+
borderRadius: isDemo ? 10 : 14,
|
|
621
625
|
overflow: "hidden",
|
|
622
|
-
boxShadow:
|
|
623
|
-
"0
|
|
626
|
+
boxShadow: isDemo
|
|
627
|
+
? "0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04)"
|
|
628
|
+
: "0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
624
629
|
}}
|
|
625
630
|
>
|
|
626
631
|
{/* macOS title bar */}
|
|
@@ -633,30 +638,9 @@ const HighlightClip: React.FC<{
|
|
|
633
638
|
gap: 8,
|
|
634
639
|
}}
|
|
635
640
|
>
|
|
636
|
-
<div
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
height: 12,
|
|
640
|
-
borderRadius: 6,
|
|
641
|
-
backgroundColor: "#ff5555",
|
|
642
|
-
}}
|
|
643
|
-
/>
|
|
644
|
-
<div
|
|
645
|
-
style={{
|
|
646
|
-
width: 12,
|
|
647
|
-
height: 12,
|
|
648
|
-
borderRadius: 6,
|
|
649
|
-
backgroundColor: "#f1fa8c",
|
|
650
|
-
}}
|
|
651
|
-
/>
|
|
652
|
-
<div
|
|
653
|
-
style={{
|
|
654
|
-
width: 12,
|
|
655
|
-
height: 12,
|
|
656
|
-
borderRadius: 6,
|
|
657
|
-
backgroundColor: "#50fa7b",
|
|
658
|
-
}}
|
|
659
|
-
/>
|
|
641
|
+
<div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#ff5555" }} />
|
|
642
|
+
<div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#f1fa8c" }} />
|
|
643
|
+
<div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#50fa7b" }} />
|
|
660
644
|
<div style={{ flex: 1 }} />
|
|
661
645
|
</div>
|
|
662
646
|
|
|
@@ -664,11 +648,11 @@ const HighlightClip: React.FC<{
|
|
|
664
648
|
<div
|
|
665
649
|
style={{
|
|
666
650
|
backgroundColor: TERM_BG,
|
|
667
|
-
padding:
|
|
668
|
-
minHeight:
|
|
651
|
+
padding: termPadding,
|
|
652
|
+
minHeight: termMinHeight,
|
|
669
653
|
}}
|
|
670
654
|
>
|
|
671
|
-
{
|
|
655
|
+
{lines.map((line, lineIdx) => {
|
|
672
656
|
const lineFrame = firstLineFrame + lineIdx * lineDelay;
|
|
673
657
|
const lineSpring = spring({
|
|
674
658
|
fps,
|
|
@@ -676,19 +660,18 @@ const HighlightClip: React.FC<{
|
|
|
676
660
|
config: { damping: 20, stiffness: 120 },
|
|
677
661
|
});
|
|
678
662
|
const lineOpacity = interpolate(lineSpring, [0, 1], [0, 1]);
|
|
679
|
-
const lineX = interpolate(lineSpring, [0, 1], [12, 0]);
|
|
663
|
+
const lineX = isDemo ? 0 : interpolate(lineSpring, [0, 1], [12, 0]);
|
|
680
664
|
|
|
681
665
|
// Strip leading "$ " from text — renderer adds its own $ prefix
|
|
682
666
|
const cleanText = line.isPrompt ? line.text.replace(/^\$\s*/, "") : line.text;
|
|
683
667
|
let displayText = cleanText;
|
|
684
668
|
let isTyping = false;
|
|
685
669
|
if (line.isPrompt) {
|
|
686
|
-
const
|
|
670
|
+
const typingDur = isDemo ? 0.8 : 0.6;
|
|
671
|
+
const typingEnd = lineFrame + fps * typingDur;
|
|
687
672
|
if (frame < typingEnd) {
|
|
688
673
|
const progress = interpolate(
|
|
689
|
-
frame,
|
|
690
|
-
[lineFrame, typingEnd],
|
|
691
|
-
[0, 1],
|
|
674
|
+
frame, [lineFrame, typingEnd], [0, 1],
|
|
692
675
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
693
676
|
);
|
|
694
677
|
const chars = Math.floor(progress * cleanText.length);
|
|
@@ -697,14 +680,11 @@ const HighlightClip: React.FC<{
|
|
|
697
680
|
}
|
|
698
681
|
}
|
|
699
682
|
|
|
700
|
-
const isZoomed = highlight.zoomLine === lineIdx;
|
|
683
|
+
const isZoomed = !isDemo && highlight.zoomLine === lineIdx;
|
|
701
684
|
const lineZoom = isZoomed
|
|
702
|
-
? interpolate(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
[1, 1.05],
|
|
706
|
-
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
707
|
-
)
|
|
685
|
+
? interpolate(frame, [lineFrame + fps * 0.5, lineFrame + fps * 1], [1, 1.05], {
|
|
686
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
687
|
+
})
|
|
708
688
|
: 1;
|
|
709
689
|
|
|
710
690
|
const showCursor = lineIdx === cursorLineIdx;
|
|
@@ -717,8 +697,8 @@ const HighlightClip: React.FC<{
|
|
|
717
697
|
transform: `translateX(${lineX}px) scale(${lineZoom})`,
|
|
718
698
|
transformOrigin: "left center",
|
|
719
699
|
fontFamily: MONO,
|
|
720
|
-
fontSize:
|
|
721
|
-
lineHeight: 1.7,
|
|
700
|
+
fontSize: termFontSize,
|
|
701
|
+
lineHeight: isDemo ? 1.6 : 1.7,
|
|
722
702
|
color: line.dim ? DIM : line.color || WHITE,
|
|
723
703
|
fontWeight: line.bold ? 700 : 400,
|
|
724
704
|
whiteSpace: "pre",
|
|
@@ -738,11 +718,11 @@ const HighlightClip: React.FC<{
|
|
|
738
718
|
</div>
|
|
739
719
|
</AbsoluteFill>
|
|
740
720
|
|
|
741
|
-
{/* Mouse pointer —
|
|
742
|
-
<MousePointer />
|
|
721
|
+
{/* Mouse pointer — reel only */}
|
|
722
|
+
{!isDemo && <MousePointer />}
|
|
743
723
|
|
|
744
|
-
{/* Text overlay */}
|
|
745
|
-
{highlight.overlay && (
|
|
724
|
+
{/* Text overlay — reel only */}
|
|
725
|
+
{!isDemo && highlight.overlay && (
|
|
746
726
|
<TextOverlay text={highlight.overlay} durationSec={durationSec} />
|
|
747
727
|
)}
|
|
748
728
|
</AbsoluteFill>
|
|
@@ -757,7 +737,8 @@ const BrowserHighlightClip: React.FC<{
|
|
|
757
737
|
total: number;
|
|
758
738
|
transition: TransitionStyle;
|
|
759
739
|
durationSec: number;
|
|
760
|
-
|
|
740
|
+
isDemo?: boolean;
|
|
741
|
+
}> = ({ highlight, index, total, transition, durationSec, isDemo }) => {
|
|
761
742
|
const frame = useCurrentFrame();
|
|
762
743
|
const { fps } = useVideoConfig();
|
|
763
744
|
|
|
@@ -768,57 +749,50 @@ const BrowserHighlightClip: React.FC<{
|
|
|
768
749
|
});
|
|
769
750
|
const entry = getEntryTransform(transition, enterSpring);
|
|
770
751
|
|
|
771
|
-
const
|
|
752
|
+
const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
|
|
753
|
+
|
|
754
|
+
const fadeIn = interpolate(frame, [0, fps * timing.transition], [0, 1], {
|
|
772
755
|
extrapolateLeft: "clamp",
|
|
773
756
|
extrapolateRight: "clamp",
|
|
774
757
|
});
|
|
775
758
|
const fadeOut = interpolate(
|
|
776
759
|
frame,
|
|
777
|
-
[fps * (durationSec -
|
|
760
|
+
[fps * (durationSec - timing.transition), fps * durationSec],
|
|
778
761
|
[1, 0],
|
|
779
762
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
780
763
|
);
|
|
781
764
|
const opacity = Math.min(fadeIn, fadeOut);
|
|
782
765
|
|
|
783
|
-
// Focal zoom —
|
|
766
|
+
// Focal zoom — reel only
|
|
784
767
|
const fx = highlight.focusX ?? 0.5;
|
|
785
768
|
const fy = highlight.focusY ?? 0.5;
|
|
786
769
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
frame,
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
extrapolateRight: "clamp",
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
);
|
|
802
|
-
const focalZoom = frame < fps * 3.5 ? focalZoomIn : focalZoomOut;
|
|
803
|
-
|
|
804
|
-
// Entry pan
|
|
805
|
-
const panY = interpolate(
|
|
806
|
-
frame,
|
|
807
|
-
[fps * 1.0, fps * 3.0, fps * (durationSec - 1.0)],
|
|
808
|
-
[0, -10, 5],
|
|
809
|
-
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
810
|
-
);
|
|
770
|
+
let focalZoom = 1;
|
|
771
|
+
let panY = 0;
|
|
772
|
+
if (!isDemo) {
|
|
773
|
+
const focalZoomIn = interpolate(frame, [fps * 1.0, fps * 3.0], [1, 1.15], {
|
|
774
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic),
|
|
775
|
+
});
|
|
776
|
+
const focalZoomOut = interpolate(frame, [fps * 3.5, fps * (durationSec - 0.5)], [1.15, 1.02], {
|
|
777
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic),
|
|
778
|
+
});
|
|
779
|
+
focalZoom = frame < fps * 3.5 ? focalZoomIn : focalZoomOut;
|
|
780
|
+
panY = interpolate(frame, [fps * 1.0, fps * 3.0, fps * (durationSec - 1.0)], [0, -10, 5], {
|
|
781
|
+
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
|
782
|
+
});
|
|
783
|
+
}
|
|
811
784
|
|
|
812
785
|
const videoSrc = highlight.videoSrc!;
|
|
813
786
|
const startFrom = Math.round((highlight.videoStartSec || 0) * fps);
|
|
787
|
+
const browserWidth = isDemo ? 1600 : 880;
|
|
814
788
|
|
|
815
789
|
return (
|
|
816
790
|
<AbsoluteFill style={{ opacity }}>
|
|
817
|
-
{/*
|
|
791
|
+
{/* Chapter label */}
|
|
818
792
|
<div
|
|
819
793
|
style={{
|
|
820
794
|
position: "absolute",
|
|
821
|
-
top: 45,
|
|
795
|
+
top: isDemo ? 24 : 45,
|
|
822
796
|
left: 0,
|
|
823
797
|
width: "100%",
|
|
824
798
|
textAlign: "center",
|
|
@@ -828,36 +802,30 @@ const BrowserHighlightClip: React.FC<{
|
|
|
828
802
|
<span
|
|
829
803
|
style={{
|
|
830
804
|
fontFamily: MONO,
|
|
831
|
-
fontSize: 13,
|
|
805
|
+
fontSize: isDemo ? 15 : 13,
|
|
832
806
|
color: ACCENT,
|
|
833
|
-
letterSpacing: 4,
|
|
807
|
+
letterSpacing: isDemo ? 3 : 4,
|
|
834
808
|
textTransform: "uppercase",
|
|
835
|
-
opacity: interpolate(enterSpring, [0, 1], [0, 0.6]),
|
|
809
|
+
opacity: interpolate(enterSpring, [0, 1], [0, isDemo ? 0.8 : 0.6]),
|
|
836
810
|
}}
|
|
837
811
|
>
|
|
838
812
|
{highlight.label}
|
|
839
813
|
</span>
|
|
840
|
-
|
|
841
|
-
style={{
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
backgroundColor:
|
|
856
|
-
i === index ? ACCENT : "rgba(255,255,255,0.12)",
|
|
857
|
-
}}
|
|
858
|
-
/>
|
|
859
|
-
))}
|
|
860
|
-
</div>
|
|
814
|
+
{!isDemo && (
|
|
815
|
+
<div style={{ marginTop: 10, display: "flex", justifyContent: "center", gap: 8 }}>
|
|
816
|
+
{Array.from({ length: total }).map((_, i) => (
|
|
817
|
+
<div
|
|
818
|
+
key={i}
|
|
819
|
+
style={{
|
|
820
|
+
width: i === index ? 24 : 8,
|
|
821
|
+
height: 6,
|
|
822
|
+
borderRadius: 3,
|
|
823
|
+
backgroundColor: i === index ? ACCENT : "rgba(255,255,255,0.12)",
|
|
824
|
+
}}
|
|
825
|
+
/>
|
|
826
|
+
))}
|
|
827
|
+
</div>
|
|
828
|
+
)}
|
|
861
829
|
</div>
|
|
862
830
|
|
|
863
831
|
{/* Browser window */}
|
|
@@ -865,20 +833,23 @@ const BrowserHighlightClip: React.FC<{
|
|
|
865
833
|
style={{
|
|
866
834
|
justifyContent: "center",
|
|
867
835
|
alignItems: "center",
|
|
868
|
-
padding: 40,
|
|
869
|
-
paddingTop: 100,
|
|
870
|
-
paddingBottom: 100,
|
|
836
|
+
padding: isDemo ? 20 : 40,
|
|
837
|
+
paddingTop: isDemo ? 60 : 100,
|
|
838
|
+
paddingBottom: isDemo ? 40 : 100,
|
|
871
839
|
}}
|
|
872
840
|
>
|
|
873
841
|
<div
|
|
874
842
|
style={{
|
|
875
|
-
transform:
|
|
843
|
+
transform: isDemo
|
|
844
|
+
? `scale(${enterSpring})`
|
|
845
|
+
: `scale(${entry.scale}) translate(${entry.x}px, ${entry.y + panY}px)`,
|
|
876
846
|
transformOrigin: "center center",
|
|
877
|
-
width:
|
|
878
|
-
borderRadius: 14,
|
|
847
|
+
width: browserWidth,
|
|
848
|
+
borderRadius: isDemo ? 10 : 14,
|
|
879
849
|
overflow: "hidden",
|
|
880
|
-
boxShadow:
|
|
881
|
-
"0
|
|
850
|
+
boxShadow: isDemo
|
|
851
|
+
? "0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04)"
|
|
852
|
+
: "0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
882
853
|
}}
|
|
883
854
|
>
|
|
884
855
|
{/* Browser chrome */}
|
|
@@ -946,8 +917,8 @@ const BrowserHighlightClip: React.FC<{
|
|
|
946
917
|
</div>
|
|
947
918
|
</AbsoluteFill>
|
|
948
919
|
|
|
949
|
-
{/* Text overlay */}
|
|
950
|
-
{highlight.overlay && (
|
|
920
|
+
{/* Text overlay — reel only */}
|
|
921
|
+
{!isDemo && highlight.overlay && (
|
|
951
922
|
<TextOverlay text={highlight.overlay} durationSec={durationSec} />
|
|
952
923
|
)}
|
|
953
924
|
</AbsoluteFill>
|
|
@@ -1080,9 +1051,10 @@ const BrowserCursor: React.FC<{
|
|
|
1080
1051
|
|
|
1081
1052
|
// ─── End Card (CTA) ───────────────────────────────────────
|
|
1082
1053
|
|
|
1083
|
-
const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
1054
|
+
const EndCard: React.FC<{ text: string; url?: string; isDemo?: boolean }> = ({ text, url, isDemo }) => {
|
|
1084
1055
|
const frame = useCurrentFrame();
|
|
1085
1056
|
const { fps } = useVideoConfig();
|
|
1057
|
+
const timing = isDemo ? DEMO_TIMING : REEL_TIMING;
|
|
1086
1058
|
|
|
1087
1059
|
const cmdSpring = spring({ fps, frame, config: { damping: 14 } });
|
|
1088
1060
|
const urlSpring = spring({
|
|
@@ -1095,7 +1067,7 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
|
1095
1067
|
frame: Math.max(0, frame - 18),
|
|
1096
1068
|
config: { damping: 14 },
|
|
1097
1069
|
});
|
|
1098
|
-
const endZoom = interpolate(frame, [0, fps *
|
|
1070
|
+
const endZoom = isDemo ? 1 : interpolate(frame, [0, fps * timing.end], [1.05, 1], {
|
|
1099
1071
|
extrapolateLeft: "clamp",
|
|
1100
1072
|
extrapolateRight: "clamp",
|
|
1101
1073
|
easing: Easing.out(Easing.cubic),
|
|
@@ -1117,8 +1089,8 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
|
1117
1089
|
>
|
|
1118
1090
|
<div
|
|
1119
1091
|
style={{
|
|
1120
|
-
fontFamily: MONO,
|
|
1121
|
-
fontSize: 30,
|
|
1092
|
+
fontFamily: isDemo ? SANS : MONO,
|
|
1093
|
+
fontSize: isDemo ? 28 : 30,
|
|
1122
1094
|
color: WHITE,
|
|
1123
1095
|
padding: "18px 36px",
|
|
1124
1096
|
borderRadius: 14,
|
|
@@ -1127,9 +1099,9 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
|
1127
1099
|
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
|
1128
1100
|
}}
|
|
1129
1101
|
>
|
|
1130
|
-
<span style={{ color: ACCENT }}>$ </span>
|
|
1102
|
+
{!isDemo && <span style={{ color: ACCENT }}>$ </span>}
|
|
1131
1103
|
{text}
|
|
1132
|
-
<Cursor visible blink />
|
|
1104
|
+
{!isDemo && <Cursor visible blink />}
|
|
1133
1105
|
</div>
|
|
1134
1106
|
</div>
|
|
1135
1107
|
|
|
@@ -1140,7 +1112,7 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
|
1140
1112
|
opacity: urlSpring,
|
|
1141
1113
|
transform: `translateY(${interpolate(urlSpring, [0, 1], [15, 0])}px)`,
|
|
1142
1114
|
fontFamily: SANS,
|
|
1143
|
-
fontSize: 20,
|
|
1115
|
+
fontSize: isDemo ? 18 : 20,
|
|
1144
1116
|
color: DIM,
|
|
1145
1117
|
letterSpacing: 0.5,
|
|
1146
1118
|
}}
|