Speed up timeline load with portrait thumbs, bootstrap API, and caching.

Add catalog bootstrap endpoint, portrait thumbnail pipeline, lazy queued timeline images, gzip compression, and 3D texture throttling with code-split VirtualGallery.
This commit is contained in:
Danila Khodjaef
2026-07-06 14:27:29 +03:00
parent bdddadc4d6
commit 99b8559607
117 changed files with 646 additions and 171 deletions
+16 -38
View File
@@ -10,7 +10,8 @@ import type {
ArtistNavigation,
MovementArtistGroup,
} from '../types';
import { galleryImageUrlWithRevision, imageUrl, api, preloadArtistImages } from '../api/client';
import { galleryImageUrlWithRevision, imageUrl, api } from '../api/client';
import { loadTextureQueued } from '../utils/textureLoadQueue';
import { comparePaintingsChronological, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
import { cloneSurfaceTexture, getSurfaceTexture } from '../utils/galleryProceduralTextures';
import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles';
@@ -561,11 +562,9 @@ function usePaintingTexture(url: string | null) {
setFailed(false);
let disposed = false;
let loaded: THREE.Texture | null = null;
const loader = new THREE.TextureLoader();
loader.setCrossOrigin('anonymous');
loader.load(
url,
(tex) => {
loadTextureQueued(url)
.then((tex) => {
if (disposed) {
tex.dispose();
return;
@@ -580,12 +579,10 @@ function usePaintingTexture(url: string | null) {
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
setTexture(tex);
},
undefined,
() => {
})
.catch(() => {
if (!disposed) setFailed(true);
}
);
});
return () => {
disposed = true;
@@ -1675,32 +1672,13 @@ export default function VirtualGallery(props: Props) {
return;
}
let cancelled = false;
const artistId = props.data.artist.id;
(async () => {
try {
setSyncStatus('Syncing images…');
const timeout = new Promise<void>((resolve) => setTimeout(resolve, 2000));
await Promise.race([preloadArtistImages(artistId), timeout]);
const fresh = await api.getArtist(artistId);
if (!cancelled) {
setPaintings(fresh.paintings);
setPeriods(fresh.periods);
const withImg = fresh.paintings.filter((p) => p.image_path || p.thumbnail_path).length;
setSyncStatus(
withImg < fresh.paintings.length
? `${withImg} of ${fresh.paintings.length} works have images`
: ''
);
}
} catch {
if (!cancelled) setSyncStatus('');
}
})();
return () => {
cancelled = true;
};
}, [hallKey, props.mode, initialPaintings.length, props.mode === 'artist' ? props.data.artist.id : null]);
const withImg = initialPaintings.filter((p) => p.image_path || p.thumbnail_path).length;
setSyncStatus(
withImg < initialPaintings.length
? `${withImg} of ${initialPaintings.length} works have images`
: ''
);
}, [hallKey, props.mode, initialPaintings]);
const interiorStyle = useMemo(
() => (isMovement ? resolveMovementInteriorStyle(props.data.movement) : undefined),
@@ -2017,7 +1995,7 @@ export default function VirtualGallery(props: Props) {
intensity={interiorStyle ? 0.85 : 0.65}
color={interiorStyle?.sunLight ?? '#fff8f0'}
castShadow
shadow-mapSize={[2048, 2048]}
shadow-mapSize={[1024, 1024]}
/>
<Suspense fallback={null}>
<Environment