mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-01-28 07:43:18 +00:00
📱 Add PortalPresentationMobile composition for mobile (1080x1920)
- Create PortalPresentationMobile.tsx for portrait mobile resolution - Register PortalPresentationMobile composition in Root.tsx - Add comprehensive tests for the mobile composition - Reuse existing portal scenes which adapt via useVideoConfig() - Mobile version has same scene structure and timing as desktop Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
398
videos/src/PortalPresentationMobile.test.tsx
Normal file
398
videos/src/PortalPresentationMobile.test.tsx
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { render, cleanup } from "@testing-library/react";
|
||||||
|
import { PortalPresentationMobile } from "./PortalPresentationMobile";
|
||||||
|
|
||||||
|
/* eslint-disable @remotion/warn-native-media-tag */
|
||||||
|
// Mock Remotion hooks
|
||||||
|
vi.mock("remotion", () => ({
|
||||||
|
useCurrentFrame: vi.fn(() => 60),
|
||||||
|
useVideoConfig: vi.fn(() => ({
|
||||||
|
fps: 30,
|
||||||
|
width: 1080,
|
||||||
|
height: 1920,
|
||||||
|
durationInFrames: 2700,
|
||||||
|
})),
|
||||||
|
interpolate: vi.fn((value, inputRange, outputRange, options) => {
|
||||||
|
const [inMin, inMax] = inputRange;
|
||||||
|
const [outMin, outMax] = outputRange;
|
||||||
|
let progress = (value - inMin) / (inMax - inMin);
|
||||||
|
if (options?.extrapolateLeft === "clamp") {
|
||||||
|
progress = Math.max(0, progress);
|
||||||
|
}
|
||||||
|
if (options?.extrapolateRight === "clamp") {
|
||||||
|
progress = Math.min(1, progress);
|
||||||
|
}
|
||||||
|
return outMin + progress * (outMax - outMin);
|
||||||
|
}),
|
||||||
|
spring: vi.fn(() => 1),
|
||||||
|
AbsoluteFill: vi.fn(({ children, className, style }) => (
|
||||||
|
<div data-testid="absolute-fill" className={className} style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
Img: vi.fn(({ src, className, style }) => (
|
||||||
|
<img data-testid="remotion-img" src={src} className={className} style={style} />
|
||||||
|
)),
|
||||||
|
staticFile: vi.fn((path: string) => `/static/${path}`),
|
||||||
|
Sequence: vi.fn(({ children, from, durationInFrames, premountFor }) => (
|
||||||
|
<div
|
||||||
|
data-testid="sequence"
|
||||||
|
data-from={from}
|
||||||
|
data-duration={durationInFrames}
|
||||||
|
data-premount={premountFor}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
Easing: {
|
||||||
|
out: vi.fn((fn) => fn),
|
||||||
|
cubic: vi.fn((t: number) => t),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock @remotion/media
|
||||||
|
vi.mock("@remotion/media", () => ({
|
||||||
|
Audio: vi.fn(({ src, volume, loop }) => (
|
||||||
|
<audio
|
||||||
|
data-testid="audio"
|
||||||
|
src={src}
|
||||||
|
data-volume={volume}
|
||||||
|
data-loop={loop ? "true" : "false"}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
/* eslint-enable @remotion/warn-native-media-tag */
|
||||||
|
|
||||||
|
// Mock all scene components
|
||||||
|
vi.mock("./scenes/portal/PortalIntroScene", () => ({
|
||||||
|
PortalIntroScene: vi.fn(() => (
|
||||||
|
<div data-testid="portal-intro-scene">PortalIntroScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/PortalTitleScene", () => ({
|
||||||
|
PortalTitleScene: vi.fn(() => (
|
||||||
|
<div data-testid="portal-title-scene">PortalTitleScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/DashboardOverviewScene", () => ({
|
||||||
|
DashboardOverviewScene: vi.fn(() => (
|
||||||
|
<div data-testid="dashboard-overview-scene">DashboardOverviewScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/MeetupShowcaseScene", () => ({
|
||||||
|
MeetupShowcaseScene: vi.fn(() => (
|
||||||
|
<div data-testid="meetup-showcase-scene">MeetupShowcaseScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/TopMeetupsScene", () => ({
|
||||||
|
TopMeetupsScene: vi.fn(() => (
|
||||||
|
<div data-testid="top-meetups-scene">TopMeetupsScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/ActivityFeedScene", () => ({
|
||||||
|
ActivityFeedScene: vi.fn(() => (
|
||||||
|
<div data-testid="activity-feed-scene">ActivityFeedScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/CallToActionScene", () => ({
|
||||||
|
CallToActionScene: vi.fn(() => (
|
||||||
|
<div data-testid="call-to-action-scene">CallToActionScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./scenes/portal/PortalOutroScene", () => ({
|
||||||
|
PortalOutroScene: vi.fn(() => (
|
||||||
|
<div data-testid="portal-outro-scene">PortalOutroScene</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* eslint-disable @remotion/warn-native-media-tag, @remotion/no-string-assets */
|
||||||
|
// Mock PortalAudioManager to verify it's rendered
|
||||||
|
vi.mock("./components/PortalAudioManager", () => ({
|
||||||
|
PortalAudioManager: vi.fn(() => (
|
||||||
|
<div data-testid="portal-audio-manager">
|
||||||
|
<audio
|
||||||
|
data-testid="background-music"
|
||||||
|
src="/static/music/background-music.mp3"
|
||||||
|
data-volume="0.25"
|
||||||
|
data-loop="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
/* eslint-enable @remotion/warn-native-media-tag, @remotion/no-string-assets */
|
||||||
|
|
||||||
|
// Mock fonts
|
||||||
|
vi.mock("./fonts/inconsolata", () => ({
|
||||||
|
inconsolataFont: "Inconsolata, monospace",
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("PortalPresentationMobile", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders without errors", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
expect(container).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the AbsoluteFill container with correct classes", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const absoluteFill = container.querySelector('[data-testid="absolute-fill"]');
|
||||||
|
expect(absoluteFill).toBeInTheDocument();
|
||||||
|
expect(absoluteFill).toHaveClass("bg-gradient-to-br");
|
||||||
|
expect(absoluteFill).toHaveClass("from-zinc-900");
|
||||||
|
expect(absoluteFill).toHaveClass("to-zinc-800");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the PortalAudioManager for background music", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const audioManager = container.querySelector('[data-testid="portal-audio-manager"]');
|
||||||
|
expect(audioManager).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders background music audio element within PortalAudioManager", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const backgroundMusic = container.querySelector('[data-testid="background-music"]');
|
||||||
|
expect(backgroundMusic).toBeInTheDocument();
|
||||||
|
expect(backgroundMusic?.getAttribute("src")).toContain("background-music.mp3");
|
||||||
|
expect(backgroundMusic?.getAttribute("data-loop")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the wallpaper background image", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const images = container.querySelectorAll('[data-testid="remotion-img"]');
|
||||||
|
const wallpaper = Array.from(images).find((img) =>
|
||||||
|
img.getAttribute("src")?.includes("einundzwanzig-wallpaper.png")
|
||||||
|
);
|
||||||
|
expect(wallpaper).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders all 9 scene sequences", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
expect(sequences.length).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 1: PortalIntroScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="portal-intro-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 2: PortalTitleScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="portal-title-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 3: DashboardOverviewScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="dashboard-overview-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 4: MeetupShowcaseScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="meetup-showcase-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 6: TopMeetupsScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="top-meetups-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 7: ActivityFeedScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="activity-feed-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 8: CallToActionScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="call-to-action-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 9: PortalOutroScene", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const scene = container.querySelector('[data-testid="portal-outro-scene"]');
|
||||||
|
expect(scene).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders sequences with correct durations totaling 90 seconds", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
const durations = Array.from(sequences).map((seq) =>
|
||||||
|
parseInt(seq.getAttribute("data-duration") || "0", 10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 90 seconds * 30fps = 2700 frames total
|
||||||
|
const totalDuration = durations.reduce((sum, d) => sum + d, 0);
|
||||||
|
expect(totalDuration).toBe(2700);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 1 (Logo Reveal) with 6 second duration (180 frames)", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
const firstSequence = sequences[0];
|
||||||
|
expect(firstSequence?.getAttribute("data-duration")).toBe("180");
|
||||||
|
expect(firstSequence?.getAttribute("data-from")).toBe("0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders Scene 9 (Outro) with 12 second duration (360 frames)", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
const lastSequence = sequences[sequences.length - 1];
|
||||||
|
expect(lastSequence?.getAttribute("data-duration")).toBe("360");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies Inconsolata font family to the composition", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const absoluteFill = container.querySelector('[data-testid="absolute-fill"]');
|
||||||
|
expect(absoluteFill?.getAttribute("style")).toContain("Inconsolata");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PortalPresentationMobile audio integration", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("integrates PortalAudioManager at the composition level", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const audioManager = container.querySelector('[data-testid="portal-audio-manager"]');
|
||||||
|
|
||||||
|
// PortalAudioManager should be present as a direct child of AbsoluteFill
|
||||||
|
expect(audioManager).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ensures background music is set to loop", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const backgroundMusic = container.querySelector('[data-testid="background-music"]');
|
||||||
|
expect(backgroundMusic?.getAttribute("data-loop")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ensures background music uses correct file path", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const backgroundMusic = container.querySelector('[data-testid="background-music"]');
|
||||||
|
expect(backgroundMusic?.getAttribute("src")).toBe("/static/music/background-music.mp3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders audio manager before scene sequences in DOM order", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const absoluteFill = container.querySelector('[data-testid="absolute-fill"]');
|
||||||
|
const children = absoluteFill?.children;
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
// Audio manager should be one of the first elements (before scenes)
|
||||||
|
const childArray = Array.from(children);
|
||||||
|
const audioManagerIndex = childArray.findIndex(
|
||||||
|
(el) => el.getAttribute("data-testid") === "portal-audio-manager"
|
||||||
|
);
|
||||||
|
expect(audioManagerIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(audioManagerIndex).toBeLessThan(3); // Should be in first few elements
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PortalPresentationMobile scene timing", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sequences scenes in correct order with proper timing", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
|
||||||
|
// Expected scene timings (at 30fps):
|
||||||
|
// Scene 1: 0-180 (6s)
|
||||||
|
// Scene 2: 180-300 (4s)
|
||||||
|
// Scene 3: 300-660 (12s)
|
||||||
|
// Scene 4: 660-1020 (12s)
|
||||||
|
// Scene 5: 1020-1380 (12s)
|
||||||
|
// Scene 6: 1380-1680 (10s)
|
||||||
|
// Scene 7: 1680-1980 (10s)
|
||||||
|
// Scene 8: 1980-2340 (12s)
|
||||||
|
// Scene 9: 2340-2700 (12s)
|
||||||
|
|
||||||
|
const expectedFromValues = [0, 180, 300, 660, 1020, 1380, 1680, 1980, 2340];
|
||||||
|
|
||||||
|
sequences.forEach((seq, index) => {
|
||||||
|
const fromValue = parseInt(seq.getAttribute("data-from") || "0", 10);
|
||||||
|
expect(fromValue).toBe(expectedFromValues[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("all sequences have premountFor set to 1 second (30 frames)", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
|
||||||
|
sequences.forEach((seq) => {
|
||||||
|
const premount = seq.getAttribute("data-premount");
|
||||||
|
expect(premount).toBe("30");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PortalPresentationMobile mobile-specific", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is configured for mobile dimensions in the mock", () => {
|
||||||
|
// Verify the mock configuration is set for mobile dimensions
|
||||||
|
// The actual composition receives 1080x1920 from Root.tsx
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
expect(container).toBeInTheDocument();
|
||||||
|
// The mock useVideoConfig returns width: 1080, height: 1920
|
||||||
|
// This test verifies the component renders correctly with mobile mock config
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has same scene structure as desktop version", () => {
|
||||||
|
const { container } = render(<PortalPresentationMobile />);
|
||||||
|
const sequences = container.querySelectorAll('[data-testid="sequence"]');
|
||||||
|
|
||||||
|
// Same 9 scenes as desktop
|
||||||
|
expect(sequences.length).toBe(9);
|
||||||
|
|
||||||
|
// Same scene components are rendered
|
||||||
|
expect(container.querySelector('[data-testid="portal-intro-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="portal-title-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="dashboard-overview-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="meetup-showcase-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="top-meetups-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="activity-feed-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="call-to-action-scene"]')).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="portal-outro-scene"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
238
videos/src/PortalPresentationMobile.tsx
Normal file
238
videos/src/PortalPresentationMobile.tsx
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import { AbsoluteFill, Sequence, useVideoConfig, Img, staticFile } from "remotion";
|
||||||
|
import { inconsolataFont } from "./fonts/inconsolata";
|
||||||
|
import { PortalIntroScene } from "./scenes/portal/PortalIntroScene";
|
||||||
|
import { PortalTitleScene } from "./scenes/portal/PortalTitleScene";
|
||||||
|
import { DashboardOverviewScene } from "./scenes/portal/DashboardOverviewScene";
|
||||||
|
import { MeetupShowcaseScene } from "./scenes/portal/MeetupShowcaseScene";
|
||||||
|
import { TopMeetupsScene } from "./scenes/portal/TopMeetupsScene";
|
||||||
|
import { ActivityFeedScene } from "./scenes/portal/ActivityFeedScene";
|
||||||
|
import { CallToActionScene } from "./scenes/portal/CallToActionScene";
|
||||||
|
import { PortalOutroScene } from "./scenes/portal/PortalOutroScene";
|
||||||
|
import { PortalAudioManager } from "./components/PortalAudioManager";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PortalPresentationMobile - Mobile composition for the Einundzwanzig Portal presentation video
|
||||||
|
*
|
||||||
|
* Resolution: 1080x1920 (9:16 portrait)
|
||||||
|
*
|
||||||
|
* Scene Structure (90 seconds total @ 30fps = 2700 frames):
|
||||||
|
* 1. Logo Reveal (6s) - Frames 0-180
|
||||||
|
* 2. Portal Title (4s) - Frames 180-300
|
||||||
|
* 3. Dashboard Overview (12s) - Frames 300-660
|
||||||
|
* 4. Meine Meetups (12s) - Frames 660-1020
|
||||||
|
* 5. Top Länder (12s) - Frames 1020-1380
|
||||||
|
* 6. Top Meetups (10s) - Frames 1380-1680
|
||||||
|
* 7. Activity Feed (10s) - Frames 1680-1980
|
||||||
|
* 8. Call to Action (12s) - Frames 1980-2340
|
||||||
|
* 9. Outro (12s) - Frames 2340-2700
|
||||||
|
*/
|
||||||
|
export const PortalPresentationMobile: React.FC = () => {
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
// Scene durations in seconds (same as desktop)
|
||||||
|
const SCENE_DURATIONS = {
|
||||||
|
logoReveal: 6,
|
||||||
|
portalTitle: 4,
|
||||||
|
dashboardOverview: 12,
|
||||||
|
meineMeetups: 12,
|
||||||
|
topLaender: 12,
|
||||||
|
topMeetups: 10,
|
||||||
|
activityFeed: 10,
|
||||||
|
callToAction: 12,
|
||||||
|
outro: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate frame positions for each scene
|
||||||
|
const sceneFrames = {
|
||||||
|
logoReveal: { from: 0, duration: SCENE_DURATIONS.logoReveal * fps },
|
||||||
|
portalTitle: {
|
||||||
|
from: SCENE_DURATIONS.logoReveal * fps,
|
||||||
|
duration: SCENE_DURATIONS.portalTitle * fps,
|
||||||
|
},
|
||||||
|
dashboardOverview: {
|
||||||
|
from: (SCENE_DURATIONS.logoReveal + SCENE_DURATIONS.portalTitle) * fps,
|
||||||
|
duration: SCENE_DURATIONS.dashboardOverview * fps,
|
||||||
|
},
|
||||||
|
meineMeetups: {
|
||||||
|
from:
|
||||||
|
(SCENE_DURATIONS.logoReveal +
|
||||||
|
SCENE_DURATIONS.portalTitle +
|
||||||
|
SCENE_DURATIONS.dashboardOverview) *
|
||||||
|
fps,
|
||||||
|
duration: SCENE_DURATIONS.meineMeetups * fps,
|
||||||
|
},
|
||||||
|
topLaender: {
|
||||||
|
from:
|
||||||
|
(SCENE_DURATIONS.logoReveal +
|
||||||
|
SCENE_DURATIONS.portalTitle +
|
||||||
|
SCENE_DURATIONS.dashboardOverview +
|
||||||
|
SCENE_DURATIONS.meineMeetups) *
|
||||||
|
fps,
|
||||||
|
duration: SCENE_DURATIONS.topLaender * fps,
|
||||||
|
},
|
||||||
|
topMeetups: {
|
||||||
|
from:
|
||||||
|
(SCENE_DURATIONS.logoReveal +
|
||||||
|
SCENE_DURATIONS.portalTitle +
|
||||||
|
SCENE_DURATIONS.dashboardOverview +
|
||||||
|
SCENE_DURATIONS.meineMeetups +
|
||||||
|
SCENE_DURATIONS.topLaender) *
|
||||||
|
fps,
|
||||||
|
duration: SCENE_DURATIONS.topMeetups * fps,
|
||||||
|
},
|
||||||
|
activityFeed: {
|
||||||
|
from:
|
||||||
|
(SCENE_DURATIONS.logoReveal +
|
||||||
|
SCENE_DURATIONS.portalTitle +
|
||||||
|
SCENE_DURATIONS.dashboardOverview +
|
||||||
|
SCENE_DURATIONS.meineMeetups +
|
||||||
|
SCENE_DURATIONS.topLaender +
|
||||||
|
SCENE_DURATIONS.topMeetups) *
|
||||||
|
fps,
|
||||||
|
duration: SCENE_DURATIONS.activityFeed * fps,
|
||||||
|
},
|
||||||
|
callToAction: {
|
||||||
|
from:
|
||||||
|
(SCENE_DURATIONS.logoReveal +
|
||||||
|
SCENE_DURATIONS.portalTitle +
|
||||||
|
SCENE_DURATIONS.dashboardOverview +
|
||||||
|
SCENE_DURATIONS.meineMeetups +
|
||||||
|
SCENE_DURATIONS.topLaender +
|
||||||
|
SCENE_DURATIONS.topMeetups +
|
||||||
|
SCENE_DURATIONS.activityFeed) *
|
||||||
|
fps,
|
||||||
|
duration: SCENE_DURATIONS.callToAction * fps,
|
||||||
|
},
|
||||||
|
outro: {
|
||||||
|
from:
|
||||||
|
(SCENE_DURATIONS.logoReveal +
|
||||||
|
SCENE_DURATIONS.portalTitle +
|
||||||
|
SCENE_DURATIONS.dashboardOverview +
|
||||||
|
SCENE_DURATIONS.meineMeetups +
|
||||||
|
SCENE_DURATIONS.topLaender +
|
||||||
|
SCENE_DURATIONS.topMeetups +
|
||||||
|
SCENE_DURATIONS.activityFeed +
|
||||||
|
SCENE_DURATIONS.callToAction) *
|
||||||
|
fps,
|
||||||
|
duration: SCENE_DURATIONS.outro * fps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill
|
||||||
|
className="bg-gradient-to-br from-zinc-900 to-zinc-800"
|
||||||
|
style={{ fontFamily: inconsolataFont }}
|
||||||
|
>
|
||||||
|
{/* Background Music with fade in/out */}
|
||||||
|
<PortalAudioManager />
|
||||||
|
|
||||||
|
{/* Wallpaper Background */}
|
||||||
|
<Img
|
||||||
|
src={staticFile("einundzwanzig-wallpaper.png")}
|
||||||
|
className="absolute inset-0 w-full h-full object-cover opacity-20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Scene 1: Logo Reveal (6s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.logoReveal.from}
|
||||||
|
durationInFrames={sceneFrames.logoReveal.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<PortalIntroScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 2: Portal Title (4s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.portalTitle.from}
|
||||||
|
durationInFrames={sceneFrames.portalTitle.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<PortalTitleScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 3: Dashboard Overview (12s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.dashboardOverview.from}
|
||||||
|
durationInFrames={sceneFrames.dashboardOverview.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<DashboardOverviewScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 4: Meine Meetups (12s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.meineMeetups.from}
|
||||||
|
durationInFrames={sceneFrames.meineMeetups.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<MeetupShowcaseScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 5: Top Länder (12s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.topLaender.from}
|
||||||
|
durationInFrames={sceneFrames.topLaender.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<PlaceholderScene name="Top Länder" sceneNumber={5} />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 6: Top Meetups (10s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.topMeetups.from}
|
||||||
|
durationInFrames={sceneFrames.topMeetups.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<TopMeetupsScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 7: Activity Feed (10s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.activityFeed.from}
|
||||||
|
durationInFrames={sceneFrames.activityFeed.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<ActivityFeedScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 8: Call to Action (12s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.callToAction.from}
|
||||||
|
durationInFrames={sceneFrames.callToAction.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<CallToActionScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Scene 9: Outro (12s) */}
|
||||||
|
<Sequence
|
||||||
|
from={sceneFrames.outro.from}
|
||||||
|
durationInFrames={sceneFrames.outro.duration}
|
||||||
|
premountFor={fps}
|
||||||
|
>
|
||||||
|
<PortalOutroScene />
|
||||||
|
</Sequence>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder component for scenes that haven't been implemented yet.
|
||||||
|
* Displays a centered scene name with visual indicators.
|
||||||
|
*/
|
||||||
|
const PlaceholderScene: React.FC<{ name: string; sceneNumber: number }> = ({
|
||||||
|
name,
|
||||||
|
sceneNumber,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill className="flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-orange-500/20 border-2 border-orange-500/50 flex items-center justify-center">
|
||||||
|
<span className="text-4xl font-bold text-orange-500">{sceneNumber}</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl font-bold text-white mb-2">{name}</h2>
|
||||||
|
<p className="text-lg text-neutral-400">Scene placeholder</p>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { MyComposition } from "./Composition";
|
|||||||
import { Nip05Tutorial } from "./Nip05Tutorial";
|
import { Nip05Tutorial } from "./Nip05Tutorial";
|
||||||
import { Nip05TutorialMobile } from "./Nip05TutorialMobile";
|
import { Nip05TutorialMobile } from "./Nip05TutorialMobile";
|
||||||
import { PortalPresentation } from "./PortalPresentation";
|
import { PortalPresentation } from "./PortalPresentation";
|
||||||
|
import { PortalPresentationMobile } from "./PortalPresentationMobile";
|
||||||
|
|
||||||
export const RemotionRoot: React.FC = () => {
|
export const RemotionRoot: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -43,6 +44,14 @@ export const RemotionRoot: React.FC = () => {
|
|||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
/>
|
/>
|
||||||
|
<Composition
|
||||||
|
id="PortalPresentationMobile"
|
||||||
|
component={PortalPresentationMobile}
|
||||||
|
durationInFrames={90 * 30}
|
||||||
|
fps={30}
|
||||||
|
width={1080}
|
||||||
|
height={1920}
|
||||||
|
/>
|
||||||
</Folder>
|
</Folder>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user