Add movement gallery wings with period interiors and expand timeline features.

Movement galleries split large catalogs into chronological wings (~55 works), use era-themed 3D interiors with side-wall windows, wing navigator on the back exit, and front archways between wings. Also adds painting annotations, timeline event guides, portrait hover highlights, and documentation/API updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-21 16:54:19 +03:00
co-authored by Cursor
parent 0972b5df99
commit df29848d89
121 changed files with 4765 additions and 396 deletions
+259
View File
@@ -0,0 +1,259 @@
import { useMemo } from 'react';
import * as THREE from 'three';
import type { GalleryWindowSpec, GalleryWindowStyle } from '../data/movement-interior-styles';
const WALL_HEIGHT = 4.2;
interface Props {
windows: GalleryWindowSpec[];
halfW: number;
halfD: number;
trimColor: string;
}
function windowWorldPosition(
spec: GalleryWindowSpec,
halfW: number,
halfD: number
): { position: [number, number, number]; rotation: [number, number, number] } {
const inset = 0.1;
switch (spec.wall) {
case 'back':
return {
position: [spec.x, spec.y, -halfD + inset],
rotation: [0, 0, 0],
};
case 'left':
return {
position: [-halfW + inset, spec.y, spec.x],
rotation: [0, Math.PI / 2, 0],
};
case 'right':
return {
position: [halfW - inset, spec.y, spec.x],
rotation: [0, -Math.PI / 2, 0],
};
case 'ceiling':
return {
position: [spec.x, WALL_HEIGHT - 0.06, spec.x === 0 ? 0 : spec.x],
rotation: [Math.PI / 2, 0, 0],
};
default:
return { position: [0, spec.y, -halfD + inset], rotation: [0, 0, 0] };
}
}
function WindowFrame({
style,
width,
height,
trimColor,
}: {
style: GalleryWindowStyle;
width: number;
height: number;
trimColor: string;
}) {
const frameW = 0.08;
const depth = 0.12;
const arch =
style === 'roman-arch' || style === 'gothic-lancet' || style === 'art-nouveau';
return (
<group>
{/* Outer frame */}
<mesh position={[0, 0, -depth / 2]}>
<boxGeometry args={[width + frameW * 2, height + frameW * 2, depth]} />
<meshStandardMaterial color={trimColor} roughness={0.35} metalness={0.45} />
</mesh>
{arch && style === 'gothic-lancet' && (
<mesh position={[0, height / 2 + 0.15, -depth / 2 + 0.02]}>
<coneGeometry args={[width / 2 + frameW, 0.5, 4]} />
<meshStandardMaterial color={trimColor} roughness={0.4} metalness={0.35} />
</mesh>
)}
{style === 'roman-arch' && (
<mesh position={[0, height / 2 + 0.05, -depth / 2 + 0.02]} rotation={[0, 0, Math.PI]}>
<sphereGeometry args={[width / 2, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2]} />
<meshStandardMaterial color={trimColor} roughness={0.4} metalness={0.35} />
</mesh>
)}
{style === 'factory' && (
<>
{[-width / 3, 0, width / 3].map((ox) => (
<mesh key={ox} position={[ox, 0, -depth / 2 + 0.02]}>
<boxGeometry args={[0.06, height, depth + 0.02]} />
<meshStandardMaterial color="#3a3a3a" roughness={0.5} metalness={0.6} />
</mesh>
))}
{[height / 4, -height / 4].map((oy) => (
<mesh key={oy} position={[0, oy, -depth / 2 + 0.02]}>
<boxGeometry args={[width, 0.06, depth + 0.02]} />
<meshStandardMaterial color="#3a3a3a" roughness={0.5} metalness={0.6} />
</mesh>
))}
</>
)}
{style === 'glass-block' &&
Array.from({ length: 4 }, (_, row) =>
Array.from({ length: 3 }, (_, col) => (
<mesh
key={`${row}-${col}`}
position={[
-width / 3 + col * (width / 3),
-height / 4 + row * (height / 4),
-depth / 2 + 0.03,
]}
>
<boxGeometry args={[width / 3 - 0.06, height / 4 - 0.06, 0.04]} />
<meshStandardMaterial
color="#d0e8f8"
roughness={0.1}
metalness={0.05}
transparent
opacity={0.75}
/>
</mesh>
))
)}
{style === 'sash' && (
<mesh position={[0, 0, -depth / 2 + 0.03]}>
<boxGeometry args={[0.05, height, depth]} />
<meshStandardMaterial color={trimColor} roughness={0.45} metalness={0.3} />
</mesh>
)}
{style === 'baroque-pair' && (
<mesh position={[0, 0, -depth / 2 + 0.03]}>
<boxGeometry args={[0.06, height, depth]} />
<meshStandardMaterial color={trimColor} roughness={0.3} metalness={0.55} />
</mesh>
)}
</group>
);
}
function SingleWindow({
spec,
trimColor,
}: {
spec: GalleryWindowSpec;
trimColor: string;
}) {
const glassColor = useMemo(() => new THREE.Color(spec.lightColor), [spec.lightColor]);
return (
<group>
<WindowFrame style={spec.style} width={spec.width} height={spec.height} trimColor={trimColor} />
{/* Sky / daylight pane */}
<mesh position={[0, 0, 0.02]}>
<planeGeometry args={[spec.width - 0.12, spec.height - 0.12]} />
<meshStandardMaterial
color={spec.lightColor}
emissive={spec.lightColor}
emissiveIntensity={0.85}
roughness={0.05}
metalness={0.02}
toneMapped={false}
transparent
opacity={0.92}
/>
</mesh>
{/* Soft sky gradient overlay */}
<mesh position={[0, spec.height * 0.15, 0.03]}>
<planeGeometry args={[spec.width - 0.2, spec.height * 0.5]} />
<meshBasicMaterial color="#ffffff" transparent opacity={0.25} toneMapped={false} />
</mesh>
{/* Daylight into room */}
<spotLight
position={[0, 0, 0.15]}
angle={Math.min(1.2, (spec.width / Math.max(spec.height, 0.5)) * 0.55)}
penumbra={0.95}
intensity={spec.lightIntensity}
distance={14}
color={spec.lightColor}
castShadow={false}
/>
<pointLight
position={[0, 0, 0.25]}
intensity={spec.lightIntensity * 0.45}
distance={10}
color={glassColor}
decay={2}
/>
</group>
);
}
export default function GalleryWindows({ windows, halfW, halfD, trimColor }: Props) {
return (
<group>
{windows.map((spec, i) => {
const { position, rotation } = windowWorldPosition(spec, halfW, halfD);
return (
<group key={`${spec.wall}-${spec.x}-${i}`} position={position} rotation={rotation}>
<SingleWindow spec={spec} trimColor={trimColor} />
</group>
);
})}
</group>
);
}
/** Ceiling-mounted gallery track lights for even illumination. */
export function GalleryTrackLights({
width,
depth,
intensity,
color,
}: {
width: number;
depth: number;
intensity: number;
color: string;
}) {
const positions = useMemo(() => {
const pts: [number, number, number][] = [];
const cols = Math.max(2, Math.min(5, Math.floor(width / 4)));
const rows = Math.max(1, Math.min(3, Math.floor(depth / 6)));
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const x = -width / 2 + (width / (cols + 1)) * (c + 1);
const z = -depth / 2 + (depth / (rows + 1)) * (r + 1);
pts.push([x, WALL_HEIGHT - 0.15, z]);
}
}
return pts;
}, [width, depth]);
return (
<group>
{positions.map(([x, y, z], i) => (
<group key={i} position={[x, y, z]}>
<mesh rotation={[Math.PI, 0, 0]}>
<cylinderGeometry args={[0.02, 0.025, 0.12, 8]} />
<meshStandardMaterial color="#2a2a2a" metalness={0.8} roughness={0.25} />
</mesh>
<spotLight
position={[0, -0.04, 0]}
angle={0.55}
penumbra={0.85}
intensity={intensity}
distance={8}
color={color}
castShadow={false}
/>
</group>
))}
</group>
);
}