feat(videos): redesign all scenes with real app screenshots and proper scaling

This commit is contained in:
SnapOtter
2026-05-08 18:37:36 +08:00
parent 86e1fdeef5
commit c17d540072
15 changed files with 387 additions and 1210 deletions
@@ -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,
}}
/>