81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import Image from "next/image"
|
|
|
|
interface RoomBackgroundProps {
|
|
src: string
|
|
durationMs?: number
|
|
}
|
|
|
|
interface Layer {
|
|
id: number
|
|
src: string
|
|
}
|
|
|
|
// Stacks background images and crossfades between them whenever `src`
|
|
// changes. Each new layer only starts fading in once it has actually
|
|
// finished loading, and layers below a fully-faded-in layer are dropped
|
|
// on transitionend rather than a guessed timeout — this keeps rapid
|
|
// room switches (and slow/cold-cache images) visually consistent.
|
|
export function RoomBackground({ src, durationMs = 700 }: RoomBackgroundProps) {
|
|
const idRef = useRef(0)
|
|
const [layers, setLayers] = useState<Layer[]>(() => [{ id: idRef.current, src }])
|
|
const [revealed, setRevealed] = useState<Set<number>>(() => new Set([idRef.current]))
|
|
|
|
useEffect(() => {
|
|
setLayers((prev) => {
|
|
if (prev[prev.length - 1]?.src === src) return prev
|
|
idRef.current += 1
|
|
return [...prev, { id: idRef.current, src }]
|
|
})
|
|
}, [src])
|
|
|
|
const reveal = (id: number) => {
|
|
setRevealed((prev) => {
|
|
if (prev.has(id)) return prev
|
|
const next = new Set(prev)
|
|
next.add(id)
|
|
return next
|
|
})
|
|
}
|
|
|
|
const handleTopLayerTransitionEnd = (id: number) => {
|
|
// Once the newest layer has finished fading in, everything below it
|
|
// is fully hidden and can be dropped.
|
|
setLayers((prev) => {
|
|
const idx = prev.findIndex((l) => l.id === id)
|
|
if (idx <= 0) return prev
|
|
return prev.slice(idx)
|
|
})
|
|
}
|
|
|
|
const topId = layers[layers.length - 1]?.id
|
|
|
|
return (
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
{layers.map((layer, i) => {
|
|
const isTop = i === layers.length - 1
|
|
const isRevealed = revealed.has(layer.id)
|
|
return (
|
|
<Image
|
|
key={layer.id}
|
|
src={layer.src || "/placeholder.svg"}
|
|
alt=""
|
|
fill
|
|
priority
|
|
className="object-cover transition-opacity ease-in-out"
|
|
style={{
|
|
transitionDuration: `${durationMs}ms`,
|
|
opacity: isRevealed ? 1 : 0,
|
|
}}
|
|
onLoadingComplete={() => reveal(layer.id)}
|
|
onTransitionEnd={() => {
|
|
if (isTop && layer.id === topId) handleTopLayerTransitionEnd(layer.id)
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
} |