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 <noreply@anthropic.com>
This commit is contained in:
Lara 2026-11-09 14:14:29 +00:00
parent 704bdcd0a3
commit 490444123c
3 changed files with 32 additions and 18 deletions

View file

@ -34,6 +34,7 @@ const FPS = 30;
const s = (t: number) => Math.round(t * FPS); const s = (t: number) => Math.round(t * FPS);
export const PLATZ = {pino: 760, noa: 980, lulu: 1300}; export const PLATZ = {pino: 760, noa: 980, lulu: 1300};
const WEIT = {x: 1030, y: 640, z: 1.2}; 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; 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; if (z.start - 0.25 <= t) i = k;
}); });
const bilder = zeilen.map((z, k) => szene(k, z)); 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; const imRefrain = zeilen[i].abschnitt.toLowerCase().startsWith('chorus') && !b.ruhig;
// Kamera: jedes Zeilen-Bild gilt ab Zeilenbeginn, 0,7 s Fahrt dorthin // 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]]; 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 fr = s(z.start - 0.25);
const vorher = punkte[punkte.length - 1]; 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]); 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 kam = kamFahrt(f, punkte);
const takt = (f / FPS) * (96 / 60) * Math.PI; const takt = (f / FPS) * (96 / 60) * Math.PI;
const wipp = (phase: number, h: number) => (imRefrain ? -Math.abs(Math.sin(takt + phase)) * h : 0); 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 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; 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 ( return (
<> <>

View file

@ -103,8 +103,8 @@ const NOA: Posenfolge = [
[s(T.v4z[2]), 'noa_A5_blick-links'], [s(T.v4z[2]), 'noa_A5_blick-links'],
] as Posenfolge) ] as Posenfolge)
: []), : []),
// Mitmach-Teil: Noa zeigt erst auf das Gefragte, bei der Antwort auf das andere (Seite: siehe noaZeigtLinks) // 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.antwort - 0.2 : c.frage), seite === 'keins' ? 'noa_A3_lachen' : 'noa_B_zeigen'] as [number, string])), ...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), 'noa_A4_staunen'],
[s(T.gag + 4.5), 'noa_A3_lachen'], [s(T.gag + 4.5), 'noa_A3_lachen'],
...klatschen(T.c4, T.cz(T.c4)[3]), ...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; 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 // 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 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 // 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'}); 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 // … und legt sie zu Beginn von Refrain 2 neben sich ins Gras, wo sie verblasst

View file

@ -15,7 +15,8 @@ type Zeiten = {
}; };
// zeigt: wohin Noa bei Frage und Antwort zeigt (rechts = Lulu/Apfel, links = Pino/Blume, keins = sie selbst) // zeigt: wohin Noa bei Frage und Antwort zeigt (rechts = Lulu/Apfel, links = Pino/Blume, keins = sie selbst)
type Seite = 'rechts' | 'links' | 'keins'; 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 REFRAIN = (z: [number, number, number, number]) => z;
const C_DE = { const C_DE = {
@ -53,10 +54,10 @@ const DAUER_DE = Math.round(139.68 * 30);
// Mitmach-Teil: Frage beginnt, Antwort des Chors // Mitmach-Teil: Frage beginnt, Antwort des Chors
const KARTEN_DE: Karte[] = [ const KARTEN_DE: Karte[] = [
{frage: 74.22, antwort: 78.95, wort: 'groß', gegenteil: 'klein', zeigt: ['rechts', 'links']}, {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']}, {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']}, {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']}, // ~ Antwortzeit {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[] => [ 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)!, cz: (start: number) => Object.values(C_ES).find((z) => z[0] === start)!,
}; };
const KARTEN_ES: Karte[] = [ const KARTEN_ES: Karte[] = [
{frage: 61.07, antwort: 64.71, wort: 'pequeño', gegenteil: 'grande', 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']}, {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']}, {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']}, {frage: 75.19, antwort: 78.59, wort: 'rápido', gegenteil: 'despacio', zeigt: ['links', 'rechts'], wechsel: 77.41},
]; ];
const ZEILEN_ES: LiedZeile[] = [ const ZEILEN_ES: LiedZeile[] = [
'Uno@5.03 pequeño,@6.25 una@6.99 grande,@7.13', '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)!, cz: (start: number) => Object.values(C_FR).find((z) => z[0] === start)!,
}; };
const KARTEN_FR: Karte[] = [ const KARTEN_FR: Karte[] = [
{frage: 66.29, antwort: 69.47, wort: 'petit', gegenteil: 'grande', 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']}, {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']}, {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']}, {frage: 80.13, antwort: 82.69, wort: 'vite', gegenteil: 'lentement', zeigt: ['links', 'rechts'], wechsel: 82.05},
]; ];
const ZEILEN_FR: LiedZeile[] = [ const ZEILEN_FR: LiedZeile[] = [
'Un@11.33 tout@11.95 petit,@12.21 un@12.89 très@12.93 grand,@13.09', 'Un@11.33 tout@11.95 petit,@12.21 un@12.89 très@12.93 grand,@13.09',