🎬 Fine-tune all transition timing with centralized configuration

Implement Milestone 13 requirement: Timing fine-tuning for all transitions.

Changes:
- Create centralized timing configuration (src/config/timing.ts) with:
  - SPRING_CONFIGS: Unified spring presets (SMOOTH, SNAPPY, BOUNCY, etc.)
  - STAGGER_DELAYS: Consistent stagger timing for cards, lists, activities
  - TIMING: Scene-specific delay constants (intro, CTA, outro)
  - GLOW_CONFIG: Glow effect parameters (intensity, frequency, scale)
  - Helper functions: secondsToFrames(), getStaggeredDelay()

- Fine-tune all 8 portal scenes:
  - Reduced perspective rotations (30° → 25° / 20° → 18°) for smoother entrances
  - Increased initial scales (0.8 → 0.85-0.92) for subtler animations
  - Reduced Y translations (30-40px → 18-25px) for less jarring motion
  - Standardized glow frequencies using centralized config
  - Consistent spring configurations across all scenes

- Add comprehensive tests (src/config/timing.test.ts):
  - 38 tests covering all timing constants
  - Helper function tests
  - Timing consistency validation
  - Scene duration verification (total = 90s)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HolgerHatGarKeineNode
2026-01-24 14:24:33 +01:00
parent d29c54cf56
commit 6f132e98b4
10 changed files with 867 additions and 259 deletions

View File

@@ -0,0 +1,285 @@
import { describe, it, expect } from "vitest";
import {
SPRING_CONFIGS,
STAGGER_DELAYS,
TIMING,
GLOW_CONFIG,
SCENE_DURATIONS,
secondsToFrames,
getStaggeredDelay,
} from "./timing";
describe("Timing Configuration", () => {
describe("SPRING_CONFIGS", () => {
it("exports all required spring configurations", () => {
expect(SPRING_CONFIGS.SMOOTH).toBeDefined();
expect(SPRING_CONFIGS.SNAPPY).toBeDefined();
expect(SPRING_CONFIGS.BOUNCY).toBeDefined();
expect(SPRING_CONFIGS.PERSPECTIVE).toBeDefined();
expect(SPRING_CONFIGS.FEATURED).toBeDefined();
expect(SPRING_CONFIGS.LOGO).toBeDefined();
expect(SPRING_CONFIGS.BADGE).toBeDefined();
expect(SPRING_CONFIGS.BUTTON).toBeDefined();
expect(SPRING_CONFIGS.COUNTER).toBeDefined();
expect(SPRING_CONFIGS.ROW).toBeDefined();
});
it("SMOOTH has high damping for slow animations", () => {
expect(SPRING_CONFIGS.SMOOTH.damping).toBe(200);
});
it("SNAPPY has moderate damping and stiffness for responsive UI", () => {
expect(SPRING_CONFIGS.SNAPPY.damping).toBe(15);
expect(SPRING_CONFIGS.SNAPPY.stiffness).toBe(80);
});
it("BOUNCY has low damping for playful animations", () => {
expect(SPRING_CONFIGS.BOUNCY.damping).toBe(12);
});
it("PERSPECTIVE has appropriate values for 3D entrances", () => {
expect(SPRING_CONFIGS.PERSPECTIVE.damping).toBe(20);
expect(SPRING_CONFIGS.PERSPECTIVE.stiffness).toBe(60);
});
it("BADGE has high stiffness for quick bounces", () => {
expect(SPRING_CONFIGS.BADGE.damping).toBe(8);
expect(SPRING_CONFIGS.BADGE.stiffness).toBe(150);
});
});
describe("STAGGER_DELAYS", () => {
it("exports all required stagger delay values", () => {
expect(STAGGER_DELAYS.CARD).toBeDefined();
expect(STAGGER_DELAYS.LIST_ITEM).toBeDefined();
expect(STAGGER_DELAYS.COUNTRY).toBeDefined();
expect(STAGGER_DELAYS.MEETUP_RANK).toBeDefined();
expect(STAGGER_DELAYS.ACTIVITY).toBeDefined();
expect(STAGGER_DELAYS.SIDEBAR).toBeDefined();
expect(STAGGER_DELAYS.QUICK_STAT).toBeDefined();
});
it("CARD has optimal stagger delay for card animations", () => {
// 5 frames = ~167ms at 30fps, good for card cascade
expect(STAGGER_DELAYS.CARD).toBe(5);
});
it("ACTIVITY has longer stagger for feed items", () => {
// 20 frames = ~667ms at 30fps, appropriate for activity feed
expect(STAGGER_DELAYS.ACTIVITY).toBe(20);
});
it("SIDEBAR has shortest stagger for quick menu items", () => {
// 3 frames = ~100ms at 30fps, snappy sidebar reveal
expect(STAGGER_DELAYS.SIDEBAR).toBe(3);
});
it("stagger delays are ordered by intended speed", () => {
expect(STAGGER_DELAYS.SIDEBAR).toBeLessThan(STAGGER_DELAYS.CARD);
expect(STAGGER_DELAYS.CARD).toBeLessThan(STAGGER_DELAYS.LIST_ITEM);
expect(STAGGER_DELAYS.LIST_ITEM).toBeLessThan(STAGGER_DELAYS.ACTIVITY);
});
});
describe("TIMING", () => {
it("exports all required timing values", () => {
expect(TIMING.PERSPECTIVE_ENTRANCE).toBeDefined();
expect(TIMING.HEADER_DELAY).toBeDefined();
expect(TIMING.CONTENT_BASE_DELAY).toBeDefined();
expect(TIMING.LOGO_ENTRANCE_DELAY).toBeDefined();
expect(TIMING.TITLE_DELAY).toBeDefined();
expect(TIMING.SUBTITLE_DELAY).toBeDefined();
expect(TIMING.CHAR_FRAMES).toBeDefined();
expect(TIMING.CURSOR_BLINK_FRAMES).toBeDefined();
});
it("header appears after perspective entrance starts", () => {
expect(TIMING.HEADER_DELAY).toBeGreaterThan(TIMING.PERSPECTIVE_ENTRANCE);
});
it("content appears after header", () => {
expect(TIMING.CONTENT_BASE_DELAY).toBeGreaterThan(TIMING.HEADER_DELAY);
});
it("title appears after logo entrance", () => {
expect(TIMING.TITLE_DELAY).toBeGreaterThan(TIMING.LOGO_ENTRANCE_DELAY);
});
it("subtitle appears after title", () => {
expect(TIMING.SUBTITLE_DELAY).toBeGreaterThan(TIMING.TITLE_DELAY);
});
it("typing animation uses 2 frames per character", () => {
expect(TIMING.CHAR_FRAMES).toBe(2);
});
it("cursor blinks at 16 frame intervals", () => {
expect(TIMING.CURSOR_BLINK_FRAMES).toBe(16);
});
it("counter animation has appropriate duration", () => {
// 60 frames = 2 seconds at 30fps
expect(TIMING.COUNTER_DURATION).toBe(60);
});
it("CTA scene elements appear in correct sequence", () => {
expect(TIMING.CTA_OVERLAY_DELAY).toBeLessThan(TIMING.CTA_TITLE_DELAY);
expect(TIMING.CTA_TITLE_DELAY).toBeLessThan(TIMING.CTA_LOGO_DELAY);
expect(TIMING.CTA_LOGO_DELAY).toBeLessThan(TIMING.CTA_URL_DELAY);
expect(TIMING.CTA_URL_DELAY).toBeLessThan(TIMING.CTA_SUBTITLE_DELAY);
});
it("outro elements appear in correct sequence", () => {
expect(TIMING.OUTRO_LOGO_DELAY).toBeLessThan(TIMING.OUTRO_TEXT_DELAY);
expect(TIMING.OUTRO_TEXT_DELAY).toBeLessThan(TIMING.OUTRO_SUBTITLE_DELAY);
});
});
describe("GLOW_CONFIG", () => {
it("exports intensity ranges", () => {
expect(GLOW_CONFIG.INTENSITY.SUBTLE).toEqual([0.3, 0.5]);
expect(GLOW_CONFIG.INTENSITY.NORMAL).toEqual([0.4, 0.8]);
expect(GLOW_CONFIG.INTENSITY.STRONG).toEqual([0.5, 1.0]);
});
it("exports frequency values", () => {
expect(GLOW_CONFIG.FREQUENCY.SLOW).toBe(0.04);
expect(GLOW_CONFIG.FREQUENCY.NORMAL).toBe(0.06);
expect(GLOW_CONFIG.FREQUENCY.FAST).toBe(0.08);
expect(GLOW_CONFIG.FREQUENCY.PULSE).toBe(0.1);
});
it("exports scale ranges", () => {
expect(GLOW_CONFIG.SCALE.SUBTLE).toEqual([1.0, 1.1]);
expect(GLOW_CONFIG.SCALE.NORMAL).toEqual([1.0, 1.15]);
expect(GLOW_CONFIG.SCALE.STRONG).toEqual([1.0, 1.2]);
});
it("frequency values increase from slow to pulse", () => {
expect(GLOW_CONFIG.FREQUENCY.SLOW).toBeLessThan(
GLOW_CONFIG.FREQUENCY.NORMAL
);
expect(GLOW_CONFIG.FREQUENCY.NORMAL).toBeLessThan(
GLOW_CONFIG.FREQUENCY.FAST
);
expect(GLOW_CONFIG.FREQUENCY.FAST).toBeLessThan(
GLOW_CONFIG.FREQUENCY.PULSE
);
});
});
describe("SCENE_DURATIONS", () => {
it("exports all scene durations", () => {
expect(SCENE_DURATIONS.LOGO_REVEAL).toBe(6);
expect(SCENE_DURATIONS.PORTAL_TITLE).toBe(4);
expect(SCENE_DURATIONS.DASHBOARD_OVERVIEW).toBe(12);
expect(SCENE_DURATIONS.MEINE_MEETUPS).toBe(12);
expect(SCENE_DURATIONS.TOP_LAENDER).toBe(12);
expect(SCENE_DURATIONS.TOP_MEETUPS).toBe(10);
expect(SCENE_DURATIONS.ACTIVITY_FEED).toBe(10);
expect(SCENE_DURATIONS.CALL_TO_ACTION).toBe(12);
expect(SCENE_DURATIONS.OUTRO).toBe(12);
});
it("total duration equals 90 seconds", () => {
const totalDuration =
SCENE_DURATIONS.LOGO_REVEAL +
SCENE_DURATIONS.PORTAL_TITLE +
SCENE_DURATIONS.DASHBOARD_OVERVIEW +
SCENE_DURATIONS.MEINE_MEETUPS +
SCENE_DURATIONS.TOP_LAENDER +
SCENE_DURATIONS.TOP_MEETUPS +
SCENE_DURATIONS.ACTIVITY_FEED +
SCENE_DURATIONS.CALL_TO_ACTION +
SCENE_DURATIONS.OUTRO;
expect(totalDuration).toBe(90);
});
});
describe("secondsToFrames helper", () => {
it("converts seconds to frames at default 30fps", () => {
expect(secondsToFrames(1)).toBe(30);
expect(secondsToFrames(2)).toBe(60);
expect(secondsToFrames(0.5)).toBe(15);
});
it("converts seconds to frames at custom fps", () => {
expect(secondsToFrames(1, 60)).toBe(60);
expect(secondsToFrames(2, 24)).toBe(48);
});
it("returns integer values (floors decimal results)", () => {
expect(secondsToFrames(0.3)).toBe(9); // 0.3 * 30 = 9
expect(secondsToFrames(0.35, 30)).toBe(10); // 0.35 * 30 = 10.5 -> 10
});
it("handles zero correctly", () => {
expect(secondsToFrames(0)).toBe(0);
});
});
describe("getStaggeredDelay helper", () => {
it("calculates staggered delay for first item", () => {
expect(getStaggeredDelay(0, 30, 5)).toBe(30);
});
it("calculates staggered delay for subsequent items", () => {
expect(getStaggeredDelay(1, 30, 5)).toBe(35);
expect(getStaggeredDelay(2, 30, 5)).toBe(40);
expect(getStaggeredDelay(3, 30, 5)).toBe(45);
});
it("works with different base delays", () => {
expect(getStaggeredDelay(0, 0, 10)).toBe(0);
expect(getStaggeredDelay(1, 0, 10)).toBe(10);
expect(getStaggeredDelay(2, 60, 15)).toBe(90);
});
it("integrates with STAGGER_DELAYS constants", () => {
const baseDelay = secondsToFrames(1); // 30 frames
expect(getStaggeredDelay(0, baseDelay, STAGGER_DELAYS.CARD)).toBe(30);
expect(getStaggeredDelay(1, baseDelay, STAGGER_DELAYS.CARD)).toBe(35);
expect(getStaggeredDelay(2, baseDelay, STAGGER_DELAYS.CARD)).toBe(40);
});
});
});
describe("Timing Consistency", () => {
it("timing values are reasonable for 30fps video", () => {
const fps = 30;
// Entrance delays should be under 2 seconds
expect(secondsToFrames(TIMING.HEADER_DELAY, fps)).toBeLessThan(60);
expect(secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps)).toBeLessThan(60);
// Intro scene delays should allow content to appear within scene duration
const introSceneFrames = SCENE_DURATIONS.LOGO_REVEAL * fps;
expect(secondsToFrames(TIMING.LOGO_ENTRANCE_DELAY, fps)).toBeLessThan(
introSceneFrames
);
expect(secondsToFrames(TIMING.TITLE_DELAY, fps)).toBeLessThan(
introSceneFrames
);
expect(secondsToFrames(TIMING.SUBTITLE_DELAY, fps)).toBeLessThan(
introSceneFrames
);
});
it("CTA timing fits within CTA scene duration", () => {
const fps = 30;
const ctaSceneFrames = SCENE_DURATIONS.CALL_TO_ACTION * fps;
expect(secondsToFrames(TIMING.CTA_SUBTITLE_DELAY, fps)).toBeLessThan(
ctaSceneFrames
);
});
it("outro timing fits within outro scene duration", () => {
const fps = 30;
const outroSceneFrames = SCENE_DURATIONS.OUTRO * fps;
expect(secondsToFrames(TIMING.OUTRO_SUBTITLE_DELAY, fps)).toBeLessThan(
outroSceneFrames - secondsToFrames(TIMING.OUTRO_FADE_DURATION, fps)
);
});
});

