225 lines
10 KiB
TypeScript
225 lines
10 KiB
TypeScript
import React from 'react';
|
||
import {Figur, NOA_HOEHE, type Posenfolge} from '../baukasten/figur';
|
||
import {FIGUREN} from '../baukasten/posen';
|
||
import {punkt} from '../baukasten/helfer';
|
||
import {BODEN, Ort} from '../baukasten/orte';
|
||
|
||
// Bildteile für Folge 8 „Noas Laterne“: Abendgarten mit Hauswand, die gebastelte Laterne,
|
||
// Schatten an der Wand und Hand-Schatten. Alles in Szenenkoordinaten (1920 × 1080).
|
||
|
||
// Hauswand rechts im Garten (Szene 3) bzw. als ganzer Hintergrund an der Wand (Szene 4–7)
|
||
export const WAND = {x0: 1060, x1: 1980, oben: 170, unten: BODEN - 30};
|
||
export const FENSTER = {x: 1180, y: 280, b: 230, h: 210};
|
||
|
||
// Abendgarten in der „blauen Stunde“: Nachtgarten aufgehellt, damit es freundlich und nicht unheimlich wirkt
|
||
export const GartenAbend: React.FC<{wandVoll?: boolean; papaWinkt?: number}> = ({wandVoll = false, papaWinkt = 0}) => {
|
||
const x0 = wandVoll ? -60 : WAND.x0;
|
||
const oben = wandVoll ? 70 : WAND.oben;
|
||
return (
|
||
<>
|
||
<div style={{position: 'absolute', inset: 0, filter: 'brightness(1.45) saturate(0.9)'}}>
|
||
<Ort name="garten_nacht" />
|
||
</div>
|
||
<div style={{position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(110,130,200,0) 0%, rgba(150,150,200,0.12) 55%, rgba(240,180,140,0.22) 72%, rgba(60,70,110,0) 100%)'}} />
|
||
<svg viewBox="0 0 1920 1080" style={{position: 'absolute', inset: 0, width: 1920, height: 1080, overflow: 'visible'}}>
|
||
<defs>
|
||
<linearGradient id="putz" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0" stopColor="#b9b5c9" />
|
||
<stop offset="1" stopColor="#a7a2b6" />
|
||
</linearGradient>
|
||
<radialGradient id="fensterlicht">
|
||
<stop offset="0" stopColor="#ffe3a0" stopOpacity="0.55" />
|
||
<stop offset="1" stopColor="#ffe3a0" stopOpacity="0" />
|
||
</radialGradient>
|
||
</defs>
|
||
{/* Wand mit Dachkante und Sockel */}
|
||
<rect x={x0} y={oben} width={WAND.x1 - x0} height={WAND.unten - oben} fill="url(#putz)" />
|
||
<rect x={x0 - 20} y={oben - 34} width={WAND.x1 - x0 + 40} height={40} fill="#5a4a5e" />
|
||
<rect x={x0} y={WAND.unten - 34} width={WAND.x1 - x0} height={34} fill="#8f889c" />
|
||
{!wandVoll && <rect x={x0 - 8} y={WAND.oben} width={16} height={WAND.unten - WAND.oben} fill="#9690a6" />}
|
||
{!wandVoll && (
|
||
<>
|
||
{/* erleuchtetes Fenster mit Papa als Umriss */}
|
||
<ellipse cx={FENSTER.x + FENSTER.b / 2} cy={FENSTER.y + FENSTER.h / 2} rx={FENSTER.b * 1.1} ry={FENSTER.h * 1.1} fill="url(#fensterlicht)" />
|
||
<rect x={FENSTER.x} y={FENSTER.y} width={FENSTER.b} height={FENSTER.h} rx={6} fill="#f6cf7a" stroke="#4b3f55" strokeWidth={12} />
|
||
<g opacity={0.85}>
|
||
<circle cx={FENSTER.x + 130} cy={FENSTER.y + 92} r={28} fill="#6b5560" />
|
||
<path d={`M ${FENSTER.x + 82} ${FENSTER.y + FENSTER.h} C ${FENSTER.x + 86} ${FENSTER.y + 140}, ${FENSTER.x + 174} ${FENSTER.y + 140}, ${FENSTER.x + 178} ${FENSTER.y + FENSTER.h} Z`} fill="#6b5560" />
|
||
{/* winkender Arm */}
|
||
<g transform={`rotate(${-20 + 25 * Math.sin(papaWinkt)} ${FENSTER.x + 168} ${FENSTER.y + 150})`}>
|
||
<rect x={FENSTER.x + 162} y={FENSTER.y + 70} width={16} height={84} rx={8} fill="#6b5560" />
|
||
<circle cx={FENSTER.x + 170} cy={FENSTER.y + 66} r={13} fill="#6b5560" />
|
||
</g>
|
||
</g>
|
||
<line x1={FENSTER.x + FENSTER.b / 2} y1={FENSTER.y} x2={FENSTER.x + FENSTER.b / 2} y2={FENSTER.y + FENSTER.h} stroke="#4b3f55" strokeWidth={8} />
|
||
<line x1={FENSTER.x} y1={FENSTER.y + FENSTER.h / 2} x2={FENSTER.x + FENSTER.b} y2={FENSTER.y + FENSTER.h / 2} stroke="#4b3f55" strokeWidth={8} />
|
||
<rect x={FENSTER.x - 18} y={FENSTER.y + FENSTER.h} width={FENSTER.b + 36} height={16} rx={4} fill="#6d6278" />
|
||
</>
|
||
)}
|
||
{/* Haken für die Laterne (Szene 8) */}
|
||
<path d={`M ${FENSTER.x + FENSTER.b + 40} ${FENSTER.y + 40} l 26 0 l 0 22`} stroke="#3f3646" strokeWidth={7} fill="none" strokeLinecap="round" />
|
||
</svg>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export const HAKEN = {x: FENSTER.x + FENSTER.b + 66, y: FENSTER.y + 62};
|
||
export const WAND_VOLL = {...WAND, x0: -60, oben: 70}; // an der Wand (Szene 4–7): höher, damit ganze Schatten draufpassen
|
||
|
||
// Die gebastelte Laterne: gelbes Papier, fünf Glitzersterne, ein Papier-Igel, innen ein Lämpchen.
|
||
// (x, y) = Mitte; sterne = wie viele schon aufgeklebt sind; igel 0–1; licht 0–1.
|
||
const STERNE: [number, number][] = [
|
||
[-34, -28],
|
||
[22, -40],
|
||
[40, 6],
|
||
[-6, 30],
|
||
[-44, 18],
|
||
];
|
||
const Stern: React.FC<{x: number; y: number; r: number; glanz: number}> = ({x, y, r, glanz}) => {
|
||
const pkt = new Array(10)
|
||
.fill(0)
|
||
.map((_, i) => {
|
||
const w = (Math.PI / 5) * i - Math.PI / 2;
|
||
const rr = i % 2 ? r * 0.45 : r;
|
||
return `${x + Math.cos(w) * rr},${y + Math.sin(w) * rr}`;
|
||
})
|
||
.join(' ');
|
||
return <polygon points={pkt} fill={glanz > 0.5 ? '#fff6c2' : '#f2c84b'} stroke="#c9952a" strokeWidth={1.5} />;
|
||
};
|
||
|
||
export const Laterne: React.FC<{x: number; y: number; s?: number; sterne?: number; igel?: number; licht?: number; schwung?: number; stab?: boolean}> = ({
|
||
x,
|
||
y,
|
||
s = 1,
|
||
sterne = 5,
|
||
igel = 1,
|
||
licht = 0,
|
||
schwung = 0,
|
||
stab = true,
|
||
}) => (
|
||
<div style={{position: 'absolute', left: x, top: y, width: 0, height: 0, transform: `scale(${s}) rotate(${schwung}deg)`, transformOrigin: '0 -70px'}}>
|
||
{licht > 0 && (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: -260,
|
||
top: -260,
|
||
width: 520,
|
||
height: 520,
|
||
borderRadius: '50%',
|
||
background: `radial-gradient(closest-side, rgba(255,214,120,${0.55 * licht}), rgba(255,200,100,${0.18 * licht}) 55%, rgba(255,200,100,0))`,
|
||
}}
|
||
/>
|
||
)}
|
||
<svg width={200} height={260} viewBox="-100 -160 200 260" style={{position: 'absolute', left: -100, top: -160, overflow: 'visible'}}>
|
||
<defs>
|
||
<radialGradient id="papier" cx="0.45" cy="0.42" r="0.65">
|
||
<stop offset="0" stopColor={licht > 0 ? '#fffbe2' : '#fbe79a'} />
|
||
<stop offset="0.6" stopColor={licht > 0 ? '#ffd65c' : '#f2cf5c'} />
|
||
<stop offset="1" stopColor={licht > 0 ? '#f0a531' : '#d9b03e'} />
|
||
</radialGradient>
|
||
</defs>
|
||
{stab && <line x1={0} y1={-70} x2={70} y2={-150} stroke="#7a5233" strokeWidth={6} strokeLinecap="round" />}
|
||
<line x1={0} y1={-70} x2={0} y2={-56} stroke="#5a3d28" strokeWidth={2} />
|
||
<rect x={-24} y={-60} width={48} height={10} rx={4} fill="#b8433a" />
|
||
<ellipse cx={0} cy={0} rx={62} ry={56} fill="url(#papier)" />
|
||
{[-0.62, -0.2, 0.2, 0.62].map((k) => (
|
||
<path key={k} d={`M ${k * 62} -50 Q ${k * 100} 0 ${k * 62} 50`} stroke="#d39a2c" strokeWidth={1.6} fill="none" opacity={0.6} />
|
||
))}
|
||
{STERNE.slice(0, sterne).map(([sx, sy], i) => (
|
||
<Stern key={i} x={sx} y={sy} r={11} glanz={licht} />
|
||
))}
|
||
{igel > 0 && (
|
||
<g opacity={igel} transform="translate(8 2)">
|
||
<ellipse cx={0} cy={0} rx={17} ry={12} fill="#8a5a36" />
|
||
{[-14, -8, -2, 4, 10].map((dx) => (
|
||
<path key={dx} d={`M ${dx} -8 l 3 -8 l 3 8`} fill="#6e4426" />
|
||
))}
|
||
<ellipse cx={-15} cy={3} rx={7} ry={6} fill="#e9c9a0" />
|
||
<circle cx={-19} cy={3} r={1.8} fill="#2a1d14" />
|
||
</g>
|
||
)}
|
||
<rect x={-24} y={50} width={48} height={10} rx={4} fill="#b8433a" />
|
||
</svg>
|
||
</div>
|
||
);
|
||
|
||
// Weicher Schatten einer Figur an der Wand. Er steht hinter der Figur, ist um `k` vergrößert und
|
||
// liegt vom Licht weg verschoben (`versatz`: Anteil des Abstands Licht–Figur); je näher die Figur am Licht, desto größer (k aus `schattenGroesse`).
|
||
// Weich, rund und bläulich-dunkel, ohne harten Rand (Drehbuch: groß, aber nicht unheimlich).
|
||
export const schattenGroesse = (abstand: number) => Math.min(3.2, Math.max(1.05, 1 + 260 / Math.max(60, abstand)));
|
||
|
||
export const WandSchatten: React.FC<{
|
||
pose: string | Posenfolge;
|
||
x: number; // Fußpunkt der Figur
|
||
licht: {x: number; y: number};
|
||
hoehe: number;
|
||
k: number;
|
||
wand?: {x0: number; x1: number; oben: number; unten: number};
|
||
deckkraft?: number;
|
||
drehung?: number;
|
||
stauchY?: number;
|
||
versatz?: number;
|
||
}> = ({pose, x, licht, hoehe, k, wand = WAND, deckkraft = 0.42, drehung = 0, stauchY = 1, versatz = 0.35}) => (
|
||
<div style={{position: 'absolute', left: wand.x0, top: wand.oben, width: wand.x1 - wand.x0, height: wand.unten - wand.oben, overflow: 'hidden'}}>
|
||
<div style={{position: 'absolute', left: -wand.x0, top: -wand.oben}}>
|
||
<Figur
|
||
pose={pose}
|
||
x={x + (x - licht.x) * versatz}
|
||
y={wand.unten - 6}
|
||
hoehe={hoehe * k}
|
||
schatten={false}
|
||
drehung={drehung}
|
||
stauchY={stauchY}
|
||
deckkraft={deckkraft}
|
||
filter="brightness(0) blur(5px)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// Hand-Schatten an der Wand: Hase (zwei Ohren) und Igel (Faust mit gespreizten Fingern)
|
||
export const HandSchatten: React.FC<{art: 'hase' | 'igel'; x: number; y: number; s?: number; deckkraft?: number; wackeln?: number}> = ({art, x, y, s = 1, deckkraft = 0.45, wackeln = 0}) => (
|
||
<svg width={400} height={400} viewBox="-200 -200 400 400" style={{position: 'absolute', left: x - 200, top: y - 200, transform: `scale(${s})`, overflow: 'visible', opacity: deckkraft, filter: 'blur(4px)'}}>
|
||
{art === 'hase' ? (
|
||
<g fill="#1d1a2e">
|
||
<ellipse cx={0} cy={40} rx={95} ry={70} />
|
||
<ellipse cx={-40} cy={-60} rx={22} ry={80} transform={`rotate(${-12 + wackeln * 6} -40 0)`} />
|
||
<ellipse cx={30} cy={-62} rx={22} ry={82} transform={`rotate(${10 - wackeln * 6} 30 0)`} />
|
||
<ellipse cx={-80} cy={50} rx={40} ry={30} />
|
||
<rect x={-20} y={90} width={70} height={120} rx={20} />
|
||
</g>
|
||
) : (
|
||
<g fill="#1d1a2e">
|
||
<ellipse cx={0} cy={40} rx={100} ry={64} />
|
||
{[-70, -35, 0, 35, 70].map((dx, i) => (
|
||
<ellipse key={dx} cx={dx * 1.05} cy={-30} rx={16} ry={56} transform={`rotate(${dx * 0.35 + (i % 2 ? wackeln * 4 : -wackeln * 4)} ${dx} 20)`} />
|
||
))}
|
||
<ellipse cx={-105} cy={60} rx={34} ry={24} />
|
||
<rect x={-10} y={90} width={70} height={120} rx={20} />
|
||
</g>
|
||
)}
|
||
</svg>
|
||
);
|
||
|
||
// Wo die Laterne in der Pose noa_A_laterne hängt, und wie groß unsere Laterne sein muss, um die orange
|
||
// Laterne des Bildes ganz abzudecken (gemessen: 131 × 134 Bildpunkte, Mitte bei 73/423 im Bild, Bild bei 339/156)
|
||
export const laterneInNoasHand = (noaX: number, noaY = BODEN) => ({
|
||
...punkt('noa', noaX, noaY, 339 + 73.5, 156 + 423),
|
||
s: (1.2 * NOA_HOEHE) / FIGUREN.noa.refHoehe,
|
||
});
|
||
|
||
// Abend-Dämmerung über der ganzen Szene, aufgehellt um die Laterne (warmes Licht, Rest bläulich)
|
||
export const Abenddunkel: React.FC<{licht: {x: number; y: number}; staerke?: number; radius?: number}> = ({licht, staerke = 0.4, radius = 500}) => (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
left: -400,
|
||
top: -400,
|
||
width: 2720,
|
||
height: 1880,
|
||
pointerEvents: 'none',
|
||
background: `radial-gradient(circle at ${licht.x + 400}px ${licht.y + 400}px, rgba(255,210,130,0.12) 0px, rgba(30,34,70,0) ${radius * 0.45}px, rgba(24,28,62,${staerke}) ${radius * 1.6}px)`,
|
||
}}
|
||
/>
|
||
);
|