// phone-scroll-sync —— Phone scrolls while browsing + synced finger (mobile / interaction) // Phone fades in → the finger swipes up from the bottom-right of the screen, and the feed content scrolls in sync with the finger's movement (two scrolls + // a pause in between) → the finger fades out and the whole phone breathes on hold once settled. The finger is vector art (semi-transparent capsule + fingertip), // no Lottie gestures. The screen is a grayscale vector mockup, deterministic rendering. import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion'; import { Phone, SCREEN_W } from '../../_fixtures/PhoneFixtures'; const CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const; export const PHONE_SCROLL_SYNC_DURATION = 180; // 6s @ 30fps // Timeline (180f) const PHONE_IN = 4; // phone fades in const HAND_IN = 26; // finger enters const SCROLL_1_START = 34; // first scroll segment const SCROLL_1_END = 66; // first segment ends const PAUSE_1 = 76; // first pause (content still, finger floats slightly) const SCROLL_2_START = 86; // second scroll segment const SCROLL_2_END = 118; // second segment ends const PAUSE_2 = 134; // second pause const HAND_OUT = 142; // finger fades out const HOLD = 152; // all settled → breathing const FEED_BASE = [ { w: 64, h: 64, lines: [170, 240, 140] }, { w: 56, h: 56, lines: [150, 220, 130] }, { w: 60, h: 60, lines: [160, 230, 120] }, { w: 68, h: 68, lines: [180, 250, 150] }, { w: 52, h: 52, lines: [140, 210, 110] }, { w: 62, h: 62, lines: [155, 225, 135] }, ]; // Feed content is duplicated so cards remain at the bottom after both scrolls const FEED = [...FEED_BASE, ...FEED_BASE]; // Finger swipe offset (on-screen coords): two segments, each -120px const handY = (f: number) => { const s1 = interpolate(f, [SCROLL_1_START, SCROLL_1_END], [0, 1], { easing: Easing.out(Easing.cubic), ...CL, }); const s2 = interpolate(f, [SCROLL_2_START, SCROLL_2_END], [0, 1], { easing: Easing.out(Easing.cubic), ...CL, }); return 560 - 140 * s1 - 140 * s2; }; // Content scroll offset: synced with the finger (each 120px of finger swipe moves the content up 40% of a screen) const contentY = (f: number) => { const s1 = interpolate(f, [SCROLL_1_START, SCROLL_1_END], [0, 1], { easing: Easing.out(Easing.cubic), ...CL, }); const s2 = interpolate(f, [SCROLL_2_START, SCROLL_2_END], [0, 1], { easing: Easing.out(Easing.cubic), ...CL, }); return -200 * s1 - 200 * s2; }; export const PhoneScrollSync: React.FC = () => { const f = useCurrentFrame(); const phoneIn = interpolate(f, [PHONE_IN, PHONE_IN + 16], [0.85, 1], { easing: Easing.out(Easing.cubic), ...CL, }); const phoneY = (1 - phoneIn) * 20; // Finger visibility: spring-like fade in on entry, fades out between/after the two scroll segments const handIn = interpolate(f, [HAND_IN, HAND_IN + 10], [0, 1], { easing: Easing.out(Easing.cubic), ...CL, }); const handOut = interpolate(f, [HAND_OUT, HAND_OUT + 10], [1, 0], CL); // Finger floats up and down slightly during pauses const float = f >= PAUSE_1 && f < SCROLL_2_START ? Math.sin((f - PAUSE_1) * 0.25) * 3 : 0; // Breathing const breathe = f < HOLD ? 1 : 1 + 0.008 * Math.sin(((f - HOLD) / 48) * Math.PI * 2); return (
{/* Header */}
Discover
{/* Scrolling content */}
{/* Top placeholder card (visible at the start) */} {FEED.map((c, i) => (
{c.lines.map((lw, j) => (
))}
))}
{/* Finger: semi-transparent capsule + fingertip dot, at the right edge of the screen */} {f >= HAND_IN && f < HAND_OUT + 12 && (
{/* Fingertip (small circle at the top) */}
{/* Finger body (gradient capsule) */}
)}
); };