From 1b2241df49de998fa7712f969d96504109db6c99 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 25 May 2026 17:57:51 +0200 Subject: [PATCH] feat: add gallery lighting controls --- docs/user/gallery.md | 5 +- src/index.css | 105 ++++++++++++++++++++++++++ src/pages/gallery/page.tsx | 147 +++++++++++++++++++++++++++++++++++-- 3 files changed, 251 insertions(+), 6 deletions(-) diff --git a/docs/user/gallery.md b/docs/user/gallery.md index d14e59d..d3e958a 100644 --- a/docs/user/gallery.md +++ b/docs/user/gallery.md @@ -11,7 +11,8 @@ Cette page sert à remercier et valoriser le travail des designers du projet La 1. Ouvrir `/gallery`. 2. Utiliser les flèches en bas de l'écran pour passer au modèle précédent ou suivant. 3. Tourner autour du modèle avec la souris ou le doigt. -4. Lire le diagnostic texture discret pour savoir si le modèle chargé semble correct côté textures. +4. Utiliser le bouton de réglages à droite pour ouvrir ou fermer le panneau lumière. +5. Lire le diagnostic texture discret pour savoir si le modèle chargé semble correct côté textures. ## Fonctionnement @@ -20,6 +21,8 @@ Cette page sert à remercier et valoriser le travail des designers du projet La - `OrbitControls` permet de manipuler la caméra autour du modèle. - `Bounds` et `Center` recadrent automatiquement le modèle actif. - `SkyModel` réutilise la skybox du jeu, avec un matériau non éclairé uniquement dans la galerie pour éviter que certaines faces deviennent noires avec une caméra orbitale libre. +- Les lumières reprennent les valeurs par défaut du jeu, puis peuvent être ajustées dans le panneau latéral. +- `OrbitControls` autorise une orbite verticale complète pour inspecter le dessous des modèles. - Les animations GLTF présentes dans un modèle sont lancées automatiquement. - Un diagnostic simple inspecte les matériaux chargés pour signaler les textures absentes ou non exploitables. diff --git a/src/index.css b/src/index.css index cc7bc18..475103a 100644 --- a/src/index.css +++ b/src/index.css @@ -169,6 +169,107 @@ canvas { color: rgba(226, 232, 240, 0.72); } +.gallery-light-panel { + position: absolute; + top: 108px; + right: 0; + z-index: 3; + display: flex; + align-items: flex-start; + transform: translateX(260px); + transition: transform 180ms ease; +} + +.gallery-light-panel.is-open { + transform: translateX(0); +} + +.gallery-light-panel-toggle { + display: grid; + place-items: center; + width: 42px; + height: 42px; + border: 1px solid rgba(248, 250, 252, 0.14); + border-right: 0; + border-radius: 999px 0 0 999px; + background: rgba(3, 7, 18, 0.68); + color: rgba(248, 250, 252, 0.84); + cursor: pointer; + backdrop-filter: blur(14px); +} + +.gallery-light-panel-toggle:hover, +.gallery-light-panel-toggle:focus-visible { + color: #f8fafc; + outline: none; +} + +.gallery-light-panel-content { + width: 236px; + padding: 16px; + border: 1px solid rgba(248, 250, 252, 0.14); + border-right: 0; + border-radius: 18px 0 0 18px; + background: rgba(3, 7, 18, 0.72); + box-shadow: 0 18px 52px rgba(0, 0, 0, 0.28); + backdrop-filter: blur(18px); +} + +.gallery-light-panel-content header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.gallery-light-panel-content header span { + color: rgba(248, 250, 252, 0.86); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.18em; +} + +.gallery-light-panel-content header button { + border: 0; + background: transparent; + color: rgba(203, 213, 225, 0.72); + cursor: pointer; + font-size: 12px; + font-weight: 700; +} + +.gallery-light-panel-content header button:hover, +.gallery-light-panel-content header button:focus-visible { + color: #f8fafc; + outline: none; +} + +.gallery-light-control { + display: grid; + gap: 8px; + margin-top: 12px; +} + +.gallery-light-control span { + display: flex; + align-items: center; + justify-content: space-between; + color: rgba(226, 232, 240, 0.78); + font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; + font-weight: 700; +} + +.gallery-light-control strong { + color: rgba(248, 250, 252, 0.88); + font-variant-numeric: tabular-nums; +} + +.gallery-light-control input { + width: 100%; + accent-color: #dbeafe; +} + @media (max-width: 720px) { .gallery-title { right: 50%; @@ -196,6 +297,10 @@ canvas { left: auto; transform: translateX(50%); } + + .gallery-light-panel { + top: 78px; + } } /* Docs layout */ diff --git a/src/pages/gallery/page.tsx b/src/pages/gallery/page.tsx index b32c670..92f0378 100644 --- a/src/pages/gallery/page.tsx +++ b/src/pages/gallery/page.tsx @@ -19,27 +19,53 @@ import { ArrowLeft, ArrowRight, CheckCircle2, + SlidersHorizontal, TriangleAlert, } from "lucide-react"; import * as THREE from "three"; import { SkyModel } from "@/components/three/world/SkyModel"; import { galleryModels, type GalleryModel } from "@/data/galleryModels"; +import { + AMBIENT_LIGHT_COLOR, + LIGHTING_DEFAULTS, + SUN_LIGHT_COLOR, +} from "@/data/world/lightingConfig"; import { GAME_SCENE_FALLBACK_SKY_MODEL_PATH, GAME_SCENE_FALLBACK_SKY_MODEL_SCALE, GAME_SCENE_SKY_MODEL_PATH, GAME_SCENE_SKY_MODEL_SCALE, } from "@/data/world/environmentConfig"; -import { Lighting } from "@/world/Lighting"; interface GalleryModelProps { model: GalleryModel; } interface GallerySceneProps extends GalleryModelProps { + lighting: GalleryLightingConfig; onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void; } +interface GalleryModelPreviewProps extends GalleryModelProps { + onTextureDiagnosticReady: (diagnostic: TextureDiagnostic) => void; +} + +interface GalleryLightingConfig { + ambientIntensity: number; + sunIntensity: number; + sunX: number; + sunY: number; + sunZ: number; +} + +interface GalleryLightControl { + key: keyof GalleryLightingConfig; + label: string; + min: number; + max: number; + step: number; +} + interface TextureDiagnostic { modelId: string | null; status: "loading" | "ok" | "warning"; @@ -71,6 +97,14 @@ const LOADING_TEXTURE_DIAGNOSTIC: TextureDiagnostic = { summary: "Analyse des textures...", }; +const GALLERY_LIGHT_CONTROLS: GalleryLightControl[] = [ + { key: "ambientIntensity", label: "Ambiance", min: 0, max: 5, step: 0.1 }, + { key: "sunIntensity", label: "Soleil", min: 0, max: 8, step: 0.1 }, + { key: "sunX", label: "Soleil X", min: -100, max: 100, step: 1 }, + { key: "sunY", label: "Soleil Y", min: -100, max: 150, step: 1 }, + { key: "sunZ", label: "Soleil Z", min: -100, max: 100, step: 1 }, +]; + class GalleryViewerErrorBoundary extends Component< GalleryViewerErrorBoundaryProps, GalleryViewerErrorBoundaryState @@ -106,7 +140,7 @@ class GalleryViewerErrorBoundary extends Component< function GalleryModelPreview({ model, onTextureDiagnosticReady, -}: GallerySceneProps): React.JSX.Element { +}: GalleryModelPreviewProps): React.JSX.Element { const groupRef = useRef(null); const { animations, scene } = useGLTF(model.path); const modelScene = useMemo(() => scene.clone(true), [scene]); @@ -140,6 +174,7 @@ function GalleryModelPreview({ } function GalleryScene({ + lighting, model, onTextureDiagnosticReady, }: GallerySceneProps): React.JSX.Element { @@ -153,7 +188,7 @@ function GalleryScene({ scale={GAME_SCENE_SKY_MODEL_SCALE} unlit /> - +
+ + ); +} + +function GalleryLighting({ + lighting, +}: { + lighting: GalleryLightingConfig; +}): React.JSX.Element { + return ( + <> + + ); @@ -192,6 +247,62 @@ function TextureStatusBadge({ ); } +function GalleryLightingPanel({ + lighting, + onChange, + onReset, + onToggle, + open, +}: { + lighting: GalleryLightingConfig; + onChange: (key: keyof GalleryLightingConfig, value: number) => void; + onReset: () => void; + onToggle: () => void; + open: boolean; +}): React.JSX.Element { + return ( + + ); +} + function getTextureDiagnostic( modelId: string, modelScene: THREE.Object3D, @@ -247,6 +358,10 @@ function getTextureDiagnostic( export function GalleryPage(): React.JSX.Element { const [activeModelIndex, setActiveModelIndex] = useState(0); + const [lightPanelOpen, setLightPanelOpen] = useState(false); + const [lighting, setLighting] = useState({ + ...LIGHTING_DEFAULTS, + }); const [textureDiagnostic, setTextureDiagnostic] = useState( LOADING_TEXTURE_DIAGNOSTIC, ); @@ -269,6 +384,20 @@ export function GalleryPage(): React.JSX.Element { ); }; + const handleLightChange = ( + key: keyof GalleryLightingConfig, + value: number, + ): void => { + setLighting((currentLighting) => ({ + ...currentLighting, + [key]: value, + })); + }; + + const resetLighting = (): void => { + setLighting({ ...LIGHTING_DEFAULTS }); + }; + return (

GALERIE

@@ -278,6 +407,7 @@ export function GalleryPage(): React.JSX.Element { @@ -310,6 +440,13 @@ export function GalleryPage(): React.JSX.Element { + setLightPanelOpen((open) => !open)} + open={lightPanelOpen} + />
); }