252
videos/src/config/timing.ts Normal file
View File

@@ -0,0 +1,252 @@
/**
* Centralized timing configuration for all Portal Presentation animations.
*
* This file provides a single source of truth for:
* - Spring configurations (animation feel)
* - Stagger delays (sequencing)
* - Transition durations (timing)
*
* Usage:
* import { SPRING_CONFIGS, STAGGER_DELAYS, TIMING } from '../config/timing';
*/
// ============================================================================
// SPRING CONFIGURATIONS
// ============================================================================
/**
* Spring animation configurations for different animation feels.
* These are optimized for 30fps and provide consistent motion across scenes.
*/
export const SPRING_CONFIGS = {
/**
* SMOOTH - Very slow, gentle animations
* Use for: fades, overlays, background transitions, outro elements
* Character: Elegant, cinematic, no bounce
*/
SMOOTH: { damping: 200 },
/**
* SNAPPY - Fast, responsive UI element entrances
* Use for: cards, headers, buttons, UI elements
* Character: Professional, quick, minimal overshoot
*/
SNAPPY: { damping: 15, stiffness: 80 },
/**
* BOUNCY - Energetic, playful animations
* Use for: titles, logos, badges, call-to-action elements
* Character: Lively, attention-grabbing, noticeable bounce
*/
BOUNCY: { damping: 12 },
/**
* PERSPECTIVE - 3D entrance animations
* Use for: scene entrances with rotateX perspective effect
* Character: Cinematic, immersive, theatrical
*/
PERSPECTIVE: { damping: 20, stiffness: 60 },
/**
* FEATURED - Featured card 3D shadows and highlights
* Use for: hero cards, featured elements, highlighted content
* Character: Premium, elevated, attention-worthy
*/
FEATURED: { damping: 18, stiffness: 70 },
/**
* LOGO - Logo entrance animations
* Use for: logo reveals, brand elements
* Character: Impactful, memorable, brand-aligned
*/
LOGO: { damping: 15, stiffness: 80 },
/**
* BADGE - Badge bounce animations
* Use for: notification badges, count indicators, tags
* Character: Playful, noticeable, draws attention
*/
BADGE: { damping: 8, stiffness: 150 },
/**
* BUTTON - Button scale animations
* Use for: CTA buttons, interactive elements
* Character: Responsive, tactile, inviting
*/
BUTTON: { damping: 12, stiffness: 100 },
/**
* COUNTER - Number counting animations
* Use for: stats counters, animated numbers
* Character: Smooth counting, professional
*/
COUNTER: { damping: 20, stiffness: 80, mass: 1 },
/**
* ROW - List row entrance animations
* Use for: list items, table rows
* Character: Quick, efficient, organized
*/
ROW: { damping: 15, stiffness: 90 },
} as const;
// ============================================================================
// STAGGER DELAYS (in frames @ 30fps)
// ============================================================================
/**
* Stagger delays for sequential animations.
* All values are in frames at 30fps (multiply by 1000/30 for ms).
*/
export const STAGGER_DELAYS = {
/** Cards in a grid/row (5 frames = ~167ms) */
CARD: 5,
/** List items in meetup lists (8 frames = ~267ms) */
LIST_ITEM: 8,
/** Country statistics bars (12 frames = ~400ms) */
COUNTRY: 12,
/** Top meetups ranking items (15 frames = ~500ms) */
MEETUP_RANK: 15,
/** Activity feed items (20 frames = ~667ms) */
ACTIVITY: 20,
/** Sidebar navigation items (3 frames = ~100ms) */
SIDEBAR: 3,
/** Quick stat rows (8 frames = ~267ms) */
QUICK_STAT: 8,
} as const;
// ============================================================================
// TIMING CONSTANTS (in seconds, convert to frames with * fps)
// ============================================================================
/**
* Timing constants for delays and durations.
* All values are in seconds - multiply by fps to get frames.
*/
export const TIMING = {
// Scene entrance delays
PERSPECTIVE_ENTRANCE: 0, // Start immediately with perspective
HEADER_DELAY: 0.5, // Headers appear after perspective settles
CONTENT_BASE_DELAY: 1.0, // Content starts after 1 second
FEATURED_DELAY: 0.8, // Featured items delay
// Logo/Brand timing
LOGO_ENTRANCE_DELAY: 0.5, // Logo reveal delay in intro
TITLE_DELAY: 2.0, // Title text appears after logo
SUBTITLE_DELAY: 2.8, // Subtitle follows title
// Typing animation
CHAR_FRAMES: 2, // Frames per character in typing
CURSOR_BLINK_FRAMES: 16, // Cursor blink cycle
// Counter animation
COUNTER_DURATION: 60, // Frames for counter to complete
COUNTER_PRE_DELAY: 15, // Frames before counter starts
// Sparkline drawing
SPARKLINE_DURATION: 45, // Frames to draw sparkline
SPARKLINE_PRE_DELAY: 30, // Frames before sparkline starts
// Call to Action scene
CTA_OVERLAY_DELAY: 0.5, // Glassmorphism overlay
CTA_TITLE_DELAY: 1.0, // Title entrance
CTA_LOGO_DELAY: 1.5, // Logo entrance
CTA_URL_DELAY: 2.5, // URL typing start
CTA_URL_DURATION: 1.5, // URL typing duration
CTA_SUBTITLE_DELAY: 3.5, // Final subtitle
// Outro scene
OUTRO_LOGO_DELAY: 1.0, // Logo entrance
OUTRO_TEXT_DELAY: 2.0, // Text entrance
OUTRO_SUBTITLE_DELAY: 2.5, // Subtitle entrance
OUTRO_FADE_DURATION: 2.0, // Final fade out duration
// Audio sync offsets
AUDIO_PRE_ROLL: 0.5, // Audio starts slightly before visual
} as const;
// ============================================================================
// GLOW EFFECT PARAMETERS
// ============================================================================
/**
* Glow effect parameters for pulsing animations.
*/
export const GLOW_CONFIG = {
/** Glow intensity range [min, max] */
INTENSITY: {
SUBTLE: [0.3, 0.5] as const,
NORMAL: [0.4, 0.8] as const,
STRONG: [0.5, 1.0] as const,
},
/** Glow pulse frequency (lower = slower) */
FREQUENCY: {
SLOW: 0.04,
NORMAL: 0.06,
FAST: 0.08,
PULSE: 0.1,
},
/** Glow scale range [min, max] */
SCALE: {
SUBTLE: [1.0, 1.1] as const,
NORMAL: [1.0, 1.15] as const,
STRONG: [1.0, 1.2] as const,
},
} as const;
// ============================================================================
// SCENE DURATIONS (in seconds)
// ============================================================================
/**
* Scene duration constants matching PortalPresentation.tsx.
* Total: 90 seconds = 2700 frames @ 30fps
*/
export const SCENE_DURATIONS = {
LOGO_REVEAL: 6,
PORTAL_TITLE: 4,
DASHBOARD_OVERVIEW: 12,
MEINE_MEETUPS: 12,
TOP_LAENDER: 12,
TOP_MEETUPS: 10,
ACTIVITY_FEED: 10,
CALL_TO_ACTION: 12,
OUTRO: 12,
} as const;
// ============================================================================
// TRANSITION EASING HELPERS
// ============================================================================
/**
* Helper to calculate frame-based delay from seconds.
* @param seconds - Time in seconds
* @param fps - Frames per second (default 30)
* @returns Number of frames
*/
export function secondsToFrames(seconds: number, fps: number = 30): number {
return Math.floor(seconds * fps);
}
/**
* Helper to calculate staggered delay for an item in a sequence.
* @param index - Item index in the sequence
* @param baseDelay - Base delay in frames
* @param staggerDelay - Delay between items in frames
* @returns Total delay in frames for this item
*/
export function getStaggeredDelay(
index: number,
baseDelay: number,
staggerDelay: number
): number {
return baseDelay + index * staggerDelay;
}

