mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(videos): replace gradient mockups with real photos in compositions
Use real screenshots and sample photos from apps/videos/public/screenshots/ instead of CSS-gradient PhotoPlaceholder components: - PipelineFlow: landscape photo for thumbnails with object-position variation - AiMagicReel: portrait photo for bg removal, B&W photo for colorization, landscape for super resolution and restoration segments - PrivacyPromise: real portrait/landscape/B&W photos in shield, larger padlock - OneCommand: real app-full.png screenshot instead of wireframe AppMockup, increased amber glow to 12% - CloudVsLocal: real portrait photos for all thumbnails, larger server icons (36px), amplified shake (5px) and vibrate (3px), escalated red tint overlay - ShieldIcon: increased stroke width to 3.5px, added gradient fill
This commit is contained in:
@@ -40,11 +40,19 @@ export const ShieldIcon: React.FC<{
|
|||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="shield-fill-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="rgba(255,255,255,0.12)" />
|
||||||
|
<stop offset="100%" stopColor="rgba(255,255,255,0.02)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
<path
|
<path
|
||||||
d={SHIELD_PATH}
|
d={SHIELD_PATH}
|
||||||
stroke="white"
|
stroke="white"
|
||||||
strokeWidth={2.5}
|
strokeWidth={3.5}
|
||||||
fill={`rgba(255,255,255,${fillOpacity})`}
|
fill={fillOpacity > 0 ? "url(#shield-fill-grad)" : "none"}
|
||||||
|
fillOpacity={fillOpacity > 0 ? fillOpacity * 12 : 0}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
AbsoluteFill,
|
AbsoluteFill,
|
||||||
|
Img,
|
||||||
interpolate,
|
interpolate,
|
||||||
interpolateColors,
|
interpolateColors,
|
||||||
random,
|
random,
|
||||||
Sequence,
|
Sequence,
|
||||||
spring,
|
spring,
|
||||||
|
staticFile,
|
||||||
useCurrentFrame,
|
useCurrentFrame,
|
||||||
useVideoConfig,
|
useVideoConfig,
|
||||||
} from "remotion";
|
} from "remotion";
|
||||||
@@ -133,19 +135,8 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
|||||||
easing: EASE.smooth,
|
easing: EASE.smooth,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Geometric pattern background
|
// Checkerboard background for transparency effect
|
||||||
const patternBg = `
|
const checkerBg = CHECKERBOARD_BG;
|
||||||
linear-gradient(45deg, rgba(245,158,11,0.15) 25%, transparent 25%),
|
|
||||||
linear-gradient(-45deg, rgba(245,158,11,0.15) 25%, transparent 25%),
|
|
||||||
linear-gradient(45deg, transparent 75%, rgba(245,158,11,0.1) 75%),
|
|
||||||
linear-gradient(-45deg, transparent 75%, rgba(245,158,11,0.1) 75%)
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Silhouette: a head-and-shoulders shape via gradient
|
|
||||||
const silhouetteGradient = `
|
|
||||||
radial-gradient(ellipse 90px 100px at 50% 30%, hsl(25, 65%, 65%) 0%, hsl(25, 65%, 65%) 70%, transparent 71%),
|
|
||||||
radial-gradient(ellipse 130px 200px at 50% 70%, hsl(30, 55%, 55%) 0%, hsl(30, 55%, 55%) 70%, transparent 71%)
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Scan line particle trail
|
// Scan line particle trail
|
||||||
const particles = Array.from({ length: 5 }, (_, i) => {
|
const particles = Array.from({ length: 5 }, (_, i) => {
|
||||||
@@ -187,28 +178,19 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
|||||||
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* "Before" layer: photo with geometric background */}
|
{/* "Before" layer: real portrait photo */}
|
||||||
<div
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
backgroundImage: patternBg,
|
width: "100%",
|
||||||
backgroundSize: "24px 24px",
|
height: "100%",
|
||||||
backgroundColor: "hsl(30, 20%, 35%)",
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{/* Silhouette subject */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
backgroundImage: silhouetteGradient,
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* "After" layer: checkerboard + subject only */}
|
{/* "After" layer: checkerboard + portrait (revealed via clip) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -222,16 +204,18 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
background: CHECKERBOARD_BG,
|
background: checkerBg,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Subject only */}
|
{/* Subject portrait on top of checkerboard */}
|
||||||
<div
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
backgroundImage: silhouetteGradient,
|
width: "100%",
|
||||||
backgroundRepeat: "no-repeat",
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +230,7 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
|||||||
width: 2,
|
width: 2,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
background: COLOR.accent,
|
background: COLOR.accent,
|
||||||
boxShadow: `0 0 20px 10px rgba(245,158,11,0.3)`,
|
boxShadow: "0 0 20px 10px rgba(245,158,11,0.3)",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -260,7 +244,7 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
|||||||
key={`scan-p-${i}`}
|
key={`scan-p-${i}`}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: photoX + p.x - photoX,
|
left: p.x,
|
||||||
top: p.y,
|
top: p.y,
|
||||||
width: 3,
|
width: 3,
|
||||||
height: 3,
|
height: 3,
|
||||||
@@ -358,26 +342,38 @@ const SegmentColorization: React.FC = () => {
|
|||||||
transform: `scale(${holdScale})`,
|
transform: `scale(${holdScale})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Grayscale layer (on top) */}
|
{/* B&W layer (on top, real B&W photo) */}
|
||||||
<div
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-bw.jpg")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
background: `linear-gradient(135deg, hsl(30, 60%, 65%) 0%, hsl(200, 50%, 55%) 50%, hsl(340, 55%, 60%) 100%)`,
|
width: "100%",
|
||||||
filter: "grayscale(1)",
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Color layer (revealed underneath via clip) */}
|
{/* Color layer (revealed via clip, using portrait as color version) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
background: `linear-gradient(135deg, hsl(30, 72%, 65%) 0%, hsl(200, 60%, 55%) 50%, hsl(340, 66%, 60%) 100%)`,
|
|
||||||
filter: "saturate(1.2)",
|
|
||||||
clipPath: `inset(0 ${100 - waveProgress}% 0 0)`,
|
clipPath: `inset(0 ${100 - waveProgress}% 0 0)`,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
filter: "saturate(1.2)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Color particles */}
|
{/* Color particles */}
|
||||||
{colorParticles.map(
|
{colorParticles.map(
|
||||||
@@ -411,11 +407,11 @@ const SegmentColorization: React.FC = () => {
|
|||||||
const PIXEL_GRID_SIZE = 8;
|
const PIXEL_GRID_SIZE = 8;
|
||||||
const PIXEL_GAP = 0.5;
|
const PIXEL_GAP = 0.5;
|
||||||
|
|
||||||
/** Generate deterministic colors for the pixel grid */
|
/** Generate deterministic colors for the pixel grid (blues and greens from landscape) */
|
||||||
const pixelColors: string[] = Array.from({ length: PIXEL_GRID_SIZE * PIXEL_GRID_SIZE }, (_, i) => {
|
const pixelColors: string[] = Array.from({ length: PIXEL_GRID_SIZE * PIXEL_GRID_SIZE }, (_, i) => {
|
||||||
const hue = random(`pixel-hue-${i}`) * 60 + 15; // warm hues
|
const hue = random(`pixel-hue-${i}`) * 80 + 180; // blues and greens (180-260)
|
||||||
const sat = 40 + random(`pixel-sat-${i}`) * 30;
|
const sat = 35 + random(`pixel-sat-${i}`) * 35;
|
||||||
const lit = 40 + random(`pixel-lit-${i}`) * 30;
|
const lit = 35 + random(`pixel-lit-${i}`) * 30;
|
||||||
return `hsl(${hue}, ${sat}%, ${lit}%)`;
|
return `hsl(${hue}, ${sat}%, ${lit}%)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -518,14 +514,17 @@ const SegmentSuperResolution: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Smooth "resolved" version */}
|
{/* Smooth "resolved" version - actual landscape photo */}
|
||||||
{showSmooth && (
|
{showSmooth && (
|
||||||
<div
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-landscape.jpg")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
background: `linear-gradient(135deg, hsl(25, 55%, 50%) 0%, hsl(40, 50%, 55%) 50%, hsl(30, 60%, 45%) 100%)`,
|
|
||||||
opacity: smoothOpacity,
|
opacity: smoothOpacity,
|
||||||
border: "1.5px solid rgba(255,255,255,0.15)",
|
border: "1.5px solid rgba(255,255,255,0.15)",
|
||||||
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
||||||
@@ -630,8 +629,8 @@ const SegmentRestoration: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Color shift from sepia to warm
|
// Color shift from sepia to warm
|
||||||
const bgFrom = interpolateColors(frame, [80, 95], ["hsl(35, 40%, 45%)", "hsl(30, 55%, 50%)"]);
|
const _bgFrom = interpolateColors(frame, [80, 95], ["hsl(35, 40%, 45%)", "hsl(30, 55%, 50%)"]);
|
||||||
const bgTo = interpolateColors(frame, [80, 95], ["hsl(40, 35%, 55%)", "hsl(45, 50%, 58%)"]);
|
const _bgTo = interpolateColors(frame, [80, 95], ["hsl(40, 35%, 55%)", "hsl(45, 50%, 58%)"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AbsoluteFill style={{ background: COLOR.dark }}>
|
<AbsoluteFill style={{ background: COLOR.dark }}>
|
||||||
@@ -653,15 +652,18 @@ const SegmentRestoration: React.FC = () => {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
||||||
filter: `sepia(${sepiaAmount}) contrast(${contrastAmount})`,
|
filter: `sepia(${sepiaAmount * 0.8}) contrast(${contrastAmount})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Base photo gradient */}
|
{/* Base photo - real landscape */}
|
||||||
<div
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-landscape.jpg")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
background: `linear-gradient(135deg, ${bgFrom} 0%, ${bgTo} 100%)`,
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import { evolvePath } from "@remotion/paths";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
AbsoluteFill,
|
AbsoluteFill,
|
||||||
|
Img,
|
||||||
interpolate,
|
interpolate,
|
||||||
random,
|
random,
|
||||||
Sequence,
|
Sequence,
|
||||||
spring,
|
spring,
|
||||||
|
staticFile,
|
||||||
useCurrentFrame,
|
useCurrentFrame,
|
||||||
useVideoConfig,
|
useVideoConfig,
|
||||||
} from "remotion";
|
} from "remotion";
|
||||||
import { ClipReveal } from "@/components/ClipReveal";
|
import { ClipReveal } from "@/components/ClipReveal";
|
||||||
import { Counter } from "@/components/Counter";
|
import { Counter } from "@/components/Counter";
|
||||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
|
||||||
import { COLOR } from "@/lib/colors";
|
import { COLOR } from "@/lib/colors";
|
||||||
import { FONT, TEXT } from "@/lib/fonts";
|
import { FONT, TEXT } from "@/lib/fonts";
|
||||||
import { EASE, SPRING } from "@/lib/motion";
|
import { EASE, SPRING } from "@/lib/motion";
|
||||||
@@ -113,10 +114,10 @@ const Station: React.FC<{
|
|||||||
border: `2px solid ${station.color}${glowing || pulseGlow ? "cc" : "66"}`,
|
border: `2px solid ${station.color}${glowing || pulseGlow ? "cc" : "66"}`,
|
||||||
boxShadow:
|
boxShadow:
|
||||||
glowOpacity > 0
|
glowOpacity > 0
|
||||||
? `0 0 20px ${station.color}${Math.round(glowOpacity * 255)
|
? `0 4px 20px rgba(0,0,0,0.4), 0 0 20px ${station.color}${Math.round(glowOpacity * 255)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
.padStart(2, "0")}`
|
.padStart(2, "0")}`
|
||||||
: "none",
|
: "0 4px 20px rgba(0,0,0,0.4)",
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${scale})`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@@ -386,7 +387,17 @@ const SingleFlow: React.FC = () => {
|
|||||||
zIndex: frame >= 108 ? 0 : 5,
|
zIndex: frame >= 108 ? 0 : 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PhotoPlaceholder width={thumbSize} height={thumbSize} hue={30} />
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-landscape.jpg")}
|
||||||
|
style={{
|
||||||
|
width: thumbSize,
|
||||||
|
height: thumbSize,
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1.5px solid rgba(255,255,255,0.15)",
|
||||||
|
boxShadow: "0 4px 20px rgba(0,0,0,0.4)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/* Watermark stamp */}
|
{/* Watermark stamp */}
|
||||||
{stampOpacity > 0 && (
|
{stampOpacity > 0 && (
|
||||||
<span
|
<span
|
||||||
@@ -395,7 +406,7 @@ const SingleFlow: React.FC = () => {
|
|||||||
bottom: 2,
|
bottom: 2,
|
||||||
right: 3,
|
right: 3,
|
||||||
fontFamily: FONT.body,
|
fontFamily: FONT.body,
|
||||||
fontSize: 5,
|
fontSize: 8,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: `rgba(255,255,255,${stampOpacity})`,
|
color: `rgba(255,255,255,${stampOpacity})`,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
@@ -489,9 +500,10 @@ const BatchThumb: React.FC<{
|
|||||||
|
|
||||||
if (localFrame < 0) return null;
|
if (localFrame < 0) return null;
|
||||||
|
|
||||||
const hue = random(`thumb-hue-${index}`) * 360;
|
|
||||||
const size = 24 + random(`thumb-size-${index}`) * 8;
|
const size = 24 + random(`thumb-size-${index}`) * 8;
|
||||||
const yOffset = (random(`thumb-y-${index}`) - 0.5) * 30;
|
const yOffset = (random(`thumb-y-${index}`) - 0.5) * 30;
|
||||||
|
const objPosX = Math.round(random(`thumb-opx-${index}`) * 100);
|
||||||
|
const objPosY = Math.round(random(`thumb-opy-${index}`) * 100);
|
||||||
|
|
||||||
// Fast traversal: cover pipeline in ~28 frames per thumb
|
// Fast traversal: cover pipeline in ~28 frames per thumb
|
||||||
const thumbX = interpolate(localFrame, [0, 28], [THUMB_START_X, FOLDER_X + 10], {
|
const thumbX = interpolate(localFrame, [0, 28], [THUMB_START_X, FOLDER_X + 10], {
|
||||||
@@ -517,7 +529,17 @@ const BatchThumb: React.FC<{
|
|||||||
zIndex: 4,
|
zIndex: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PhotoPlaceholder width={size} height={size} hue={hue} />
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-landscape.jpg")}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: `${objPosX}% ${objPosY}%`,
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -601,13 +623,10 @@ export const PipelineFlow: React.FC = () => {
|
|||||||
</Sequence>
|
</Sequence>
|
||||||
|
|
||||||
{/* Act 1 stations (visible throughout until Act 2 takes over) */}
|
{/* Act 1 stations (visible throughout until Act 2 takes over) */}
|
||||||
{frame < 40 && (
|
{frame < 40 &&
|
||||||
<>
|
STATIONS.map((s, i) => (
|
||||||
{STATIONS.map((s, i) => (
|
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
||||||
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
))}
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Act 2: Single Image Flow (frame 40-170) */}
|
{/* Act 2: Single Image Flow (frame 40-170) */}
|
||||||
<Sequence from={40} durationInFrames={130}>
|
<Sequence from={40} durationInFrames={130}>
|
||||||
@@ -640,13 +659,11 @@ export const PipelineFlow: React.FC = () => {
|
|||||||
</Sequence>
|
</Sequence>
|
||||||
|
|
||||||
{/* Act 3 stations when not covered by SingleFlow or BatchStream */}
|
{/* Act 3 stations when not covered by SingleFlow or BatchStream */}
|
||||||
{frame >= 170 && frame < 180 && (
|
{frame >= 170 &&
|
||||||
<>
|
frame < 180 &&
|
||||||
{STATIONS.map((s, i) => (
|
STATIONS.map((s, i) => (
|
||||||
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
||||||
))}
|
))}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Act 4: Result (frame 340-390) */}
|
{/* Act 4: Result (frame 340-390) */}
|
||||||
{frame >= 340 && (
|
{frame >= 340 && (
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
AbsoluteFill,
|
AbsoluteFill,
|
||||||
|
Img,
|
||||||
interpolate,
|
interpolate,
|
||||||
interpolateColors,
|
interpolateColors,
|
||||||
spring,
|
spring,
|
||||||
|
staticFile,
|
||||||
useCurrentFrame,
|
useCurrentFrame,
|
||||||
useVideoConfig,
|
useVideoConfig,
|
||||||
} from "remotion";
|
} from "remotion";
|
||||||
import { AppMockup } from "@/components/AppMockup";
|
|
||||||
import { ClipReveal } from "@/components/ClipReveal";
|
import { ClipReveal } from "@/components/ClipReveal";
|
||||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
import { TerminalWindow } from "@/components/TerminalWindow";
|
import { TerminalWindow } from "@/components/TerminalWindow";
|
||||||
@@ -189,7 +190,7 @@ export const OneCommand: React.FC = () => {
|
|||||||
/* ================================================================ */
|
/* ================================================================ */
|
||||||
|
|
||||||
/* Amber glow behind browser */
|
/* Amber glow behind browser */
|
||||||
const glowOpacity = interpolate(frame, [320, 340], [0, 0.05], {
|
const glowOpacity = interpolate(frame, [320, 340], [0, 0.12], {
|
||||||
extrapolateLeft: "clamp",
|
extrapolateLeft: "clamp",
|
||||||
extrapolateRight: "clamp",
|
extrapolateRight: "clamp",
|
||||||
});
|
});
|
||||||
@@ -304,16 +305,26 @@ export const OneCommand: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* App mockup (appears during morph) */}
|
{/* Real app screenshot (appears during morph) */}
|
||||||
{showAppMockup && (
|
{showAppMockup && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
opacity: appMockupOpacity,
|
opacity: appMockupOpacity,
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "0 0 12px 12px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppMockup startFrame={260} />
|
<Img
|
||||||
|
src={staticFile("screenshots/app-full.png")}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "top center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TerminalWindow>
|
</TerminalWindow>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
import {
|
||||||
|
AbsoluteFill,
|
||||||
|
Img,
|
||||||
|
interpolate,
|
||||||
|
spring,
|
||||||
|
staticFile,
|
||||||
|
useCurrentFrame,
|
||||||
|
useVideoConfig,
|
||||||
|
} from "remotion";
|
||||||
import { ClipReveal } from "@/components/ClipReveal";
|
import { ClipReveal } from "@/components/ClipReveal";
|
||||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
|
||||||
import { ShieldIcon } from "@/components/ShieldIcon";
|
import { ShieldIcon } from "@/components/ShieldIcon";
|
||||||
import { TEXT } from "@/lib/fonts";
|
import { TEXT } from "@/lib/fonts";
|
||||||
import { EASE, SPRING } from "@/lib/motion";
|
import { EASE, SPRING } from "@/lib/motion";
|
||||||
@@ -12,9 +19,23 @@ import { EASE, SPRING } from "@/lib/motion";
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
const PHOTOS = [
|
const PHOTOS = [
|
||||||
{ hue: 210, w: 180, h: 135, fromX: -300, fromY: -250, delay: 0 },
|
{
|
||||||
{ hue: 150, w: 160, h: 120, fromX: 320, fromY: 280, delay: 4 },
|
src: "screenshots/sample-photo-portrait.jpg",
|
||||||
{ hue: 280, w: 150, h: 115, fromX: 280, fromY: -260, delay: 8 },
|
w: 180,
|
||||||
|
h: 135,
|
||||||
|
fromX: -300,
|
||||||
|
fromY: -250,
|
||||||
|
delay: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "screenshots/sample-photo-landscape.jpg",
|
||||||
|
w: 160,
|
||||||
|
h: 120,
|
||||||
|
fromX: 320,
|
||||||
|
fromY: 280,
|
||||||
|
delay: 4,
|
||||||
|
},
|
||||||
|
{ src: "screenshots/sample-photo-bw.jpg", w: 150, h: 115, fromX: 280, fromY: -260, delay: 8 },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -23,8 +44,8 @@ const PHOTOS = [
|
|||||||
|
|
||||||
const PadlockIcon: React.FC<{ opacity: number }> = ({ opacity }) => (
|
const PadlockIcon: React.FC<{ opacity: number }> = ({ opacity }) => (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={36}
|
||||||
height={24}
|
height={36}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
style={{ opacity }}
|
style={{ opacity }}
|
||||||
role="img"
|
role="img"
|
||||||
@@ -190,13 +211,17 @@ export const PrivacyPromise: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PhotoPlaceholder
|
<Img
|
||||||
key={photo.hue}
|
key={photo.src}
|
||||||
width={photo.w}
|
src={staticFile(photo.src)}
|
||||||
height={photo.h}
|
|
||||||
hue={photo.hue}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
width: photo.w,
|
||||||
|
height: photo.h,
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "2px solid rgba(255,255,255,0.2)",
|
||||||
|
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
||||||
left: `calc(50% + ${exitX}px - ${photo.w / 2}px)`,
|
left: `calc(50% + ${exitX}px - ${photo.w / 2}px)`,
|
||||||
top: `calc(50% + ${exitY}px - ${photo.h / 2}px)`,
|
top: `calc(50% + ${exitY}px - ${photo.h / 2}px)`,
|
||||||
transform: `scale(${entrySpring}) rotate(${rotation}deg)`,
|
transform: `scale(${entrySpring}) rotate(${rotation}deg)`,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "remotion";
|
} from "remotion";
|
||||||
import { ClipReveal } from "@/components/ClipReveal";
|
import { ClipReveal } from "@/components/ClipReveal";
|
||||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
|
||||||
import { ShieldIcon } from "@/components/ShieldIcon";
|
import { ShieldIcon } from "@/components/ShieldIcon";
|
||||||
import { COLOR } from "@/lib/colors";
|
import { COLOR } from "@/lib/colors";
|
||||||
import { FONT, TEXT } from "@/lib/fonts";
|
import { FONT, TEXT } from "@/lib/fonts";
|
||||||
@@ -35,9 +34,9 @@ const CLOUD_SVG_PATH = "M6 19a5 5 0 0 1-1-9.9A7 7 0 0 1 19.07 11 4.5 4.5 0 0 1 1
|
|||||||
const LOCK_SVG_PATH =
|
const LOCK_SVG_PATH =
|
||||||
"M5 11V7a5 5 0 0 1 10 0v4M3 11h14a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z";
|
"M5 11V7a5 5 0 0 1 10 0v4M3 11h14a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z";
|
||||||
|
|
||||||
const SERVER_SVG_PATH = "M2 2h16v6H2zM2 10h16v6H2zM6 5h.01M6 13h.01";
|
const _SERVER_SVG_PATH = "M2 2h16v6H2zM2 10h16v6H2zM6 5h.01M6 13h.01";
|
||||||
|
|
||||||
const EYE_SVG_PATH =
|
const _EYE_SVG_PATH =
|
||||||
"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z";
|
"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -487,7 +486,7 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Shield outline */
|
/* Shield outline */
|
||||||
const shieldDrawProgress = interpolate(frame, [135, 160], [0, 1], {
|
const _shieldDrawProgress = interpolate(frame, [135, 160], [0, 1], {
|
||||||
extrapolateLeft: "clamp",
|
extrapolateLeft: "clamp",
|
||||||
extrapolateRight: "clamp",
|
extrapolateRight: "clamp",
|
||||||
easing: EASE.enter,
|
easing: EASE.enter,
|
||||||
@@ -559,14 +558,14 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
return { ...badge, scale: s };
|
return { ...badge, scale: s };
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Left side red tint escalation */
|
/* Left side red tint escalation (starts at 5%, escalates to 20% by Act 4) */
|
||||||
const escalatedLeftTint = interpolate(frame, [160, 320], [0.03, 0.15], {
|
const escalatedLeftTint = interpolate(frame, [160, 320], [0.05, 0.2], {
|
||||||
extrapolateLeft: "clamp",
|
extrapolateLeft: "clamp",
|
||||||
extrapolateRight: "clamp",
|
extrapolateRight: "clamp",
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Shake animation (frame 300-320) */
|
/* Shake animation (frame 300-320) */
|
||||||
const shakeX = frame >= 300 && frame <= 320 ? Math.sin(frame * 2.5) * 2 : 0;
|
const shakeX = frame >= 300 && frame <= 320 ? Math.sin(frame * 2.5) * 5 : 0;
|
||||||
|
|
||||||
/* Right: extra photos arrive calmly */
|
/* Right: extra photos arrive calmly */
|
||||||
const rightExtraPhotos = Array.from({ length: 4 }, (_, i) => {
|
const rightExtraPhotos = Array.from({ length: 4 }, (_, i) => {
|
||||||
@@ -593,13 +592,13 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
/* ================================================================ */
|
/* ================================================================ */
|
||||||
|
|
||||||
/* Left: deeper red tint */
|
/* Left: deeper red tint */
|
||||||
const peakLeftTint = interpolate(frame, [320, 360], [0.15, 0.18], {
|
const peakLeftTint = interpolate(frame, [320, 360], [0.2, 0.25], {
|
||||||
extrapolateLeft: "clamp",
|
extrapolateLeft: "clamp",
|
||||||
extrapolateRight: "clamp",
|
extrapolateRight: "clamp",
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Left vibrate */
|
/* Left vibrate */
|
||||||
const vibrateX = frame >= 320 && frame < 420 ? Math.sin(frame * 3) * 1.5 : 0;
|
const vibrateX = frame >= 320 && frame < 420 ? Math.sin(frame * 3) * 3 : 0;
|
||||||
|
|
||||||
/* Right glow */
|
/* Right glow */
|
||||||
const rightGlowOpacity = interpolate(frame, [360, 400], [0, 0.12], {
|
const rightGlowOpacity = interpolate(frame, [360, 400], [0, 0.12], {
|
||||||
@@ -736,12 +735,14 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
|
|
||||||
{/* Photo thumbnail (Act 2) */}
|
{/* Photo thumbnail (Act 2) */}
|
||||||
{frame >= 40 && frame < 85 && (
|
{frame >= 40 && frame < 85 && (
|
||||||
<PhotoPlaceholder
|
<Img
|
||||||
width={48}
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
height={48}
|
|
||||||
hue={30}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 6,
|
||||||
left: leftPhotoX - 24,
|
left: leftPhotoX - 24,
|
||||||
top: leftPhotoY - 24,
|
top: leftPhotoY - 24,
|
||||||
opacity: leftPhotoOpacity,
|
opacity: leftPhotoOpacity,
|
||||||
@@ -784,7 +785,7 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
opacity: Math.min(scatterProgress * 2, 1),
|
opacity: Math.min(scatterProgress * 2, 1),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerIcon size={24} />
|
<ServerIcon size={36} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -794,13 +795,15 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
const copyX = interpolate(scatterProgress, [0, 1], [400, server.x]);
|
const copyX = interpolate(scatterProgress, [0, 1], [400, server.x]);
|
||||||
const copyY = interpolate(scatterProgress, [0, 1], [160, server.y - 30]);
|
const copyY = interpolate(scatterProgress, [0, 1], [160, server.y - 30]);
|
||||||
return (
|
return (
|
||||||
<PhotoPlaceholder
|
<Img
|
||||||
key={`copy-${i}`}
|
key={`copy-${i}`}
|
||||||
width={28}
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
height={28}
|
|
||||||
hue={30 + i * 20}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 4,
|
||||||
left: copyX - 14,
|
left: copyX - 14,
|
||||||
top: copyY - 14,
|
top: copyY - 14,
|
||||||
opacity: scatterProgress,
|
opacity: scatterProgress,
|
||||||
@@ -865,13 +868,16 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
const py = interpolate(photo.progress, [0, 1], [750, 160]);
|
const py = interpolate(photo.progress, [0, 1], [750, 160]);
|
||||||
if (photo.progress <= 0) return null;
|
if (photo.progress <= 0) return null;
|
||||||
return (
|
return (
|
||||||
<PhotoPlaceholder
|
<Img
|
||||||
key={`extra-${photo.index}`}
|
key={`extra-${photo.index}`}
|
||||||
width={36}
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
height={36}
|
|
||||||
hue={photo.hue}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: `${photo.index * 25}% center`,
|
||||||
|
borderRadius: 6,
|
||||||
left: px - 18,
|
left: px - 18,
|
||||||
top: py - 18,
|
top: py - 18,
|
||||||
opacity: photo.progress,
|
opacity: photo.progress,
|
||||||
@@ -893,7 +899,7 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
opacity: Math.min(server.scale * 2, 1),
|
opacity: Math.min(server.scale * 2, 1),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerIcon size={22} />
|
<ServerIcon size={36} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -1132,12 +1138,14 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
|
|
||||||
{/* First photo (Act 2) */}
|
{/* First photo (Act 2) */}
|
||||||
{frame >= 40 && frame < 80 && (
|
{frame >= 40 && frame < 80 && (
|
||||||
<PhotoPlaceholder
|
<Img
|
||||||
width={48}
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
height={48}
|
|
||||||
hue={140}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 6,
|
||||||
left: rightPhotoX - DIVIDER_X - 24,
|
left: rightPhotoX - DIVIDER_X - 24,
|
||||||
top: rightPhotoY - 24,
|
top: rightPhotoY - 24,
|
||||||
opacity: rightPhotoOpacity,
|
opacity: rightPhotoOpacity,
|
||||||
@@ -1250,7 +1258,16 @@ export const CloudVsLocal: React.FC = () => {
|
|||||||
key={`rphoto-${photo.index}`}
|
key={`rphoto-${photo.index}`}
|
||||||
style={{ position: "absolute", left: px, top: py }}
|
style={{ position: "absolute", left: px, top: py }}
|
||||||
>
|
>
|
||||||
<PhotoPlaceholder width={32} height={32} hue={photo.hue} />
|
<Img
|
||||||
|
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: `${photo.index * 25}% center`,
|
||||||
|
borderRadius: 6,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{photo.checkScale > 0.1 && (
|
{photo.checkScale > 0.1 && (
|
||||||
<div style={{ position: "absolute", right: -8, bottom: -8 }}>
|
<div style={{ position: "absolute", right: -8, bottom: -8 }}>
|
||||||
<Checkmark size={14} color={COLOR.safe} scale={photo.checkScale} />
|
<Checkmark size={14} color={COLOR.safe} scale={photo.checkScale} />
|
||||||
|
|||||||
Reference in New Issue
Block a user