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}
|
||||
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
|
||||
d={SHIELD_PATH}
|
||||
stroke="white"
|
||||
strokeWidth={2.5}
|
||||
fill={`rgba(255,255,255,${fillOpacity})`}
|
||||
strokeWidth={3.5}
|
||||
fill={fillOpacity > 0 ? "url(#shield-fill-grad)" : "none"}
|
||||
fillOpacity={fillOpacity > 0 ? fillOpacity * 12 : 0}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
interpolate,
|
||||
interpolateColors,
|
||||
random,
|
||||
Sequence,
|
||||
spring,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
@@ -133,19 +135,8 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
||||
easing: EASE.smooth,
|
||||
});
|
||||
|
||||
// Geometric pattern background
|
||||
const patternBg = `
|
||||
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%)
|
||||
`;
|
||||
// Checkerboard background for transparency effect
|
||||
const checkerBg = CHECKERBOARD_BG;
|
||||
|
||||
// Scan line particle trail
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
{/* "Before" layer: photo with geometric background */}
|
||||
<div
|
||||
{/* "Before" layer: real portrait photo */}
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage: patternBg,
|
||||
backgroundSize: "24px 24px",
|
||||
backgroundColor: "hsl(30, 20%, 35%)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
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
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -222,16 +204,18 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: CHECKERBOARD_BG,
|
||||
background: checkerBg,
|
||||
}}
|
||||
/>
|
||||
{/* Subject only */}
|
||||
<div
|
||||
{/* Subject portrait on top of checkerboard */}
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage: silhouetteGradient,
|
||||
backgroundRepeat: "no-repeat",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -246,7 +230,7 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
||||
width: 2,
|
||||
height: "100%",
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
@@ -260,7 +244,7 @@ const SegmentBackgroundRemoval: React.FC = () => {
|
||||
key={`scan-p-${i}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: photoX + p.x - photoX,
|
||||
left: p.x,
|
||||
top: p.y,
|
||||
width: 3,
|
||||
height: 3,
|
||||
@@ -358,26 +342,38 @@ const SegmentColorization: React.FC = () => {
|
||||
transform: `scale(${holdScale})`,
|
||||
}}
|
||||
>
|
||||
{/* Grayscale layer (on top) */}
|
||||
<div
|
||||
{/* B&W layer (on top, real B&W photo) */}
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-bw.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: `linear-gradient(135deg, hsl(30, 60%, 65%) 0%, hsl(200, 50%, 55%) 50%, hsl(340, 55%, 60%) 100%)`,
|
||||
filter: "grayscale(1)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Color layer (revealed underneath via clip) */}
|
||||
{/* Color layer (revealed via clip, using portrait as color version) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
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)`,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<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 */}
|
||||
{colorParticles.map(
|
||||
@@ -411,11 +407,11 @@ const SegmentColorization: React.FC = () => {
|
||||
const PIXEL_GRID_SIZE = 8;
|
||||
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 hue = random(`pixel-hue-${i}`) * 60 + 15; // warm hues
|
||||
const sat = 40 + random(`pixel-sat-${i}`) * 30;
|
||||
const lit = 40 + random(`pixel-lit-${i}`) * 30;
|
||||
const hue = random(`pixel-hue-${i}`) * 80 + 180; // blues and greens (180-260)
|
||||
const sat = 35 + random(`pixel-sat-${i}`) * 35;
|
||||
const lit = 35 + random(`pixel-lit-${i}`) * 30;
|
||||
return `hsl(${hue}, ${sat}%, ${lit}%)`;
|
||||
});
|
||||
|
||||
@@ -518,14 +514,17 @@ const SegmentSuperResolution: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Smooth "resolved" version */}
|
||||
{/* Smooth "resolved" version - actual landscape photo */}
|
||||
{showSmooth && (
|
||||
<div
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-landscape.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 8,
|
||||
background: `linear-gradient(135deg, hsl(25, 55%, 50%) 0%, hsl(40, 50%, 55%) 50%, hsl(30, 60%, 45%) 100%)`,
|
||||
opacity: smoothOpacity,
|
||||
border: "1.5px solid rgba(255,255,255,0.15)",
|
||||
boxShadow: "0 8px 24px rgba(0,0,0,0.3)",
|
||||
@@ -630,8 +629,8 @@ const SegmentRestoration: React.FC = () => {
|
||||
});
|
||||
|
||||
// Color shift from sepia to warm
|
||||
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 _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%)"]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: COLOR.dark }}>
|
||||
@@ -653,15 +652,18 @@ const SegmentRestoration: React.FC = () => {
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
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 */}
|
||||
<div
|
||||
{/* Base photo - real landscape */}
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-landscape.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
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 {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
interpolate,
|
||||
random,
|
||||
Sequence,
|
||||
spring,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { Counter } from "@/components/Counter";
|
||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
import { FONT, TEXT } from "@/lib/fonts";
|
||||
import { EASE, SPRING } from "@/lib/motion";
|
||||
@@ -113,10 +114,10 @@ const Station: React.FC<{
|
||||
border: `2px solid ${station.color}${glowing || pulseGlow ? "cc" : "66"}`,
|
||||
boxShadow:
|
||||
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)
|
||||
.padStart(2, "0")}`
|
||||
: "none",
|
||||
: "0 4px 20px rgba(0,0,0,0.4)",
|
||||
transform: `scale(${scale})`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@@ -386,7 +387,17 @@ const SingleFlow: React.FC = () => {
|
||||
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 */}
|
||||
{stampOpacity > 0 && (
|
||||
<span
|
||||
@@ -395,7 +406,7 @@ const SingleFlow: React.FC = () => {
|
||||
bottom: 2,
|
||||
right: 3,
|
||||
fontFamily: FONT.body,
|
||||
fontSize: 5,
|
||||
fontSize: 8,
|
||||
fontWeight: 700,
|
||||
color: `rgba(255,255,255,${stampOpacity})`,
|
||||
whiteSpace: "nowrap",
|
||||
@@ -489,9 +500,10 @@ const BatchThumb: React.FC<{
|
||||
|
||||
if (localFrame < 0) return null;
|
||||
|
||||
const hue = random(`thumb-hue-${index}`) * 360;
|
||||
const size = 24 + random(`thumb-size-${index}`) * 8;
|
||||
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
|
||||
const thumbX = interpolate(localFrame, [0, 28], [THUMB_START_X, FOLDER_X + 10], {
|
||||
@@ -517,7 +529,17 @@ const BatchThumb: React.FC<{
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -601,13 +623,10 @@ export const PipelineFlow: React.FC = () => {
|
||||
</Sequence>
|
||||
|
||||
{/* Act 1 stations (visible throughout until Act 2 takes over) */}
|
||||
{frame < 40 && (
|
||||
<>
|
||||
{STATIONS.map((s, i) => (
|
||||
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{frame < 40 &&
|
||||
STATIONS.map((s, i) => (
|
||||
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
||||
))}
|
||||
|
||||
{/* Act 2: Single Image Flow (frame 40-170) */}
|
||||
<Sequence from={40} durationInFrames={130}>
|
||||
@@ -640,13 +659,11 @@ export const PipelineFlow: React.FC = () => {
|
||||
</Sequence>
|
||||
|
||||
{/* Act 3 stations when not covered by SingleFlow or BatchStream */}
|
||||
{frame >= 170 && frame < 180 && (
|
||||
<>
|
||||
{STATIONS.map((s, i) => (
|
||||
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{frame >= 170 &&
|
||||
frame < 180 &&
|
||||
STATIONS.map((s, i) => (
|
||||
<Station key={s.id} station={s} index={i} glowing={false} pulseGlow={false} />
|
||||
))}
|
||||
|
||||
{/* Act 4: Result (frame 340-390) */}
|
||||
{frame >= 340 && (
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
interpolate,
|
||||
interpolateColors,
|
||||
spring,
|
||||
staticFile,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
} from "remotion";
|
||||
import { AppMockup } from "@/components/AppMockup";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||
import { TerminalWindow } from "@/components/TerminalWindow";
|
||||
@@ -189,7 +190,7 @@ export const OneCommand: React.FC = () => {
|
||||
/* ================================================================ */
|
||||
|
||||
/* Amber glow behind browser */
|
||||
const glowOpacity = interpolate(frame, [320, 340], [0, 0.05], {
|
||||
const glowOpacity = interpolate(frame, [320, 340], [0, 0.12], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
@@ -304,16 +305,26 @@ export const OneCommand: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* App mockup (appears during morph) */}
|
||||
{/* Real app screenshot (appears during morph) */}
|
||||
{showAppMockup && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
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>
|
||||
)}
|
||||
</TerminalWindow>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
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 { GrainOverlay } from "@/components/GrainOverlay";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { ShieldIcon } from "@/components/ShieldIcon";
|
||||
import { TEXT } from "@/lib/fonts";
|
||||
import { EASE, SPRING } from "@/lib/motion";
|
||||
@@ -12,9 +19,23 @@ import { EASE, SPRING } from "@/lib/motion";
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
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 },
|
||||
{ hue: 280, w: 150, h: 115, fromX: 280, fromY: -260, delay: 8 },
|
||||
{
|
||||
src: "screenshots/sample-photo-portrait.jpg",
|
||||
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;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -23,8 +44,8 @@ const PHOTOS = [
|
||||
|
||||
const PadlockIcon: React.FC<{ opacity: number }> = ({ opacity }) => (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
width={36}
|
||||
height={36}
|
||||
viewBox="0 0 24 24"
|
||||
style={{ opacity }}
|
||||
role="img"
|
||||
@@ -190,13 +211,17 @@ export const PrivacyPromise: React.FC = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<PhotoPlaceholder
|
||||
key={photo.hue}
|
||||
width={photo.w}
|
||||
height={photo.h}
|
||||
hue={photo.hue}
|
||||
<Img
|
||||
key={photo.src}
|
||||
src={staticFile(photo.src)}
|
||||
style={{
|
||||
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)`,
|
||||
top: `calc(50% + ${exitY}px - ${photo.h / 2}px)`,
|
||||
transform: `scale(${entrySpring}) rotate(${rotation}deg)`,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "remotion";
|
||||
import { ClipReveal } from "@/components/ClipReveal";
|
||||
import { GrainOverlay } from "@/components/GrainOverlay";
|
||||
import { PhotoPlaceholder } from "@/components/PhotoPlaceholder";
|
||||
import { ShieldIcon } from "@/components/ShieldIcon";
|
||||
import { COLOR } from "@/lib/colors";
|
||||
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 =
|
||||
"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";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -487,7 +486,7 @@ export const CloudVsLocal: React.FC = () => {
|
||||
});
|
||||
|
||||
/* Shield outline */
|
||||
const shieldDrawProgress = interpolate(frame, [135, 160], [0, 1], {
|
||||
const _shieldDrawProgress = interpolate(frame, [135, 160], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: EASE.enter,
|
||||
@@ -559,14 +558,14 @@ export const CloudVsLocal: React.FC = () => {
|
||||
return { ...badge, scale: s };
|
||||
});
|
||||
|
||||
/* Left side red tint escalation */
|
||||
const escalatedLeftTint = interpolate(frame, [160, 320], [0.03, 0.15], {
|
||||
/* Left side red tint escalation (starts at 5%, escalates to 20% by Act 4) */
|
||||
const escalatedLeftTint = interpolate(frame, [160, 320], [0.05, 0.2], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
/* 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 */
|
||||
const rightExtraPhotos = Array.from({ length: 4 }, (_, i) => {
|
||||
@@ -593,13 +592,13 @@ export const CloudVsLocal: React.FC = () => {
|
||||
/* ================================================================ */
|
||||
|
||||
/* 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",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
/* 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 */
|
||||
const rightGlowOpacity = interpolate(frame, [360, 400], [0, 0.12], {
|
||||
@@ -736,12 +735,14 @@ export const CloudVsLocal: React.FC = () => {
|
||||
|
||||
{/* Photo thumbnail (Act 2) */}
|
||||
{frame >= 40 && frame < 85 && (
|
||||
<PhotoPlaceholder
|
||||
width={48}
|
||||
height={48}
|
||||
hue={30}
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 48,
|
||||
height: 48,
|
||||
objectFit: "cover",
|
||||
borderRadius: 6,
|
||||
left: leftPhotoX - 24,
|
||||
top: leftPhotoY - 24,
|
||||
opacity: leftPhotoOpacity,
|
||||
@@ -784,7 +785,7 @@ export const CloudVsLocal: React.FC = () => {
|
||||
opacity: Math.min(scatterProgress * 2, 1),
|
||||
}}
|
||||
>
|
||||
<ServerIcon size={24} />
|
||||
<ServerIcon size={36} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -794,13 +795,15 @@ export const CloudVsLocal: React.FC = () => {
|
||||
const copyX = interpolate(scatterProgress, [0, 1], [400, server.x]);
|
||||
const copyY = interpolate(scatterProgress, [0, 1], [160, server.y - 30]);
|
||||
return (
|
||||
<PhotoPlaceholder
|
||||
<Img
|
||||
key={`copy-${i}`}
|
||||
width={28}
|
||||
height={28}
|
||||
hue={30 + i * 20}
|
||||
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 28,
|
||||
height: 28,
|
||||
objectFit: "cover",
|
||||
borderRadius: 4,
|
||||
left: copyX - 14,
|
||||
top: copyY - 14,
|
||||
opacity: scatterProgress,
|
||||
@@ -865,13 +868,16 @@ export const CloudVsLocal: React.FC = () => {
|
||||
const py = interpolate(photo.progress, [0, 1], [750, 160]);
|
||||
if (photo.progress <= 0) return null;
|
||||
return (
|
||||
<PhotoPlaceholder
|
||||
<Img
|
||||
key={`extra-${photo.index}`}
|
||||
width={36}
|
||||
height={36}
|
||||
hue={photo.hue}
|
||||
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 36,
|
||||
height: 36,
|
||||
objectFit: "cover",
|
||||
objectPosition: `${photo.index * 25}% center`,
|
||||
borderRadius: 6,
|
||||
left: px - 18,
|
||||
top: py - 18,
|
||||
opacity: photo.progress,
|
||||
@@ -893,7 +899,7 @@ export const CloudVsLocal: React.FC = () => {
|
||||
opacity: Math.min(server.scale * 2, 1),
|
||||
}}
|
||||
>
|
||||
<ServerIcon size={22} />
|
||||
<ServerIcon size={36} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1132,12 +1138,14 @@ export const CloudVsLocal: React.FC = () => {
|
||||
|
||||
{/* First photo (Act 2) */}
|
||||
{frame >= 40 && frame < 80 && (
|
||||
<PhotoPlaceholder
|
||||
width={48}
|
||||
height={48}
|
||||
hue={140}
|
||||
<Img
|
||||
src={staticFile("screenshots/sample-photo-portrait.jpg")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 48,
|
||||
height: 48,
|
||||
objectFit: "cover",
|
||||
borderRadius: 6,
|
||||
left: rightPhotoX - DIVIDER_X - 24,
|
||||
top: rightPhotoY - 24,
|
||||
opacity: rightPhotoOpacity,
|
||||
@@ -1250,7 +1258,16 @@ export const CloudVsLocal: React.FC = () => {
|
||||
key={`rphoto-${photo.index}`}
|
||||
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 && (
|
||||
<div style={{ position: "absolute", right: -8, bottom: -8 }}>
|
||||
<Checkmark size={14} color={COLOR.safe} scale={photo.checkScale} />
|
||||
|
||||
Reference in New Issue
Block a user