= ({
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";
diff --git a/videos/src/scenes/portal/PortalIntroScene.tsx b/videos/src/scenes/portal/PortalIntroScene.tsx
index 847b489..5ebfba6 100644
--- a/videos/src/scenes/portal/PortalIntroScene.tsx
+++ b/videos/src/scenes/portal/PortalIntroScene.tsx
@@ -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 (
diff --git a/videos/src/scenes/portal/PortalOutroScene.tsx b/videos/src/scenes/portal/PortalOutroScene.tsx
index 816f32f..75b9109 100644
--- a/videos/src/scenes/portal/PortalOutroScene.tsx
+++ b/videos/src/scenes/portal/PortalOutroScene.tsx
@@ -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],
diff --git a/videos/src/scenes/portal/PortalTitleScene.tsx b/videos/src/scenes/portal/PortalTitleScene.tsx
index 50eec6f..0d20b8d 100644
--- a/videos/src/scenes/portal/PortalTitleScene.tsx
+++ b/videos/src/scenes/portal/PortalTitleScene.tsx
@@ -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 (
{
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) => (
@@ -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 = ({
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 = ({
// 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