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:
SnapOtter
2026-05-08 18:48:22 +08:00
parent fa352f837b
commit a44214cc6d
6 changed files with 205 additions and 125 deletions
+10 -2
View File
@@ -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} />