youtube/remotion/src/baukasten/buch.tsx
2026-11-04 20:23:28 +00:00

164 lines
6.2 KiB
TypeScript

import React from 'react';
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
import {kurve} from '../bewegung';
import {Figur, Kamera} from './figur';
import {Blende, Vignette, kamFahrt} from './helfer';
import {Blatt} from './natur';
import {W} from './sprache';
import {SCHRIFT, Untertitel} from './untertitel';
import {plane, type Plan, type Zeile} from './zeit';
// Das Bilderbuch: öffnet sich am Anfang (Intro) und klappt am Ende zu.
// Buchrücken bei x = 960; geschlossen liegt der Deckel rechts davon.
const OBEN = 190;
const HOEHE = 700;
const BREITE = 700;
const Seite: React.FC<{links: number; children?: React.ReactNode}> = ({links, children}) => (
<div
style={{
position: 'absolute',
left: links,
top: OBEN,
width: BREITE,
height: HOEHE,
background: 'linear-gradient(90deg, #efe3c9 0%, #fbf4e4 12%, #fbf4e4 88%, #f1e6cd 100%)',
boxShadow: '0 10px 40px rgba(60,40,20,0.25)',
overflow: 'hidden',
}}
>
{children}
</div>
);
const Titel: React.FC<{text: string; groesse: number; farbe?: string; y: number}> = ({text, groesse, farbe = '#c8642c', y}) => (
<div style={{position: 'absolute', left: 0, right: 0, top: y, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: groesse, lineHeight: 1.05, whiteSpace: 'pre-line', color: farbe}}>{text}</div>
);
// Deckel: dreht sich um den Buchrücken (0° = zu, -180° = offen)
const Deckel: React.FC<{winkel: number}> = ({winkel}) => (
<div style={{position: 'absolute', left: 960, top: OBEN - 14, width: BREITE + 16, height: HOEHE + 28, transformOrigin: '0 50%', transform: `perspective(2600px) rotateY(${winkel}deg)`, transformStyle: 'preserve-3d'}}>
<div
style={{
position: 'absolute',
inset: 0,
backfaceVisibility: 'hidden',
borderRadius: '6px 18px 18px 6px',
background: 'radial-gradient(ellipse at 40% 30%, #74a05c, #4f7a3e 70%, #3f6532)',
boxShadow: 'inset 14px 0 18px rgba(0,0,0,0.25), 0 14px 40px rgba(40,30,20,0.35)',
}}
>
<div style={{position: 'absolute', left: 40, right: 40, top: 40, bottom: 40, border: '6px solid #e9c97a', borderRadius: 14}} />
<Titel text={W.kanalZeilen.join('\n')} groesse={W.kanalZeilen.length > 1 ? 72 : 84} farbe="#f6dc95" y={W.kanalZeilen.length > 1 ? 92 : 120} />
<div style={{position: 'absolute', left: 0, right: 0, top: 260, height: 360}}>
<Figur pose="pino_A3_lachen" x={250} y={330} hoehe={120} schatten={false} />
<Figur pose="noa_A3_lachen" x={370} y={330} hoehe={250} schatten={false} />
<Figur pose="lulu_A3_lachen" x={520} y={330} hoehe={330} schatten={false} />
</div>
</div>
<div style={{position: 'absolute', inset: 0, backfaceVisibility: 'hidden', transform: 'rotateY(180deg)', background: '#f1e6cd', borderRadius: '18px 6px 6px 18px'}}>
<div style={{position: 'absolute', inset: 14, background: 'linear-gradient(270deg, #efe3c9 0%, #fbf4e4 12%)', overflow: 'hidden'}}>
{[0, 1, 2, 3, 4, 5].map((i) => (
<Blatt key={i} x={120 + i * 95} y={600 + Math.sin(i * 2.1) * 40} r={i * 47} s={0.9} farbe={['#c8642c', '#d99a2b', '#a8452a'][i % 3]} />
))}
<Titel text={W.kanalZeilen.join('\n')} groesse={70} y={W.kanalZeilen.length > 1 ? 180 : 230} />
<Titel text={W.buchUntertitel} groesse={34} farbe="#8a6a4a" y={W.kanalZeilen.length > 1 ? 365 : 340} />
</div>
</div>
</div>
);
const Innenseiten: React.FC<{frame: number; auf: number; titel: string; posen: [string, string, string]}> = ({frame, auf, titel, posen}) => {
const {fps} = useVideoConfig();
const pop = (i: number) => spring({frame: frame - auf - 8 - i * 6, fps, config: {damping: 11, stiffness: 120}});
return (
<>
<Seite links={960}>
<Titel text={titel} groesse={68} y={60} farbe="#3f5a8a" />
{posen.map((p, i) => {
const s = pop(i);
const x = [180, 340, 520][i];
const h = [96, 230, 440][i];
return <Figur key={p} pose={p} x={x} y={650} hoehe={h} stauchY={s} stauchX={0.6 + 0.4 * s} deckkraft={Math.min(1, s * 2)} />;
})}
</Seite>
</>
);
};
const Tisch: React.FC = () => (
<div style={{position: 'absolute', inset: -400, background: 'radial-gradient(ellipse at 50% 45%, #e9cfa4 0%, #c99a63 60%, #a87a4a 100%)'}} />
);
// Intro: Buch geht auf, Figuren springen auf die Seite, Kamera fährt in die Seite.
// Die Folge plant die Zeilen selbst (mit `introZeilen`), damit Aufnahmen greifen.
export const introZeilen = (folgenTitel: string): Zeile[] => [
{id: 'buch', dauer: 2.2},
{id: 't1', wer: 'ERZ', text: `${W.kanal}.`},
{id: 't2', wer: 'ERZ', text: W.heute(folgenTitel)},
];
export const INTRO_OPTIONEN = {vorlauf: 0.3, nachlauf: 1.6};
type IntroProps = {plan: Plan; titel: string; posen?: [string, string, string]};
export const Intro: React.FC<IntroProps> = ({plan, titel, posen = ['pino_A3_lachen', 'noa_A3_lachen', 'lulu_A3_lachen']}) => {
const frame = useCurrentFrame();
const {z, dauer} = plan;
const auf = 30;
const winkel = kurve(frame, [
[auf, 0],
[auf + 40, -180],
]);
const kam = kamFahrt(frame, [
[0, 1310, 540, 1.25],
[auf, 1310, 540, 1.25],
[auf + 45, 960, 540, 1.05],
[z.t2.ende, 960, 540, 1.08],
[dauer, 1310, 520, 2.6],
]);
return (
<>
<Kamera {...kam} frei>
<Tisch />
{winkel < -90 && <Innenseiten frame={frame} auf={auf + 30} titel={titel} posen={posen} />}
{winkel >= -90 && <Seite links={960} />}
<Deckel winkel={winkel} />
</Kamera>
<Vignette />
<Untertitel zeilen={plan.liste} />
<Blende dauer={dauer} rein={12} raus={14} />
</>
);
};
// Outro: Seite mit winkenden Figuren, Buch klappt zu.
export const OUTRO = plane([{id: 'buch', dauer: 4.5}], {vorlauf: 0.2, nachlauf: 0.6});
export const Outro: React.FC<{posen?: [string, string, string]}> = ({posen = ['pino_B3_schlafen', 'noa_A_arm-hoch-winken', 'lulu_A3_lachen']}) => {
const frame = useCurrentFrame();
const {dauer} = OUTRO;
const zu = 60;
const winkel = kurve(frame, [
[zu, -180],
[zu + 40, 0],
]);
const kam = kamFahrt(frame, [
[0, 1310, 520, 2.4],
[25, 960, 540, 1.05],
[zu + 20, 1150, 540, 1.1],
[dauer, 1310, 540, 1.2],
]);
return (
<>
<Kamera {...kam} frei>
<Tisch />
{winkel < -90 && <Innenseiten frame={frame} auf={-60} titel={W.bisBald} posen={posen} />}
{winkel >= -90 && <Seite links={960} />}
<Deckel winkel={winkel} />
</Kamera>
<Vignette />
<Blende dauer={dauer} rein={10} raus={20} farbe="#1c1a24" />
</>
);
};