Drachen für „Lulu und der Drachen“: roter Papierdrachen mit Schwanz und drei Schleifen
- baukasten/drachen.tsx: <Drachen> als SVG (Raute mit Holzkreuz, leichter Stift-Rand), Schwanz mit wandernder Welle, weht mit `schwanz` zur Seite, liegt im Gras flach (`liegt`) und knickt an der Graskante um (`boden`); Schleifen gelb, grün, blau leuchten einzeln (`leuchten`, zum Zählen) - drachenHalt(): Knotenpunkt für die Drachenschnur, dreht mit - Flugbahnen: pendeln, steigen (auch aus dem Gras), trudeln (zwei Runden ins Gras), plumps (Windstoß hebt ihn kurz an); LIEGE_HOEHE für den liegenden Drachen - DrachenschnurTest zeigt die ganze Folge (14 s) mit Noa und Schnur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
21324b0ef6
commit
bc807312a1
3 changed files with 225 additions and 21 deletions
|
|
@ -29,7 +29,7 @@ export const Root: React.FC = () => (
|
|||
<Composition id="GangTest" component={GangTest} durationInFrames={GANGTEST_DAUER} {...BILD} />
|
||||
<Composition id="LaterneTest" component={LaterneTest} durationInFrames={420} {...BILD} />
|
||||
<Composition id="VerformTest" component={VerformTest} durationInFrames={240} {...BILD} />
|
||||
<Composition id="DrachenschnurTest" component={DrachenschnurTest} durationInFrames={150} {...BILD} />
|
||||
<Composition id="DrachenschnurTest" component={DrachenschnurTest} durationInFrames={420} {...BILD} />
|
||||
<Composition id="HintergrundAbend" component={HintergrundAbend} durationInFrames={1} {...BILD} />
|
||||
<Composition id="VordergrundAbend" component={VordergrundAbend} durationInFrames={1} {...BILD} />
|
||||
<Composition id="PapierStruktur" component={PapierStruktur} durationInFrames={1} {...BILD} />
|
||||
|
|
|
|||
193
remotion/src/baukasten/drachen.tsx
Normal file
193
remotion/src/baukasten/drachen.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import React from 'react';
|
||||
import {Easing, interpolate, useCurrentFrame} from 'remotion';
|
||||
|
||||
// Noas roter Papierdrachen („Lulu und der Drachen“): Raute mit Holzkreuz,
|
||||
// langer Schwanz mit drei Schleifen (gelb, grün, blau).
|
||||
// (x, y) = Kreuzungspunkt der Leisten auf dem Bildschirm, s = Größe (1 ≈ 120 px hoch).
|
||||
// Der Körper dreht sich mit `drehung`, der Schwanz hängt immer nach unten bzw. weht
|
||||
// mit dem Wind (`schwanz`: Winkel aus der Senkrechten, + = nach rechts).
|
||||
|
||||
// Raute in Drachen-Koordinaten (Kreuzungspunkt = 0,0)
|
||||
const OBEN = {x: 0, y: -42};
|
||||
const LINKS = {x: -40, y: 0};
|
||||
const RECHTS = {x: 40, y: 0};
|
||||
const UNTEN = {x: 0, y: 78};
|
||||
const SCHWANZ_LAENGE = 230;
|
||||
// Lage der Schleifen auf dem Schwanz (Anteil der Länge) und ihre Farben
|
||||
export const SCHLEIFEN = [
|
||||
{t: 0.3, farbe: '#f2c84b', rand: '#c9952a'}, // gelb
|
||||
{t: 0.6, farbe: '#7fb069', rand: '#4f7f3d'}, // grün
|
||||
{t: 0.9, farbe: '#5b8fd1', rand: '#335f99'}, // blau
|
||||
] as const;
|
||||
|
||||
const dreh = (p: {x: number; y: number}, grad: number, s: number) => {
|
||||
const w = (grad * Math.PI) / 180;
|
||||
return {x: (p.x * Math.cos(w) - p.y * Math.sin(w)) * s, y: (p.x * Math.sin(w) + p.y * Math.cos(w)) * s};
|
||||
};
|
||||
|
||||
// Punkt, an dem die Schnur am Drachen festgeknotet ist (Waage, knapp unter dem Kreuz).
|
||||
export const drachenHalt = (x: number, y: number, s = 1, drehung = 0) => {
|
||||
const p = dreh({x: 0, y: 14}, drehung, s);
|
||||
return {x: x + p.x, y: y + p.y};
|
||||
};
|
||||
|
||||
type DrachenProps = {
|
||||
x: number;
|
||||
y: number;
|
||||
s?: number;
|
||||
drehung?: number;
|
||||
schwanz?: number; // Winkel des Schwanzes aus der Senkrechten (Grad), Wind von links ≈ 25
|
||||
flattern?: number; // Stärke der Schwanzwelle (px), 0 = hängt ruhig
|
||||
liegt?: boolean; // liegt im Gras (drehung 90): Schwanz flach zur Seite
|
||||
boden?: number; // y des Grases auf dem Bildschirm: der Schwanz geht nicht darunter
|
||||
leuchten?: [number, number, number]; // je Schleife 0…1 (Zählen)
|
||||
deckkraft?: number;
|
||||
frame?: number;
|
||||
};
|
||||
|
||||
export const Drachen: React.FC<DrachenProps> = ({
|
||||
x,
|
||||
y,
|
||||
s = 1,
|
||||
drehung = 0,
|
||||
schwanz = 20,
|
||||
flattern = 14,
|
||||
liegt = false,
|
||||
boden,
|
||||
leuchten = [0, 0, 0],
|
||||
deckkraft = 1,
|
||||
frame,
|
||||
}) => {
|
||||
const jetzt = useCurrentFrame();
|
||||
const f = frame ?? jetzt;
|
||||
const id = `drachen-${Math.round(x)}-${Math.round(y)}`;
|
||||
|
||||
// Schwanz in Bildschirm-Koordinaten: beginnt an der unteren Spitze, läuft in Richtung
|
||||
// `schwanz` (liegend: flach nach rechts) und schlägt eine wandernde Welle.
|
||||
const start = dreh(UNTEN, drehung, s);
|
||||
const richtung = ((liegt ? -84 : schwanz) * Math.PI) / 180; // liegend: von der Spitze flach nach links
|
||||
const ax = Math.sin(richtung);
|
||||
const ay = Math.cos(richtung);
|
||||
const welle = liegt ? 0 : flattern;
|
||||
const schwanzPunkt = (t: number) => {
|
||||
const l = t * SCHWANZ_LAENGE * s;
|
||||
const quer = Math.sin(t * 7 - f * 0.35) * welle * s * (0.25 + t); // nach hinten stärker
|
||||
const sackt = liegt ? 0 : t * t * 30 * s * Math.sin(richtung); // weht er seitlich, hängt das Ende etwas
|
||||
// Erreicht der Schwanz das Gras, liegt der Rest flach darauf (mit dem Wind nach rechts).
|
||||
const grund = boden === undefined ? Infinity : boden - y - 2;
|
||||
const lAuf = ay > 0.05 ? (grund - start.y) / ay : Infinity;
|
||||
if (l > lAuf) return {x: start.x + ax * lAuf + (l - lAuf) * 0.95, y: grund - Math.abs(Math.sin(t * 9)) * 2 * s};
|
||||
return {x: start.x + ax * l + ay * quer, y: Math.min(grund, start.y + ay * l - ax * quer + sackt)};
|
||||
};
|
||||
const punkte = Array.from({length: 25}, (_, i) => schwanzPunkt(i / 24));
|
||||
const schwanzPfad = punkte.map((p, i) => `${i ? 'L' : 'M'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');
|
||||
|
||||
const r = (p: {x: number; y: number}) => dreh(p, drehung, s);
|
||||
const [o, li, re, u] = [r(OBEN), r(LINKS), r(RECHTS), r(UNTEN)];
|
||||
|
||||
return (
|
||||
<svg
|
||||
style={{position: 'absolute', left: x, top: y, width: 1, height: 1, overflow: 'visible', pointerEvents: 'none', opacity: deckkraft}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`${id}-papier`} x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stopColor="#e2584a" />
|
||||
<stop offset="0.55" stopColor="#c8402c" />
|
||||
<stop offset="1" stopColor="#a8352a" />
|
||||
</linearGradient>
|
||||
<filter id={`${id}-stift`} x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="2" seed="4" result="r" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="r" scale={2.5} xChannelSelector="R" yChannelSelector="G" />
|
||||
</filter>
|
||||
<radialGradient id={`${id}-glanz`}>
|
||||
<stop offset="0" stopColor="#fffbe0" stopOpacity="0.95" />
|
||||
<stop offset="1" stopColor="#fff3b0" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
{/* Schwanz: Kordel */}
|
||||
<path d={schwanzPfad} fill="none" stroke="#6b4a32" strokeWidth={1.8 * s} strokeLinecap="round" opacity={0.85} />
|
||||
{/* Schleifen */}
|
||||
{SCHLEIFEN.map((sl, i) => {
|
||||
const p = schwanzPunkt(sl.t);
|
||||
const q = schwanzPunkt(Math.min(1, sl.t + 0.03));
|
||||
const winkel = (Math.atan2(q.y - p.y, q.x - p.x) * 180) / Math.PI;
|
||||
const g = leuchten[i] ?? 0;
|
||||
const k = s * (1 + 0.25 * g);
|
||||
return (
|
||||
<g key={i} transform={`translate(${p.x} ${p.y}) rotate(${winkel + 90}) scale(${k})`}>
|
||||
{g > 0 && <circle r={26} fill={`url(#${id}-glanz)`} opacity={g} />}
|
||||
<g filter={`url(#${id}-stift)`}>
|
||||
<path d="M 0 0 L -14 -8 Q -17 0 -14 8 Z" fill={sl.farbe} stroke={sl.rand} strokeWidth={1.2} strokeLinejoin="round" />
|
||||
<path d="M 0 0 L 14 -8 Q 17 0 14 8 Z" fill={sl.farbe} stroke={sl.rand} strokeWidth={1.2} strokeLinejoin="round" />
|
||||
<circle r={2.6} fill={sl.rand} />
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{/* Körper */}
|
||||
<g filter={`url(#${id}-stift)`}>
|
||||
<polygon
|
||||
points={`${o.x},${o.y} ${re.x},${re.y} ${u.x},${u.y} ${li.x},${li.y}`}
|
||||
fill={`url(#${id}-papier)`}
|
||||
stroke="#8e2f22"
|
||||
strokeWidth={2.2 * s}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
{/* helleres oberes Feld wie aufgeklebtes zweites Papier */}
|
||||
<polygon points={`${o.x},${o.y} ${re.x},${re.y} 0,0 ${li.x},${li.y}`} fill="#f07a5e" opacity={0.35} />
|
||||
{/* Holzleisten */}
|
||||
<line x1={o.x} y1={o.y} x2={u.x} y2={u.y} stroke="#8a6040" strokeWidth={2.6 * s} strokeLinecap="round" opacity={0.9} />
|
||||
<line x1={li.x} y1={li.y} x2={re.x} y2={re.y} stroke="#8a6040" strokeWidth={2.6 * s} strokeLinecap="round" opacity={0.9} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------- Flugbahnen ----------
|
||||
// Alle liefern {x, y, drehung} für <Drachen> (und damit auch für drachenHalt).
|
||||
export type DrachenLage = {x: number; y: number; drehung: number; liegt?: boolean};
|
||||
// Im Gras liegt der Kreuzungspunkt so hoch über dem Boden (Raute auf der Seite, s = 1).
|
||||
export const LIEGE_HOEHE = 36;
|
||||
type Punkt = {x: number; y: number};
|
||||
const klemm = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const;
|
||||
|
||||
// Ruhiges Tanzen am Himmel (z. B. Szene 6): sanftes Auf und Ab, leichtes Kippen.
|
||||
export const pendeln = (frame: number, ort: Punkt, staerke = 1): DrachenLage => ({
|
||||
x: ort.x + Math.sin(frame * 0.045) * 40 * staerke,
|
||||
y: ort.y + Math.sin(frame * 0.07 + 0.8) * 18 * staerke,
|
||||
drehung: Math.sin(frame * 0.045 + 0.6) * 12 * staerke,
|
||||
});
|
||||
|
||||
// Steigen von `von` nach `nach` in `dauer` Frames, mit kleinen Rucken im Wind;
|
||||
// danach pendelt er am Ziel weiter. Aus dem Gras: von = {x, boden - LIEGE_HOEHE}, startDrehung = 90.
|
||||
export const steigen = (frame: number, start: number, dauer: number, von: Punkt, nach: Punkt, startDrehung = 0): DrachenLage => {
|
||||
const t = interpolate(frame, [start, start + dauer], [0, 1], {...klemm, easing: Easing.bezier(0.3, 0, 0.25, 1)});
|
||||
const ruck = Math.sin(t * Math.PI * 4) * (1 - t) * 18;
|
||||
const p = pendeln(frame, {x: von.x + (nach.x - von.x) * t, y: von.y + (nach.y - von.y) * t}, t);
|
||||
const aufrichten = startDrehung * Math.max(0, 1 - t * 5); // z. B. aus dem Gras (90) aufrichten
|
||||
return {x: p.x + ruck * 0.5, y: p.y + ruck, drehung: p.drehung + (1 - t) * 15 * Math.sin(frame * 0.3) + aufrichten, liegt: frame < start};
|
||||
};
|
||||
|
||||
// Trudeln: dreht sich im Kreis und fällt in einer Spirale ins Gras (`boden` = y des Grases).
|
||||
export const trudeln = (frame: number, start: number, dauer: number, von: Punkt, boden: number, nachRechts = 160): DrachenLage => {
|
||||
const t = interpolate(frame, [start, start + dauer], [0, 1], klemm);
|
||||
const fall = Easing.in(Easing.quad)(t);
|
||||
const kreis = t * Math.PI * 4; // zwei Runden
|
||||
const radius = 45 * (1 - t);
|
||||
return {
|
||||
x: von.x + nachRechts * t + Math.sin(kreis) * radius,
|
||||
y: von.y + (boden - LIEGE_HOEHE - von.y) * fall - Math.cos(kreis) * radius * 0.5,
|
||||
drehung: t * 720 + 90 * t * t,
|
||||
liegt: t >= 1,
|
||||
};
|
||||
};
|
||||
|
||||
// Plumps: liegt im Gras (x = Mitte, `boden` = y des Grases), ein Windstoß hebt ihn kurz an,
|
||||
// dann fällt er schaukelnd zurück ins Gras.
|
||||
export const plumps = (frame: number, start: number, dauer: number, x: number, boden: number, hoehe = 140): DrachenLage => {
|
||||
const t = interpolate(frame, [start, start + dauer], [0, 1], klemm);
|
||||
const hub = Math.sin(t * Math.PI) * hoehe;
|
||||
const wackeln = Math.sin(t * Math.PI * 5) * 25 * Math.sin(t * Math.PI);
|
||||
const aufrecht = interpolate(t, [0, 0.2, 0.8, 1], [90, 0, 0, 90]);
|
||||
return {x: x + Math.sin(t * Math.PI) * 40, y: boden - LIEGE_HOEHE - hub, drehung: aufrecht + wackeln, liegt: t <= 0 || t >= 1};
|
||||
};
|
||||
|
|
@ -1,32 +1,43 @@
|
|||
import React from 'react';
|
||||
import {AbsoluteFill, useCurrentFrame} from 'remotion';
|
||||
import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion';
|
||||
import {Figur, NOA_HOEHE} from '../baukasten/figur';
|
||||
import {Drachen, type DrachenLage, LIEGE_HOEHE, drachenHalt, pendeln, plumps, steigen, trudeln} from '../baukasten/drachen';
|
||||
import {Drachenschnur, spulenPunkt} from '../baukasten/drachenschnur';
|
||||
|
||||
// Prüfszene: Noa mit Drachenspule, Schnur zu einem Platzhalter-Drachen, der pendelt.
|
||||
// Links normal, rechts gespiegelt und größer (prüft die Umrechnung).
|
||||
const Platzhalter: React.FC<{x: number; y: number}> = ({x, y}) => (
|
||||
<div style={{position: 'absolute', left: x - 40, top: y - 55, width: 80, height: 110}}>
|
||||
<svg width={80} height={110}>
|
||||
<polygon points="40,0 80,45 40,110 0,45" fill="#c8402c" opacity={0.8} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
// Prüfszene Drachen (14 s): Noa hält die Spule; der Drachen liegt im Gras, plumpst,
|
||||
// steigt, tanzt, trudelt zurück. Oben links ein zweiter Drachen zum Zählen der Schleifen.
|
||||
const BODEN = 900;
|
||||
const klemm = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const;
|
||||
|
||||
export const DrachenschnurTest: React.FC = () => {
|
||||
const f = useCurrentFrame();
|
||||
const drachen1 = {x: 760 + Math.sin(f * 0.05) * 60, y: 160 + Math.sin(f * 0.08) * 25};
|
||||
const drachen2 = {x: 1180 - Math.sin(f * 0.05) * 60, y: 200 + Math.cos(f * 0.07) * 25};
|
||||
const noa1 = {x: 420, y: 900, hoehe: NOA_HOEHE};
|
||||
const noa2 = {x: 1560, y: 980, hoehe: NOA_HOEHE * 1.5, spiegeln: true};
|
||||
const noa = {x: 420, y: BODEN, hoehe: NOA_HOEHE};
|
||||
const himmel = {x: 1150, y: 230};
|
||||
|
||||
let lage: DrachenLage;
|
||||
if (f < 100) lage = plumps(f, 30, 60, 980, BODEN);
|
||||
else if (f < 300) lage = steigen(f, 100, 120, {x: 980, y: BODEN - LIEGE_HOEHE}, himmel, 90);
|
||||
else {
|
||||
const ab = pendeln(300, himmel);
|
||||
lage = trudeln(f, 300, 80, ab, BODEN);
|
||||
}
|
||||
// Wind von links: Schwanz weht nach rechts, am Boden kaum
|
||||
const schwanz = interpolate(lage.y, [300, BODEN - 120], [30, 5], klemm);
|
||||
|
||||
// Zählen: Schleifen leuchten nacheinander auf
|
||||
const zaehl = (i: number) => interpolate(f % 120, [i * 30 + 10, i * 30 + 18, i * 30 + 34, i * 30 + 44], [0, 1, 1, 0], klemm);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{background: '#f4ead8'}}>
|
||||
<Platzhalter {...drachen1} />
|
||||
<Platzhalter {...drachen2} />
|
||||
<Figur pose="noa_A_drachenschnur" {...noa1} />
|
||||
<Drachenschnur von={spulenPunkt(noa1.x, noa1.y, noa1.hoehe)} nach={{x: drachen1.x, y: drachen1.y + 55}} />
|
||||
<Figur pose="noa_A_drachenschnur" {...noa2} />
|
||||
<Drachenschnur von={spulenPunkt(noa2.x, noa2.y, noa2.hoehe, true)} nach={{x: drachen2.x, y: drachen2.y + 55}} staerke={3} durchhang={0.1} />
|
||||
<div style={{position: 'absolute', left: 0, top: BODEN - 10, width: 1920, height: 200, background: '#a9c27a'}} />
|
||||
<Drachen x={260} y={260} s={1.3} leuchten={[zaehl(0), zaehl(1), zaehl(2)]} schwanz={8} flattern={8} />
|
||||
<Figur pose="noa_A_drachenschnur" {...noa} />
|
||||
<Drachen {...lage} schwanz={schwanz} flattern={lage.liegt ? 0 : 14} boden={BODEN} />
|
||||
<Drachenschnur
|
||||
von={spulenPunkt(noa.x, noa.y, noa.hoehe)}
|
||||
nach={drachenHalt(lage.x, lage.y, 1, lage.drehung)}
|
||||
durchhang={lage.y > BODEN - 200 ? 0.12 : 0.05}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue