106 lines
4.6 KiB
TypeScript
106 lines
4.6 KiB
TypeScript
import React from 'react';
|
||
import {AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig} from 'remotion';
|
||
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
|
||
import type {Geplant} from '../baukasten/zeit';
|
||
import {TEXTE} from './plan';
|
||
|
||
// Lern-Shorts im Hochformat (1080 × 1920). Die Szene spielt auf der gewohnten Bühne (1920 × 1080,
|
||
// Figuren und Orte wie in den Folgen); `Buehne` schneidet daraus den Hochformat-Ausschnitt.
|
||
// Frage oben, Untertitel über dem Bereich, den YouTube unten mit Titel und Knöpfen verdeckt.
|
||
|
||
export const W = 1080;
|
||
export const H = 1920;
|
||
export const VOLL = H / 1080; // kleinste Vergrößerung, bei der die Bühne das Hochformat füllt
|
||
|
||
export const Buehne: React.FC<{x: number; y?: number; z?: number; children: React.ReactNode}> = ({x, y = 540, z = VOLL, children}) => {
|
||
const halbB = W / 2 / z;
|
||
const halbH = H / 2 / z;
|
||
// Ist der Ausschnitt größer als die Bühne, bleibt sie mittig (oben/unten Papierrand)
|
||
const bx = halbB * 2 >= 1920 ? 960 : Math.min(1920 - halbB, Math.max(halbB, x));
|
||
const by = halbH * 2 >= 1080 ? 540 : Math.min(1080 - halbH, Math.max(halbH, y));
|
||
return (
|
||
<div style={{position: 'absolute', width: 1920, height: 1080, transformOrigin: '0 0', transform: `translate(${W / 2}px, ${H / 2}px) scale(${z}) translate(${-bx}px, ${-by}px)`}}>
|
||
{children}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const KARTE = {
|
||
background: 'rgba(255, 250, 240, 0.92)',
|
||
borderRadius: 40,
|
||
boxShadow: '0 8px 30px rgba(60, 40, 20, 0.22)',
|
||
fontFamily: SCHRIFT,
|
||
textAlign: 'center',
|
||
} as const;
|
||
|
||
// Frage oben, darunter klein der Kanalname
|
||
export const FrageKarte: React.FC<{text: string}> = ({text}) => {
|
||
const f = useCurrentFrame();
|
||
const {fps} = useVideoConfig();
|
||
const s = spring({frame: f - 4, fps, config: {damping: 14}});
|
||
return (
|
||
<div style={{position: 'absolute', top: 150, left: 60, right: 60, display: 'flex', justifyContent: 'center', transform: `scale(${0.8 + 0.2 * s})`, opacity: s}}>
|
||
<div style={{...KARTE, padding: '26px 44px 22px'}}>
|
||
<div style={{fontSize: 74, fontWeight: 700, color: '#3f5a8a', lineHeight: 1.1}}>{text}</div>
|
||
<div style={{fontSize: 32, fontWeight: 600, color: '#c8502c', marginTop: 8}}>{TEXTE.zeilen.kanal}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Untertitel der Folgen, nach oben versetzt (unten liegen im Shorts-Player Titel und Knöpfe);
|
||
// das Mitmach-„?“ erscheint unter der Frage-Karte statt darauf
|
||
export const HochUntertitel: React.FC<{zeilen: Geplant[]}> = ({zeilen}) => (
|
||
<div style={{position: 'absolute', left: 40, right: 40, top: 380, bottom: 340}}>
|
||
<Untertitel zeilen={zeilen} />
|
||
</div>
|
||
);
|
||
|
||
// Runde Bildkarte, die bei `ab` aufploppt (Bildschirm-Koordinaten des Hochformats)
|
||
export const BildKarte: React.FC<{x: number; y: number; ab: number; r?: number; aus?: number; children: React.ReactNode}> = ({x, y, ab, r = 150, aus = 1e9, children}) => {
|
||
const f = useCurrentFrame();
|
||
const {fps} = useVideoConfig();
|
||
const s = spring({frame: f - ab, fps, config: {damping: 11}});
|
||
const weg = interpolate(f, [aus, aus + 10], [1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'});
|
||
if (f < ab) return null;
|
||
return (
|
||
<div style={{position: 'absolute', left: x - r, top: y - r, width: 2 * r, height: 2 * r, ...KARTE, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', transform: `scale(${s * weg})`}}>
|
||
{children}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Großes Wort zum Mitsagen (klein – mittel – groß)
|
||
// `puls`: Zeitpunkte, an denen das Wort noch einmal kurz groß wird (beim Mitsagen)
|
||
export const Wort: React.FC<{text: string; x: number; y: number; ab: number; puls?: number[]; groesse?: number; farbe?: string}> = ({text, x, y, ab, puls = [], groesse = 90, farbe = '#c8502c'}) => {
|
||
const f = useCurrentFrame();
|
||
const {fps} = useVideoConfig();
|
||
if (f < ab) return null;
|
||
const pulsen = puls.reduce((m, p) => m + 0.35 * Math.max(0, Math.sin(Math.PI * Math.min(1, Math.max(0, (f - p) / 18)))), 0);
|
||
const s = spring({frame: f - ab, fps, config: {damping: 10}}) + pulsen;
|
||
return (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: x - 300,
|
||
width: 600,
|
||
top: y - groesse / 2,
|
||
textAlign: 'center',
|
||
fontFamily: SCHRIFT,
|
||
fontWeight: 700,
|
||
fontSize: groesse,
|
||
color: farbe,
|
||
WebkitTextStroke: '10px #fbf4e4',
|
||
paintOrder: 'stroke fill',
|
||
transform: `scale(${s})`,
|
||
}}
|
||
>
|
||
{text}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// `hintergrund`: füllt oben und unten, wenn die Bühne das Hochformat nicht ganz ausfüllt
|
||
export const Hochformat: React.FC<{hintergrund?: string; children: React.ReactNode}> = ({hintergrund = '#f4ead8', children}) => (
|
||
<AbsoluteFill style={{background: hintergrund, overflow: 'hidden'}}>{children}</AbsoluteFill>
|
||
);
|