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,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