From 490444123cacedaf657cd2ff43953803cfd6e2d0 Mon Sep 17 00:00:00 2001 From: Lara Date: Mon, 9 Nov 2026 14:14:29 +0000 Subject: [PATCH] Lied 3: Noa zeigt sofort aufs zweite Ding, wenn es gesungen wird; Vorlage: Leuchten/Karte enden 2,5 s nach der Zeile (kein Kreis im Zwischenspiel) Co-Authored-By: Claude Opus 5.5 --- remotion/src/lieder/einfach/EinfachesLied.tsx | 17 ++++++++++-- remotion/src/lieder/lied03/Lied03.tsx | 6 ++--- remotion/src/lieder/lied03/zeiten.ts | 27 ++++++++++--------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/remotion/src/lieder/einfach/EinfachesLied.tsx b/remotion/src/lieder/einfach/EinfachesLied.tsx index b923d22..a1e0b39 100644 --- a/remotion/src/lieder/einfach/EinfachesLied.tsx +++ b/remotion/src/lieder/einfach/EinfachesLied.tsx @@ -34,6 +34,7 @@ const FPS = 30; const s = (t: number) => Math.round(t * FPS); export const PLATZ = {pino: 760, noa: 980, lulu: 1300}; const WEIT = {x: 1030, y: 640, z: 1.2}; +const NACHHALL = 2.5; export const wortZeit = (z: Zeile, wort: number) => zerlege(z.zeile)[wort]?.t ?? z.start; @@ -118,7 +119,14 @@ const Garten: React.FC<{daten: LiedDaten; szene: (i: number, z: Zeile) => Bild}> if (z.start - 0.25 <= t) i = k; }); const bilder = zeilen.map((z, k) => szene(k, z)); - const b = bilder[i]; + // Nach dem letzten Wort einer Zeile gilt ihr Bild noch NACHHALL Sekunden – in langen Zwischenspielen + // kehrt alles zur Ausgangslage zurück (kein Leuchten ohne Text) + const letztes = (k: number) => { + const w = zerlege(zeilen[k].zeile); + return w[w.length - 1].t; + }; + const aktiv = t <= letztes(i) + NACHHALL; + const b = aktiv ? bilder[i] : {}; const imRefrain = zeilen[i].abschnitt.toLowerCase().startsWith('chorus') && !b.ruhig; // Kamera: jedes Zeilen-Bild gilt ab Zeilenbeginn, 0,7 s Fahrt dorthin const punkte: [number, number, number, number][] = [[0, WEIT.x, WEIT.y, WEIT.z]]; @@ -127,12 +135,17 @@ const Garten: React.FC<{daten: LiedDaten; szene: (i: number, z: Zeile) => Bild}> const fr = s(z.start - 0.25); const vorher = punkte[punkte.length - 1]; punkte.push([Math.max(vorher[0] + 1, fr - 21), vorher[1], vorher[2], vorher[3]], [Math.max(vorher[0] + 2, fr), ziel.x, ziel.y, ziel.z]); + const naechste = zeilen[k + 1]?.start ?? Infinity; + if (naechste - letztes(k) > NACHHALL + 2 && ziel !== WEIT) { + const ende = s(letztes(k) + NACHHALL); + punkte.push([ende, ziel.x, ziel.y, ziel.z], [ende + 21, WEIT.x, WEIT.y, WEIT.z]); + } }); const kam = kamFahrt(f, punkte); const takt = (f / FPS) * (96 / 60) * Math.PI; const wipp = (phase: number, h: number) => (imRefrain ? -Math.abs(Math.sin(takt + phase)) * h : 0); const schwank = (phase: number, g: number) => (imRefrain ? Math.sin(takt / 2 + phase) * g : 0); - const zeilenEnde = (k: number) => s(zeilen[k + 1]?.start ?? zeilen[k].start + 4) - 6; + const zeilenEnde = (k: number) => Math.min(s(zeilen[k + 1]?.start ?? zeilen[k].start + 4) - 6, s(letztes(k) + NACHHALL)); const strecken = b.luluStreckt ? interpolate(f, [s(zeilen[i].start - 0.2), s(zeilen[i].start + 0.4)], [1, 1.08], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}) : 1; return ( <> diff --git a/remotion/src/lieder/lied03/Lied03.tsx b/remotion/src/lieder/lied03/Lied03.tsx index 02181d9..983a131 100644 --- a/remotion/src/lieder/lied03/Lied03.tsx +++ b/remotion/src/lieder/lied03/Lied03.tsx @@ -103,8 +103,8 @@ const NOA: Posenfolge = [ [s(T.v4z[2]), 'noa_A5_blick-links'], ] as Posenfolge) : []), - // Mitmach-Teil: Noa zeigt erst auf das Gefragte, bei der Antwort auf das andere (Seite: siehe noaZeigtLinks) - ...KARTEN.flatMap((c) => c.zeigt.map((seite, k) => [s(k ? c.antwort - 0.2 : c.frage), seite === 'keins' ? 'noa_A3_lachen' : 'noa_B_zeigen'] as [number, string])), + // Mitmach-Teil: Noa zeigt erst auf das Gefragte, dann auf das zweite Ding, sobald es gesungen wird (Seite: siehe noaZeigtLinks) + ...KARTEN.flatMap((c) => c.zeigt.map((seite, k) => [s(k ? c.wechsel - 0.15 : c.frage), seite === 'keins' ? 'noa_A3_lachen' : 'noa_B_zeigen'] as [number, string])), [s(T.gag), 'noa_A4_staunen'], [s(T.gag + 4.5), 'noa_A3_lachen'], ...klatschen(T.c4, T.cz(T.c4)[3]), @@ -249,7 +249,7 @@ const Garten: React.FC = () => { const pinoDuck = v4 && T.juchhe !== undefined ? interpolate(f, [s(v4[3]), s(v4[3] + 0.3), s(T.juchhe), s(T.juchhe + 0.15)], [1, 0.8, 0.8, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}) : 1; // Noa zeigt im Mitmach-Teil nach links (gespiegelt), wenn die Antwort bzw. Frage Pino oder die Blume meint const karte = KARTEN.findIndex((c, k) => f >= s(c.frage) && f < s(KARTEN[k + 1]?.frage ?? T.gag)); - const noaZeigtLinks = karte >= 0 && KARTEN[karte].zeigt[f >= s(KARTEN[karte].antwort - 0.2) ? 1 : 0] === 'links'; + const noaZeigtLinks = karte >= 0 && KARTEN[karte].zeigt[f >= s(KARTEN[karte].wechsel - 0.15) ? 1 : 0] === 'links'; // Strophe 1: Pino pflückt die Blume – sie fliegt in seine Pfote und bleibt dort bis in Refrain 2 const pflueck = interpolate(f, [s(T.v1z[3]), s(T.v1z[3] + 0.8)], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); // … und legt sie zu Beginn von Refrain 2 neben sich ins Gras, wo sie verblasst diff --git a/remotion/src/lieder/lied03/zeiten.ts b/remotion/src/lieder/lied03/zeiten.ts index 8060e7b..0373aea 100644 --- a/remotion/src/lieder/lied03/zeiten.ts +++ b/remotion/src/lieder/lied03/zeiten.ts @@ -15,7 +15,8 @@ type Zeiten = { }; // zeigt: wohin Noa bei Frage und Antwort zeigt (rechts = Lulu/Apfel, links = Pino/Blume, keins = sie selbst) type Seite = 'rechts' | 'links' | 'keins'; -type Karte = {frage: number; antwort: number; wort: string; gegenteil: string; zeigt: [Seite, Seite]}; +// wechsel: wann Noa auf die zweite Seite zeigt – sobald das zweite Ding gesungen wird („… und Pino ist …“) +type Karte = {frage: number; antwort: number; wort: string; gegenteil: string; zeigt: [Seite, Seite]; wechsel: number}; const REFRAIN = (z: [number, number, number, number]) => z; const C_DE = { @@ -53,10 +54,10 @@ const DAUER_DE = Math.round(139.68 * 30); // Mitmach-Teil: Frage beginnt, Antwort des Chors const KARTEN_DE: Karte[] = [ - {frage: 74.22, antwort: 78.95, wort: 'groß', gegenteil: 'klein', zeigt: ['rechts', 'links']}, - {frage: 79.05, antwort: 83.59, wort: 'oben', gegenteil: 'unten', zeigt: ['rechts', 'links']}, - {frage: 83.74, antwort: 88.22, wort: 'laut', gegenteil: 'leise', zeigt: ['keins', 'keins']}, - {frage: 88.38, antwort: 92.9, wort: 'schnell', gegenteil: 'langsam', zeigt: ['links', 'rechts']}, // ~ Antwortzeit + {frage: 74.22, antwort: 78.95, wort: 'groß', gegenteil: 'klein', zeigt: ['rechts', 'links'], wechsel: 76.61}, + {frage: 79.05, antwort: 83.59, wort: 'oben', gegenteil: 'unten', zeigt: ['rechts', 'links'], wechsel: 81.32}, + {frage: 83.74, antwort: 88.22, wort: 'laut', gegenteil: 'leise', zeigt: ['keins', 'keins'], wechsel: 85.78}, + {frage: 88.38, antwort: 92.9, wort: 'schnell', gegenteil: 'langsam', zeigt: ['links', 'rechts'], wechsel: 90.59}, // ~ Antwortzeit ]; const refrain = (z: [number, number, number, number], d = [0, 0, 0, 0]): LiedZeile[] => [ @@ -151,10 +152,10 @@ const T_ES: Zeiten = { cz: (start: number) => Object.values(C_ES).find((z) => z[0] === start)!, }; const KARTEN_ES: Karte[] = [ - {frage: 61.07, antwort: 64.71, wort: 'pequeño', gegenteil: 'grande', zeigt: ['links', 'rechts']}, - {frage: 65.85, antwort: 69.39, wort: 'arriba', gegenteil: 'abajo', zeigt: ['rechts', 'links']}, - {frage: 70.73, antwort: 73.79, wort: 'fuerte', gegenteil: 'bajito', zeigt: ['keins', 'links']}, - {frage: 75.19, antwort: 78.59, wort: 'rápido', gegenteil: 'despacio', zeigt: ['links', 'rechts']}, + {frage: 61.07, antwort: 64.71, wort: 'pequeño', gegenteil: 'grande', zeigt: ['links', 'rechts'], wechsel: 63.41}, + {frage: 65.85, antwort: 69.39, wort: 'arriba', gegenteil: 'abajo', zeigt: ['rechts', 'links'], wechsel: 68.05}, + {frage: 70.73, antwort: 73.79, wort: 'fuerte', gegenteil: 'bajito', zeigt: ['keins', 'links'], wechsel: 72.53}, + {frage: 75.19, antwort: 78.59, wort: 'rápido', gegenteil: 'despacio', zeigt: ['links', 'rechts'], wechsel: 77.41}, ]; const ZEILEN_ES: LiedZeile[] = [ 'Uno@5.03 pequeño,@6.25 una@6.99 grande,@7.13', @@ -222,10 +223,10 @@ const T_FR: Zeiten = { cz: (start: number) => Object.values(C_FR).find((z) => z[0] === start)!, }; const KARTEN_FR: Karte[] = [ - {frage: 66.29, antwort: 69.47, wort: 'petit', gegenteil: 'grande', zeigt: ['links', 'rechts']}, - {frage: 71.01, antwort: 74.53, wort: 'en haut', gegenteil: 'en bas', zeigt: ['rechts', 'links']}, - {frage: 76.01, antwort: 78.43, wort: 'fort', gegenteil: 'doucement', zeigt: ['keins', 'links']}, - {frage: 80.13, antwort: 82.69, wort: 'vite', gegenteil: 'lentement', zeigt: ['links', 'rechts']}, + {frage: 66.29, antwort: 69.47, wort: 'petit', gegenteil: 'grande', zeigt: ['links', 'rechts'], wechsel: 68.77}, + {frage: 71.01, antwort: 74.53, wort: 'en haut', gegenteil: 'en bas', zeigt: ['rechts', 'links'], wechsel: 73.59}, + {frage: 76.01, antwort: 78.43, wort: 'fort', gegenteil: 'doucement', zeigt: ['keins', 'links'], wechsel: 77.85}, + {frage: 80.13, antwort: 82.69, wort: 'vite', gegenteil: 'lentement', zeigt: ['links', 'rechts'], wechsel: 82.05}, ]; const ZEILEN_FR: LiedZeile[] = [ 'Un@11.33 tout@11.95 petit,@12.21 un@12.89 très@12.93 grand,@13.09',