youtube/remotion/src/baukasten/folge.tsx
Lara 69990b6c0f Stand 03.11.2026: Komponist, sechs Lieder-Entwürfe, Suno-Stimme und Auswahl, Remotion-Arbeiten
- Neuer Mitarbeiter Komponist (.claude/agents/komponist.md)
- Liederbuch: Plan + Lieder 1, 2, 3, 4, 5, 12 (drehbuecher/lieder/)
- Suno: feste Stimme aus dem Favoriten, Stilvorlage, ausgewählte Fassungen (suno-auswahl.md)
- Suno-Lizenzantwort für Noa in der Mitte (wissen/)
- Werkzeuge für Melodie-Skizzen und Leseseite
- Laufende Remotion-, Stimmen- und Figurenarbeiten des Tages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MyLus898ZL9jjJJDvvxa8
2026-11-03 21:29:39 +01:00

110 lines
3.8 KiB
TypeScript

import React from 'react';
import {AbsoluteFill, Composition, Series, getInputProps, useCurrentFrame} from 'remotion';
import {Musik, type MusikEintrag} from './musik';
import {SCHRIFT} from './untertitel';
import type {Geplant} from './zeit';
// Eine Folge = Liste von Szenen. Daraus entstehen die Komposition der ganzen
// Folge und je Szene eine eigene Komposition (zum einzelnen Rendern):
// F02 ganze Folge
// F02-03-... nur Szene 3
// Mit --props='{"entwurf": true}' blendet jede Szene oben links Szene, Zeit
// und Zeilen-ID ein, damit Rückmeldungen genau sagen können, wo etwas ist.
export type SzenenEintrag = {
name: string;
datei: string; // Quelldatei der Szene (für werkzeuge/render.mjs)
plan: {dauer: number; liste: Geplant[]};
K: React.FC;
musik?: MusikEintrag; // Hintergrundmusik der Szene (siehe baukasten/musik.tsx)
};
const BILD = {fps: 30, width: 1920, height: 1080} as const;
const zeit = (f: number) => {
const s = Math.floor(f / 30);
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
};
export const kennung = (kuerzel: string, i: number, name: string) =>
`${kuerzel}-${String(i + 1).padStart(2, '0')}-${name
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/[^A-Za-z0-9]+/g, '-')
.replace(/^-|-$/g, '')}`;
const Entwurf: React.FC<{szenen: SzenenEintrag[]; index: number; versatz: number}> = ({szenen, index, versatz}) => {
const f = useCurrentFrame();
const sz = szenen[index];
const zeile = sz.plan.liste.find((l) => f >= l.start && f < l.frei + 10);
return (
<div
style={{
position: 'absolute',
left: 16,
top: 14,
padding: '6px 14px',
borderRadius: 10,
background: 'rgba(20, 16, 24, 0.72)',
color: '#fff',
fontFamily: SCHRIFT,
fontSize: 26,
fontVariantNumeric: 'tabular-nums',
}}
>
{zeit(versatz + f)} · Szene {index + 1} „{sz.name}“ · {zeit(f)}
{zeile ? ` · ${zeile.id}` : ''}
</div>
);
};
const istEntwurf = () => getInputProps().entwurf === true;
const versatzVon = (szenen: SzenenEintrag[], index: number) => szenen.slice(0, index).reduce((s, x) => s + x.plan.dauer, 0);
export const folgenDauer = (szenen: SzenenEintrag[]) => versatzVon(szenen, szenen.length);
const EinzelSzene: React.FC<{szenen: SzenenEintrag[]; index: number}> = ({szenen, index}) => {
const {K, musik, plan} = szenen[index];
return (
<AbsoluteFill style={{background: '#f4ead8'}}>
<K />
{musik && <Musik {...musik} dauer={plan.dauer} zeilen={plan.liste} />}
{istEntwurf() && <Entwurf szenen={szenen} index={index} versatz={versatzVon(szenen, index)} />}
</AbsoluteFill>
);
};
// Komponenten einmal je Folge bauen (Kompositions-Props müssen ohne
// Funktionen auskommen, darum stecken die Szenen in den Komponenten selbst).
export const baueFolge = (kuerzel: string, szenen: SzenenEintrag[]) => {
const Ganz: React.FC = () => (
<AbsoluteFill style={{background: '#f4ead8'}}>
<Series>
{szenen.map((s, i) => (
<Series.Sequence key={s.name} durationInFrames={s.plan.dauer} name={s.name}>
<EinzelSzene szenen={szenen} index={i} />
</Series.Sequence>
))}
</Series>
</AbsoluteFill>
);
const einzeln = szenen.map((s, i) => {
const E: React.FC = () => <EinzelSzene szenen={szenen} index={i} />;
return {id: kennung(kuerzel, i, s.name), K: E, dauer: s.plan.dauer, datei: s.datei, name: s.name};
});
return {kuerzel, dauer: folgenDauer(szenen), Ganz, einzeln};
};
export type Folge = ReturnType<typeof baueFolge>;
// Alle Kompositionen einer Folge (für Root.tsx)
export const FolgenKompositionen: React.FC<{folge: Folge}> = ({folge}) => (
<>
<Composition id={folge.kuerzel} component={folge.Ganz} durationInFrames={folge.dauer} {...BILD} />
{folge.einzeln.map((e) => (
<Composition key={e.id} id={e.id} component={e.K} durationInFrames={e.dauer} {...BILD} />
))}
</>
);