// skeleton-reveal — slack-promo 4–15s combined // three-stage reveal: a full-screen hand-drawn rough-brush doodle placeholder (round-tipped blob line // art, boiling jitter) → replaced within one beat by a gray-bar skeleton UI window → the skeleton // message list rolls in, and as the camera pushes in the gray bars "develop" line by line into // avatar + text content (the last word of the last line arrives half a beat late). import React from 'react'; import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring, Easing, } from 'remotion'; const mulberry32 = (a: number) => () => { let t = (a += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; const INK = '#2f2f2f'; const PAPER = '#f2f0ea'; // ——— hand-drawn doodle helpers: jitter polylines/circles with a seed and stroke them thick with round caps ——— const wobbleLine = ( x1: number, y1: number, x2: number, y2: number, seed: number, amp = 7, segs = 8, ) => { const rnd = mulberry32(seed); const pts: string[] = []; for (let i = 0; i <= segs; i++) { const t = i / segs; const jx = (rnd() - 0.5) * amp * 2; const jy = (rnd() - 0.5) * amp * 2; pts.push(`${i === 0 ? 'M' : 'L'} ${x1 + (x2 - x1) * t + jx} ${y1 + (y2 - y1) * t + jy}`); } return pts.join(' '); }; const wobbleBlobRect = ( x: number, y: number, w: number, h: number, seed: number, amp = 10, r = 70, ) => { // round-tipped blob rect: edges jitter mid-segment, corners are large arcs const rnd = mulberry32(seed); const j = () => (rnd() - 0.5) * amp * 2; return [ `M ${x + r + j()} ${y + j()}`, `L ${x + w / 2 + j()} ${y + j()}`, `L ${x + w - r + j()} ${y + j()}`, `Q ${x + w + j()} ${y + j()} ${x + w + j()} ${y + r + j()}`, `L ${x + w + j()} ${y + h / 2 + j()}`, `L ${x + w + j()} ${y + h - r + j()}`, `Q ${x + w + j()} ${y + h + j()} ${x + w - r + j()} ${y + h + j()}`, `L ${x + w / 2 + j()} ${y + h + j()}`, `L ${x + r + j()} ${y + h + j()}`, `Q ${x + j()} ${y + h + j()} ${x + j()} ${y + h - r + j()}`, `L ${x + j()} ${y + h / 2 + j()}`, `L ${x + j()} ${y + r + j()}`, `Q ${x + j()} ${y + j()} ${x + r + j()} ${y + j()}`, ].join(' '); }; const wobbleCircle = (cx: number, cy: number, r: number, seed: number, amp = 6) => { const rnd = mulberry32(seed); const n = 14; const pts: string[] = []; for (let i = 0; i <= n; i++) { const a = (i / n) * Math.PI * 2; const rr = r + (rnd() - 0.5) * amp * 2; pts.push(`${i === 0 ? 'M' : 'L'} ${cx + Math.cos(a) * rr} ${cy + Math.sin(a) * rr}`); } return pts.join(' ') + ' Z'; }; const Doodle: React.FC<{ boil: number }> = ({ boil }) => { const S = boil * 977; // each boil swaps in a fresh set of jitter seeds const stroke = { fill: 'none' as const, stroke: INK, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, }; return ( {/* big window blob */} {/* sidebar divider */} {/* sidebar logo blob + short lines */} {Array.from({ length: 6 }).map((_, i) => ( ))} {/* main-area message rows: round avatar blobs + wavy text lines */} {Array.from({ length: 4 }).map((_, i) => { const y = 320 + i * 160; return ( ); })} ); }; // ——— skeleton/content message rows ——— const NAMES = ['Ana', 'Ben', 'Kai', 'Mia']; const AVA = ['#5a5a58', '#7a7a78', '#4a4a48', '#8f8f8d']; const MSGS = [ 'Morning! Kicking off the rebrand today', 'Logo drafts are ready for review', 'Nice — shipping the deck this afternoon', 'Love it. Can we make it pink?', ]; const Row: React.FC<{ i: number; dev: number; wordAt: (w: number, n: number) => number }> = ({ i, dev, wordAt, }) => { const words = MSGS[i].split(' '); return (
{/* skeleton layer */}
{/* content layer (word-by-word develop) */}
0.02 ? 1 : 0 }}>
{NAMES[i][0]}
{NAMES[i]} 9:0{i + 1} AM
{words.map((w, wi) => { const p = wordAt(wi, words.length); return ( {w} ); })}
); }; export const SkeletonReveal: React.FC = () => { const f = useCurrentFrame(); const { fps } = useVideoConfig(); const SWAP = 32; // the beat where doodle → skeleton // doodle: boiling jitter + pulled back and replaced within one beat (quick shrink-out + fade, accelerating the exit) const boil = Math.floor(f / 5); const doodleOut = interpolate(f, [SWAP, SWAP + 8], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic), }); const doodleVisible = f < SWAP + 9; // skeleton window: springs in at the swap const winIn = spring({ frame: f - SWAP, fps, config: { damping: 16, stiffness: 160, mass: 0.7 } }); // skeleton list rolls in (second beat) const rowSlide = (i: number) => interpolate(f, [SWAP + 12 + i * 6, SWAP + 34 + i * 6], [520, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); // camera push-in const zoom = interpolate(f, [66, 142], [1, 1.34], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.cubic), }); // per-line develop const devAt = (i: number) => interpolate(f, [80 + i * 13, 92 + i * 13], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); // word-by-word entrance; the last word of the last line arrives half a beat late (+14 frames) const wordAt = (row: number) => (w: number, n: number) => { const isLastWordOfLastRow = row === 3 && w === n - 1; const start = 82 + row * 13 + w * 2.5 + (isLastWordOfLastRow ? 14 : 0); return interpolate(f, [start, start + 9], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); }; return ( {/* third/second beat: skeleton UI window */} {f >= SWAP && (
{/* sidebar */}
{Array.from({ length: 7 }).map((_, i) => (
))}
{/* main area */}
{Array.from({ length: 4 }).map((_, i) => (
500 ? 0 : 1 }}>
))}
)} {/* first beat: hand-drawn doodle placeholder (above the skeleton, shrinks away within one beat to make room) */} {doodleVisible && ( )} ); };