mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(videos): redesign all scenes with real app screenshots and proper scaling
This commit is contained in:
@@ -1,206 +1,13 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { BeforeAfter } from "@/components/BeforeAfter";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { FONT, TEXT } from "@/lib/fonts";
|
||||
|
||||
const Checkerboard: React.FC<{ w: number; h: number }> = ({ w, h }) => (
|
||||
<div
|
||||
style={{
|
||||
width: w,
|
||||
height: h,
|
||||
backgroundImage:
|
||||
"linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)",
|
||||
backgroundSize: "16px 16px",
|
||||
backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const AiToolsScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
{/* Background removal demo (0-180) */}
|
||||
<Sequence from={0} durationInFrames={180}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<AppWindow
|
||||
title="Remove Background"
|
||||
width={1200}
|
||||
height={700}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
gap: 40,
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<Sequence from={0} durationInFrames={60}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
border: "3px solid #3b82f6",
|
||||
borderTop: "3px solid transparent",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontFamily: FONT.body, fontSize: 14, color: "#3b82f6" }}>
|
||||
Running rembg model locally...
|
||||
</span>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={60} durationInFrames={120}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<BeforeAfter
|
||||
before={<PhotoPlaceholder width={450} height={340} hue={30} />}
|
||||
after={
|
||||
<div style={{ position: "relative", width: 450, height: 340 }}>
|
||||
<Checkerboard w={450} h={340} />
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 50,
|
||||
left: 140,
|
||||
width: 170,
|
||||
height: 240,
|
||||
borderRadius: "50% 50% 45% 45%",
|
||||
background: "linear-gradient(135deg, #e8a87c, #d4856b)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
scanStartFrame={10}
|
||||
scanDuration={50}
|
||||
width={450}
|
||||
height={340}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
{/* Quick AI tool montage (180-360) */}
|
||||
<Sequence from={180} durationInFrames={90}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<AppWindow
|
||||
title="Upscale 4x"
|
||||
width={1000}
|
||||
height={500}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
gap: 30,
|
||||
}}
|
||||
>
|
||||
<PhotoPlaceholder width={100} height={100} hue={200} />
|
||||
<span
|
||||
style={{
|
||||
fontFamily: FONT.heading,
|
||||
fontWeight: 800,
|
||||
fontSize: 32,
|
||||
color: COLOR.accent,
|
||||
}}
|
||||
>
|
||||
4x
|
||||
</span>
|
||||
<PhotoPlaceholder width={300} height={300} hue={200} />
|
||||
</div>
|
||||
</AppWindow>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={270} durationInFrames={90}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<AppWindow
|
||||
title="OCR / Text Extraction"
|
||||
width={1000}
|
||||
height={500}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div style={{ display: "flex", height: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 0.5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<PhotoPlaceholder width={300} height={350} hue={50} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 0.5,
|
||||
padding: 24,
|
||||
borderLeft: "1px solid #e5e5e5",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ fontFamily: FONT.body, fontWeight: 600, fontSize: 14, color: "#0a0a0a" }}
|
||||
>
|
||||
Extracted Text
|
||||
</div>
|
||||
{["Lorem ipsum dolor sit", "amet consectetur", "adipiscing elit sed do"].map(
|
||||
(line, i) => (
|
||||
<div
|
||||
key={`ocr-${i}`}
|
||||
style={{
|
||||
fontFamily: FONT.mono,
|
||||
fontSize: 13,
|
||||
color: "#525252",
|
||||
padding: "4px 8px",
|
||||
backgroundColor: "#eff6ff",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
{/* Bottom text */}
|
||||
<div style={{ position: "absolute", bottom: 40, width: "100%", textAlign: "center" }}>
|
||||
<ClipReveal startFrame={300}>
|
||||
<span style={{ ...TEXT.sectionTitle, fontSize: 32, color: "#0a0a0a" }}>
|
||||
All on your hardware.
|
||||
</span>
|
||||
</ClipReveal>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="remove-bg.png"
|
||||
caption="15 AI models. Your hardware."
|
||||
captionFrame={120}
|
||||
panX={10}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,84 +1,14 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { TypeWriter } from "@/components/TypeWriter";
|
||||
import { FONT, TEXT } from "@/lib/fonts";
|
||||
|
||||
const CURL_SEGMENTS = [
|
||||
{ text: "curl ", color: "#e6edf3" },
|
||||
{ text: "-X POST ", color: "#79c0ff" },
|
||||
{ text: "localhost:1349/api/v1/tools/resize ", color: "#a5d6ff" },
|
||||
{ text: '-F "file=@photo.jpg" ', color: "#7ee787" },
|
||||
{ text: "-F ", color: "#79c0ff" },
|
||||
{ text: '"settings={\\"width\\":800}"', color: "#ffa657" },
|
||||
];
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const ApiDocsScene: React.FC = () => {
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
<AppWindow
|
||||
title="API Docs"
|
||||
width={1200}
|
||||
height={600}
|
||||
topBarColor="#1e293b"
|
||||
bodyColor="#0d1117"
|
||||
>
|
||||
<div style={{ padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 12,
|
||||
color: "#8b949e",
|
||||
marginBottom: 8,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
Example Request
|
||||
</div>
|
||||
<div
|
||||
style={{ backgroundColor: "#161b22", borderRadius: 8, padding: 16, marginBottom: 16 }}
|
||||
>
|
||||
<TypeWriter
|
||||
segments={CURL_SEGMENTS}
|
||||
startFrame={10}
|
||||
speed={1}
|
||||
style={{ fontSize: 14, lineHeight: 1.6 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClipReveal startFrame={80}>
|
||||
<div style={{ backgroundColor: "#161b22", borderRadius: 8, padding: 16 }}>
|
||||
<pre
|
||||
style={{
|
||||
fontFamily: FONT.mono,
|
||||
fontSize: 13,
|
||||
color: "#e6edf3",
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{`{
|
||||
"downloadUrl": "/api/v1/download/abc123",
|
||||
"originalSize": 2400000,
|
||||
"processedSize": 340000
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</ClipReveal>
|
||||
</div>
|
||||
</AppWindow>
|
||||
|
||||
<div style={{ position: "absolute", bottom: 40, width: "100%", textAlign: "center" }}>
|
||||
<ClipReveal startFrame={100}>
|
||||
<span style={{ ...TEXT.sectionTitle, fontSize: 28, color: "#0a0a0a" }}>
|
||||
Every tool via REST API.
|
||||
</span>
|
||||
</ClipReveal>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="api-docs.png"
|
||||
caption="Every tool via REST API."
|
||||
captionFrame={120}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,127 +1,12 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { ProgressBar } from "@/components/ProgressBar";
|
||||
import { FONT, TEXT } from "@/lib/fonts";
|
||||
import { SPRING } from "@/lib/motion";
|
||||
|
||||
const FILES = [
|
||||
{ name: "photo_001.jpg", from: "1.8 MB", to: "95 KB" },
|
||||
{ name: "photo_002.jpg", from: "3.2 MB", to: "210 KB" },
|
||||
{ name: "landscape.png", from: "4.1 MB", to: "280 KB" },
|
||||
{ name: "portrait.jpg", from: "2.7 MB", to: "160 KB" },
|
||||
{ name: "macro_01.jpg", from: "5.0 MB", to: "320 KB" },
|
||||
{ name: "event_23.jpg", from: "1.5 MB", to: "85 KB" },
|
||||
{ name: "scan_hires.png", from: "8.2 MB", to: "450 KB" },
|
||||
{ name: "product_a.jpg", from: "2.0 MB", to: "120 KB" },
|
||||
];
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const BatchProcessingScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const processStartFrame = 100;
|
||||
const filesPerSecond = 3;
|
||||
const framesPerFile = Math.floor(30 / filesPerSecond);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
<AppWindow
|
||||
title="Compress"
|
||||
width={1600}
|
||||
height={900}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div style={{ padding: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{FILES.map((file, i) => {
|
||||
const enterDelay = 20 + i * 4;
|
||||
const enterSpring = spring({ frame: frame - enterDelay, fps, config: SPRING.popIn });
|
||||
const fileProcessFrame = processStartFrame + i * framesPerFile;
|
||||
const isDone = frame > fileProcessFrame + framesPerFile;
|
||||
const isProcessing = frame >= fileProcessFrame && !isDone;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.name}
|
||||
style={{
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${isDone ? "#22c55e40" : "#e5e5e5"}`,
|
||||
backgroundColor: isDone ? "#f0fdf4" : "white",
|
||||
opacity: enterSpring,
|
||||
transform: `scale(${0.8 + enterSpring * 0.2})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 12,
|
||||
color: "#0a0a0a",
|
||||
fontWeight: 500,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
{isProcessing && (
|
||||
<ProgressBar
|
||||
startFrame={0}
|
||||
duration={framesPerFile}
|
||||
width={200}
|
||||
height={4}
|
||||
color="#3b82f6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDone && (
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<span style={{ color: "#22c55e", fontSize: 14 }}>✓</span>
|
||||
<span style={{ fontFamily: FONT.mono, fontSize: 11, color: "#a3a3a3" }}>
|
||||
{file.from}
|
||||
</span>
|
||||
<span style={{ fontFamily: FONT.mono, fontSize: 11, color: "#a3a3a3" }}>
|
||||
{"→"}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: FONT.mono,
|
||||
fontSize: 11,
|
||||
color: "#22c55e",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{file.to}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{frame > 280 && (
|
||||
<div style={{ marginTop: 24, textAlign: "center" }}>
|
||||
<ClipReveal startFrame={280}>
|
||||
<span style={{ ...TEXT.sectionTitle, fontSize: 32, color: "#0a0a0a" }}>
|
||||
Unlimited batch. No caps.
|
||||
</span>
|
||||
</ClipReveal>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppWindow>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="compress-tool.png"
|
||||
caption="Unlimited batch. No caps."
|
||||
captionFrame={160}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,149 +1,13 @@
|
||||
import type React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
interpolate,
|
||||
spring,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { CATEGORY_LABELS, CATEGORY_ORDER, COLOR } from "@/lib/colors";
|
||||
import { FONT } from "@/lib/fonts";
|
||||
import { SPRING } from "@/lib/motion";
|
||||
import { TOOLS } from "@/lib/tools";
|
||||
|
||||
const VISIBLE_TOOLS = TOOLS.slice(0, 12);
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const DashboardScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const windowScale = spring({ frame, fps, config: SPRING.snappy });
|
||||
const searchText = "resize";
|
||||
const typingStart = 60;
|
||||
const charsVisible = Math.floor(Math.max(0, frame - typingStart) / 3);
|
||||
const typed = searchText.slice(0, charsVisible);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
<div style={{ transform: `scale(${0.95 + windowScale * 0.05})`, opacity: windowScale }}>
|
||||
<AppWindow
|
||||
title="SnapOtter"
|
||||
width={1600}
|
||||
height={900}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div style={{ padding: 24 }}>
|
||||
{/* Top bar */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 20 }}>
|
||||
<Img src={staticFile("logo.png")} style={{ width: 32, height: 32 }} />
|
||||
<span
|
||||
style={{
|
||||
fontFamily: FONT.heading,
|
||||
fontWeight: 800,
|
||||
fontSize: 20,
|
||||
color: "#0a0a0a",
|
||||
}}
|
||||
>
|
||||
SnapOtter
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
maxWidth: 400,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
border: "1px solid #e5e5e5",
|
||||
padding: "0 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 14,
|
||||
color: typed ? "#0a0a0a" : "#a3a3a3",
|
||||
}}
|
||||
>
|
||||
{typed || "Search tools..."}
|
||||
{frame >= typingStart &&
|
||||
charsVisible < searchText.length &&
|
||||
Math.floor(frame / 8) % 2 === 0 && (
|
||||
<span
|
||||
style={{ width: 2, height: 16, backgroundColor: "#3b82f6", marginLeft: 1 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category pills */}
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 24, flexWrap: "wrap" }}>
|
||||
{CATEGORY_ORDER.map((cat) => (
|
||||
<div
|
||||
key={cat}
|
||||
style={{
|
||||
padding: "4px 12px",
|
||||
borderRadius: 16,
|
||||
fontSize: 12,
|
||||
fontFamily: FONT.body,
|
||||
fontWeight: 500,
|
||||
backgroundColor: `${COLOR.category[cat]}15`,
|
||||
color: COLOR.category[cat],
|
||||
border: `1px solid ${COLOR.category[cat]}30`,
|
||||
}}
|
||||
>
|
||||
{CATEGORY_LABELS[cat]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tool grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
|
||||
{VISIBLE_TOOLS.map((tool) => {
|
||||
const matches = !typed || tool.name.toLowerCase().includes(typed.toLowerCase());
|
||||
const cardOpacity = typed ? (matches ? 1 : 0.2) : 1;
|
||||
const cardScale = typed ? (matches ? 1 : 0.95) : 1;
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
border: "1px solid #e5e5e5",
|
||||
backgroundColor: "white",
|
||||
opacity: cardOpacity,
|
||||
transform: `scale(${cardScale})`,
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: COLOR.category[tool.category] ?? COLOR.accent,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
color: "#0a0a0a",
|
||||
}}
|
||||
>
|
||||
{tool.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="dashboard.png"
|
||||
caption="49 tools. One dashboard."
|
||||
captionFrame={120}
|
||||
panX={-20}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,188 +1,12 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { FONT } from "@/lib/fonts";
|
||||
import { SPRING } from "@/lib/motion";
|
||||
|
||||
const TOOLBAR_ICONS = [
|
||||
"Move",
|
||||
"Select",
|
||||
"Brush",
|
||||
"Eraser",
|
||||
"Clone",
|
||||
"Dodge",
|
||||
"Text",
|
||||
"Shape",
|
||||
"Line",
|
||||
"Gradient",
|
||||
"Fill",
|
||||
"Crop",
|
||||
"Eyedropper",
|
||||
"Zoom",
|
||||
"Hand",
|
||||
];
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const ImageEditorScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const enterSpring = spring({ frame, fps, config: SPRING.snappy });
|
||||
const brushFrame = 60;
|
||||
const brushProgress = interpolate(frame, [brushFrame, brushFrame + 60], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const activeToolIdx = frame < 60 ? 2 : frame < 120 ? 6 : frame < 200 ? 11 : 2;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
<div style={{ transform: `scale(${0.95 + enterSpring * 0.05})`, opacity: enterSpring }}>
|
||||
<AppWindow
|
||||
title="Editor"
|
||||
width={1600}
|
||||
height={900}
|
||||
topBarColor="#1e293b"
|
||||
bodyColor="#0f172a"
|
||||
>
|
||||
<div style={{ display: "flex", height: "100%" }}>
|
||||
{/* Toolbar */}
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
backgroundColor: "#1e293b",
|
||||
borderRight: "1px solid #334155",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
paddingTop: 8,
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{TOOLBAR_ICONS.map((icon, i) => (
|
||||
<div
|
||||
key={icon}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
backgroundColor: i === activeToolIdx ? "#3b82f6" : "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 10,
|
||||
color: i === activeToolIdx ? "white" : "#94a3b8",
|
||||
fontFamily: FONT.body,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{icon.slice(0, 2)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Canvas */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<PhotoPlaceholder width={900} height={600} hue={180} />
|
||||
|
||||
{/* Brush stroke */}
|
||||
{brushProgress > 0 && (
|
||||
<svg
|
||||
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<path
|
||||
d="M 300 350 Q 450 280 600 320 T 900 300"
|
||||
fill="none"
|
||||
stroke={COLOR.accent}
|
||||
strokeWidth={4}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={800}
|
||||
strokeDashoffset={800 * (1 - brushProgress)}
|
||||
opacity={0.7}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* Text overlay */}
|
||||
{frame > 130 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 80,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
fontFamily: FONT.heading,
|
||||
fontWeight: 700,
|
||||
fontSize: 28,
|
||||
color: "white",
|
||||
textShadow: "0 2px 8px rgba(0,0,0,0.5)",
|
||||
opacity: interpolate(frame, [130, 145], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
SnapOtter
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Layers panel */}
|
||||
<div
|
||||
style={{
|
||||
width: 200,
|
||||
backgroundColor: "#1e293b",
|
||||
borderLeft: "1px solid #334155",
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 11,
|
||||
color: "#94a3b8",
|
||||
fontWeight: 600,
|
||||
marginBottom: 8,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
Layers
|
||||
</div>
|
||||
{["Text Layer", "Brush Stroke", "Background"].map((layer, i) => (
|
||||
<div
|
||||
key={layer}
|
||||
style={{
|
||||
padding: "6px 8px",
|
||||
borderRadius: 4,
|
||||
backgroundColor: i === 0 ? "#334155" : "transparent",
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 12,
|
||||
color: "#e2e8f0",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{layer}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="editor.png"
|
||||
caption="Full image editor built in."
|
||||
captionFrame={100}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,121 +1,14 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { FONT } from "@/lib/fonts";
|
||||
import { EASE, SPRING } from "@/lib/motion";
|
||||
|
||||
const BLOCKS = [
|
||||
{ name: "Resize", color: COLOR.category.essentials, enterFrame: 40, x: 250 },
|
||||
{ name: "Compress", color: COLOR.category.optimization, enterFrame: 80, x: 580 },
|
||||
{ name: "Watermark", color: COLOR.category.watermark, enterFrame: 120, x: 910 },
|
||||
];
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const PipelineBuilderScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
<AppWindow
|
||||
title="Pipeline Builder"
|
||||
width={1600}
|
||||
height={900}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
{/* Blocks */}
|
||||
{BLOCKS.map((block) => {
|
||||
const s = spring({ frame: frame - block.enterFrame, fps, config: SPRING.popIn });
|
||||
return (
|
||||
<div
|
||||
key={block.name}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: block.x,
|
||||
top: 280,
|
||||
width: 160,
|
||||
height: 80,
|
||||
borderRadius: 12,
|
||||
backgroundColor: `${block.color}20`,
|
||||
border: `2px solid ${block.color}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: FONT.body,
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
color: block.color,
|
||||
opacity: s,
|
||||
transform: `scale(${0.5 + s * 0.5})`,
|
||||
}}
|
||||
>
|
||||
{block.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Connection lines */}
|
||||
{frame > 140 &&
|
||||
[0, 1].map((ci) => {
|
||||
const lineProgress = interpolate(frame, [140 + ci * 20, 170 + ci * 20], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: EASE.enter,
|
||||
});
|
||||
const x1 = BLOCKS[ci].x + 160;
|
||||
const x2 = BLOCKS[ci + 1].x;
|
||||
return (
|
||||
<svg
|
||||
key={`line-${ci}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<line
|
||||
x1={x1}
|
||||
y1={320}
|
||||
x2={x1 + (x2 - x1) * lineProgress}
|
||||
y2={320}
|
||||
stroke="#a3a3a3"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="6,4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Running indicator */}
|
||||
{frame > 220 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 14,
|
||||
color: "#22c55e",
|
||||
fontWeight: 500,
|
||||
opacity: interpolate(frame, [220, 235], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
✓ Pipeline completed -- 5 images processed
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppWindow>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="automate.png"
|
||||
caption="Chain tools into workflows."
|
||||
captionFrame={120}
|
||||
zoomFrom={1.05}
|
||||
zoomTo={1.0}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { TEXT } from "@/lib/fonts";
|
||||
|
||||
export const ScreenshotScene: React.FC<{
|
||||
screenshot: string;
|
||||
caption?: string;
|
||||
captionFrame?: number;
|
||||
zoomFrom?: number;
|
||||
zoomTo?: number;
|
||||
panX?: number;
|
||||
panY?: number;
|
||||
topBarColor?: string;
|
||||
bodyColor?: string;
|
||||
}> = ({
|
||||
screenshot,
|
||||
caption,
|
||||
captionFrame = 60,
|
||||
zoomFrom = 1.0,
|
||||
zoomTo = 1.06,
|
||||
panX = 0,
|
||||
panY = 0,
|
||||
topBarColor = "#1a1a2e",
|
||||
bodyColor = "#0f172a",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const totalFrames = 300;
|
||||
|
||||
const scale = interpolate(frame, [0, totalFrames], [zoomFrom, zoomTo], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const translateX = interpolate(frame, [0, totalFrames], [0, panX], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const translateY = interpolate(frame, [0, totalFrames], [0, panY], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const windowOpacity = interpolate(frame, [0, 15], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f0f0f0" }}
|
||||
>
|
||||
<div style={{ opacity: windowOpacity }}>
|
||||
<AppWindow
|
||||
title="SnapOtter"
|
||||
width={1700}
|
||||
height={950}
|
||||
topBarColor={topBarColor}
|
||||
bodyColor={bodyColor}
|
||||
>
|
||||
<div style={{ overflow: "hidden", width: "100%", height: "100%" }}>
|
||||
<Img
|
||||
src={staticFile(`screenshots/${screenshot}`)}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</div>
|
||||
|
||||
{caption && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 30,
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<ClipReveal startFrame={captionFrame}>
|
||||
<span
|
||||
style={{
|
||||
...TEXT.sectionTitle,
|
||||
fontSize: 32,
|
||||
color: "#1a1a2e",
|
||||
backgroundColor: "rgba(255,255,255,0.85)",
|
||||
padding: "8px 24px",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
{caption}
|
||||
</span>
|
||||
</ClipReveal>
|
||||
</div>
|
||||
)}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -1,195 +1,14 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { ProgressBar } from "@/components/ProgressBar";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { FONT } from "@/lib/fonts";
|
||||
import { SPRING } from "@/lib/motion";
|
||||
import { ScreenshotScene } from "./ScreenshotScene";
|
||||
|
||||
export const SingleToolScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const slideIn = spring({ frame, fps, config: SPRING.snappy });
|
||||
const dropFrame = 40;
|
||||
const dropProgress = spring({ frame: frame - dropFrame, fps, config: SPRING.natural });
|
||||
const processFrame = 180;
|
||||
const showResult = frame > processFrame + 60;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{ justifyContent: "center", alignItems: "center", backgroundColor: "#f5f5f4" }}
|
||||
>
|
||||
<div style={{ transform: `translateX(${(1 - slideIn) * 300}px)`, opacity: slideIn }}>
|
||||
<AppWindow
|
||||
title="Resize"
|
||||
width={1600}
|
||||
height={900}
|
||||
topBarColor="#ffffff"
|
||||
bodyColor="#ffffff"
|
||||
>
|
||||
<div style={{ display: "flex", height: "100%" }}>
|
||||
{/* Dropzone */}
|
||||
<div
|
||||
style={{
|
||||
flex: 0.6,
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{frame < dropFrame ? (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 400,
|
||||
border: "2px dashed #d4d4d4",
|
||||
borderRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 16,
|
||||
color: "#a3a3a3",
|
||||
}}
|
||||
>
|
||||
Drop an image here
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
transform: `translateY(${(1 - dropProgress) * -200}px)`,
|
||||
opacity: dropProgress,
|
||||
}}
|
||||
>
|
||||
<PhotoPlaceholder width={500} height={375} hue={30} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
<div
|
||||
style={{
|
||||
flex: 0.4,
|
||||
padding: 24,
|
||||
backgroundColor: "#fafafa",
|
||||
borderLeft: "1px solid #e5e5e5",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.heading,
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
color: "#0a0a0a",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
|
||||
{/* Width field */}
|
||||
<label
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 13,
|
||||
color: "#737373",
|
||||
display: "block",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Width
|
||||
</label>
|
||||
<div
|
||||
style={{
|
||||
height: 36,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #e5e5e5",
|
||||
padding: "0 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontFamily: FONT.mono,
|
||||
fontSize: 14,
|
||||
color: "#0a0a0a",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
1920
|
||||
</div>
|
||||
|
||||
{/* Height field */}
|
||||
<label
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 13,
|
||||
color: "#737373",
|
||||
display: "block",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Height
|
||||
</label>
|
||||
<div
|
||||
style={{
|
||||
height: 36,
|
||||
borderRadius: 6,
|
||||
border: "1px solid #e5e5e5",
|
||||
padding: "0 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontFamily: FONT.mono,
|
||||
fontSize: 14,
|
||||
color: "#a3a3a3",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
auto
|
||||
</div>
|
||||
|
||||
{/* Process button / results */}
|
||||
{frame >= processFrame && !showResult && (
|
||||
<ProgressBar
|
||||
startFrame={0}
|
||||
duration={60}
|
||||
width={300}
|
||||
color="#3b82f6"
|
||||
style={{ marginTop: 20 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showResult && (
|
||||
<div style={{ marginTop: 20, display: "flex", gap: 16 }}>
|
||||
<div style={{ fontFamily: FONT.body, fontSize: 14, color: "#a3a3a3" }}>
|
||||
2.4 MB
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 14,
|
||||
color: "#22c55e",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
340 KB
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FONT.heading,
|
||||
fontSize: 14,
|
||||
color: COLOR.accent,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
-86%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
<ScreenshotScene
|
||||
screenshot="resize-tool.png"
|
||||
caption="Upload. Configure. Process."
|
||||
captionFrame={120}
|
||||
zoomTo={1.08}
|
||||
panY={-15}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
||||
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||
import { GradientBlob } from "@/components/GradientBlob";
|
||||
|
||||
const BLOBS = [
|
||||
@@ -41,6 +41,9 @@ const BLOBS = [
|
||||
},
|
||||
];
|
||||
|
||||
const SCREENSHOTS = ["dashboard.png", "resize-tool.png", "editor.png"];
|
||||
const CYCLE_FRAMES = 30;
|
||||
|
||||
export const AmbientOpenScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const fadeIn = interpolate(frame, [0, 45], [0, 1], {
|
||||
@@ -48,11 +51,65 @@ export const AmbientOpenScene: React.FC = () => {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const currentIdx = Math.floor(frame / CYCLE_FRAMES) % SCREENSHOTS.length;
|
||||
const nextIdx = (currentIdx + 1) % SCREENSHOTS.length;
|
||||
const cycleProgress = (frame % CYCLE_FRAMES) / CYCLE_FRAMES;
|
||||
|
||||
const currentOpacity = interpolate(cycleProgress, [0, 0.8, 1], [0.6, 0.6, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const nextOpacity = interpolate(cycleProgress, [0.7, 1], [0, 0.6], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const scale = interpolate(frame % CYCLE_FRAMES, [0, CYCLE_FRAMES], [1.05, 1.0], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const rotation = (currentIdx % 2 === 0 ? 1 : -1) * 3.5;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ opacity: fadeIn }}>
|
||||
{BLOBS.map((blob, i) => (
|
||||
<GradientBlob key={`ambient-${i}`} config={blob} duration={600} />
|
||||
))}
|
||||
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
width: 700,
|
||||
height: 450,
|
||||
position: "relative",
|
||||
transform: `rotate(${rotation}deg) scale(${scale})`,
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.4)",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={staticFile(`screenshots/${SCREENSHOTS[currentIdx]}`)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
opacity: currentOpacity,
|
||||
}}
|
||||
/>
|
||||
<Img
|
||||
src={staticFile(`screenshots/${SCREENSHOTS[nextIdx]}`)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
opacity: nextOpacity,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
||||
import { NumberPunch } from "@/components/NumberPunch";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
|
||||
const PUNCHES = [
|
||||
{ number: "49", descriptor: "tools", frame: 0, size: 120, shake: 2 },
|
||||
{ number: "15", descriptor: "AI models", frame: 37, size: 120, shake: 2 },
|
||||
{ number: "55+", descriptor: "formats", frame: 74, size: 120, shake: 2 },
|
||||
{ number: "1", descriptor: "container", frame: 111, size: 144, shake: 3 },
|
||||
{ number: "49", descriptor: "tools", frame: 0 },
|
||||
{ number: "15", descriptor: "AI models", frame: 37 },
|
||||
{ number: "55+", descriptor: "formats", frame: 74 },
|
||||
{ number: "1", descriptor: "container", frame: 111 },
|
||||
];
|
||||
|
||||
export const NumberPunchScene: React.FC = () => {
|
||||
@@ -16,27 +15,33 @@ export const NumberPunchScene: React.FC = () => {
|
||||
return (
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
{PUNCHES.map((p, i) => {
|
||||
const visible = frame >= p.frame && frame < p.frame + 37;
|
||||
const flashStart = p.frame - 2;
|
||||
const flashOpacity =
|
||||
i > 0 && frame >= flashStart && frame < flashStart + 2
|
||||
? interpolate(frame, [flashStart, flashStart + 2], [1, 0])
|
||||
: 0;
|
||||
const segmentEnd = i < PUNCHES.length - 1 ? PUNCHES[i + 1].frame : p.frame + 37;
|
||||
const fadeInOpacity = interpolate(frame, [p.frame, p.frame + 4], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const fadeOutOpacity = interpolate(frame, [segmentEnd - 6, segmentEnd], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const isLastPunch = i === PUNCHES.length - 1;
|
||||
const visible = frame >= p.frame && frame < segmentEnd;
|
||||
const opacity = isLastPunch ? fadeInOpacity : Math.min(fadeInOpacity, fadeOutOpacity);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<AbsoluteFill key={p.number} style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
{i > 0 && (
|
||||
<AbsoluteFill style={{ backgroundColor: COLOR.dark, opacity: flashOpacity }} />
|
||||
)}
|
||||
{visible && (
|
||||
<NumberPunch
|
||||
number={p.number}
|
||||
descriptor={p.descriptor}
|
||||
enterFrame={p.frame}
|
||||
numberSize={p.size}
|
||||
shakeIntensity={p.shake}
|
||||
/>
|
||||
)}
|
||||
<AbsoluteFill
|
||||
key={p.number}
|
||||
style={{ justifyContent: "center", alignItems: "center", opacity }}
|
||||
>
|
||||
<NumberPunch
|
||||
number={p.number}
|
||||
descriptor={p.descriptor}
|
||||
enterFrame={p.frame}
|
||||
numberSize={200}
|
||||
shakeIntensity={2}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill } from "remotion";
|
||||
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||
import { RotatingTaglines } from "@/components/RotatingTaglines";
|
||||
|
||||
const TAGLINES = [
|
||||
@@ -12,13 +12,33 @@ const TAGLINES = [
|
||||
];
|
||||
|
||||
export const TaglineCascadeScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const totalFrames = 150;
|
||||
|
||||
const bgScale = interpolate(frame, [0, totalFrames], [1.0, 1.1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<Img
|
||||
src={staticFile("screenshots/dashboard.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
opacity: 0.15,
|
||||
transform: `scale(${bgScale})`,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
|
||||
<RotatingTaglines
|
||||
lines={TAGLINES}
|
||||
startFrame={0}
|
||||
framesPerLine={25}
|
||||
fontSize={40}
|
||||
fontSize={56}
|
||||
color="white"
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
|
||||
@@ -1,127 +1,56 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, Sequence, useCurrentFrame } from "remotion";
|
||||
import { BeforeAfter } from "@/components/BeforeAfter";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||
import { FONT } from "@/lib/fonts";
|
||||
|
||||
const Checkerboard: React.FC<{ width: number; height: number }> = ({ width, height }) => (
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
backgroundImage:
|
||||
"linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%)",
|
||||
backgroundSize: "16px 16px",
|
||||
backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px",
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const BgRemovalDemo: React.FC = () => (
|
||||
<BeforeAfter
|
||||
before={<PhotoPlaceholder width={400} height={300} hue={30} />}
|
||||
after={
|
||||
<div style={{ position: "relative", width: 400, height: 300 }}>
|
||||
<Checkerboard width={400} height={300} />
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 60,
|
||||
left: 120,
|
||||
width: 160,
|
||||
height: 200,
|
||||
borderRadius: "50% 50% 45% 45%",
|
||||
background: "linear-gradient(135deg, #e8a87c, #d4856b)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
scanStartFrame={5}
|
||||
scanDuration={40}
|
||||
width={400}
|
||||
height={300}
|
||||
/>
|
||||
);
|
||||
|
||||
const UpscaleDemo: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 30 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: 2,
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 16 }).map((_, i) => (
|
||||
<div
|
||||
key={`px-${i}`}
|
||||
style={{
|
||||
backgroundColor: `hsl(${(i * 23) % 360}, 60%, ${50 + (i % 3) * 10}%)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span
|
||||
style={{ color: COLOR.accent, fontFamily: FONT.heading, fontWeight: 800, fontSize: 36 }}
|
||||
>
|
||||
4x
|
||||
</span>
|
||||
<PhotoPlaceholder width={160} height={160} hue={200} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AiShowcaseScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const totalFrames = 180;
|
||||
|
||||
const crossfade = interpolate(frame, [80, 100], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const scale1 = interpolate(frame, [0, 90], [1.0, 1.05], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const scale2 = interpolate(frame, [90, totalFrames], [1.0, 1.05], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const textOpacity = interpolate(frame, [30, 45], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Sequence from={0} durationInFrames={60}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<BgRemovalDemo />
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
<AbsoluteFill>
|
||||
<Img
|
||||
src={staticFile("screenshots/remove-bg.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
transform: `scale(${scale1})`,
|
||||
opacity: 1 - crossfade,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
|
||||
<Sequence from={60} durationInFrames={60}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<UpscaleDemo />
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
<Sequence from={120} durationInFrames={60}>
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<BeforeAfter
|
||||
before={
|
||||
<div style={{ position: "relative", width: 400, height: 300 }}>
|
||||
<PhotoPlaceholder width={400} height={300} hue={180} />
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 150,
|
||||
width: 100,
|
||||
height: 100,
|
||||
border: `2px dashed ${COLOR.accent}`,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
after={<PhotoPlaceholder width={400} height={300} hue={180} />}
|
||||
scanStartFrame={5}
|
||||
scanDuration={40}
|
||||
width={400}
|
||||
height={300}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
<AbsoluteFill>
|
||||
<Img
|
||||
src={staticFile("screenshots/resize-tool.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
transform: `scale(${scale2})`,
|
||||
opacity: crossfade,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
|
||||
<div
|
||||
style={{
|
||||
@@ -132,7 +61,9 @@ export const AiShowcaseScene: React.FC = () => {
|
||||
fontFamily: FONT.body,
|
||||
fontWeight: 600,
|
||||
fontSize: 24,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
color: "rgba(255,255,255,0.9)",
|
||||
opacity: textOpacity,
|
||||
textShadow: "0 2px 12px rgba(0,0,0,0.7)",
|
||||
}}
|
||||
>
|
||||
15 AI models. Your hardware. No cloud.
|
||||
|
||||
@@ -3,29 +3,42 @@ import { AbsoluteFill } from "remotion";
|
||||
import { FeaturePill } from "@/components/FeaturePill";
|
||||
|
||||
const FEATURES = [
|
||||
{ label: "Batch Processing", x: 340, y: 380, from: "left" as const },
|
||||
{ label: "Pipeline Automation", x: 560, y: 380, from: "right" as const },
|
||||
{ label: "REST API", x: 340, y: 440, from: "left" as const },
|
||||
{ label: "55+ Input Formats", x: 560, y: 440, from: "right" as const },
|
||||
{ label: "Image Editor", x: 340, y: 500, from: "left" as const },
|
||||
{ label: "One Container", x: 560, y: 500, from: "right" as const },
|
||||
{ label: "Multi-arch", x: 340, y: 560, from: "left" as const },
|
||||
{ label: "15 AI Models", x: 560, y: 560, from: "right" as const },
|
||||
{ label: "Batch Processing", col: 0, row: 0, from: "left" as const },
|
||||
{ label: "Pipeline Automation", col: 1, row: 0, from: "right" as const },
|
||||
{ label: "REST API", col: 0, row: 1, from: "left" as const },
|
||||
{ label: "55+ Input Formats", col: 1, row: 1, from: "right" as const },
|
||||
{ label: "Image Editor", col: 0, row: 2, from: "left" as const },
|
||||
{ label: "One Container", col: 1, row: 2, from: "right" as const },
|
||||
{ label: "Multi-arch", col: 0, row: 3, from: "left" as const },
|
||||
{ label: "15 AI Models", col: 1, row: 3, from: "right" as const },
|
||||
];
|
||||
|
||||
const PILL_WIDTH = 200;
|
||||
const GAP_X = 20;
|
||||
const GAP_Y = 16;
|
||||
const PILL_HEIGHT = 50;
|
||||
const GRID_WIDTH = PILL_WIDTH * 2 + GAP_X;
|
||||
const GRID_HEIGHT = PILL_HEIGHT * 4 + GAP_Y * 3;
|
||||
const START_X = (1080 - GRID_WIDTH) / 2;
|
||||
const START_Y = (1080 - GRID_HEIGHT) / 2;
|
||||
|
||||
export const FeatureBurstScene: React.FC = () => {
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
{FEATURES.map((f, i) => (
|
||||
<FeaturePill
|
||||
key={f.label}
|
||||
label={f.label}
|
||||
enterFrame={i * 8}
|
||||
targetX={f.x}
|
||||
targetY={f.y}
|
||||
fromDirection={f.from}
|
||||
/>
|
||||
))}
|
||||
{FEATURES.map((f, i) => {
|
||||
const x = START_X + f.col * (PILL_WIDTH + GAP_X);
|
||||
const y = START_Y + f.row * (PILL_HEIGHT + GAP_Y);
|
||||
return (
|
||||
<FeaturePill
|
||||
key={f.label}
|
||||
label={f.label}
|
||||
enterFrame={i * 8}
|
||||
targetX={x}
|
||||
targetY={y}
|
||||
fromDirection={f.from}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill } from "remotion";
|
||||
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||
import { RotatingTaglines } from "@/components/RotatingTaglines";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
|
||||
@@ -11,13 +11,33 @@ const TAGLINES = [
|
||||
];
|
||||
|
||||
export const PrivacyBeatScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const totalFrames = 120;
|
||||
|
||||
const bgScale = interpolate(frame, [0, totalFrames], [1.0, 1.05], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<AbsoluteFill>
|
||||
<Img
|
||||
src={staticFile("screenshots/dashboard.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
opacity: 0.12,
|
||||
transform: `scale(${bgScale})`,
|
||||
}}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
|
||||
<RotatingTaglines
|
||||
lines={TAGLINES}
|
||||
startFrame={0}
|
||||
framesPerLine={30}
|
||||
fontSize={36}
|
||||
fontSize={48}
|
||||
color={COLOR.accent}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import type React from "react";
|
||||
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
||||
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion";
|
||||
import { AppWindow } from "@/components/AppWindow";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { ToolGrid } from "@/components/ToolGrid";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { TEXT } from "@/lib/fonts";
|
||||
|
||||
export const ToolGridRevealScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const totalFrames = 180;
|
||||
|
||||
const gridOpacity = interpolate(frame, [0, 20], [0, 1], {
|
||||
const scale = interpolate(frame, [0, totalFrames], [1.0, 1.08], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const windowOpacity = interpolate(frame, [0, 20], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 60,
|
||||
left: 40,
|
||||
right: 40,
|
||||
opacity: gridOpacity,
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "top center",
|
||||
}}
|
||||
>
|
||||
<ToolGrid startFrame={10} cellWidth={110} cellHeight={26} gap={4} />
|
||||
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
|
||||
<div style={{ opacity: windowOpacity }}>
|
||||
<AppWindow
|
||||
title="SnapOtter"
|
||||
width={900}
|
||||
height={900}
|
||||
topBarColor="#1a1a2e"
|
||||
bodyColor="#0f172a"
|
||||
>
|
||||
<div style={{ overflow: "hidden", width: "100%", height: "100%" }}>
|
||||
<Img
|
||||
src={staticFile("screenshots/dashboard.png")}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
transform: `scale(${scale})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AppWindow>
|
||||
</div>
|
||||
|
||||
<div style={{ position: "absolute", bottom: 60, right: 80 }}>
|
||||
@@ -37,7 +49,7 @@ export const ToolGridRevealScene: React.FC = () => {
|
||||
duration={90}
|
||||
style={{
|
||||
...TEXT.counter,
|
||||
fontSize: 64,
|
||||
fontSize: 120,
|
||||
color: COLOR.accent,
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user