diff --git a/remotion/src/lieder/einfach/EinfachesLied.tsx b/remotion/src/lieder/einfach/EinfachesLied.tsx index a1e0b39..e59efd9 100644 --- a/remotion/src/lieder/einfach/EinfachesLied.tsx +++ b/remotion/src/lieder/einfach/EinfachesLied.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {AbsoluteFill, Audio, Sequence, interpolate, staticFile, useCurrentFrame} from 'remotion'; import {INTRO_OPTIONEN, Intro, OUTRO, Outro} from '../../baukasten/buch'; -import {Figur, Kamera} from '../../baukasten/figur'; +import {Figur, Kamera, type Posenfolge} from '../../baukasten/figur'; import {Blende, Vignette, kamFahrt} from '../../baukasten/helfer'; import {BODEN, Ort} from '../../baukasten/orte'; import {SCHRIFT} from '../../baukasten/untertitel'; @@ -21,7 +21,8 @@ export type Bild = { kamera?: {x: number; y: number; z: number}; leuchten?: (Punkt & {farbe: string; r?: number; ab?: number})[]; karte?: React.ReactNode; - noa?: string; + noa?: string | Posenfolge; // Posenfolge mit Frames ab Liedbeginn (z. B. Pose je Wort) + tipps?: number[]; // Sekunden, an denen Noa kurz im Takt nachfedert (Tippen) pino?: string; lulu?: string; luluStreckt?: boolean; @@ -152,7 +153,7 @@ const Garten: React.FC<{daten: LiedDaten; szene: (i: number, z: Zeile) => Bild}> - + m + 0.03 * Math.max(0, 1 - Math.abs(f - s(tp)) / 4), 0)} blende={4} /> {bilder.map((bb, k) => (bb.leuchten ?? []).map((l, j) => ), diff --git a/remotion/src/lieder/lied04/Lied04.tsx b/remotion/src/lieder/lied04/Lied04.tsx index 5e8b5e8..28a4d21 100644 --- a/remotion/src/lieder/lied04/Lied04.tsx +++ b/remotion/src/lieder/lied04/Lied04.tsx @@ -1,29 +1,14 @@ import React from 'react'; -import {Figur} from '../../baukasten/figur'; -import {punkt} from '../../baukasten/helfer'; -import {BODEN} from '../../baukasten/orte'; +import {Figur, type Posenfolge} from '../../baukasten/figur'; import {SPRACHE} from '../../baukasten/sprache'; -import {type Bild, EinfachesLied, type LiedDaten, PLATZ, type Punkt, type Zeile, wortZeit} from '../einfach/EinfachesLied'; +import {type Bild, EinfachesLied, type LiedDaten, PLATZ, type Zeile, wortZeit} from '../einfach/EinfachesLied'; import {DE, ES, FR} from '../einfach/daten/lied04'; // Liedvideo „Wer hat den längsten Hals?“ (Pinos Liederbuch, Lied 4: Körperteile) in DE/ES/FR. -// Refrain: Lulu streckt den Hals. Strophen (Ohren, Hände, Beine, Arme): nah bei Noa, die Stellen leuchten, -// die Karte zeigt eine kleine Noa mit der leuchtenden Stelle. Turbo-Teil (3×): jedes genannte Körperteil -// leuchtet zu seinem Wort, im dritten Durchgang langsam. Drehbuch drehbuecher/lieder/lied04-koerperteile/. +// Refrain: Lulu streckt den Hals, Noa zeigt auf ihn. Strophen (Ohren, Hände, Beine, Arme): nah bei Noa, sie tippt +// auf das Körperteil (Higgsfield-Posen noa_B_tippt-*), die Karte zeigt die Pose klein. Turbo-Teil (3×): zu jedem +// gesungenen Körperteil die passende Pose, im dritten Durchgang langsam. Keine Leuchtkreise (Nutzerin 09.11.). Drehbuch drehbuecher/lieder/lied04-koerperteile/. -// Körperteile in noa_A3_lachen (Leinwand = Bild + 339/156, gemessen) -const TEIL: Record = { - ohren: [[607, 306], [809, 306]], - haende: [[629, 576], [809, 551]], - beine: [[684, 616], [744, 616]], - arme: [[629, 476], [799, 466]], - nase: [[734, 296]], - bauch: [[709, 486]], - hals: [[709, 361]], - fuesse: [[684, 691], [754, 691]], - zeh: [[759, 721]], -}; -const FARBE = '#d9482b'; // kräftiges Rot-Orange: hebt sich von Jacke und Gesicht ab const WOERTER: [RegExp, string][] = [ [/^(ohren|orejas|oreilles)/i, 'ohren'], [/^(h[aä]nde|manos|mains)/i, 'haende'], @@ -36,16 +21,25 @@ const WOERTER: [RegExp, string][] = [ [/^(zeh|deditos|orteils)/i, 'zeh'], ]; const teilVon = (wort: string) => WOERTER.find(([re]) => re.test(wort.replace(/[^\p{L}]/gu, '')))?.[1]; -const anNoa = (teil: string): Punkt[] => TEIL[teil].map(([cx, cy]) => punkt('noa', PLATZ.noa, BODEN, cx, cy)); -// Karte: kleine Noa, die genannte Stelle leuchtet +// Higgsfield-Posen (09.11.2026): Noa tippt wirklich auf das Körperteil. Hände = klatschen, Hals = sie zeigt +// auf Lulus langen Hals, Zeh = wie Füße. +const POSE: Record = { + ohren: 'noa_B_tippt-ohren', + nase: 'noa_B_tippt-nase', + bauch: 'noa_B_tippt-bauch', + beine: 'noa_B_tippt-beine', + arme: 'noa_B_tippt-arme', + fuesse: 'noa_B_tippt-fuesse', + zeh: 'noa_B_tippt-fuesse', + haende: 'noa_A_klatschen', + hals: 'noa_B_zeigen', +}; + +// Karte: kleine Noa in derselben Pose (Bild statt Text) const NoaKarte: React.FC<{teil: string}> = ({teil}) => (
- - {TEIL[teil].map(([cx, cy], k) => { - const p = punkt('noa', 102, 198, cx, cy, 180); - return
; - })} +
); @@ -64,28 +58,32 @@ const lage = (i: number) => { return i - k; }; const NOA_NAH = {x: PLATZ.noa, y: 765, z: 1.75}; // ganze Noa über dem Mitlesetext -const LULU_HALS = punkt('lulu', PLATZ.lulu, BODEN, 322 + 320, 23 + 230); + +const istTipp = (w: string) => /^(tipp|toc)/i.test(w.replace(/[^\p{L}]/gu, '')); const szene = (i: number, z: Zeile): Bild => { const a = z.abschnitt.toLowerCase(); const n = lage(i); const woerter = z.zeile.split(' ').map((w) => w.split('@')[0]); + const tipps = woerter.flatMap((w, k) => (istTipp(w) ? [wortZeit(z, k)] : [])); if (a.startsWith('chorus')) { if (n === 0) return {kamera: {x: PLATZ.lulu - 60, y: 420, z: 1.5}, luluStreckt: true, lulu: 'lulu_A3_lachen'}; - if (n === 1) return {leuchten: [{...LULU_HALS, farbe: FARBE, r: 90}]}; - return {}; + if (n === 1) return {noa: 'noa_B_zeigen', luluStreckt: true, tipps}; + return {tipps}; } if (a.startsWith('verse')) { const teil = woerter.map(teilVon).find(Boolean) ?? 'ohren'; if (n === 0) return {kamera: NOA_NAH, noa: i % 2 ? 'noa_A5_blick-links' : 'noa_A6_blick-rechts', ruhig: true, karte: }; - return {kamera: NOA_NAH, leuchten: anNoa(teil).map((p) => ({...p, farbe: FARBE, r: 46})), karte: }; + return {kamera: NOA_NAH, noa: POSE[teil], tipps, karte: }; } if (a.startsWith('bridge')) { - const leuchten = woerter.flatMap((w, k) => { + // zu jedem gesungenen Körperteil die passende Pose (knapp vor dem Wort, damit sie beim Wort steht) + const folge: Posenfolge = [[0, 'noa_A3_lachen']]; + woerter.forEach((w, k) => { const teil = teilVon(w); - return teil ? anNoa(teil).map((p) => ({...p, farbe: FARBE, r: 40, ab: wortZeit(z, k) - 0.05})) : []; + if (teil) folge.push([Math.round(wortZeit(z, k) * 30) - 4, POSE[teil]]); }); - return {kamera: {x: PLATZ.noa - 40, y: 720, z: 1.5}, leuchten, ruhig: a.includes('slow')}; + return {kamera: {x: PLATZ.noa - 40, y: 720, z: 1.5}, noa: folge.length > 1 ? folge : 'noa_A3_lachen', luluStreckt: woerter.some((w) => teilVon(w) === 'hals'), ruhig: a.includes('slow')}; } return {ruhig: true}; };