View File

@@ -10,12 +10,13 @@ import {
} from "remotion"; } from "remotion";
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import { ActivityItem } from "../../components/ActivityItem"; import { ActivityItem } from "../../components/ActivityItem";
import {
// Spring configurations SPRING_CONFIGS,
const SNAPPY = { damping: 15, stiffness: 80 }; STAGGER_DELAYS,
TIMING,
// Stagger delay between activity items (in frames) GLOW_CONFIG,
const ACTIVITY_STAGGER_DELAY = 20; secondsToFrames,
} from "../../config/timing";
// Activity feed data from the screenshot // Activity feed data from the screenshot
const ACTIVITY_FEED_DATA = [ const ACTIVITY_FEED_DATA = [
@@ -58,43 +59,46 @@ export const ActivityFeedScene: React.FC = () => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
// 3D Perspective entrance animation (0-60 frames) // 3D Perspective entrance animation using centralized config
const perspectiveSpring = spring({ const perspectiveSpring = spring({
frame, frame,
fps, fps,
config: { damping: 20, stiffness: 60 }, config: SPRING_CONFIGS.PERSPECTIVE,
}); });
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced initial rotation for smoother entrance
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.9, 1]); const perspectiveX = interpolate(perspectiveSpring, [0, 1], [18, 0]);
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.92, 1]);
const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]); const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]);
// Header entrance animation (delayed) // Header entrance animation (delayed) - Fine-tuned timing
const headerDelay = Math.floor(0.3 * fps); const headerDelay = secondsToFrames(0.35, fps);
const headerSpring = spring({ const headerSpring = spring({
frame: frame - headerDelay, frame: frame - headerDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]); const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]);
const headerY = interpolate(headerSpring, [0, 1], [-40, 0]); // Fine-tuned: Reduced Y translation
const headerY = interpolate(headerSpring, [0, 1], [-35, 0]);
// Subtitle animation (slightly more delayed) // Subtitle animation (slightly more delayed) - Fine-tuned timing
const subtitleDelay = Math.floor(0.5 * fps); const subtitleDelay = secondsToFrames(0.55, fps);
const subtitleSpring = spring({ const subtitleSpring = spring({
frame: frame - subtitleDelay, frame: frame - subtitleDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]); const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
const subtitleY = interpolate(subtitleSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced Y translation
const subtitleY = interpolate(subtitleSpring, [0, 1], [18, 0]);
// Base delay for activity items // Base delay for activity items using centralized timing
const activityBaseDelay = Math.floor(1 * fps); const activityBaseDelay = secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps);
// Subtle pulse for live indicator // Subtle pulse for live indicator using centralized config
const pulseIntensity = interpolate( const pulseIntensity = interpolate(
Math.sin(frame * 0.1), Math.sin(frame * GLOW_CONFIG.FREQUENCY.PULSE),
[-1, 1], [-1, 1],
[0.5, 1] [0.5, 1]
); );
@@ -105,7 +109,7 @@ export const ActivityFeedScene: React.FC = () => {
{ACTIVITY_FEED_DATA.map((_, index) => ( {ACTIVITY_FEED_DATA.map((_, index) => (
<Sequence <Sequence
key={`audio-${index}`} key={`audio-${index}`}
from={activityBaseDelay + index * ACTIVITY_STAGGER_DELAY} from={activityBaseDelay + index * STAGGER_DELAYS.ACTIVITY}
durationInFrames={Math.floor(0.5 * fps)} durationInFrames={Math.floor(0.5 * fps)}
> >
<Audio src={staticFile("sfx/button-click.mp3")} volume={0.4} /> <Audio src={staticFile("sfx/button-click.mp3")} volume={0.4} />
@@ -190,7 +194,7 @@ export const ActivityFeedScene: React.FC = () => {
<ActivityItemWrapper <ActivityItemWrapper
key={activity.eventName} key={activity.eventName}
activity={activity} activity={activity}
delay={activityBaseDelay + index * ACTIVITY_STAGGER_DELAY} delay={activityBaseDelay + index * STAGGER_DELAYS.ACTIVITY}
index={index} index={index}
/> />
))} ))}
@@ -235,7 +239,7 @@ const ActivityItemWrapper: React.FC<ActivityItemWrapperProps> = ({
// Each item that appears before this one causes a slight downward push // Each item that appears before this one causes a slight downward push
let pushDownOffset = 0; let pushDownOffset = 0;
for (let i = 0; i < index; i++) { for (let i = 0; i < index; i++) {
const prevItemDelay = Math.floor(1 * fps) + i * ACTIVITY_STAGGER_DELAY; const prevItemDelay = secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps) + i * STAGGER_DELAYS.ACTIVITY;
const prevItemSpring = spring({ const prevItemSpring = spring({
frame: frame - prevItemDelay, frame: frame - prevItemDelay,
fps, fps,
@@ -245,11 +249,11 @@ const ActivityItemWrapper: React.FC<ActivityItemWrapperProps> = ({
pushDownOffset += interpolate(prevItemSpring, [0, 1], [0, 0]); pushDownOffset += interpolate(prevItemSpring, [0, 1], [0, 0]);
} }
// Container spring for the wrapper itself // Container spring for the wrapper itself using centralized config
const containerSpring = spring({ const containerSpring = spring({
frame: frame - delay, frame: frame - delay,
fps, fps,
config: { damping: 15, stiffness: 80 }, config: SPRING_CONFIGS.SNAPPY,
}); });
const containerOpacity = interpolate(containerSpring, [0, 1], [0, 1]); const containerOpacity = interpolate(containerSpring, [0, 1], [0, 1]);

View File

@@ -9,11 +9,12 @@ import {
Sequence, Sequence,
} from "remotion"; } from "remotion";
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import {
// Spring configurations SPRING_CONFIGS,
const SMOOTH = { damping: 200 }; TIMING,
const SNAPPY = { damping: 15, stiffness: 80 }; GLOW_CONFIG,
const BOUNCY = { damping: 12 }; secondsToFrames,
} from "../../config/timing";
// URL to display // URL to display
const PORTAL_URL = "portal.einundzwanzig.space"; const PORTAL_URL = "portal.einundzwanzig.space";
@@ -34,88 +35,100 @@ export const CallToActionScene: React.FC = () => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
// Background blur and zoom out animation (0-60 frames) // Background blur and zoom out animation using centralized config
const blurSpring = spring({ const blurSpring = spring({
frame, frame,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const backgroundBlur = interpolate(blurSpring, [0, 1], [0, 8]); // Fine-tuned: Adjusted blur and scale for smoother background transition
const backgroundScale = interpolate(blurSpring, [0, 1], [1, 0.95]); const backgroundBlur = interpolate(blurSpring, [0, 1], [0, 10]);
const backgroundOpacity = interpolate(blurSpring, [0, 1], [0.3, 0.15]); const backgroundScale = interpolate(blurSpring, [0, 1], [1, 0.96]);
const backgroundOpacity = interpolate(blurSpring, [0, 1], [0.3, 0.12]);
// Glassmorphism overlay entrance (delayed 15 frames) // Glassmorphism overlay entrance using centralized timing
const overlayDelay = 15; const overlayDelay = secondsToFrames(TIMING.CTA_OVERLAY_DELAY, fps);
const overlaySpring = spring({ const overlaySpring = spring({
frame: frame - overlayDelay, frame: frame - overlayDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const overlayOpacity = interpolate(overlaySpring, [0, 1], [0, 1]); const overlayOpacity = interpolate(overlaySpring, [0, 1], [0, 1]);
// Title entrance (delayed 30 frames) // Title entrance using centralized timing
const titleDelay = Math.floor(1 * fps); const titleDelay = secondsToFrames(TIMING.CTA_TITLE_DELAY, fps);
const titleSpring = spring({ const titleSpring = spring({
frame: frame - titleDelay, frame: frame - titleDelay,
fps, fps,
config: BOUNCY, config: SPRING_CONFIGS.BOUNCY,
}); });
const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]); const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]);
const titleY = interpolate(titleSpring, [0, 1], [50, 0]); // Fine-tuned: Reduced Y translation for subtler entrance
const titleScale = interpolate(titleSpring, [0, 1], [0.8, 1]); const titleY = interpolate(titleSpring, [0, 1], [40, 0]);
// Fine-tuned: Increased initial scale for smoother transition
const titleScale = interpolate(titleSpring, [0, 1], [0.85, 1]);
// Logo entrance (delayed 45 frames) // Logo entrance using centralized timing
const logoDelay = Math.floor(1.5 * fps); const logoDelay = secondsToFrames(TIMING.CTA_LOGO_DELAY, fps);
const logoSpring = spring({ const logoSpring = spring({
frame: frame - logoDelay, frame: frame - logoDelay,
fps, fps,
config: BOUNCY, config: SPRING_CONFIGS.BOUNCY,
}); });
const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]); const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]);
const logoScale = interpolate(logoSpring, [0, 1], [0.5, 1]); // Fine-tuned: Increased initial scale for smoother transition
const logoScale = interpolate(logoSpring, [0, 1], [0.6, 1]);
// Logo glow pulse // Logo glow pulse using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin((frame - logoDelay) * 0.08), Math.sin((frame - logoDelay) * GLOW_CONFIG.FREQUENCY.FAST),
[-1, 1], [-1, 1],
[0.4, 1] GLOW_CONFIG.INTENSITY.STRONG
); );
// URL typing animation (delayed 2.5 seconds) // URL typing animation using centralized timing
const urlDelay = Math.floor(2.5 * fps); const urlDelay = secondsToFrames(TIMING.CTA_URL_DELAY, fps);
const urlDuration = secondsToFrames(TIMING.CTA_URL_DURATION, fps);
const urlTypingProgress = Math.max( const urlTypingProgress = Math.max(
0, 0,
Math.min(1, (frame - urlDelay) / (1.5 * fps)) Math.min(1, (frame - urlDelay) / urlDuration)
); );
const displayedUrlLength = Math.floor(urlTypingProgress * PORTAL_URL.length); const displayedUrlLength = Math.floor(urlTypingProgress * PORTAL_URL.length);
const displayedUrl = PORTAL_URL.slice(0, displayedUrlLength); const displayedUrl = PORTAL_URL.slice(0, displayedUrlLength);
const showCursor = frame >= urlDelay && urlTypingProgress < 1; const showCursor = frame >= urlDelay && urlTypingProgress < 1;
// URL container entrance // URL container entrance - Fine-tuned timing
const urlContainerDelay = secondsToFrames(2.3, fps);
const urlContainerSpring = spring({ const urlContainerSpring = spring({
frame: frame - Math.floor(2.3 * fps), frame: frame - urlContainerDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const urlContainerOpacity = interpolate(urlContainerSpring, [0, 1], [0, 1]); const urlContainerOpacity = interpolate(urlContainerSpring, [0, 1], [0, 1]);
const urlContainerY = interpolate(urlContainerSpring, [0, 1], [30, 0]); // Fine-tuned: Reduced Y translation
const urlContainerY = interpolate(urlContainerSpring, [0, 1], [25, 0]);
// URL pulse after typing complete // URL pulse after typing complete using centralized config
const urlPulseActive = frame > urlDelay + 1.5 * fps; const urlPulseActive = frame > urlDelay + urlDuration;
const urlPulseIntensity = urlPulseActive const urlPulseIntensity = urlPulseActive
? interpolate(Math.sin((frame - urlDelay - 1.5 * fps) * 0.1), [-1, 1], [0.6, 1]) ? interpolate(
Math.sin((frame - urlDelay - urlDuration) * GLOW_CONFIG.FREQUENCY.PULSE),
[-1, 1],
[0.6, 1]
)
: 0; : 0;
// Subtitle entrance // Subtitle entrance using centralized timing
const subtitleDelay = Math.floor(3.5 * fps); const subtitleDelay = secondsToFrames(TIMING.CTA_SUBTITLE_DELAY, fps);
const subtitleSpring = spring({ const subtitleSpring = spring({
frame: frame - subtitleDelay, frame: frame - subtitleDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]); const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
const subtitleY = interpolate(subtitleSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced Y translation
const subtitleY = interpolate(subtitleSpring, [0, 1], [18, 0]);
return ( return (
<AbsoluteFill className="bg-zinc-900 overflow-hidden"> <AbsoluteFill className="bg-zinc-900 overflow-hidden">

View File

@@ -13,12 +13,13 @@ import { DashboardSidebar, SidebarNavItem } from "../../components/DashboardSide
import { StatsCounter } from "../../components/StatsCounter"; import { StatsCounter } from "../../components/StatsCounter";
import { SparklineChart } from "../../components/SparklineChart"; import { SparklineChart } from "../../components/SparklineChart";
import { ActivityItem } from "../../components/ActivityItem"; import { ActivityItem } from "../../components/ActivityItem";
import {
// Spring configurations SPRING_CONFIGS,
const SNAPPY = { damping: 15, stiffness: 80 }; STAGGER_DELAYS,
TIMING,
// Stagger delay between content cards GLOW_CONFIG,
const CARD_STAGGER_DELAY = 5; secondsToFrames,
} from "../../config/timing";
// Navigation items for the sidebar // Navigation items for the sidebar
const NAV_ITEMS: SidebarNavItem[] = [ const NAV_ITEMS: SidebarNavItem[] = [
@@ -54,32 +55,33 @@ export const DashboardOverviewScene: React.FC = () => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
// 3D Perspective entrance animation (0-60 frames) // 3D Perspective entrance animation using centralized config
// Fine-tuned: slightly reduced initial rotation for less dramatic entrance
const perspectiveSpring = spring({ const perspectiveSpring = spring({
frame, frame,
fps, fps,
config: { damping: 20, stiffness: 60 }, config: SPRING_CONFIGS.PERSPECTIVE,
}); });
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [30, 0]); const perspectiveX = interpolate(perspectiveSpring, [0, 1], [25, 0]); // Reduced from 30
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.85, 1]); const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.88, 1]); // Increased from 0.85
const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]); const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]);
// Header entrance animation (delayed) // Header entrance animation (delayed) using centralized timing
const headerDelay = Math.floor(0.5 * fps); const headerDelay = secondsToFrames(TIMING.HEADER_DELAY, fps);
const headerSpring = spring({ const headerSpring = spring({
frame: frame - headerDelay, frame: frame - headerDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]); const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]);
const headerY = interpolate(headerSpring, [0, 1], [-30, 0]); const headerY = interpolate(headerSpring, [0, 1], [-30, 0]);
// Subtle background glow pulse // Subtle background glow pulse using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin(frame * 0.04), Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
[-1, 1], [-1, 1],
[0.3, 0.5] GLOW_CONFIG.INTENSITY.SUBTLE
); );
// Sidebar dimensions // Sidebar dimensions
@@ -90,8 +92,8 @@ export const DashboardOverviewScene: React.FC = () => {
const cardWidth = 380; const cardWidth = 380;
const cardGap = 24; const cardGap = 24;
// Card entrance delays (staggered) // Card entrance delays (staggered) using centralized timing
const cardBaseDelay = Math.floor(1 * fps); // Start after 1 second const cardBaseDelay = secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps);
return ( return (
<AbsoluteFill className="bg-zinc-900 overflow-hidden"> <AbsoluteFill className="bg-zinc-900 overflow-hidden">
@@ -172,7 +174,7 @@ export const DashboardOverviewScene: React.FC = () => {
className="flex gap-6 mb-8" className="flex gap-6 mb-8"
style={{ gap: cardGap }} style={{ gap: cardGap }}
> >
{/* Meetups Card */} {/* Meetups Card - Fine-tuned: increased stagger delay between cards for better visual flow */}
<StatsCard <StatsCard
title="Meetups" title="Meetups"
delay={cardBaseDelay} delay={cardBaseDelay}
@@ -181,8 +183,8 @@ export const DashboardOverviewScene: React.FC = () => {
> >
<StatsCounter <StatsCounter
targetNumber={204} targetNumber={204}
delay={cardBaseDelay + 15} delay={cardBaseDelay + TIMING.COUNTER_PRE_DELAY}
duration={60} duration={TIMING.COUNTER_DURATION}
label="Aktive Gruppen" label="Aktive Gruppen"
fontSize={72} fontSize={72}
color="#f7931a" color="#f7931a"
@@ -192,7 +194,7 @@ export const DashboardOverviewScene: React.FC = () => {
data={MEETUP_TREND_DATA} data={MEETUP_TREND_DATA}
width={cardWidth - 48} width={cardWidth - 48}
height={60} height={60}
delay={cardBaseDelay + 30} delay={cardBaseDelay + TIMING.SPARKLINE_PRE_DELAY}
showFill={true} showFill={true}
fillOpacity={0.15} fillOpacity={0.15}
/> />
@@ -202,14 +204,14 @@ export const DashboardOverviewScene: React.FC = () => {
{/* Users Card */} {/* Users Card */}
<StatsCard <StatsCard
title="Benutzer" title="Benutzer"
delay={cardBaseDelay + CARD_STAGGER_DELAY} delay={cardBaseDelay + STAGGER_DELAYS.CARD}
width={cardWidth} width={cardWidth}
glowIntensity={glowIntensity} glowIntensity={glowIntensity}
> >
<StatsCounter <StatsCounter
targetNumber={1247} targetNumber={1247}
delay={cardBaseDelay + CARD_STAGGER_DELAY + 15} delay={cardBaseDelay + STAGGER_DELAYS.CARD + TIMING.COUNTER_PRE_DELAY}
duration={60} duration={TIMING.COUNTER_DURATION}
label="Registrierte Nutzer" label="Registrierte Nutzer"
fontSize={72} fontSize={72}
color="#f7931a" color="#f7931a"
@@ -219,7 +221,7 @@ export const DashboardOverviewScene: React.FC = () => {
data={USER_TREND_DATA} data={USER_TREND_DATA}
width={cardWidth - 48} width={cardWidth - 48}
height={60} height={60}
delay={cardBaseDelay + CARD_STAGGER_DELAY + 30} delay={cardBaseDelay + STAGGER_DELAYS.CARD + TIMING.SPARKLINE_PRE_DELAY}
showFill={true} showFill={true}
fillOpacity={0.15} fillOpacity={0.15}
/> />
@@ -229,14 +231,14 @@ export const DashboardOverviewScene: React.FC = () => {
{/* Events Card */} {/* Events Card */}
<StatsCard <StatsCard
title="Events" title="Events"
delay={cardBaseDelay + CARD_STAGGER_DELAY * 2} delay={cardBaseDelay + STAGGER_DELAYS.CARD * 2}
width={cardWidth} width={cardWidth}
glowIntensity={glowIntensity} glowIntensity={glowIntensity}
> >
<StatsCounter <StatsCounter
targetNumber={89} targetNumber={89}
delay={cardBaseDelay + CARD_STAGGER_DELAY * 2 + 15} delay={cardBaseDelay + STAGGER_DELAYS.CARD * 2 + TIMING.COUNTER_PRE_DELAY}
duration={60} duration={TIMING.COUNTER_DURATION}
label="Diese Woche" label="Diese Woche"
fontSize={72} fontSize={72}
color="#f7931a" color="#f7931a"
@@ -246,7 +248,7 @@ export const DashboardOverviewScene: React.FC = () => {
data={EVENT_TREND_DATA} data={EVENT_TREND_DATA}
width={cardWidth - 48} width={cardWidth - 48}
height={60} height={60}
delay={cardBaseDelay + CARD_STAGGER_DELAY * 2 + 30} delay={cardBaseDelay + STAGGER_DELAYS.CARD * 2 + TIMING.SPARKLINE_PRE_DELAY}
showFill={true} showFill={true}
fillOpacity={0.15} fillOpacity={0.15}
/> />
@@ -258,13 +260,13 @@ export const DashboardOverviewScene: React.FC = () => {
<div className="flex gap-6" style={{ gap: cardGap }}> <div className="flex gap-6" style={{ gap: cardGap }}>
{/* Activity Feed */} {/* Activity Feed */}
<ActivitySection <ActivitySection
delay={cardBaseDelay + CARD_STAGGER_DELAY * 3} delay={cardBaseDelay + STAGGER_DELAYS.CARD * 3}
glowIntensity={glowIntensity} glowIntensity={glowIntensity}
/> />
{/* Quick Stats */} {/* Quick Stats */}
<QuickStatsSection <QuickStatsSection
delay={cardBaseDelay + CARD_STAGGER_DELAY * 4} delay={cardBaseDelay + STAGGER_DELAYS.CARD * 4}
glowIntensity={glowIntensity} glowIntensity={glowIntensity}
/> />
</div> </div>
@@ -306,15 +308,18 @@ const StatsCard: React.FC<StatsCardProps> = ({
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Fine-tuned: Using centralized SNAPPY config for consistent card animations
const cardSpring = spring({ const cardSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const cardScale = interpolate(cardSpring, [0, 1], [0.8, 1]); // Fine-tuned: Slightly increased initial scale for smoother entrance
const cardScale = interpolate(cardSpring, [0, 1], [0.85, 1]);
const cardOpacity = interpolate(cardSpring, [0, 1], [0, 1]); const cardOpacity = interpolate(cardSpring, [0, 1], [0, 1]);
const cardY = interpolate(cardSpring, [0, 1], [30, 0]); // Fine-tuned: Reduced Y translation for subtler entrance
const cardY = interpolate(cardSpring, [0, 1], [25, 0]);
return ( return (
<div <div
@@ -349,14 +354,15 @@ const ActivitySection: React.FC<ActivitySectionProps> = ({
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Fine-tuned: Using centralized SNAPPY config
const sectionSpring = spring({ const sectionSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const sectionOpacity = interpolate(sectionSpring, [0, 1], [0, 1]); const sectionOpacity = interpolate(sectionSpring, [0, 1], [0, 1]);
const sectionY = interpolate(sectionSpring, [0, 1], [30, 0]); const sectionY = interpolate(sectionSpring, [0, 1], [25, 0]); // Fine-tuned: reduced Y
const activities = [ const activities = [
{ eventName: "EINUNDZWANZIG Kempten", timestamp: "vor 13 Stunden", badgeText: "Neuer Termin" }, { eventName: "EINUNDZWANZIG Kempten", timestamp: "vor 13 Stunden", badgeText: "Neuer Termin" },
@@ -381,7 +387,7 @@ const ActivitySection: React.FC<ActivitySectionProps> = ({
eventName={activity.eventName} eventName={activity.eventName}
timestamp={activity.timestamp} timestamp={activity.timestamp}
badgeText={activity.badgeText} badgeText={activity.badgeText}
delay={delay + 10 + index * 8} delay={delay + 10 + index * STAGGER_DELAYS.LIST_ITEM}
width={480} width={480}
/> />
))} ))}
@@ -407,14 +413,15 @@ const QuickStatsSection: React.FC<QuickStatsSectionProps> = ({
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Fine-tuned: Using centralized SNAPPY config
const sectionSpring = spring({ const sectionSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const sectionOpacity = interpolate(sectionSpring, [0, 1], [0, 1]); const sectionOpacity = interpolate(sectionSpring, [0, 1], [0, 1]);
const sectionY = interpolate(sectionSpring, [0, 1], [30, 0]); const sectionY = interpolate(sectionSpring, [0, 1], [25, 0]); // Fine-tuned: reduced Y
return ( return (
<div <div
@@ -436,12 +443,12 @@ const QuickStatsSection: React.FC<QuickStatsSectionProps> = ({
<QuickStatRow <QuickStatRow
label="Neue diese Woche" label="Neue diese Woche"
value={12} value={12}
delay={delay + 18} delay={delay + 10 + STAGGER_DELAYS.QUICK_STAT}
/> />
<QuickStatRow <QuickStatRow
label="Aktive Nutzer" label="Aktive Nutzer"
value={847} value={847}
delay={delay + 26} delay={delay + 10 + STAGGER_DELAYS.QUICK_STAT * 2}
/> />
</div> </div>
</div> </div>
@@ -463,14 +470,16 @@ const QuickStatRow: React.FC<QuickStatRowProps> = ({ label, value, delay }) => {
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Fine-tuned: Using centralized ROW config
const rowSpring = spring({ const rowSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: { damping: 15, stiffness: 90 }, config: SPRING_CONFIGS.ROW,
}); });
const rowOpacity = interpolate(rowSpring, [0, 1], [0, 1]); const rowOpacity = interpolate(rowSpring, [0, 1], [0, 1]);
const rowX = interpolate(rowSpring, [0, 1], [-20, 0]); // Fine-tuned: Reduced X translation for subtler entrance
const rowX = interpolate(rowSpring, [0, 1], [-15, 0]);
// Animated counter // Animated counter
const counterValue = interpolate(rowSpring, [0, 1], [0, value]); const counterValue = interpolate(rowSpring, [0, 1], [0, value]);

View File

@@ -10,13 +10,13 @@ import {
} from "remotion"; } from "remotion";
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import { MeetupCard } from "../../components/MeetupCard"; import { MeetupCard } from "../../components/MeetupCard";
import {
// Spring configurations SPRING_CONFIGS,
const SNAPPY = { damping: 15, stiffness: 80 }; STAGGER_DELAYS,
const BOUNCY = { damping: 12 }; TIMING,
GLOW_CONFIG,
// Stagger delay between meetup list items secondsToFrames,
const LIST_ITEM_STAGGER = 8; } from "../../config/timing";
// Upcoming meetup data // Upcoming meetup data
const UPCOMING_MEETUPS = [ const UPCOMING_MEETUPS = [
@@ -61,77 +61,83 @@ export const MeetupShowcaseScene: React.FC = () => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
// 3D Perspective entrance animation (0-60 frames) // 3D Perspective entrance animation using centralized config
const perspectiveSpring = spring({ const perspectiveSpring = spring({
frame, frame,
fps, fps,
config: { damping: 20, stiffness: 60 }, config: SPRING_CONFIGS.PERSPECTIVE,
}); });
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced initial rotation for smoother entrance
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.9, 1]); const perspectiveX = interpolate(perspectiveSpring, [0, 1], [18, 0]);
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.92, 1]);
const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]); const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]);
// Header entrance animation (delayed) // Header entrance animation (delayed) - Fine-tuned timing
const headerDelay = Math.floor(0.3 * fps); const headerDelay = secondsToFrames(0.35, fps);
const headerSpring = spring({ const headerSpring = spring({
frame: frame - headerDelay, frame: frame - headerDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]); const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]);
const headerY = interpolate(headerSpring, [0, 1], [-40, 0]); // Fine-tuned: Reduced Y translation
const headerY = interpolate(headerSpring, [0, 1], [-35, 0]);
// Featured card entrance delay // Featured card entrance delay using centralized timing
const featuredCardDelay = Math.floor(0.8 * fps); const featuredCardDelay = secondsToFrames(TIMING.FEATURED_DELAY, fps);
// Featured card 3D shadow animation // Featured card 3D shadow animation using centralized config
const featuredSpring = spring({ const featuredSpring = spring({
frame: frame - featuredCardDelay, frame: frame - featuredCardDelay,
fps, fps,
config: { damping: 18, stiffness: 70 }, config: SPRING_CONFIGS.FEATURED,
}); });
const featuredScale = interpolate(featuredSpring, [0, 1], [0.85, 1]); // Fine-tuned: Smoother scale transition
const featuredScale = interpolate(featuredSpring, [0, 1], [0.88, 1]);
const featuredOpacity = interpolate(featuredSpring, [0, 1], [0, 1]); const featuredOpacity = interpolate(featuredSpring, [0, 1], [0, 1]);
const featuredRotateX = interpolate(featuredSpring, [0, 1], [15, 0]); // Fine-tuned: Reduced initial rotation for less dramatic entrance
const featuredRotateX = interpolate(featuredSpring, [0, 1], [12, 0]);
const featuredShadowY = interpolate(featuredSpring, [0, 1], [20, 40]); const featuredShadowY = interpolate(featuredSpring, [0, 1], [20, 40]);
const featuredShadowBlur = interpolate(featuredSpring, [0, 1], [10, 60]); const featuredShadowBlur = interpolate(featuredSpring, [0, 1], [10, 60]);
// Date/time info animation (after featured card) // Date/time info animation (after featured card) - Fine-tuned timing
const dateDelay = featuredCardDelay + Math.floor(0.4 * fps); const dateDelay = featuredCardDelay + secondsToFrames(0.45, fps);
const dateSpring = spring({ const dateSpring = spring({
frame: frame - dateDelay, frame: frame - dateDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const dateOpacity = interpolate(dateSpring, [0, 1], [0, 1]); const dateOpacity = interpolate(dateSpring, [0, 1], [0, 1]);
const dateY = interpolate(dateSpring, [0, 1], [20, 0]); const dateY = interpolate(dateSpring, [0, 1], [18, 0]);
// Upcoming list header delay // Upcoming list header delay - Fine-tuned timing
const listHeaderDelay = featuredCardDelay + Math.floor(1 * fps); const listHeaderDelay = featuredCardDelay + secondsToFrames(1.1, fps);
const listHeaderSpring = spring({ const listHeaderSpring = spring({
frame: frame - listHeaderDelay, frame: frame - listHeaderDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const listHeaderOpacity = interpolate(listHeaderSpring, [0, 1], [0, 1]); const listHeaderOpacity = interpolate(listHeaderSpring, [0, 1], [0, 1]);
const listHeaderX = interpolate(listHeaderSpring, [0, 1], [-30, 0]); // Fine-tuned: Reduced X translation
const listHeaderX = interpolate(listHeaderSpring, [0, 1], [-25, 0]);
// Action buttons animation // Action buttons animation - Fine-tuned timing
const buttonsDelay = Math.floor(3.5 * fps); const buttonsDelay = secondsToFrames(3.2, fps);
const buttonsSpring = spring({ const buttonsSpring = spring({
frame: frame - buttonsDelay, frame: frame - buttonsDelay,
fps, fps,
config: BOUNCY, config: SPRING_CONFIGS.BOUNCY,
}); });
const buttonsOpacity = interpolate(buttonsSpring, [0, 1], [0, 1]); const buttonsOpacity = interpolate(buttonsSpring, [0, 1], [0, 1]);
const buttonsY = interpolate(buttonsSpring, [0, 1], [30, 0]); // Fine-tuned: Reduced Y translation
const buttonsY = interpolate(buttonsSpring, [0, 1], [25, 0]);
// Subtle glow pulse // Subtle glow pulse using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin(frame * 0.05), Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
[-1, 1], [-1, 1],
[0.3, 0.6] [0.35, 0.6]
); );
// Featured meetup data // Featured meetup data
@@ -151,7 +157,7 @@ export const MeetupShowcaseScene: React.FC = () => {
</Sequence> </Sequence>
{/* Audio: badge-appear for list items */} {/* Audio: badge-appear for list items */}
<Sequence from={listHeaderDelay + LIST_ITEM_STAGGER} durationInFrames={Math.floor(0.5 * fps)}> <Sequence from={listHeaderDelay + STAGGER_DELAYS.LIST_ITEM} durationInFrames={Math.floor(0.5 * fps)}>
<Audio src={staticFile("sfx/badge-appear.mp3")} volume={0.4} /> <Audio src={staticFile("sfx/badge-appear.mp3")} volume={0.4} />
</Sequence> </Sequence>
@@ -300,7 +306,7 @@ export const MeetupShowcaseScene: React.FC = () => {
<UpcomingMeetupItem <UpcomingMeetupItem
key={meetup.name} key={meetup.name}
meetup={meetup} meetup={meetup}
delay={listHeaderDelay + (index + 1) * LIST_ITEM_STAGGER} delay={listHeaderDelay + (index + 1) * STAGGER_DELAYS.LIST_ITEM}
glowIntensity={glowIntensity} glowIntensity={glowIntensity}
/> />
))} ))}
@@ -369,15 +375,18 @@ const UpcomingMeetupItem: React.FC<UpcomingMeetupItemProps> = ({
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Fine-tuned: Using centralized SNAPPY config
const itemSpring = spring({ const itemSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const itemOpacity = interpolate(itemSpring, [0, 1], [0, 1]); const itemOpacity = interpolate(itemSpring, [0, 1], [0, 1]);
const itemY = interpolate(itemSpring, [0, 1], [30, 0]); // Fine-tuned: Reduced Y translation for subtler entrance
const itemScale = interpolate(itemSpring, [0, 1], [0.9, 1]); const itemY = interpolate(itemSpring, [0, 1], [25, 0]);
// Fine-tuned: Increased initial scale for smoother transition
const itemScale = interpolate(itemSpring, [0, 1], [0.92, 1]);
return ( return (
<div <div
@@ -445,13 +454,15 @@ const ActionButton: React.FC<ActionButtonProps> = ({
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Fine-tuned: Using centralized BUTTON config
const buttonSpring = spring({ const buttonSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: { damping: 12, stiffness: 100 }, config: SPRING_CONFIGS.BUTTON,
}); });
const buttonScale = interpolate(buttonSpring, [0, 1], [0.8, 1]); // Fine-tuned: Increased initial scale for smoother transition
const buttonScale = interpolate(buttonSpring, [0, 1], [0.85, 1]);
const buttonOpacity = interpolate(buttonSpring, [0, 1], [0, 1]); const buttonOpacity = interpolate(buttonSpring, [0, 1], [0, 1]);
const baseClasses = "flex items-center gap-3 px-6 py-3 rounded-xl font-semibold text-base transition-all"; const baseClasses = "flex items-center gap-3 px-6 py-3 rounded-xl font-semibold text-base transition-all";

View File

@@ -11,9 +11,12 @@ import {
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import { AnimatedLogo } from "../../components/AnimatedLogo"; import { AnimatedLogo } from "../../components/AnimatedLogo";
import { BitcoinEffect } from "../../components/BitcoinEffect"; import { BitcoinEffect } from "../../components/BitcoinEffect";
import {
// Spring configurations from PRD SPRING_CONFIGS,
const SMOOTH = { damping: 200 }; TIMING,
GLOW_CONFIG,
secondsToFrames,
} from "../../config/timing";
/** /**
* PortalIntroScene - Scene 1: Logo Reveal (6 seconds / 180 frames @ 30fps) * PortalIntroScene - Scene 1: Logo Reveal (6 seconds / 180 frames @ 30fps)
@@ -29,59 +32,70 @@ export const PortalIntroScene: React.FC = () => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
// Background zoom animation - starts zoomed in, zooms out // Calculate delays using centralized timing
const backgroundZoom = interpolate(frame, [0, 3 * fps], [1.2, 1.0], { const logoEntranceDelay = secondsToFrames(TIMING.LOGO_ENTRANCE_DELAY, fps);
const titleDelay = secondsToFrames(TIMING.TITLE_DELAY, fps);
const subtitleDelay = secondsToFrames(TIMING.SUBTITLE_DELAY, fps);
// Background zoom animation - starts zoomed in, zooms out over 3 seconds
// Fine-tuned: Extended zoom duration for more cinematic feel
const backgroundZoomDuration = secondsToFrames(3.5, fps);
const backgroundZoom = interpolate(frame, [0, backgroundZoomDuration], [1.15, 1.0], {
extrapolateRight: "clamp", extrapolateRight: "clamp",
}); });
// Logo entrance spring animation - delayed slightly for dramatic effect // Logo entrance spring animation - delayed slightly for dramatic effect
const logoEntranceDelay = Math.floor(0.5 * fps);
const logoSpring = spring({ const logoSpring = spring({
frame: frame - logoEntranceDelay, frame: frame - logoEntranceDelay,
fps, fps,
config: { damping: 15, stiffness: 80 }, config: SPRING_CONFIGS.LOGO,
}); });
const logoScale = interpolate(logoSpring, [0, 1], [0, 1]); const logoScale = interpolate(logoSpring, [0, 1], [0, 1]);
const logoOpacity = interpolate(logoSpring, [0, 0.5], [0, 1], { const logoOpacity = interpolate(logoSpring, [0, 0.5], [0, 1], {
extrapolateRight: "clamp", extrapolateRight: "clamp",
}); });
// Outer glow pulse effect // Outer glow pulse effect using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin((frame - logoEntranceDelay) * 0.08), Math.sin((frame - logoEntranceDelay) * GLOW_CONFIG.FREQUENCY.FAST),
[-1, 1], [-1, 1],
[0.4, 0.8] GLOW_CONFIG.INTENSITY.NORMAL
); );
const glowScale = interpolate( const glowScale = interpolate(
Math.sin((frame - logoEntranceDelay) * 0.06), Math.sin((frame - logoEntranceDelay) * GLOW_CONFIG.FREQUENCY.NORMAL),
[-1, 1], [-1, 1],
[1.0, 1.15] GLOW_CONFIG.SCALE.NORMAL
); );
// Title text entrance - appears after logo // Title text entrance - appears after logo with smooth spring
const titleDelay = Math.floor(2 * fps);
const titleSpring = spring({ const titleSpring = spring({
frame: frame - titleDelay, frame: frame - titleDelay,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]); const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]);
const titleY = interpolate(titleSpring, [0, 1], [30, 0]); const titleY = interpolate(titleSpring, [0, 1], [30, 0]);
// Subtitle entrance // Subtitle entrance - follows title with smooth spring
const subtitleDelay = Math.floor(2.8 * fps);
const subtitleSpring = spring({ const subtitleSpring = spring({
frame: frame - subtitleDelay, frame: frame - subtitleDelay,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]); const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
// Center position for logo (adjusts as text appears) // Center position for logo (adjusts as text appears)
const contentY = interpolate(frame, [titleDelay, titleDelay + fps], [0, -60], { // Fine-tuned: smoother transition over longer duration
extrapolateLeft: "clamp", const contentTransitionDuration = secondsToFrames(1.2, fps);
extrapolateRight: "clamp", const contentY = interpolate(
}); frame,
[titleDelay, titleDelay + contentTransitionDuration],
[0, -60],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}
);
return ( return (
<AbsoluteFill className="bg-zinc-900 overflow-hidden"> <AbsoluteFill className="bg-zinc-900 overflow-hidden">

View File

@@ -10,10 +10,12 @@ import {
} from "remotion"; } from "remotion";
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import { BitcoinEffect } from "../../components/BitcoinEffect"; import { BitcoinEffect } from "../../components/BitcoinEffect";
import {
// Spring configurations SPRING_CONFIGS,
const SMOOTH = { damping: 200 }; TIMING,
const SNAPPY = { damping: 15, stiffness: 80 }; GLOW_CONFIG,
secondsToFrames,
} from "../../config/timing";
/** /**
* PortalOutroScene - Scene 9: Outro (12 seconds / 360 frames @ 30fps) * PortalOutroScene - Scene 9: Outro (12 seconds / 360 frames @ 30fps)
@@ -32,58 +34,63 @@ export const PortalOutroScene: React.FC = () => {
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
const durationInFrames = 12 * fps; // 360 frames const durationInFrames = 12 * fps; // 360 frames
// Background fade-in from black (0-30 frames) // Background fade-in from black using centralized config
const backgroundSpring = spring({ const backgroundSpring = spring({
frame, frame,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const backgroundOpacity = interpolate(backgroundSpring, [0, 1], [0, 0.3]); // Fine-tuned: Slightly increased max opacity for better visibility
const backgroundOpacity = interpolate(backgroundSpring, [0, 1], [0, 0.35]);
// Logo entrance animation (delayed 1 second) // Logo entrance animation using centralized timing
const logoDelay = Math.floor(1 * fps); const logoDelay = secondsToFrames(TIMING.OUTRO_LOGO_DELAY, fps);
const logoSpring = spring({ const logoSpring = spring({
frame: frame - logoDelay, frame: frame - logoDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]); const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]);
const logoScale = interpolate(logoSpring, [0, 1], [0.8, 1]); // Fine-tuned: Increased initial scale for smoother entrance
const logoY = interpolate(logoSpring, [0, 1], [30, 0]); const logoScale = interpolate(logoSpring, [0, 1], [0.85, 1]);
// Fine-tuned: Reduced Y translation
const logoY = interpolate(logoSpring, [0, 1], [25, 0]);
// Logo glow pulse effect // Logo glow pulse effect using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin((frame - logoDelay) * 0.06), Math.sin((frame - logoDelay) * GLOW_CONFIG.FREQUENCY.NORMAL),
[-1, 1], [-1, 1],
[0.4, 0.9] GLOW_CONFIG.INTENSITY.NORMAL
); );
const glowScale = interpolate( const glowScale = interpolate(
Math.sin((frame - logoDelay) * 0.04), Math.sin((frame - logoDelay) * GLOW_CONFIG.FREQUENCY.SLOW),
[-1, 1], [-1, 1],
[1.0, 1.2] GLOW_CONFIG.SCALE.STRONG
); );
// Text entrance (delayed 2 seconds) // Text entrance using centralized timing
const textDelay = Math.floor(2 * fps); const textDelay = secondsToFrames(TIMING.OUTRO_TEXT_DELAY, fps);
const textSpring = spring({ const textSpring = spring({
frame: frame - textDelay, frame: frame - textDelay,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const textOpacity = interpolate(textSpring, [0, 1], [0, 1]); const textOpacity = interpolate(textSpring, [0, 1], [0, 1]);
const textY = interpolate(textSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced Y translation
const textY = interpolate(textSpring, [0, 1], [18, 0]);
// Subtitle entrance (delayed 2.5 seconds) // Subtitle entrance using centralized timing
const subtitleDelay = Math.floor(2.5 * fps); const subtitleDelay = secondsToFrames(TIMING.OUTRO_SUBTITLE_DELAY, fps);
const subtitleSpring = spring({ const subtitleSpring = spring({
frame: frame - subtitleDelay, frame: frame - subtitleDelay,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]); const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
// Final fade out in last 2 seconds (frames 300-360) // Final fade out using centralized timing
const fadeOutStart = durationInFrames - 2 * fps; const fadeOutDuration = secondsToFrames(TIMING.OUTRO_FADE_DURATION, fps);
const fadeOutStart = durationInFrames - fadeOutDuration;
const finalFadeOpacity = interpolate( const finalFadeOpacity = interpolate(
frame, frame,
[fadeOutStart, durationInFrames], [fadeOutStart, durationInFrames],

View File

@@ -9,13 +9,12 @@ import {
Sequence, Sequence,
} from "remotion"; } from "remotion";
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import {
// Spring configurations from PRD SPRING_CONFIGS,
const SMOOTH = { damping: 200 }; TIMING,
GLOW_CONFIG,
// Typing animation configuration secondsToFrames,
const CHAR_FRAMES = 2; // Frames per character } from "../../config/timing";
const CURSOR_BLINK_FRAMES = 16;
/** /**
* PortalTitleScene - Scene 2: Title Card (4 seconds / 120 frames @ 30fps) * PortalTitleScene - Scene 2: Title Card (4 seconds / 120 frames @ 30fps)
@@ -34,48 +33,57 @@ export const PortalTitleScene: React.FC = () => {
const titleText = "EINUNDZWANZIG PORTAL"; const titleText = "EINUNDZWANZIG PORTAL";
const subtitleText = "Das Herzstück der deutschsprachigen Bitcoin-Community"; const subtitleText = "Das Herzstück der deutschsprachigen Bitcoin-Community";
// Calculate typed characters for title // Calculate typed characters for title using centralized timing
const typedTitleChars = Math.min( const typedTitleChars = Math.min(
titleText.length, titleText.length,
Math.floor(frame / CHAR_FRAMES) Math.floor(frame / TIMING.CHAR_FRAMES)
); );
const typedTitle = titleText.slice(0, typedTitleChars); const typedTitle = titleText.slice(0, typedTitleChars);
// Title typing complete frame // Title typing complete frame
const titleCompleteFrame = titleText.length * CHAR_FRAMES; const titleCompleteFrame = titleText.length * TIMING.CHAR_FRAMES;
// Cursor blink effect - only show while typing or shortly after // Cursor blink effect - only show while typing or shortly after
const showCursor = frame < titleCompleteFrame + fps; // Show cursor for 1 second after typing completes // Fine-tuned: cursor stays visible longer for better visual continuity
const cursorVisibleDuration = secondsToFrames(1.2, fps);
const showCursor = frame < titleCompleteFrame + cursorVisibleDuration;
const cursorOpacity = showCursor const cursorOpacity = showCursor
? interpolate( ? interpolate(
frame % CURSOR_BLINK_FRAMES, frame % TIMING.CURSOR_BLINK_FRAMES,
[0, CURSOR_BLINK_FRAMES / 2, CURSOR_BLINK_FRAMES], [0, TIMING.CURSOR_BLINK_FRAMES / 2, TIMING.CURSOR_BLINK_FRAMES],
[1, 0, 1], [1, 0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" } { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
) )
: 0; : 0;
// Subtitle entrance - after title typing completes // Subtitle entrance - after title typing completes with a pause
const subtitleDelay = titleCompleteFrame + Math.floor(0.3 * fps); // Fine-tuned: slightly longer pause (0.4s instead of 0.3s) for better pacing
const subtitleDelay = titleCompleteFrame + secondsToFrames(0.4, fps);
const subtitleSpring = spring({ const subtitleSpring = spring({
frame: frame - subtitleDelay, frame: frame - subtitleDelay,
fps, fps,
config: SMOOTH, config: SPRING_CONFIGS.SMOOTH,
}); });
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]); const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
const subtitleY = interpolate(subtitleSpring, [0, 1], [20, 0]); const subtitleY = interpolate(subtitleSpring, [0, 1], [20, 0]);
// Background glow that pulses subtly // Background glow that pulses subtly using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin(frame * 0.05), Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
[-1, 1], [-1, 1],
[0.3, 0.5] GLOW_CONFIG.INTENSITY.SUBTLE
); );
// Scene entrance fade from intro scene // Scene entrance fade from intro scene
const entranceFade = interpolate(frame, [0, Math.floor(0.3 * fps)], [0, 1], { // Fine-tuned: faster entrance (0.25s) for snappier transition
extrapolateRight: "clamp", const entranceFade = interpolate(
}); frame,
[0, secondsToFrames(0.25, fps)],
[0, 1],
{
extrapolateRight: "clamp",
}
);
return ( return (
<AbsoluteFill <AbsoluteFill

View File

@@ -10,13 +10,13 @@ import {
} from "remotion"; } from "remotion";
import { Audio } from "@remotion/media"; import { Audio } from "@remotion/media";
import { SparklineChart } from "../../components/SparklineChart"; import { SparklineChart } from "../../components/SparklineChart";
import {
// Spring configurations SPRING_CONFIGS,
const SNAPPY = { damping: 15, stiffness: 80 }; STAGGER_DELAYS,
const BOUNCY = { damping: 12 }; TIMING,
GLOW_CONFIG,
// Stagger delay between meetup items (in frames) secondsToFrames,
const MEETUP_STAGGER_DELAY = 15; } from "../../config/timing";
// Top meetups data with sparkline growth data // Top meetups data with sparkline growth data
const TOP_MEETUPS_DATA = [ const TOP_MEETUPS_DATA = [
@@ -83,45 +83,48 @@ export const TopMeetupsScene: React.FC = () => {
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const { fps } = useVideoConfig(); const { fps } = useVideoConfig();
// 3D Perspective entrance animation (0-60 frames) // 3D Perspective entrance animation using centralized config
const perspectiveSpring = spring({ const perspectiveSpring = spring({
frame, frame,
fps, fps,
config: { damping: 20, stiffness: 60 }, config: SPRING_CONFIGS.PERSPECTIVE,
}); });
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced initial rotation for smoother entrance
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.9, 1]); const perspectiveX = interpolate(perspectiveSpring, [0, 1], [18, 0]);
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.92, 1]);
const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]); const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]);
// Header entrance animation (delayed) // Header entrance animation (delayed) - Fine-tuned timing
const headerDelay = Math.floor(0.3 * fps); const headerDelay = secondsToFrames(0.35, fps);
const headerSpring = spring({ const headerSpring = spring({
frame: frame - headerDelay, frame: frame - headerDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]); const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]);
const headerY = interpolate(headerSpring, [0, 1], [-40, 0]); // Fine-tuned: Reduced Y translation
const headerY = interpolate(headerSpring, [0, 1], [-35, 0]);
// Subtitle animation (slightly more delayed) // Subtitle animation (slightly more delayed) - Fine-tuned timing
const subtitleDelay = Math.floor(0.5 * fps); const subtitleDelay = secondsToFrames(0.55, fps);
const subtitleSpring = spring({ const subtitleSpring = spring({
frame: frame - subtitleDelay, frame: frame - subtitleDelay,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]); const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
const subtitleY = interpolate(subtitleSpring, [0, 1], [20, 0]); // Fine-tuned: Reduced Y translation
const subtitleY = interpolate(subtitleSpring, [0, 1], [18, 0]);
// Base delay for meetup items // Base delay for meetup items using centralized timing
const meetupBaseDelay = Math.floor(1 * fps); const meetupBaseDelay = secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps);
// Subtle glow pulse for leading meetup // Subtle glow pulse for leading meetup using centralized config
const glowIntensity = interpolate( const glowIntensity = interpolate(
Math.sin(frame * 0.05), Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
[-1, 1], [-1, 1],
[0.4, 0.8] GLOW_CONFIG.INTENSITY.NORMAL
); );
return ( return (
@@ -130,7 +133,7 @@ export const TopMeetupsScene: React.FC = () => {
{TOP_MEETUPS_DATA.map((_, index) => ( {TOP_MEETUPS_DATA.map((_, index) => (
<Sequence <Sequence
key={`audio-${index}`} key={`audio-${index}`}
from={meetupBaseDelay + index * MEETUP_STAGGER_DELAY} from={meetupBaseDelay + index * STAGGER_DELAYS.MEETUP_RANK}
durationInFrames={Math.floor(0.5 * fps)} durationInFrames={Math.floor(0.5 * fps)}
> >
<Audio src={staticFile("sfx/checkmark-pop.mp3")} volume={0.4} /> <Audio src={staticFile("sfx/checkmark-pop.mp3")} volume={0.4} />
@@ -199,7 +202,7 @@ export const TopMeetupsScene: React.FC = () => {
key={meetup.name} key={meetup.name}
meetup={meetup} meetup={meetup}
maxCount={MAX_USER_COUNT} maxCount={MAX_USER_COUNT}
delay={meetupBaseDelay + index * MEETUP_STAGGER_DELAY} delay={meetupBaseDelay + index * STAGGER_DELAYS.MEETUP_RANK}
glowIntensity={glowIntensity} glowIntensity={glowIntensity}
isLeading={index === 0} isLeading={index === 0}
/> />
@@ -250,29 +253,31 @@ const MeetupRow: React.FC<MeetupRowProps> = ({
const adjustedFrame = Math.max(0, frame - delay); const adjustedFrame = Math.max(0, frame - delay);
// Row entrance spring // Row entrance spring using centralized config
const rowSpring = spring({ const rowSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: SNAPPY, config: SPRING_CONFIGS.SNAPPY,
}); });
const rowOpacity = interpolate(rowSpring, [0, 1], [0, 1]); const rowOpacity = interpolate(rowSpring, [0, 1], [0, 1]);
const rowX = interpolate(rowSpring, [0, 1], [-80, 0]); // Fine-tuned: Reduced X translation for subtler entrance
const rowX = interpolate(rowSpring, [0, 1], [-60, 0]);
// Rank badge bounce // Rank badge bounce using centralized config
// Fine-tuned: Slightly increased delay for better stagger effect
const rankSpring = spring({ const rankSpring = spring({
frame: adjustedFrame - 5, frame: adjustedFrame - 6,
fps, fps,
config: BOUNCY, config: SPRING_CONFIGS.BOUNCY,
}); });
const rankScale = interpolate(rankSpring, [0, 1], [0, 1]); const rankScale = interpolate(rankSpring, [0, 1], [0, 1]);
// User count animation // User count animation using centralized config
const countSpring = spring({ const countSpring = spring({
frame: adjustedFrame, frame: adjustedFrame,
fps, fps,
config: { damping: 18, stiffness: 70 }, config: SPRING_CONFIGS.FEATURED,
durationInFrames: 45, durationInFrames: 45,
}); });
const displayCount = Math.round(countSpring * meetup.userCount); const displayCount = Math.round(countSpring * meetup.userCount);
@@ -280,8 +285,8 @@ const MeetupRow: React.FC<MeetupRowProps> = ({
// Progress bar animation // Progress bar animation
const barProgress = interpolate(rowSpring, [0, 1], [0, meetup.userCount / maxCount]); const barProgress = interpolate(rowSpring, [0, 1], [0, meetup.userCount / maxCount]);
// Sparkline delay (appears slightly after the bar) // Sparkline delay (appears slightly after the bar) - Fine-tuned timing
const sparklineDelay = delay + 25; const sparklineDelay = delay + 22;
// Dynamic glow for leading meetup // Dynamic glow for leading meetup
const leadingGlow = isLeading const leadingGlow = isLeading