mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(videos): add Product Demo scenes 1-8 and composition
Eight scenes replicating the SnapOtter UI in light theme: Dashboard overview with search filtering, single tool resize flow, batch processing with staggered progress, pipeline builder, AI tools (bg removal, upscale, OCR), image editor, API docs, and end card.
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { AbsoluteFill, Series } from "remotion";
|
||||||
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
|
import { AiToolsScene } from "./scenes/AiToolsScene";
|
||||||
|
import { ApiDocsScene } from "./scenes/ApiDocsScene";
|
||||||
|
import { BatchProcessingScene } from "./scenes/BatchProcessingScene";
|
||||||
|
import { DashboardScene } from "./scenes/DashboardScene";
|
||||||
|
import { EndCardScene } from "./scenes/EndCardScene";
|
||||||
|
import { ImageEditorScene } from "./scenes/ImageEditorScene";
|
||||||
|
import { PipelineBuilderScene } from "./scenes/PipelineBuilderScene";
|
||||||
|
import { SingleToolScene } from "./scenes/SingleToolScene";
|
||||||
|
|
||||||
|
export const ProductDemo: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: "#f5f5f4" }}>
|
||||||
|
<Series>
|
||||||
|
<Series.Sequence durationInFrames={240}>
|
||||||
|
<DashboardScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={360}>
|
||||||
|
<SingleToolScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={360}>
|
||||||
|
<BatchProcessingScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={390}>
|
||||||
|
<PipelineBuilderScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={360}>
|
||||||
|
<AiToolsScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={300}>
|
||||||
|
<ImageEditorScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={150}>
|
||||||
|
<ApiDocsScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
<Series.Sequence durationInFrames={90}>
|
||||||
|
<EndCardScene />
|
||||||
|
</Series.Sequence>
|
||||||
|
</Series>
|
||||||
|
|
||||||
|
<GrainOverlay opacity={0.02} />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import {
|
||||||
|
AbsoluteFill,
|
||||||
|
Img,
|
||||||
|
interpolate,
|
||||||
|
spring,
|
||||||
|
staticFile,
|
||||||
|
useCurrentFrame,
|
||||||
|
useVideoConfig,
|
||||||
|
} from "remotion";
|
||||||
|
import { ClipReveal } from "@/components/ClipReveal";
|
||||||
|
import { COLOR } from "@/lib/colors";
|
||||||
|
import { FONT, TEXT } from "@/lib/fonts";
|
||||||
|
import { SPRING } from "@/lib/motion";
|
||||||
|
|
||||||
|
export const EndCardScene: React.FC = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const logoSpring = spring({ frame, fps, config: SPRING.natural });
|
||||||
|
const fadeOut = interpolate(frame, [70, 90], [1, 0], {
|
||||||
|
extrapolateLeft: "clamp",
|
||||||
|
extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
style={{
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "white",
|
||||||
|
opacity: fadeOut,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src={staticFile("logo.png")}
|
||||||
|
style={{
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
opacity: logoSpring,
|
||||||
|
transform: `scale(${0.8 + logoSpring * 0.2})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ height: 16 }} />
|
||||||
|
|
||||||
|
<ClipReveal startFrame={10}>
|
||||||
|
<span style={{ ...TEXT.sectionTitle, fontSize: 36, color: "#0a0a0a" }}>
|
||||||
|
Self-hosted image processing.
|
||||||
|
</span>
|
||||||
|
</ClipReveal>
|
||||||
|
|
||||||
|
<div style={{ height: 12 }} />
|
||||||
|
|
||||||
|
<ClipReveal startFrame={25}>
|
||||||
|
<span style={{ fontFamily: FONT.body, fontSize: 18, color: COLOR.accent }}>
|
||||||
|
snapotter.com
|
||||||
|
</span>
|
||||||
|
</ClipReveal>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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",
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user