mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-01-28 07:43:18 +00:00
🎬 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:
285
videos/src/config/timing.test.ts
Normal file
285
videos/src/config/timing.test.ts
Normal 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
252
videos/src/config/timing.ts
Normal 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;
|
||||
}
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
} from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
import { ActivityItem } from "../../components/ActivityItem";
|
||||
|
||||
// Spring configurations
|
||||
const SNAPPY = { damping: 15, stiffness: 80 };
|
||||
|
||||
// Stagger delay between activity items (in frames)
|
||||
const ACTIVITY_STAGGER_DELAY = 20;
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
STAGGER_DELAYS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
// Activity feed data from the screenshot
|
||||
const ACTIVITY_FEED_DATA = [
|
||||
@@ -58,43 +59,46 @@ export const ActivityFeedScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// 3D Perspective entrance animation (0-60 frames)
|
||||
// 3D Perspective entrance animation using centralized config
|
||||
const perspectiveSpring = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 20, stiffness: 60 },
|
||||
config: SPRING_CONFIGS.PERSPECTIVE,
|
||||
});
|
||||
|
||||
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [20, 0]);
|
||||
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.9, 1]);
|
||||
// Fine-tuned: Reduced initial rotation for smoother entrance
|
||||
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]);
|
||||
|
||||
// Header entrance animation (delayed)
|
||||
const headerDelay = Math.floor(0.3 * fps);
|
||||
// Header entrance animation (delayed) - Fine-tuned timing
|
||||
const headerDelay = secondsToFrames(0.35, fps);
|
||||
const headerSpring = spring({
|
||||
frame: frame - headerDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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)
|
||||
const subtitleDelay = Math.floor(0.5 * fps);
|
||||
// Subtitle animation (slightly more delayed) - Fine-tuned timing
|
||||
const subtitleDelay = secondsToFrames(0.55, fps);
|
||||
const subtitleSpring = spring({
|
||||
frame: frame - subtitleDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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
|
||||
const activityBaseDelay = Math.floor(1 * fps);
|
||||
// Base delay for activity items using centralized timing
|
||||
const activityBaseDelay = secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps);
|
||||
|
||||
// Subtle pulse for live indicator
|
||||
// Subtle pulse for live indicator using centralized config
|
||||
const pulseIntensity = interpolate(
|
||||
Math.sin(frame * 0.1),
|
||||
Math.sin(frame * GLOW_CONFIG.FREQUENCY.PULSE),
|
||||
[-1, 1],
|
||||
[0.5, 1]
|
||||
);
|
||||
@@ -105,7 +109,7 @@ export const ActivityFeedScene: React.FC = () => {
|
||||
{ACTIVITY_FEED_DATA.map((_, index) => (
|
||||
<Sequence
|
||||
key={`audio-${index}`}
|
||||
from={activityBaseDelay + index * ACTIVITY_STAGGER_DELAY}
|
||||
from={activityBaseDelay + index * STAGGER_DELAYS.ACTIVITY}
|
||||
durationInFrames={Math.floor(0.5 * fps)}
|
||||
>
|
||||
<Audio src={staticFile("sfx/button-click.mp3")} volume={0.4} />
|
||||
@@ -190,7 +194,7 @@ export const ActivityFeedScene: React.FC = () => {
|
||||
<ActivityItemWrapper
|
||||
key={activity.eventName}
|
||||
activity={activity}
|
||||
delay={activityBaseDelay + index * ACTIVITY_STAGGER_DELAY}
|
||||
delay={activityBaseDelay + index * STAGGER_DELAYS.ACTIVITY}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
@@ -235,7 +239,7 @@ const ActivityItemWrapper: React.FC<ActivityItemWrapperProps> = ({
|
||||
// Each item that appears before this one causes a slight downward push
|
||||
let pushDownOffset = 0;
|
||||
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({
|
||||
frame: frame - prevItemDelay,
|
||||
fps,
|
||||
@@ -245,11 +249,11 @@ const ActivityItemWrapper: React.FC<ActivityItemWrapperProps> = ({
|
||||
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({
|
||||
frame: frame - delay,
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 80 },
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
|
||||
const containerOpacity = interpolate(containerSpring, [0, 1], [0, 1]);
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
|
||||
// Spring configurations
|
||||
const SMOOTH = { damping: 200 };
|
||||
const SNAPPY = { damping: 15, stiffness: 80 };
|
||||
const BOUNCY = { damping: 12 };
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
// URL to display
|
||||
const PORTAL_URL = "portal.einundzwanzig.space";
|
||||
@@ -34,88 +35,100 @@ export const CallToActionScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Background blur and zoom out animation (0-60 frames)
|
||||
// Background blur and zoom out animation using centralized config
|
||||
const blurSpring = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: SMOOTH,
|
||||
config: SPRING_CONFIGS.SMOOTH,
|
||||
});
|
||||
|
||||
const backgroundBlur = interpolate(blurSpring, [0, 1], [0, 8]);
|
||||
const backgroundScale = interpolate(blurSpring, [0, 1], [1, 0.95]);
|
||||
const backgroundOpacity = interpolate(blurSpring, [0, 1], [0.3, 0.15]);
|
||||
// Fine-tuned: Adjusted blur and scale for smoother background transition
|
||||
const backgroundBlur = interpolate(blurSpring, [0, 1], [0, 10]);
|
||||
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)
|
||||
const overlayDelay = 15;
|
||||
// Glassmorphism overlay entrance using centralized timing
|
||||
const overlayDelay = secondsToFrames(TIMING.CTA_OVERLAY_DELAY, fps);
|
||||
const overlaySpring = spring({
|
||||
frame: frame - overlayDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
const overlayOpacity = interpolate(overlaySpring, [0, 1], [0, 1]);
|
||||
|
||||
// Title entrance (delayed 30 frames)
|
||||
const titleDelay = Math.floor(1 * fps);
|
||||
// Title entrance using centralized timing
|
||||
const titleDelay = secondsToFrames(TIMING.CTA_TITLE_DELAY, fps);
|
||||
const titleSpring = spring({
|
||||
frame: frame - titleDelay,
|
||||
fps,
|
||||
config: BOUNCY,
|
||||
config: SPRING_CONFIGS.BOUNCY,
|
||||
});
|
||||
const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]);
|
||||
const titleY = interpolate(titleSpring, [0, 1], [50, 0]);
|
||||
const titleScale = interpolate(titleSpring, [0, 1], [0.8, 1]);
|
||||
// Fine-tuned: Reduced Y translation for subtler entrance
|
||||
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)
|
||||
const logoDelay = Math.floor(1.5 * fps);
|
||||
// Logo entrance using centralized timing
|
||||
const logoDelay = secondsToFrames(TIMING.CTA_LOGO_DELAY, fps);
|
||||
const logoSpring = spring({
|
||||
frame: frame - logoDelay,
|
||||
fps,
|
||||
config: BOUNCY,
|
||||
config: SPRING_CONFIGS.BOUNCY,
|
||||
});
|
||||
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(
|
||||
Math.sin((frame - logoDelay) * 0.08),
|
||||
Math.sin((frame - logoDelay) * GLOW_CONFIG.FREQUENCY.FAST),
|
||||
[-1, 1],
|
||||
[0.4, 1]
|
||||
GLOW_CONFIG.INTENSITY.STRONG
|
||||
);
|
||||
|
||||
// URL typing animation (delayed 2.5 seconds)
|
||||
const urlDelay = Math.floor(2.5 * fps);
|
||||
// URL typing animation using centralized timing
|
||||
const urlDelay = secondsToFrames(TIMING.CTA_URL_DELAY, fps);
|
||||
const urlDuration = secondsToFrames(TIMING.CTA_URL_DURATION, fps);
|
||||
const urlTypingProgress = Math.max(
|
||||
0,
|
||||
Math.min(1, (frame - urlDelay) / (1.5 * fps))
|
||||
Math.min(1, (frame - urlDelay) / urlDuration)
|
||||
);
|
||||
const displayedUrlLength = Math.floor(urlTypingProgress * PORTAL_URL.length);
|
||||
const displayedUrl = PORTAL_URL.slice(0, displayedUrlLength);
|
||||
const showCursor = frame >= urlDelay && urlTypingProgress < 1;
|
||||
|
||||
// URL container entrance
|
||||
// URL container entrance - Fine-tuned timing
|
||||
const urlContainerDelay = secondsToFrames(2.3, fps);
|
||||
const urlContainerSpring = spring({
|
||||
frame: frame - Math.floor(2.3 * fps),
|
||||
frame: frame - urlContainerDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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
|
||||
const urlPulseActive = frame > urlDelay + 1.5 * fps;
|
||||
// URL pulse after typing complete using centralized config
|
||||
const urlPulseActive = frame > urlDelay + urlDuration;
|
||||
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;
|
||||
|
||||
// Subtitle entrance
|
||||
const subtitleDelay = Math.floor(3.5 * fps);
|
||||
// Subtitle entrance using centralized timing
|
||||
const subtitleDelay = secondsToFrames(TIMING.CTA_SUBTITLE_DELAY, fps);
|
||||
const subtitleSpring = spring({
|
||||
frame: frame - subtitleDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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 (
|
||||
<AbsoluteFill className="bg-zinc-900 overflow-hidden">
|
||||
|
||||
@@ -13,12 +13,13 @@ import { DashboardSidebar, SidebarNavItem } from "../../components/DashboardSide
|
||||
import { StatsCounter } from "../../components/StatsCounter";
|
||||
import { SparklineChart } from "../../components/SparklineChart";
|
||||
import { ActivityItem } from "../../components/ActivityItem";
|
||||
|
||||
// Spring configurations
|
||||
const SNAPPY = { damping: 15, stiffness: 80 };
|
||||
|
||||
// Stagger delay between content cards
|
||||
const CARD_STAGGER_DELAY = 5;
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
STAGGER_DELAYS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
// Navigation items for the sidebar
|
||||
const NAV_ITEMS: SidebarNavItem[] = [
|
||||
@@ -54,32 +55,33 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
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({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 20, stiffness: 60 },
|
||||
config: SPRING_CONFIGS.PERSPECTIVE,
|
||||
});
|
||||
|
||||
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [30, 0]);
|
||||
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.85, 1]);
|
||||
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [25, 0]); // Reduced from 30
|
||||
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.88, 1]); // Increased from 0.85
|
||||
const perspectiveOpacity = interpolate(perspectiveSpring, [0, 1], [0, 1]);
|
||||
|
||||
// Header entrance animation (delayed)
|
||||
const headerDelay = Math.floor(0.5 * fps);
|
||||
// Header entrance animation (delayed) using centralized timing
|
||||
const headerDelay = secondsToFrames(TIMING.HEADER_DELAY, fps);
|
||||
const headerSpring = spring({
|
||||
frame: frame - headerDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
const headerOpacity = interpolate(headerSpring, [0, 1], [0, 1]);
|
||||
const headerY = interpolate(headerSpring, [0, 1], [-30, 0]);
|
||||
|
||||
// Subtle background glow pulse
|
||||
// Subtle background glow pulse using centralized config
|
||||
const glowIntensity = interpolate(
|
||||
Math.sin(frame * 0.04),
|
||||
Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
|
||||
[-1, 1],
|
||||
[0.3, 0.5]
|
||||
GLOW_CONFIG.INTENSITY.SUBTLE
|
||||
);
|
||||
|
||||
// Sidebar dimensions
|
||||
@@ -90,8 +92,8 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
const cardWidth = 380;
|
||||
const cardGap = 24;
|
||||
|
||||
// Card entrance delays (staggered)
|
||||
const cardBaseDelay = Math.floor(1 * fps); // Start after 1 second
|
||||
// Card entrance delays (staggered) using centralized timing
|
||||
const cardBaseDelay = secondsToFrames(TIMING.CONTENT_BASE_DELAY, fps);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="bg-zinc-900 overflow-hidden">
|
||||
@@ -172,7 +174,7 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
className="flex gap-6 mb-8"
|
||||
style={{ gap: cardGap }}
|
||||
>
|
||||
{/* Meetups Card */}
|
||||
{/* Meetups Card - Fine-tuned: increased stagger delay between cards for better visual flow */}
|
||||
<StatsCard
|
||||
title="Meetups"
|
||||
delay={cardBaseDelay}
|
||||
@@ -181,8 +183,8 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
>
|
||||
<StatsCounter
|
||||
targetNumber={204}
|
||||
delay={cardBaseDelay + 15}
|
||||
duration={60}
|
||||
delay={cardBaseDelay + TIMING.COUNTER_PRE_DELAY}
|
||||
duration={TIMING.COUNTER_DURATION}
|
||||
label="Aktive Gruppen"
|
||||
fontSize={72}
|
||||
color="#f7931a"
|
||||
@@ -192,7 +194,7 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
data={MEETUP_TREND_DATA}
|
||||
width={cardWidth - 48}
|
||||
height={60}
|
||||
delay={cardBaseDelay + 30}
|
||||
delay={cardBaseDelay + TIMING.SPARKLINE_PRE_DELAY}
|
||||
showFill={true}
|
||||
fillOpacity={0.15}
|
||||
/>
|
||||
@@ -202,14 +204,14 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
{/* Users Card */}
|
||||
<StatsCard
|
||||
title="Benutzer"
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD}
|
||||
width={cardWidth}
|
||||
glowIntensity={glowIntensity}
|
||||
>
|
||||
<StatsCounter
|
||||
targetNumber={1247}
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY + 15}
|
||||
duration={60}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD + TIMING.COUNTER_PRE_DELAY}
|
||||
duration={TIMING.COUNTER_DURATION}
|
||||
label="Registrierte Nutzer"
|
||||
fontSize={72}
|
||||
color="#f7931a"
|
||||
@@ -219,7 +221,7 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
data={USER_TREND_DATA}
|
||||
width={cardWidth - 48}
|
||||
height={60}
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY + 30}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD + TIMING.SPARKLINE_PRE_DELAY}
|
||||
showFill={true}
|
||||
fillOpacity={0.15}
|
||||
/>
|
||||
@@ -229,14 +231,14 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
{/* Events Card */}
|
||||
<StatsCard
|
||||
title="Events"
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY * 2}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD * 2}
|
||||
width={cardWidth}
|
||||
glowIntensity={glowIntensity}
|
||||
>
|
||||
<StatsCounter
|
||||
targetNumber={89}
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY * 2 + 15}
|
||||
duration={60}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD * 2 + TIMING.COUNTER_PRE_DELAY}
|
||||
duration={TIMING.COUNTER_DURATION}
|
||||
label="Diese Woche"
|
||||
fontSize={72}
|
||||
color="#f7931a"
|
||||
@@ -246,7 +248,7 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
data={EVENT_TREND_DATA}
|
||||
width={cardWidth - 48}
|
||||
height={60}
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY * 2 + 30}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD * 2 + TIMING.SPARKLINE_PRE_DELAY}
|
||||
showFill={true}
|
||||
fillOpacity={0.15}
|
||||
/>
|
||||
@@ -258,13 +260,13 @@ export const DashboardOverviewScene: React.FC = () => {
|
||||
<div className="flex gap-6" style={{ gap: cardGap }}>
|
||||
{/* Activity Feed */}
|
||||
<ActivitySection
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY * 3}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD * 3}
|
||||
glowIntensity={glowIntensity}
|
||||
/>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<QuickStatsSection
|
||||
delay={cardBaseDelay + CARD_STAGGER_DELAY * 4}
|
||||
delay={cardBaseDelay + STAGGER_DELAYS.CARD * 4}
|
||||
glowIntensity={glowIntensity}
|
||||
/>
|
||||
</div>
|
||||
@@ -306,15 +308,18 @@ const StatsCard: React.FC<StatsCardProps> = ({
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Fine-tuned: Using centralized SNAPPY config for consistent card animations
|
||||
const cardSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
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 cardY = interpolate(cardSpring, [0, 1], [30, 0]);
|
||||
// Fine-tuned: Reduced Y translation for subtler entrance
|
||||
const cardY = interpolate(cardSpring, [0, 1], [25, 0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -349,14 +354,15 @@ const ActivitySection: React.FC<ActivitySectionProps> = ({
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Fine-tuned: Using centralized SNAPPY config
|
||||
const sectionSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
|
||||
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 = [
|
||||
{ eventName: "EINUNDZWANZIG Kempten", timestamp: "vor 13 Stunden", badgeText: "Neuer Termin" },
|
||||
@@ -381,7 +387,7 @@ const ActivitySection: React.FC<ActivitySectionProps> = ({
|
||||
eventName={activity.eventName}
|
||||
timestamp={activity.timestamp}
|
||||
badgeText={activity.badgeText}
|
||||
delay={delay + 10 + index * 8}
|
||||
delay={delay + 10 + index * STAGGER_DELAYS.LIST_ITEM}
|
||||
width={480}
|
||||
/>
|
||||
))}
|
||||
@@ -407,14 +413,15 @@ const QuickStatsSection: React.FC<QuickStatsSectionProps> = ({
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Fine-tuned: Using centralized SNAPPY config
|
||||
const sectionSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
|
||||
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 (
|
||||
<div
|
||||
@@ -436,12 +443,12 @@ const QuickStatsSection: React.FC<QuickStatsSectionProps> = ({
|
||||
<QuickStatRow
|
||||
label="Neue diese Woche"
|
||||
value={12}
|
||||
delay={delay + 18}
|
||||
delay={delay + 10 + STAGGER_DELAYS.QUICK_STAT}
|
||||
/>
|
||||
<QuickStatRow
|
||||
label="Aktive Nutzer"
|
||||
value={847}
|
||||
delay={delay + 26}
|
||||
delay={delay + 10 + STAGGER_DELAYS.QUICK_STAT * 2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -463,14 +470,16 @@ const QuickStatRow: React.FC<QuickStatRowProps> = ({ label, value, delay }) => {
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Fine-tuned: Using centralized ROW config
|
||||
const rowSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 90 },
|
||||
config: SPRING_CONFIGS.ROW,
|
||||
});
|
||||
|
||||
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
|
||||
const counterValue = interpolate(rowSpring, [0, 1], [0, value]);
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
} from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
import { MeetupCard } from "../../components/MeetupCard";
|
||||
|
||||
// Spring configurations
|
||||
const SNAPPY = { damping: 15, stiffness: 80 };
|
||||
const BOUNCY = { damping: 12 };
|
||||
|
||||
// Stagger delay between meetup list items
|
||||
const LIST_ITEM_STAGGER = 8;
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
STAGGER_DELAYS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
// Upcoming meetup data
|
||||
const UPCOMING_MEETUPS = [
|
||||
@@ -61,77 +61,83 @@ export const MeetupShowcaseScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// 3D Perspective entrance animation (0-60 frames)
|
||||
// 3D Perspective entrance animation using centralized config
|
||||
const perspectiveSpring = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 20, stiffness: 60 },
|
||||
config: SPRING_CONFIGS.PERSPECTIVE,
|
||||
});
|
||||
|
||||
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [20, 0]);
|
||||
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.9, 1]);
|
||||
// Fine-tuned: Reduced initial rotation for smoother entrance
|
||||
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]);
|
||||
|
||||
// Header entrance animation (delayed)
|
||||
const headerDelay = Math.floor(0.3 * fps);
|
||||
// Header entrance animation (delayed) - Fine-tuned timing
|
||||
const headerDelay = secondsToFrames(0.35, fps);
|
||||
const headerSpring = spring({
|
||||
frame: frame - headerDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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
|
||||
const featuredCardDelay = Math.floor(0.8 * fps);
|
||||
// Featured card entrance delay using centralized timing
|
||||
const featuredCardDelay = secondsToFrames(TIMING.FEATURED_DELAY, fps);
|
||||
|
||||
// Featured card 3D shadow animation
|
||||
// Featured card 3D shadow animation using centralized config
|
||||
const featuredSpring = spring({
|
||||
frame: frame - featuredCardDelay,
|
||||
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 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 featuredShadowBlur = interpolate(featuredSpring, [0, 1], [10, 60]);
|
||||
|
||||
// Date/time info animation (after featured card)
|
||||
const dateDelay = featuredCardDelay + Math.floor(0.4 * fps);
|
||||
// Date/time info animation (after featured card) - Fine-tuned timing
|
||||
const dateDelay = featuredCardDelay + secondsToFrames(0.45, fps);
|
||||
const dateSpring = spring({
|
||||
frame: frame - dateDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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
|
||||
const listHeaderDelay = featuredCardDelay + Math.floor(1 * fps);
|
||||
// Upcoming list header delay - Fine-tuned timing
|
||||
const listHeaderDelay = featuredCardDelay + secondsToFrames(1.1, fps);
|
||||
const listHeaderSpring = spring({
|
||||
frame: frame - listHeaderDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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
|
||||
const buttonsDelay = Math.floor(3.5 * fps);
|
||||
// Action buttons animation - Fine-tuned timing
|
||||
const buttonsDelay = secondsToFrames(3.2, fps);
|
||||
const buttonsSpring = spring({
|
||||
frame: frame - buttonsDelay,
|
||||
fps,
|
||||
config: BOUNCY,
|
||||
config: SPRING_CONFIGS.BOUNCY,
|
||||
});
|
||||
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(
|
||||
Math.sin(frame * 0.05),
|
||||
Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
|
||||
[-1, 1],
|
||||
[0.3, 0.6]
|
||||
[0.35, 0.6]
|
||||
);
|
||||
|
||||
// Featured meetup data
|
||||
@@ -151,7 +157,7 @@ export const MeetupShowcaseScene: React.FC = () => {
|
||||
</Sequence>
|
||||
|
||||
{/* 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} />
|
||||
</Sequence>
|
||||
|
||||
@@ -300,7 +306,7 @@ export const MeetupShowcaseScene: React.FC = () => {
|
||||
<UpcomingMeetupItem
|
||||
key={meetup.name}
|
||||
meetup={meetup}
|
||||
delay={listHeaderDelay + (index + 1) * LIST_ITEM_STAGGER}
|
||||
delay={listHeaderDelay + (index + 1) * STAGGER_DELAYS.LIST_ITEM}
|
||||
glowIntensity={glowIntensity}
|
||||
/>
|
||||
))}
|
||||
@@ -369,15 +375,18 @@ const UpcomingMeetupItem: React.FC<UpcomingMeetupItemProps> = ({
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Fine-tuned: Using centralized SNAPPY config
|
||||
const itemSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
|
||||
const itemOpacity = interpolate(itemSpring, [0, 1], [0, 1]);
|
||||
const itemY = interpolate(itemSpring, [0, 1], [30, 0]);
|
||||
const itemScale = interpolate(itemSpring, [0, 1], [0.9, 1]);
|
||||
// Fine-tuned: Reduced Y translation for subtler entrance
|
||||
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 (
|
||||
<div
|
||||
@@ -445,13 +454,15 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Fine-tuned: Using centralized BUTTON config
|
||||
const buttonSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
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 baseClasses = "flex items-center gap-3 px-6 py-3 rounded-xl font-semibold text-base transition-all";
|
||||
|
||||
@@ -11,9 +11,12 @@ import {
|
||||
import { Audio } from "@remotion/media";
|
||||
import { AnimatedLogo } from "../../components/AnimatedLogo";
|
||||
import { BitcoinEffect } from "../../components/BitcoinEffect";
|
||||
|
||||
// Spring configurations from PRD
|
||||
const SMOOTH = { damping: 200 };
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
/**
|
||||
* PortalIntroScene - Scene 1: Logo Reveal (6 seconds / 180 frames @ 30fps)
|
||||
@@ -29,59 +32,70 @@ export const PortalIntroScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Background zoom animation - starts zoomed in, zooms out
|
||||
const backgroundZoom = interpolate(frame, [0, 3 * fps], [1.2, 1.0], {
|
||||
// Calculate delays using centralized timing
|
||||
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",
|
||||
});
|
||||
|
||||
// Logo entrance spring animation - delayed slightly for dramatic effect
|
||||
const logoEntranceDelay = Math.floor(0.5 * fps);
|
||||
const logoSpring = spring({
|
||||
frame: frame - logoEntranceDelay,
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 80 },
|
||||
config: SPRING_CONFIGS.LOGO,
|
||||
});
|
||||
const logoScale = interpolate(logoSpring, [0, 1], [0, 1]);
|
||||
const logoOpacity = interpolate(logoSpring, [0, 0.5], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
// Outer glow pulse effect
|
||||
// Outer glow pulse effect using centralized config
|
||||
const glowIntensity = interpolate(
|
||||
Math.sin((frame - logoEntranceDelay) * 0.08),
|
||||
Math.sin((frame - logoEntranceDelay) * GLOW_CONFIG.FREQUENCY.FAST),
|
||||
[-1, 1],
|
||||
[0.4, 0.8]
|
||||
GLOW_CONFIG.INTENSITY.NORMAL
|
||||
);
|
||||
const glowScale = interpolate(
|
||||
Math.sin((frame - logoEntranceDelay) * 0.06),
|
||||
Math.sin((frame - logoEntranceDelay) * GLOW_CONFIG.FREQUENCY.NORMAL),
|
||||
[-1, 1],
|
||||
[1.0, 1.15]
|
||||
GLOW_CONFIG.SCALE.NORMAL
|
||||
);
|
||||
|
||||
// Title text entrance - appears after logo
|
||||
const titleDelay = Math.floor(2 * fps);
|
||||
// Title text entrance - appears after logo with smooth spring
|
||||
const titleSpring = spring({
|
||||
frame: frame - titleDelay,
|
||||
fps,
|
||||
config: SMOOTH,
|
||||
config: SPRING_CONFIGS.SMOOTH,
|
||||
});
|
||||
const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]);
|
||||
const titleY = interpolate(titleSpring, [0, 1], [30, 0]);
|
||||
|
||||
// Subtitle entrance
|
||||
const subtitleDelay = Math.floor(2.8 * fps);
|
||||
// Subtitle entrance - follows title with smooth spring
|
||||
const subtitleSpring = spring({
|
||||
frame: frame - subtitleDelay,
|
||||
fps,
|
||||
config: SMOOTH,
|
||||
config: SPRING_CONFIGS.SMOOTH,
|
||||
});
|
||||
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
|
||||
|
||||
// Center position for logo (adjusts as text appears)
|
||||
const contentY = interpolate(frame, [titleDelay, titleDelay + fps], [0, -60], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
// Fine-tuned: smoother transition over longer duration
|
||||
const contentTransitionDuration = secondsToFrames(1.2, fps);
|
||||
const contentY = interpolate(
|
||||
frame,
|
||||
[titleDelay, titleDelay + contentTransitionDuration],
|
||||
[0, -60],
|
||||
{
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill className="bg-zinc-900 overflow-hidden">
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
} from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
import { BitcoinEffect } from "../../components/BitcoinEffect";
|
||||
|
||||
// Spring configurations
|
||||
const SMOOTH = { damping: 200 };
|
||||
const SNAPPY = { damping: 15, stiffness: 80 };
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
/**
|
||||
* PortalOutroScene - Scene 9: Outro (12 seconds / 360 frames @ 30fps)
|
||||
@@ -32,58 +34,63 @@ export const PortalOutroScene: React.FC = () => {
|
||||
const { fps } = useVideoConfig();
|
||||
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({
|
||||
frame,
|
||||
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)
|
||||
const logoDelay = Math.floor(1 * fps);
|
||||
// Logo entrance animation using centralized timing
|
||||
const logoDelay = secondsToFrames(TIMING.OUTRO_LOGO_DELAY, fps);
|
||||
const logoSpring = spring({
|
||||
frame: frame - logoDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]);
|
||||
const logoScale = interpolate(logoSpring, [0, 1], [0.8, 1]);
|
||||
const logoY = interpolate(logoSpring, [0, 1], [30, 0]);
|
||||
// Fine-tuned: Increased initial scale for smoother entrance
|
||||
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(
|
||||
Math.sin((frame - logoDelay) * 0.06),
|
||||
Math.sin((frame - logoDelay) * GLOW_CONFIG.FREQUENCY.NORMAL),
|
||||
[-1, 1],
|
||||
[0.4, 0.9]
|
||||
GLOW_CONFIG.INTENSITY.NORMAL
|
||||
);
|
||||
const glowScale = interpolate(
|
||||
Math.sin((frame - logoDelay) * 0.04),
|
||||
Math.sin((frame - logoDelay) * GLOW_CONFIG.FREQUENCY.SLOW),
|
||||
[-1, 1],
|
||||
[1.0, 1.2]
|
||||
GLOW_CONFIG.SCALE.STRONG
|
||||
);
|
||||
|
||||
// Text entrance (delayed 2 seconds)
|
||||
const textDelay = Math.floor(2 * fps);
|
||||
// Text entrance using centralized timing
|
||||
const textDelay = secondsToFrames(TIMING.OUTRO_TEXT_DELAY, fps);
|
||||
const textSpring = spring({
|
||||
frame: frame - textDelay,
|
||||
fps,
|
||||
config: SMOOTH,
|
||||
config: SPRING_CONFIGS.SMOOTH,
|
||||
});
|
||||
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)
|
||||
const subtitleDelay = Math.floor(2.5 * fps);
|
||||
// Subtitle entrance using centralized timing
|
||||
const subtitleDelay = secondsToFrames(TIMING.OUTRO_SUBTITLE_DELAY, fps);
|
||||
const subtitleSpring = spring({
|
||||
frame: frame - subtitleDelay,
|
||||
fps,
|
||||
config: SMOOTH,
|
||||
config: SPRING_CONFIGS.SMOOTH,
|
||||
});
|
||||
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
|
||||
|
||||
// Final fade out in last 2 seconds (frames 300-360)
|
||||
const fadeOutStart = durationInFrames - 2 * fps;
|
||||
// Final fade out using centralized timing
|
||||
const fadeOutDuration = secondsToFrames(TIMING.OUTRO_FADE_DURATION, fps);
|
||||
const fadeOutStart = durationInFrames - fadeOutDuration;
|
||||
const finalFadeOpacity = interpolate(
|
||||
frame,
|
||||
[fadeOutStart, durationInFrames],
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
Sequence,
|
||||
} from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
|
||||
// Spring configurations from PRD
|
||||
const SMOOTH = { damping: 200 };
|
||||
|
||||
// Typing animation configuration
|
||||
const CHAR_FRAMES = 2; // Frames per character
|
||||
const CURSOR_BLINK_FRAMES = 16;
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
/**
|
||||
* PortalTitleScene - Scene 2: Title Card (4 seconds / 120 frames @ 30fps)
|
||||
@@ -34,48 +33,57 @@ export const PortalTitleScene: React.FC = () => {
|
||||
const titleText = "EINUNDZWANZIG PORTAL";
|
||||
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(
|
||||
titleText.length,
|
||||
Math.floor(frame / CHAR_FRAMES)
|
||||
Math.floor(frame / TIMING.CHAR_FRAMES)
|
||||
);
|
||||
const typedTitle = titleText.slice(0, typedTitleChars);
|
||||
|
||||
// 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
|
||||
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
|
||||
? interpolate(
|
||||
frame % CURSOR_BLINK_FRAMES,
|
||||
[0, CURSOR_BLINK_FRAMES / 2, CURSOR_BLINK_FRAMES],
|
||||
frame % TIMING.CURSOR_BLINK_FRAMES,
|
||||
[0, TIMING.CURSOR_BLINK_FRAMES / 2, TIMING.CURSOR_BLINK_FRAMES],
|
||||
[1, 0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Subtitle entrance - after title typing completes
|
||||
const subtitleDelay = titleCompleteFrame + Math.floor(0.3 * fps);
|
||||
// Subtitle entrance - after title typing completes with a pause
|
||||
// Fine-tuned: slightly longer pause (0.4s instead of 0.3s) for better pacing
|
||||
const subtitleDelay = titleCompleteFrame + secondsToFrames(0.4, fps);
|
||||
const subtitleSpring = spring({
|
||||
frame: frame - subtitleDelay,
|
||||
fps,
|
||||
config: SMOOTH,
|
||||
config: SPRING_CONFIGS.SMOOTH,
|
||||
});
|
||||
const subtitleOpacity = interpolate(subtitleSpring, [0, 1], [0, 1]);
|
||||
const subtitleY = interpolate(subtitleSpring, [0, 1], [20, 0]);
|
||||
|
||||
// Background glow that pulses subtly
|
||||
// Background glow that pulses subtly using centralized config
|
||||
const glowIntensity = interpolate(
|
||||
Math.sin(frame * 0.05),
|
||||
Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
|
||||
[-1, 1],
|
||||
[0.3, 0.5]
|
||||
GLOW_CONFIG.INTENSITY.SUBTLE
|
||||
);
|
||||
|
||||
// Scene entrance fade from intro scene
|
||||
const entranceFade = interpolate(frame, [0, Math.floor(0.3 * fps)], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
// Fine-tuned: faster entrance (0.25s) for snappier transition
|
||||
const entranceFade = interpolate(
|
||||
frame,
|
||||
[0, secondsToFrames(0.25, fps)],
|
||||
[0, 1],
|
||||
{
|
||||
extrapolateRight: "clamp",
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
} from "remotion";
|
||||
import { Audio } from "@remotion/media";
|
||||
import { SparklineChart } from "../../components/SparklineChart";
|
||||
|
||||
// Spring configurations
|
||||
const SNAPPY = { damping: 15, stiffness: 80 };
|
||||
const BOUNCY = { damping: 12 };
|
||||
|
||||
// Stagger delay between meetup items (in frames)
|
||||
const MEETUP_STAGGER_DELAY = 15;
|
||||
import {
|
||||
SPRING_CONFIGS,
|
||||
STAGGER_DELAYS,
|
||||
TIMING,
|
||||
GLOW_CONFIG,
|
||||
secondsToFrames,
|
||||
} from "../../config/timing";
|
||||
|
||||
// Top meetups data with sparkline growth data
|
||||
const TOP_MEETUPS_DATA = [
|
||||
@@ -83,45 +83,48 @@ export const TopMeetupsScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// 3D Perspective entrance animation (0-60 frames)
|
||||
// 3D Perspective entrance animation using centralized config
|
||||
const perspectiveSpring = spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 20, stiffness: 60 },
|
||||
config: SPRING_CONFIGS.PERSPECTIVE,
|
||||
});
|
||||
|
||||
const perspectiveX = interpolate(perspectiveSpring, [0, 1], [20, 0]);
|
||||
const perspectiveScale = interpolate(perspectiveSpring, [0, 1], [0.9, 1]);
|
||||
// Fine-tuned: Reduced initial rotation for smoother entrance
|
||||
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]);
|
||||
|
||||
// Header entrance animation (delayed)
|
||||
const headerDelay = Math.floor(0.3 * fps);
|
||||
// Header entrance animation (delayed) - Fine-tuned timing
|
||||
const headerDelay = secondsToFrames(0.35, fps);
|
||||
const headerSpring = spring({
|
||||
frame: frame - headerDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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)
|
||||
const subtitleDelay = Math.floor(0.5 * fps);
|
||||
// Subtitle animation (slightly more delayed) - Fine-tuned timing
|
||||
const subtitleDelay = secondsToFrames(0.55, fps);
|
||||
const subtitleSpring = spring({
|
||||
frame: frame - subtitleDelay,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
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
|
||||
const meetupBaseDelay = Math.floor(1 * fps);
|
||||
// Base delay for meetup items using centralized timing
|
||||
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(
|
||||
Math.sin(frame * 0.05),
|
||||
Math.sin(frame * GLOW_CONFIG.FREQUENCY.SLOW),
|
||||
[-1, 1],
|
||||
[0.4, 0.8]
|
||||
GLOW_CONFIG.INTENSITY.NORMAL
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -130,7 +133,7 @@ export const TopMeetupsScene: React.FC = () => {
|
||||
{TOP_MEETUPS_DATA.map((_, index) => (
|
||||
<Sequence
|
||||
key={`audio-${index}`}
|
||||
from={meetupBaseDelay + index * MEETUP_STAGGER_DELAY}
|
||||
from={meetupBaseDelay + index * STAGGER_DELAYS.MEETUP_RANK}
|
||||
durationInFrames={Math.floor(0.5 * fps)}
|
||||
>
|
||||
<Audio src={staticFile("sfx/checkmark-pop.mp3")} volume={0.4} />
|
||||
@@ -199,7 +202,7 @@ export const TopMeetupsScene: React.FC = () => {
|
||||
key={meetup.name}
|
||||
meetup={meetup}
|
||||
maxCount={MAX_USER_COUNT}
|
||||
delay={meetupBaseDelay + index * MEETUP_STAGGER_DELAY}
|
||||
delay={meetupBaseDelay + index * STAGGER_DELAYS.MEETUP_RANK}
|
||||
glowIntensity={glowIntensity}
|
||||
isLeading={index === 0}
|
||||
/>
|
||||
@@ -250,29 +253,31 @@ const MeetupRow: React.FC<MeetupRowProps> = ({
|
||||
|
||||
const adjustedFrame = Math.max(0, frame - delay);
|
||||
|
||||
// Row entrance spring
|
||||
// Row entrance spring using centralized config
|
||||
const rowSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
fps,
|
||||
config: SNAPPY,
|
||||
config: SPRING_CONFIGS.SNAPPY,
|
||||
});
|
||||
|
||||
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({
|
||||
frame: adjustedFrame - 5,
|
||||
frame: adjustedFrame - 6,
|
||||
fps,
|
||||
config: BOUNCY,
|
||||
config: SPRING_CONFIGS.BOUNCY,
|
||||
});
|
||||
const rankScale = interpolate(rankSpring, [0, 1], [0, 1]);
|
||||
|
||||
// User count animation
|
||||
// User count animation using centralized config
|
||||
const countSpring = spring({
|
||||
frame: adjustedFrame,
|
||||
fps,
|
||||
config: { damping: 18, stiffness: 70 },
|
||||
config: SPRING_CONFIGS.FEATURED,
|
||||
durationInFrames: 45,
|
||||
});
|
||||
const displayCount = Math.round(countSpring * meetup.userCount);
|
||||
@@ -280,8 +285,8 @@ const MeetupRow: React.FC<MeetupRowProps> = ({
|
||||
// Progress bar animation
|
||||
const barProgress = interpolate(rowSpring, [0, 1], [0, meetup.userCount / maxCount]);
|
||||
|
||||
// Sparkline delay (appears slightly after the bar)
|
||||
const sparklineDelay = delay + 25;
|
||||
// Sparkline delay (appears slightly after the bar) - Fine-tuned timing
|
||||
const sparklineDelay = delay + 22;
|
||||
|
||||
// Dynamic glow for leading meetup
|
||||
const leadingGlow = isLeading
|
||||
|
||||
Reference in New Issue
Block a user