From 89eec108fe221262f0db2c52a1b96cf233acc690 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 16:34:30 +0800 Subject: [PATCH] feat(videos): add PrivacyPromise hero loop with shield draw animation --- apps/videos/src/Root.tsx | 9 + .../src/compositions/hero/PrivacyPromise.tsx | 273 ++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 apps/videos/src/compositions/hero/PrivacyPromise.tsx diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx index ed9cb33d..707be692 100644 --- a/apps/videos/src/Root.tsx +++ b/apps/videos/src/Root.tsx @@ -12,6 +12,7 @@ import { FloatingTools } from "./compositions/ambient/FloatingTools"; import { AiMagicReel } from "./compositions/features/AiMagicReel"; import { FormatUniverse } from "./compositions/features/FormatUniverse"; import { PipelineFlow } from "./compositions/features/PipelineFlow"; +import { PrivacyPromise } from "./compositions/hero/PrivacyPromise"; import { TEXT } from "./lib/fonts"; const ComponentTest: React.FC = () => ( @@ -101,5 +102,13 @@ export const RemotionRoot: React.FC = () => ( width={800} height={600} /> + ); diff --git a/apps/videos/src/compositions/hero/PrivacyPromise.tsx b/apps/videos/src/compositions/hero/PrivacyPromise.tsx new file mode 100644 index 00000000..e729c637 --- /dev/null +++ b/apps/videos/src/compositions/hero/PrivacyPromise.tsx @@ -0,0 +1,273 @@ +import type React from "react"; +import { AbsoluteFill, interpolate, spring, 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"; + +/* ------------------------------------------------------------------ */ +/* Photo config */ +/* ------------------------------------------------------------------ */ + +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 }, +] as const; + +/* ------------------------------------------------------------------ */ +/* Padlock SVG (simple circle + rect) */ +/* ------------------------------------------------------------------ */ + +const PadlockIcon: React.FC<{ opacity: number }> = ({ opacity }) => ( + + + + +); + +/* ------------------------------------------------------------------ */ +/* Main composition */ +/* ------------------------------------------------------------------ */ + +export const PrivacyPromise: React.FC = () => { + const frame = useCurrentFrame(); + const { fps, width, height } = useVideoConfig(); + const cx = width / 2; + const cy = height / 2; + + /* ---- Act 3: shield + photos scale & move up ---- */ + const act3Scale = interpolate(frame, [160, 180], [1, 0.6], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.snap, + }); + const act3OffsetY = interpolate(frame, [160, 180], [0, -120], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.snap, + }); + + /* ---- Act 4: fade out ---- */ + const act4ShieldOpacity = interpolate(frame, [240, 270], [1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.exit, + }); + + /* ---- Shield fill (Act 2) ---- */ + const shieldFillOpacity = interpolate(frame, [110, 130], [0, 0.08], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + /* ---- Shield pulse glow (Act 2, frame 130-160) ---- */ + const pulseScale = frame >= 130 && frame <= 240 ? 1 + Math.sin((frame - 130) * 0.314) * 0.02 : 1; + + /* ---- Padlock fade in (Act 2) ---- */ + const padlockOpacity = interpolate(frame, [130, 145], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + /* ---- Act 4 scale-down for shield ---- */ + const act4ShieldScale = interpolate(frame, [240, 270], [1, 0.8], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.exit, + }); + + /* ---- Text fade out (Act 4) ---- */ + const act4TextOpacity = interpolate(frame, [240, 265], [1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.exit, + }); + + return ( + + {/* ---- Shield + Photos group (scales together in Act 3) ---- */} +
+ {/* Shield icon */} + + + {/* Padlock at shield base */} +
+ +
+ + {/* Photos floating around the shield */} + {PHOTOS.map((photo, i) => { + const entrySpring = spring({ + frame: frame - 15 - photo.delay, + fps, + config: SPRING.natural, + }); + + /* Act 1: drift from edges toward center */ + const driftProgress = interpolate(frame, [25, 75], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.smooth, + }); + + /* Act 2: settle inside shield */ + const settleSpring = spring({ + frame: frame - 110, + fps, + config: SPRING.settle, + }); + + /* Settled positions: clustered near center */ + const settledX = (i - 1) * 40; + const settledY = (i - 1) * 25; + + /* Current position blends drift -> settled */ + const preSettleX = photo.fromX * (1 - driftProgress); + const preSettleY = photo.fromY * (1 - driftProgress); + const photoX = + frame < 110 ? preSettleX : preSettleX + (settledX - preSettleX) * settleSpring; + const photoY = + frame < 110 ? preSettleY : preSettleY + (settledY - preSettleY) * settleSpring; + + /* Rotation: organic drift then settle */ + const driftRotation = (i % 2 === 0 ? 3 : -3) * (1 - driftProgress); + const settledRotation = (i - 1) * 2; + const rotation = + frame < 110 + ? driftRotation + : driftRotation + (settledRotation - driftRotation) * settleSpring; + + /* Act 4: photos drift back out (reverse of arrival) */ + const exitProgress = interpolate(frame, [270, 295], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.exit, + }); + const exitX = photoX + photo.fromX * exitProgress; + const exitY = photoY + photo.fromY * exitProgress; + const exitOpacity = interpolate(frame, [270, 295], [1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.exit, + }); + + /* Entry opacity (photos appear from frame 15) */ + const entryOpacity = interpolate(frame, [15 + photo.delay, 20 + photo.delay], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( + + ); + })} +
+ + {/* ---- "Your images." text (Act 2) ---- */} +
+ + Your images. + +
+ + {/* ---- "Stay yours." text (Act 2) ---- */} +
+ + Stay yours. + +
+ + {/* ---- Subtext lines (Act 3) ---- */} + {frame >= 160 && ( +
+ + No uploads to the cloud. Ever. + + + 100% local processing. + + + Free. Forever. + +
+ )} + + +
+ ); +};