- 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
110 lines
3.8 KiB
TypeScript
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} />
|
|
))}
|
|
</>
|
|
);
|