agentreel 0.1.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 +65 -0
- package/bin/agentreel.mjs +461 -0
- package/package.json +36 -0
- package/public/browser-demo.mp4 +0 -0
- package/public/music.mp3 +0 -0
- package/public/screenshot.png +0 -0
- package/scripts/browser_demo.py +215 -0
- package/scripts/cli_demo.py +272 -0
- package/src/CastVideo.tsx +1000 -0
- package/src/Root.tsx +26 -0
- package/src/index.ts +4 -0
- package/src/types.ts +82 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
AbsoluteFill,
|
|
4
|
+
Audio,
|
|
5
|
+
interpolate,
|
|
6
|
+
spring,
|
|
7
|
+
staticFile,
|
|
8
|
+
useCurrentFrame,
|
|
9
|
+
useVideoConfig,
|
|
10
|
+
Sequence,
|
|
11
|
+
Easing,
|
|
12
|
+
OffthreadVideo,
|
|
13
|
+
} from "remotion";
|
|
14
|
+
import { CastProps, Highlight } from "./types";
|
|
15
|
+
|
|
16
|
+
const ACCENT = "#50fa7b";
|
|
17
|
+
const DIM = "#6272a4";
|
|
18
|
+
const WHITE = "#f8f8f2";
|
|
19
|
+
const TERM_BG = "#282a36";
|
|
20
|
+
const TITLE_BAR = "#1e1f29";
|
|
21
|
+
const CURSOR_COLOR = "#f8f8f2";
|
|
22
|
+
|
|
23
|
+
const TITLE_DUR = 2.5;
|
|
24
|
+
const HIGHLIGHT_DUR = 4.5;
|
|
25
|
+
const TRANSITION_DUR = 0.5;
|
|
26
|
+
const END_DUR = 3.5;
|
|
27
|
+
|
|
28
|
+
const SANS =
|
|
29
|
+
'-apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif';
|
|
30
|
+
const MONO =
|
|
31
|
+
'"SF Mono", "Fira Code", "Cascadia Code", "JetBrains Mono", monospace';
|
|
32
|
+
|
|
33
|
+
// ─── Transition variants ──────────────────────────────────
|
|
34
|
+
// Each highlight enters differently to keep the eye engaged.
|
|
35
|
+
|
|
36
|
+
type TransitionStyle = "rise" | "zoomIn" | "slideLeft" | "drop";
|
|
37
|
+
const TRANSITIONS: TransitionStyle[] = ["rise", "zoomIn", "slideLeft", "drop"];
|
|
38
|
+
|
|
39
|
+
function getEntryTransform(
|
|
40
|
+
style: TransitionStyle,
|
|
41
|
+
progress: number // 0 → 1 spring
|
|
42
|
+
): { scale: number; x: number; y: number } {
|
|
43
|
+
switch (style) {
|
|
44
|
+
case "rise":
|
|
45
|
+
return {
|
|
46
|
+
scale: interpolate(progress, [0, 1], [0.82, 1]),
|
|
47
|
+
x: 0,
|
|
48
|
+
y: interpolate(progress, [0, 1], [80, 0]),
|
|
49
|
+
};
|
|
50
|
+
case "zoomIn":
|
|
51
|
+
return {
|
|
52
|
+
scale: interpolate(progress, [0, 1], [0.6, 1]),
|
|
53
|
+
x: 0,
|
|
54
|
+
y: 0,
|
|
55
|
+
};
|
|
56
|
+
case "slideLeft":
|
|
57
|
+
return {
|
|
58
|
+
scale: interpolate(progress, [0, 1], [0.9, 1]),
|
|
59
|
+
x: interpolate(progress, [0, 1], [120, 0]),
|
|
60
|
+
y: 0,
|
|
61
|
+
};
|
|
62
|
+
case "drop":
|
|
63
|
+
return {
|
|
64
|
+
scale: interpolate(progress, [0, 1], [0.85, 1]),
|
|
65
|
+
x: 0,
|
|
66
|
+
y: interpolate(progress, [0, 1], [-70, 0]),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Main Composition ─────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export const CastVideo: React.FC<CastProps> = ({
|
|
74
|
+
title,
|
|
75
|
+
subtitle,
|
|
76
|
+
highlights,
|
|
77
|
+
endText,
|
|
78
|
+
endUrl,
|
|
79
|
+
gradient,
|
|
80
|
+
}) => {
|
|
81
|
+
const frame = useCurrentFrame();
|
|
82
|
+
const { fps, durationInFrames } = useVideoConfig();
|
|
83
|
+
const g = gradient || ["#0f0f1a", "#1a0f2e"];
|
|
84
|
+
|
|
85
|
+
const titleFrames = Math.round(TITLE_DUR * fps);
|
|
86
|
+
const highlightFrames = Math.round(HIGHLIGHT_DUR * fps);
|
|
87
|
+
const endFrames = Math.round(END_DUR * fps);
|
|
88
|
+
|
|
89
|
+
// Animated gradient — hue rotates slowly over time
|
|
90
|
+
const gradAngle = interpolate(frame, [0, durationInFrames], [125, 200], {
|
|
91
|
+
extrapolateRight: "clamp",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<AbsoluteFill
|
|
96
|
+
style={{
|
|
97
|
+
background: `linear-gradient(${gradAngle}deg, ${g[0]}, ${g[1]}, ${g[0]})`,
|
|
98
|
+
backgroundSize: "200% 200%",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{/* Subtle animated glow blobs in background */}
|
|
102
|
+
<AnimatedBackground frame={frame} duration={durationInFrames} />
|
|
103
|
+
|
|
104
|
+
<MusicTrack />
|
|
105
|
+
|
|
106
|
+
<Sequence durationInFrames={titleFrames}>
|
|
107
|
+
<TitleCard title={title} subtitle={subtitle} />
|
|
108
|
+
</Sequence>
|
|
109
|
+
|
|
110
|
+
{highlights.map((h, i) => (
|
|
111
|
+
<Sequence
|
|
112
|
+
key={i}
|
|
113
|
+
from={titleFrames + i * highlightFrames}
|
|
114
|
+
durationInFrames={highlightFrames}
|
|
115
|
+
>
|
|
116
|
+
{h.videoSrc ? (
|
|
117
|
+
<BrowserHighlightClip
|
|
118
|
+
highlight={h}
|
|
119
|
+
index={i}
|
|
120
|
+
total={highlights.length}
|
|
121
|
+
transition={TRANSITIONS[i % TRANSITIONS.length]}
|
|
122
|
+
/>
|
|
123
|
+
) : (
|
|
124
|
+
<HighlightClip
|
|
125
|
+
highlight={h}
|
|
126
|
+
index={i}
|
|
127
|
+
total={highlights.length}
|
|
128
|
+
transition={TRANSITIONS[i % TRANSITIONS.length]}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</Sequence>
|
|
132
|
+
))}
|
|
133
|
+
|
|
134
|
+
<Sequence
|
|
135
|
+
from={titleFrames + highlights.length * highlightFrames}
|
|
136
|
+
durationInFrames={endFrames}
|
|
137
|
+
>
|
|
138
|
+
<EndCard text={endText || title} url={endUrl} />
|
|
139
|
+
</Sequence>
|
|
140
|
+
</AbsoluteFill>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ─── Animated Background ──────────────────────────────────
|
|
145
|
+
|
|
146
|
+
const AnimatedBackground: React.FC<{
|
|
147
|
+
frame: number;
|
|
148
|
+
duration: number;
|
|
149
|
+
}> = ({ frame, duration }) => {
|
|
150
|
+
// Two soft gradient blobs that drift slowly
|
|
151
|
+
const blob1X = interpolate(frame, [0, duration], [20, 60], {
|
|
152
|
+
extrapolateRight: "clamp",
|
|
153
|
+
});
|
|
154
|
+
const blob1Y = interpolate(frame, [0, duration], [30, 50], {
|
|
155
|
+
extrapolateRight: "clamp",
|
|
156
|
+
});
|
|
157
|
+
const blob2X = interpolate(frame, [0, duration], [70, 35], {
|
|
158
|
+
extrapolateRight: "clamp",
|
|
159
|
+
});
|
|
160
|
+
const blob2Y = interpolate(frame, [0, duration], [60, 30], {
|
|
161
|
+
extrapolateRight: "clamp",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<AbsoluteFill style={{ opacity: 0.3 }}>
|
|
166
|
+
<div
|
|
167
|
+
style={{
|
|
168
|
+
position: "absolute",
|
|
169
|
+
width: 500,
|
|
170
|
+
height: 500,
|
|
171
|
+
borderRadius: "50%",
|
|
172
|
+
background:
|
|
173
|
+
"radial-gradient(circle, rgba(80,250,123,0.15) 0%, transparent 70%)",
|
|
174
|
+
left: `${blob1X}%`,
|
|
175
|
+
top: `${blob1Y}%`,
|
|
176
|
+
transform: "translate(-50%, -50%)",
|
|
177
|
+
filter: "blur(80px)",
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
<div
|
|
181
|
+
style={{
|
|
182
|
+
position: "absolute",
|
|
183
|
+
width: 400,
|
|
184
|
+
height: 400,
|
|
185
|
+
borderRadius: "50%",
|
|
186
|
+
background:
|
|
187
|
+
"radial-gradient(circle, rgba(189,147,249,0.12) 0%, transparent 70%)",
|
|
188
|
+
left: `${blob2X}%`,
|
|
189
|
+
top: `${blob2Y}%`,
|
|
190
|
+
transform: "translate(-50%, -50%)",
|
|
191
|
+
filter: "blur(80px)",
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</AbsoluteFill>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ─── Music ────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const MusicTrack: React.FC = () => {
|
|
201
|
+
const frame = useCurrentFrame();
|
|
202
|
+
const { fps, durationInFrames } = useVideoConfig();
|
|
203
|
+
|
|
204
|
+
const fadeIn = interpolate(frame, [0, fps], [0, 0.35], {
|
|
205
|
+
extrapolateLeft: "clamp",
|
|
206
|
+
extrapolateRight: "clamp",
|
|
207
|
+
});
|
|
208
|
+
const fadeOut = interpolate(
|
|
209
|
+
frame,
|
|
210
|
+
[durationInFrames - fps * 2, durationInFrames],
|
|
211
|
+
[0.35, 0],
|
|
212
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
return (
|
|
217
|
+
<Audio src={staticFile("music.mp3")} volume={Math.min(fadeIn, fadeOut)} />
|
|
218
|
+
);
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ─── Mouse Pointer ────────────────────────────────────────
|
|
225
|
+
// macOS-style cursor that moves to the terminal and "clicks" before typing starts.
|
|
226
|
+
|
|
227
|
+
const MousePointer: React.FC = () => {
|
|
228
|
+
const frame = useCurrentFrame();
|
|
229
|
+
const { fps } = useVideoConfig();
|
|
230
|
+
|
|
231
|
+
// Pointer moves in from bottom-right, arrives at terminal center, clicks, then fades
|
|
232
|
+
const moveEnd = fps * 0.6;
|
|
233
|
+
const clickFrame = fps * 0.7;
|
|
234
|
+
const fadeStart = fps * 1.0;
|
|
235
|
+
const fadeEnd = fps * 1.3;
|
|
236
|
+
|
|
237
|
+
const moveProgress = interpolate(frame, [0, moveEnd], [0, 1], {
|
|
238
|
+
extrapolateLeft: "clamp",
|
|
239
|
+
extrapolateRight: "clamp",
|
|
240
|
+
easing: Easing.out(Easing.cubic),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const x = interpolate(moveProgress, [0, 1], [750, 450]);
|
|
244
|
+
const y = interpolate(moveProgress, [0, 1], [800, 480]);
|
|
245
|
+
|
|
246
|
+
const opacity = interpolate(frame, [0, 4, fadeStart, fadeEnd], [0, 1, 1, 0], {
|
|
247
|
+
extrapolateLeft: "clamp",
|
|
248
|
+
extrapolateRight: "clamp",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Click effect — brief scale down
|
|
252
|
+
const isClicking = frame >= clickFrame && frame < clickFrame + 4;
|
|
253
|
+
const clickScale = isClicking ? 0.85 : 1;
|
|
254
|
+
|
|
255
|
+
if (opacity <= 0) return null;
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div
|
|
259
|
+
style={{
|
|
260
|
+
position: "absolute",
|
|
261
|
+
left: x,
|
|
262
|
+
top: y,
|
|
263
|
+
zIndex: 100,
|
|
264
|
+
opacity,
|
|
265
|
+
transform: `scale(${clickScale})`,
|
|
266
|
+
transformOrigin: "top left",
|
|
267
|
+
pointerEvents: "none",
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{/* macOS cursor SVG */}
|
|
271
|
+
<svg width="24" height="28" viewBox="0 0 24 28" fill="none">
|
|
272
|
+
<path
|
|
273
|
+
d="M2 2L2 22L7.5 16.5L12.5 26L16 24.5L11 15H19L2 2Z"
|
|
274
|
+
fill="white"
|
|
275
|
+
stroke="black"
|
|
276
|
+
strokeWidth="1.5"
|
|
277
|
+
strokeLinejoin="round"
|
|
278
|
+
/>
|
|
279
|
+
</svg>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ─── Cursor ───────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
const Cursor: React.FC<{ visible: boolean; blink?: boolean }> = ({
|
|
287
|
+
visible,
|
|
288
|
+
blink = true,
|
|
289
|
+
}) => {
|
|
290
|
+
const frame = useCurrentFrame();
|
|
291
|
+
const { fps } = useVideoConfig();
|
|
292
|
+
if (!visible) return null;
|
|
293
|
+
const blinkOn = blink ? frame % Math.round(fps / 2) < fps / 4 : true;
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<span
|
|
297
|
+
style={{
|
|
298
|
+
display: "inline-block",
|
|
299
|
+
width: 9,
|
|
300
|
+
height: 19,
|
|
301
|
+
backgroundColor: blinkOn ? CURSOR_COLOR : "transparent",
|
|
302
|
+
marginLeft: 1,
|
|
303
|
+
verticalAlign: "text-bottom",
|
|
304
|
+
borderRadius: 1,
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// ─── Text Overlay (colored accent words) ──────────────────
|
|
311
|
+
|
|
312
|
+
const TextOverlay: React.FC<{ text: string }> = ({ text }) => {
|
|
313
|
+
const frame = useCurrentFrame();
|
|
314
|
+
const { fps } = useVideoConfig();
|
|
315
|
+
|
|
316
|
+
const showAt = fps * 1.8;
|
|
317
|
+
const hideAt = fps * (HIGHLIGHT_DUR - 0.8);
|
|
318
|
+
|
|
319
|
+
const enterProgress = spring({
|
|
320
|
+
fps,
|
|
321
|
+
frame: Math.max(0, frame - showAt),
|
|
322
|
+
config: { damping: 16, stiffness: 100 },
|
|
323
|
+
});
|
|
324
|
+
const exitOpacity = interpolate(frame, [hideAt, hideAt + fps * 0.3], [1, 0], {
|
|
325
|
+
extrapolateLeft: "clamp",
|
|
326
|
+
extrapolateRight: "clamp",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const opacity = Math.min(enterProgress, exitOpacity);
|
|
330
|
+
const y = interpolate(enterProgress, [0, 1], [20, 0]);
|
|
331
|
+
const scale = interpolate(enterProgress, [0, 1], [0.9, 1]);
|
|
332
|
+
|
|
333
|
+
if (frame < showAt) return null;
|
|
334
|
+
|
|
335
|
+
// Parse **bold** syntax for colored accent words
|
|
336
|
+
const parts = text.split(/(\*\*.*?\*\*)/);
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
style={{
|
|
341
|
+
position: "absolute",
|
|
342
|
+
bottom: 55,
|
|
343
|
+
left: 0,
|
|
344
|
+
width: "100%",
|
|
345
|
+
textAlign: "center",
|
|
346
|
+
zIndex: 20,
|
|
347
|
+
opacity,
|
|
348
|
+
transform: `translateY(${y}px) scale(${scale})`,
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<span
|
|
352
|
+
style={{
|
|
353
|
+
fontFamily: SANS,
|
|
354
|
+
fontSize: 36,
|
|
355
|
+
fontWeight: 700,
|
|
356
|
+
color: WHITE,
|
|
357
|
+
backgroundColor: "rgba(0,0,0,0.55)",
|
|
358
|
+
backdropFilter: "blur(12px)",
|
|
359
|
+
WebkitBackdropFilter: "blur(12px)",
|
|
360
|
+
padding: "12px 30px",
|
|
361
|
+
borderRadius: 12,
|
|
362
|
+
letterSpacing: -0.5,
|
|
363
|
+
display: "inline-block",
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
{parts.map((part, i) => {
|
|
367
|
+
if (part.startsWith("**") && part.endsWith("**")) {
|
|
368
|
+
return (
|
|
369
|
+
<span key={i} style={{ color: ACCENT, fontWeight: 800 }}>
|
|
370
|
+
{part.slice(2, -2)}
|
|
371
|
+
</span>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return <span key={i}>{part}</span>;
|
|
375
|
+
})}
|
|
376
|
+
</span>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// ─── Title Card ───────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
const TitleCard: React.FC<{ title: string; subtitle?: string }> = ({
|
|
384
|
+
title,
|
|
385
|
+
subtitle,
|
|
386
|
+
}) => {
|
|
387
|
+
const frame = useCurrentFrame();
|
|
388
|
+
const { fps } = useVideoConfig();
|
|
389
|
+
|
|
390
|
+
const titleSpring = spring({ fps, frame, config: { damping: 14 } });
|
|
391
|
+
const subSpring = spring({
|
|
392
|
+
fps,
|
|
393
|
+
frame: Math.max(0, frame - 8),
|
|
394
|
+
config: { damping: 14 },
|
|
395
|
+
});
|
|
396
|
+
const fadeOut = interpolate(
|
|
397
|
+
frame,
|
|
398
|
+
[fps * (TITLE_DUR - TRANSITION_DUR), fps * TITLE_DUR],
|
|
399
|
+
[1, 0],
|
|
400
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
401
|
+
);
|
|
402
|
+
const titleZoom = interpolate(frame, [0, fps * TITLE_DUR], [1, 1.08], {
|
|
403
|
+
extrapolateLeft: "clamp",
|
|
404
|
+
extrapolateRight: "clamp",
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<AbsoluteFill
|
|
409
|
+
style={{
|
|
410
|
+
opacity: fadeOut,
|
|
411
|
+
justifyContent: "center",
|
|
412
|
+
alignItems: "center",
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
<div
|
|
416
|
+
style={{
|
|
417
|
+
transform: `scale(${titleSpring * titleZoom}) translateY(${interpolate(titleSpring, [0, 1], [30, 0])}px)`,
|
|
418
|
+
textAlign: "center",
|
|
419
|
+
}}
|
|
420
|
+
>
|
|
421
|
+
<div
|
|
422
|
+
style={{
|
|
423
|
+
fontFamily: SANS,
|
|
424
|
+
fontSize: 76,
|
|
425
|
+
fontWeight: 800,
|
|
426
|
+
color: WHITE,
|
|
427
|
+
letterSpacing: -3,
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
{title}
|
|
431
|
+
</div>
|
|
432
|
+
{subtitle && (
|
|
433
|
+
<div
|
|
434
|
+
style={{
|
|
435
|
+
transform: `translateY(${interpolate(subSpring, [0, 1], [20, 0])}px)`,
|
|
436
|
+
opacity: subSpring,
|
|
437
|
+
fontFamily: SANS,
|
|
438
|
+
fontSize: 28,
|
|
439
|
+
color: DIM,
|
|
440
|
+
marginTop: 16,
|
|
441
|
+
letterSpacing: 1,
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
{subtitle}
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
</AbsoluteFill>
|
|
449
|
+
);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// ─── Highlight Clip ───────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
const HighlightClip: React.FC<{
|
|
455
|
+
highlight: Highlight;
|
|
456
|
+
index: number;
|
|
457
|
+
total: number;
|
|
458
|
+
transition: TransitionStyle;
|
|
459
|
+
}> = ({ highlight, index, total, transition }) => {
|
|
460
|
+
const frame = useCurrentFrame();
|
|
461
|
+
const { fps } = useVideoConfig();
|
|
462
|
+
|
|
463
|
+
// Entry animation — varies per clip
|
|
464
|
+
const enterSpring = spring({
|
|
465
|
+
fps,
|
|
466
|
+
frame,
|
|
467
|
+
config: { damping: 18, stiffness: 80 },
|
|
468
|
+
});
|
|
469
|
+
const entry = getEntryTransform(transition, enterSpring);
|
|
470
|
+
|
|
471
|
+
// Fade transitions
|
|
472
|
+
const fadeIn = interpolate(frame, [0, fps * TRANSITION_DUR], [0, 1], {
|
|
473
|
+
extrapolateLeft: "clamp",
|
|
474
|
+
extrapolateRight: "clamp",
|
|
475
|
+
});
|
|
476
|
+
const fadeOut = interpolate(
|
|
477
|
+
frame,
|
|
478
|
+
[fps * (HIGHLIGHT_DUR - TRANSITION_DUR), fps * HIGHLIGHT_DUR],
|
|
479
|
+
[1, 0],
|
|
480
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
481
|
+
);
|
|
482
|
+
const opacity = Math.min(fadeIn, fadeOut);
|
|
483
|
+
|
|
484
|
+
// Zoom in/out cycle
|
|
485
|
+
const zoomIn = interpolate(
|
|
486
|
+
frame,
|
|
487
|
+
[fps * 0.8, fps * 2.0],
|
|
488
|
+
[1, 1.12],
|
|
489
|
+
{
|
|
490
|
+
extrapolateLeft: "clamp",
|
|
491
|
+
extrapolateRight: "clamp",
|
|
492
|
+
easing: Easing.out(Easing.cubic),
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
const zoomOut = interpolate(
|
|
496
|
+
frame,
|
|
497
|
+
[fps * 2.5, fps * (HIGHLIGHT_DUR - 0.5)],
|
|
498
|
+
[1.12, 1.02],
|
|
499
|
+
{
|
|
500
|
+
extrapolateLeft: "clamp",
|
|
501
|
+
extrapolateRight: "clamp",
|
|
502
|
+
easing: Easing.inOut(Easing.cubic),
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
const zoom = frame < fps * 2.5 ? zoomIn : zoomOut;
|
|
506
|
+
|
|
507
|
+
// Vertical pan
|
|
508
|
+
const panY = interpolate(
|
|
509
|
+
frame,
|
|
510
|
+
[fps * 0.8, fps * 2.0, fps * 3.5],
|
|
511
|
+
[0, -15, 5],
|
|
512
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// Line timing
|
|
516
|
+
const lineDelay = fps * 0.15;
|
|
517
|
+
const firstLineFrame = fps * 0.35;
|
|
518
|
+
|
|
519
|
+
// Cursor tracking
|
|
520
|
+
const lastVisibleLineIdx = highlight.lines.findIndex((_, i) => {
|
|
521
|
+
return frame < firstLineFrame + (i + 1) * lineDelay;
|
|
522
|
+
});
|
|
523
|
+
const cursorLineIdx =
|
|
524
|
+
lastVisibleLineIdx === -1
|
|
525
|
+
? highlight.lines.length - 1
|
|
526
|
+
: Math.max(0, lastVisibleLineIdx - 1);
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<AbsoluteFill style={{ opacity }}>
|
|
530
|
+
{/* Label + progress dots */}
|
|
531
|
+
<div
|
|
532
|
+
style={{
|
|
533
|
+
position: "absolute",
|
|
534
|
+
top: 45,
|
|
535
|
+
left: 0,
|
|
536
|
+
width: "100%",
|
|
537
|
+
textAlign: "center",
|
|
538
|
+
zIndex: 10,
|
|
539
|
+
}}
|
|
540
|
+
>
|
|
541
|
+
<span
|
|
542
|
+
style={{
|
|
543
|
+
fontFamily: MONO,
|
|
544
|
+
fontSize: 13,
|
|
545
|
+
color: ACCENT,
|
|
546
|
+
letterSpacing: 4,
|
|
547
|
+
textTransform: "uppercase",
|
|
548
|
+
opacity: interpolate(enterSpring, [0, 1], [0, 0.6]),
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
{highlight.label}
|
|
552
|
+
</span>
|
|
553
|
+
<div
|
|
554
|
+
style={{
|
|
555
|
+
marginTop: 10,
|
|
556
|
+
display: "flex",
|
|
557
|
+
justifyContent: "center",
|
|
558
|
+
gap: 8,
|
|
559
|
+
}}
|
|
560
|
+
>
|
|
561
|
+
{Array.from({ length: total }).map((_, i) => (
|
|
562
|
+
<div
|
|
563
|
+
key={i}
|
|
564
|
+
style={{
|
|
565
|
+
width: i === index ? 24 : 8,
|
|
566
|
+
height: 6,
|
|
567
|
+
borderRadius: 3,
|
|
568
|
+
backgroundColor:
|
|
569
|
+
i === index ? ACCENT : "rgba(255,255,255,0.12)",
|
|
570
|
+
}}
|
|
571
|
+
/>
|
|
572
|
+
))}
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
{/* Terminal window */}
|
|
577
|
+
<AbsoluteFill
|
|
578
|
+
style={{
|
|
579
|
+
justifyContent: "center",
|
|
580
|
+
alignItems: "center",
|
|
581
|
+
padding: 40,
|
|
582
|
+
paddingTop: 100,
|
|
583
|
+
paddingBottom: 100,
|
|
584
|
+
}}
|
|
585
|
+
>
|
|
586
|
+
<div
|
|
587
|
+
style={{
|
|
588
|
+
transform: `scale(${entry.scale * zoom}) translate(${entry.x}px, ${entry.y + panY}px)`,
|
|
589
|
+
transformOrigin: "center center",
|
|
590
|
+
width: 820,
|
|
591
|
+
borderRadius: 14,
|
|
592
|
+
overflow: "hidden",
|
|
593
|
+
boxShadow:
|
|
594
|
+
"0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
595
|
+
}}
|
|
596
|
+
>
|
|
597
|
+
{/* macOS title bar */}
|
|
598
|
+
<div
|
|
599
|
+
style={{
|
|
600
|
+
backgroundColor: TITLE_BAR,
|
|
601
|
+
padding: "12px 16px",
|
|
602
|
+
display: "flex",
|
|
603
|
+
alignItems: "center",
|
|
604
|
+
gap: 8,
|
|
605
|
+
}}
|
|
606
|
+
>
|
|
607
|
+
<div
|
|
608
|
+
style={{
|
|
609
|
+
width: 12,
|
|
610
|
+
height: 12,
|
|
611
|
+
borderRadius: 6,
|
|
612
|
+
backgroundColor: "#ff5555",
|
|
613
|
+
}}
|
|
614
|
+
/>
|
|
615
|
+
<div
|
|
616
|
+
style={{
|
|
617
|
+
width: 12,
|
|
618
|
+
height: 12,
|
|
619
|
+
borderRadius: 6,
|
|
620
|
+
backgroundColor: "#f1fa8c",
|
|
621
|
+
}}
|
|
622
|
+
/>
|
|
623
|
+
<div
|
|
624
|
+
style={{
|
|
625
|
+
width: 12,
|
|
626
|
+
height: 12,
|
|
627
|
+
borderRadius: 6,
|
|
628
|
+
backgroundColor: "#50fa7b",
|
|
629
|
+
}}
|
|
630
|
+
/>
|
|
631
|
+
<div
|
|
632
|
+
style={{
|
|
633
|
+
flex: 1,
|
|
634
|
+
textAlign: "center",
|
|
635
|
+
fontFamily: MONO,
|
|
636
|
+
fontSize: 12,
|
|
637
|
+
color: "rgba(255,255,255,0.25)",
|
|
638
|
+
}}
|
|
639
|
+
>
|
|
640
|
+
Terminal
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
{/* Terminal body */}
|
|
645
|
+
<div
|
|
646
|
+
style={{
|
|
647
|
+
backgroundColor: TERM_BG,
|
|
648
|
+
padding: "20px 24px",
|
|
649
|
+
minHeight: 280,
|
|
650
|
+
}}
|
|
651
|
+
>
|
|
652
|
+
{highlight.lines.map((line, lineIdx) => {
|
|
653
|
+
const lineFrame = firstLineFrame + lineIdx * lineDelay;
|
|
654
|
+
const lineSpring = spring({
|
|
655
|
+
fps,
|
|
656
|
+
frame: Math.max(0, frame - lineFrame),
|
|
657
|
+
config: { damping: 20, stiffness: 120 },
|
|
658
|
+
});
|
|
659
|
+
const lineOpacity = interpolate(lineSpring, [0, 1], [0, 1]);
|
|
660
|
+
const lineX = interpolate(lineSpring, [0, 1], [12, 0]);
|
|
661
|
+
|
|
662
|
+
let displayText = line.text;
|
|
663
|
+
let isTyping = false;
|
|
664
|
+
if (line.isPrompt) {
|
|
665
|
+
const typingEnd = lineFrame + fps * 0.6;
|
|
666
|
+
if (frame < typingEnd) {
|
|
667
|
+
const progress = interpolate(
|
|
668
|
+
frame,
|
|
669
|
+
[lineFrame, typingEnd],
|
|
670
|
+
[0, 1],
|
|
671
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
672
|
+
);
|
|
673
|
+
const chars = Math.floor(progress * line.text.length);
|
|
674
|
+
displayText = line.text.slice(0, chars);
|
|
675
|
+
isTyping = chars < line.text.length;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const isZoomed = highlight.zoomLine === lineIdx;
|
|
680
|
+
const lineZoom = isZoomed
|
|
681
|
+
? interpolate(
|
|
682
|
+
frame,
|
|
683
|
+
[lineFrame + fps * 0.5, lineFrame + fps * 1],
|
|
684
|
+
[1, 1.05],
|
|
685
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
686
|
+
)
|
|
687
|
+
: 1;
|
|
688
|
+
|
|
689
|
+
const showCursor = lineIdx === cursorLineIdx;
|
|
690
|
+
|
|
691
|
+
return (
|
|
692
|
+
<div
|
|
693
|
+
key={lineIdx}
|
|
694
|
+
style={{
|
|
695
|
+
opacity: lineOpacity,
|
|
696
|
+
transform: `translateX(${lineX}px) scale(${lineZoom})`,
|
|
697
|
+
transformOrigin: "left center",
|
|
698
|
+
fontFamily: MONO,
|
|
699
|
+
fontSize: 16,
|
|
700
|
+
lineHeight: 1.7,
|
|
701
|
+
color: line.dim ? DIM : line.color || WHITE,
|
|
702
|
+
fontWeight: line.bold ? 700 : 400,
|
|
703
|
+
whiteSpace: "pre",
|
|
704
|
+
display: "flex",
|
|
705
|
+
alignItems: "center",
|
|
706
|
+
}}
|
|
707
|
+
>
|
|
708
|
+
{line.isPrompt && (
|
|
709
|
+
<span style={{ color: ACCENT, marginRight: 8 }}>$</span>
|
|
710
|
+
)}
|
|
711
|
+
<span>{displayText}</span>
|
|
712
|
+
{showCursor && <Cursor visible blink={!isTyping} />}
|
|
713
|
+
</div>
|
|
714
|
+
);
|
|
715
|
+
})}
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
</AbsoluteFill>
|
|
719
|
+
|
|
720
|
+
{/* Mouse pointer — appears at start of each clip */}
|
|
721
|
+
<MousePointer />
|
|
722
|
+
|
|
723
|
+
{/* Text overlay */}
|
|
724
|
+
{highlight.overlay && <TextOverlay text={highlight.overlay} />}
|
|
725
|
+
</AbsoluteFill>
|
|
726
|
+
);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// ─── Browser Highlight Clip ───────────────────────────────
|
|
730
|
+
|
|
731
|
+
const BrowserHighlightClip: React.FC<{
|
|
732
|
+
highlight: Highlight;
|
|
733
|
+
index: number;
|
|
734
|
+
total: number;
|
|
735
|
+
transition: TransitionStyle;
|
|
736
|
+
}> = ({ highlight, index, total, transition }) => {
|
|
737
|
+
const frame = useCurrentFrame();
|
|
738
|
+
const { fps } = useVideoConfig();
|
|
739
|
+
|
|
740
|
+
const enterSpring = spring({
|
|
741
|
+
fps,
|
|
742
|
+
frame,
|
|
743
|
+
config: { damping: 18, stiffness: 80 },
|
|
744
|
+
});
|
|
745
|
+
const entry = getEntryTransform(transition, enterSpring);
|
|
746
|
+
|
|
747
|
+
const fadeIn = interpolate(frame, [0, fps * TRANSITION_DUR], [0, 1], {
|
|
748
|
+
extrapolateLeft: "clamp",
|
|
749
|
+
extrapolateRight: "clamp",
|
|
750
|
+
});
|
|
751
|
+
const fadeOut = interpolate(
|
|
752
|
+
frame,
|
|
753
|
+
[fps * (HIGHLIGHT_DUR - TRANSITION_DUR), fps * HIGHLIGHT_DUR],
|
|
754
|
+
[1, 0],
|
|
755
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
756
|
+
);
|
|
757
|
+
const opacity = Math.min(fadeIn, fadeOut);
|
|
758
|
+
|
|
759
|
+
// Zoom in/out cycle
|
|
760
|
+
const zoomIn = interpolate(frame, [fps * 0.8, fps * 2.0], [1, 1.08], {
|
|
761
|
+
extrapolateLeft: "clamp",
|
|
762
|
+
extrapolateRight: "clamp",
|
|
763
|
+
easing: Easing.out(Easing.cubic),
|
|
764
|
+
});
|
|
765
|
+
const zoomOut = interpolate(
|
|
766
|
+
frame,
|
|
767
|
+
[fps * 2.5, fps * (HIGHLIGHT_DUR - 0.5)],
|
|
768
|
+
[1.08, 1.01],
|
|
769
|
+
{
|
|
770
|
+
extrapolateLeft: "clamp",
|
|
771
|
+
extrapolateRight: "clamp",
|
|
772
|
+
easing: Easing.inOut(Easing.cubic),
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
const zoom = frame < fps * 2.5 ? zoomIn : zoomOut;
|
|
776
|
+
|
|
777
|
+
const panY = interpolate(
|
|
778
|
+
frame,
|
|
779
|
+
[fps * 0.8, fps * 2.0, fps * 3.5],
|
|
780
|
+
[0, -10, 5],
|
|
781
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
const videoSrc = highlight.videoSrc!;
|
|
785
|
+
const startFrom = Math.round((highlight.videoStartSec || 0) * fps);
|
|
786
|
+
|
|
787
|
+
return (
|
|
788
|
+
<AbsoluteFill style={{ opacity }}>
|
|
789
|
+
{/* Label + progress dots */}
|
|
790
|
+
<div
|
|
791
|
+
style={{
|
|
792
|
+
position: "absolute",
|
|
793
|
+
top: 45,
|
|
794
|
+
left: 0,
|
|
795
|
+
width: "100%",
|
|
796
|
+
textAlign: "center",
|
|
797
|
+
zIndex: 10,
|
|
798
|
+
}}
|
|
799
|
+
>
|
|
800
|
+
<span
|
|
801
|
+
style={{
|
|
802
|
+
fontFamily: MONO,
|
|
803
|
+
fontSize: 13,
|
|
804
|
+
color: ACCENT,
|
|
805
|
+
letterSpacing: 4,
|
|
806
|
+
textTransform: "uppercase",
|
|
807
|
+
opacity: interpolate(enterSpring, [0, 1], [0, 0.6]),
|
|
808
|
+
}}
|
|
809
|
+
>
|
|
810
|
+
{highlight.label}
|
|
811
|
+
</span>
|
|
812
|
+
<div
|
|
813
|
+
style={{
|
|
814
|
+
marginTop: 10,
|
|
815
|
+
display: "flex",
|
|
816
|
+
justifyContent: "center",
|
|
817
|
+
gap: 8,
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
{Array.from({ length: total }).map((_, i) => (
|
|
821
|
+
<div
|
|
822
|
+
key={i}
|
|
823
|
+
style={{
|
|
824
|
+
width: i === index ? 24 : 8,
|
|
825
|
+
height: 6,
|
|
826
|
+
borderRadius: 3,
|
|
827
|
+
backgroundColor:
|
|
828
|
+
i === index ? ACCENT : "rgba(255,255,255,0.12)",
|
|
829
|
+
}}
|
|
830
|
+
/>
|
|
831
|
+
))}
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
{/* Browser window */}
|
|
836
|
+
<AbsoluteFill
|
|
837
|
+
style={{
|
|
838
|
+
justifyContent: "center",
|
|
839
|
+
alignItems: "center",
|
|
840
|
+
padding: 40,
|
|
841
|
+
paddingTop: 100,
|
|
842
|
+
paddingBottom: 100,
|
|
843
|
+
}}
|
|
844
|
+
>
|
|
845
|
+
<div
|
|
846
|
+
style={{
|
|
847
|
+
transform: `scale(${entry.scale * zoom}) translate(${entry.x}px, ${entry.y + panY}px)`,
|
|
848
|
+
transformOrigin: "center center",
|
|
849
|
+
width: 880,
|
|
850
|
+
borderRadius: 14,
|
|
851
|
+
overflow: "hidden",
|
|
852
|
+
boxShadow:
|
|
853
|
+
"0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
854
|
+
}}
|
|
855
|
+
>
|
|
856
|
+
{/* Browser chrome */}
|
|
857
|
+
<div
|
|
858
|
+
style={{
|
|
859
|
+
backgroundColor: TITLE_BAR,
|
|
860
|
+
padding: "10px 16px",
|
|
861
|
+
display: "flex",
|
|
862
|
+
alignItems: "center",
|
|
863
|
+
gap: 8,
|
|
864
|
+
}}
|
|
865
|
+
>
|
|
866
|
+
<div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#ff5555" }} />
|
|
867
|
+
<div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#f1fa8c" }} />
|
|
868
|
+
<div style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: "#50fa7b" }} />
|
|
869
|
+
{/* Address bar */}
|
|
870
|
+
<div
|
|
871
|
+
style={{
|
|
872
|
+
flex: 1,
|
|
873
|
+
marginLeft: 8,
|
|
874
|
+
backgroundColor: "rgba(255,255,255,0.06)",
|
|
875
|
+
borderRadius: 6,
|
|
876
|
+
padding: "6px 12px",
|
|
877
|
+
fontFamily: SANS,
|
|
878
|
+
fontSize: 12,
|
|
879
|
+
color: "rgba(255,255,255,0.4)",
|
|
880
|
+
}}
|
|
881
|
+
>
|
|
882
|
+
{highlight.videoSrc ? "localhost:3000" : ""}
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
{/* Video content */}
|
|
887
|
+
<div
|
|
888
|
+
style={{
|
|
889
|
+
width: "100%",
|
|
890
|
+
aspectRatio: "16/10",
|
|
891
|
+
backgroundColor: "#fff",
|
|
892
|
+
overflow: "hidden",
|
|
893
|
+
}}
|
|
894
|
+
>
|
|
895
|
+
<OffthreadVideo
|
|
896
|
+
src={staticFile(videoSrc)}
|
|
897
|
+
startFrom={startFrom}
|
|
898
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
899
|
+
/>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
</AbsoluteFill>
|
|
903
|
+
|
|
904
|
+
{/* Mouse pointer */}
|
|
905
|
+
<MousePointer />
|
|
906
|
+
|
|
907
|
+
{/* Text overlay */}
|
|
908
|
+
{highlight.overlay && <TextOverlay text={highlight.overlay} />}
|
|
909
|
+
</AbsoluteFill>
|
|
910
|
+
);
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
// ─── End Card (CTA) ───────────────────────────────────────
|
|
914
|
+
|
|
915
|
+
const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
916
|
+
const frame = useCurrentFrame();
|
|
917
|
+
const { fps } = useVideoConfig();
|
|
918
|
+
|
|
919
|
+
const cmdSpring = spring({ fps, frame, config: { damping: 14 } });
|
|
920
|
+
const urlSpring = spring({
|
|
921
|
+
fps,
|
|
922
|
+
frame: Math.max(0, frame - 10),
|
|
923
|
+
config: { damping: 14 },
|
|
924
|
+
});
|
|
925
|
+
const brandSpring = spring({
|
|
926
|
+
fps,
|
|
927
|
+
frame: Math.max(0, frame - 18),
|
|
928
|
+
config: { damping: 14 },
|
|
929
|
+
});
|
|
930
|
+
const endZoom = interpolate(frame, [0, fps * END_DUR], [1.05, 1], {
|
|
931
|
+
extrapolateLeft: "clamp",
|
|
932
|
+
extrapolateRight: "clamp",
|
|
933
|
+
easing: Easing.out(Easing.cubic),
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
return (
|
|
937
|
+
<AbsoluteFill
|
|
938
|
+
style={{
|
|
939
|
+
justifyContent: "center",
|
|
940
|
+
alignItems: "center",
|
|
941
|
+
transform: `scale(${endZoom})`,
|
|
942
|
+
}}
|
|
943
|
+
>
|
|
944
|
+
<div
|
|
945
|
+
style={{
|
|
946
|
+
transform: `scale(${cmdSpring}) translateY(${interpolate(cmdSpring, [0, 1], [25, 0])}px)`,
|
|
947
|
+
textAlign: "center",
|
|
948
|
+
}}
|
|
949
|
+
>
|
|
950
|
+
<div
|
|
951
|
+
style={{
|
|
952
|
+
fontFamily: MONO,
|
|
953
|
+
fontSize: 30,
|
|
954
|
+
color: WHITE,
|
|
955
|
+
padding: "18px 36px",
|
|
956
|
+
borderRadius: 14,
|
|
957
|
+
backgroundColor: "rgba(255,255,255,0.05)",
|
|
958
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
959
|
+
boxShadow: "0 20px 60px rgba(0,0,0,0.3)",
|
|
960
|
+
}}
|
|
961
|
+
>
|
|
962
|
+
<span style={{ color: ACCENT }}>$ </span>
|
|
963
|
+
{text}
|
|
964
|
+
<Cursor visible blink />
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
{url && (
|
|
969
|
+
<div
|
|
970
|
+
style={{
|
|
971
|
+
marginTop: 24,
|
|
972
|
+
opacity: urlSpring,
|
|
973
|
+
transform: `translateY(${interpolate(urlSpring, [0, 1], [15, 0])}px)`,
|
|
974
|
+
fontFamily: SANS,
|
|
975
|
+
fontSize: 20,
|
|
976
|
+
color: DIM,
|
|
977
|
+
letterSpacing: 0.5,
|
|
978
|
+
}}
|
|
979
|
+
>
|
|
980
|
+
{url}
|
|
981
|
+
</div>
|
|
982
|
+
)}
|
|
983
|
+
|
|
984
|
+
<div
|
|
985
|
+
style={{
|
|
986
|
+
position: "absolute",
|
|
987
|
+
bottom: 40,
|
|
988
|
+
opacity: brandSpring * 0.4,
|
|
989
|
+
transform: `translateY(${interpolate(brandSpring, [0, 1], [10, 0])}px)`,
|
|
990
|
+
fontFamily: MONO,
|
|
991
|
+
fontSize: 13,
|
|
992
|
+
color: DIM,
|
|
993
|
+
letterSpacing: 3,
|
|
994
|
+
}}
|
|
995
|
+
>
|
|
996
|
+
MADE WITH AGENTREEL
|
|
997
|
+
</div>
|
|
998
|
+
</AbsoluteFill>
|
|
999
|
+
);
|
|
1000
|
+
};
|