このコミットが含まれているのは:
2026-06-16 22:31:03 +09:00
コミット cda90b76d2
+99 -187
ファイルの表示
@@ -2753,26 +2753,13 @@ const GekanatorBackdrop: FC<{
nextThumbnails) nextThumbnails)
const [flipVisualSeed, setFlipVisualSeed] = useState (visualSeed) const [flipVisualSeed, setFlipVisualSeed] = useState (visualSeed)
const [isFlippingTiles, setIsFlippingTiles] = useState (false) const [isFlippingTiles, setIsFlippingTiles] = useState (false)
const [isCrossfading, setIsCrossfading] = useState (false)
const isLeavingGuessBackdrop = displayedBackdropMode === 'guess' && backdropMode !== 'guess' const renderedSettings = settingsForMode (displayedBackdropMode)
const renderBackdropMode = isLeavingGuessBackdrop ? backdropMode : displayedBackdropMode
const renderWinningRunCount = (isLeavingGuessBackdrop
? winningRunQuestionCount
: displayedWinningRunCount)
const renderThumbnails = isLeavingGuessBackdrop ? nextThumbnails : displayedThumbnails
const renderIsCrossfading = isCrossfading && !(isLeavingGuessBackdrop)
const renderedSettings = settingsForMode (renderBackdropMode)
const renderedTileCount = renderedSettings.columns * renderedSettings.rows const renderedTileCount = renderedSettings.columns * renderedSettings.rows
const renderedScale = scaleForMode (renderBackdropMode, renderWinningRunCount) const renderedScale = scaleForMode (displayedBackdropMode, displayedWinningRunCount)
const isGuessPresentation = backdropMode === 'guess' const isGuessPresentation =
const crossfadeDuration = motionMode === 'calm' ? .95 : .75 backdropMode === 'guess' || displayedBackdropMode === 'guess'
useEffect (() => { useEffect (() => {
guessAnimationControlsRef.current.forEach (control => control.stop ()) guessAnimationControlsRef.current.forEach (control => control.stop ())
@@ -2866,7 +2853,6 @@ const GekanatorBackdrop: FC<{
{ {
applyDirection () applyDirection ()
setIsFlippingTiles (false) setIsFlippingTiles (false)
setIsCrossfading (false)
setFlipVisualSeed (visualSeed) setFlipVisualSeed (visualSeed)
return return
} }
@@ -2874,7 +2860,6 @@ const GekanatorBackdrop: FC<{
if (backdropMode === 'guess' && guessThumbnail) if (backdropMode === 'guess' && guessThumbnail)
{ {
setIsFlippingTiles (false) setIsFlippingTiles (false)
setIsCrossfading (false)
setDisplayedBackdropMode ('guess') setDisplayedBackdropMode ('guess')
setDisplayedWinningRunCount (winningRunQuestionCount) setDisplayedWinningRunCount (winningRunQuestionCount)
setDisplayedThumbnails (nextThumbnails) setDisplayedThumbnails (nextThumbnails)
@@ -2894,7 +2879,6 @@ const GekanatorBackdrop: FC<{
setFromThumbnails (nextThumbnails) setFromThumbnails (nextThumbnails)
setToThumbnails (nextThumbnails) setToThumbnails (nextThumbnails)
setIsFlippingTiles (false) setIsFlippingTiles (false)
setIsCrossfading (false)
setFlipVisualSeed (visualSeed) setFlipVisualSeed (visualSeed)
return return
} }
@@ -2903,23 +2887,6 @@ const GekanatorBackdrop: FC<{
{ {
applyDirection () applyDirection ()
setIsFlippingTiles (false) setIsFlippingTiles (false)
setIsCrossfading (false)
setFlipVisualSeed (visualSeed)
return
}
if (displayedBackdropMode === 'guess' && backdropMode !== 'guess')
{
applyDirection ()
x.set (0)
y.set (0)
setIsFlippingTiles (false)
setIsCrossfading (false)
setDisplayedBackdropMode (backdropMode)
setDisplayedWinningRunCount (winningRunQuestionCount)
setDisplayedThumbnails (nextThumbnails)
setFromThumbnails (nextThumbnails)
setToThumbnails (nextThumbnails)
setFlipVisualSeed (visualSeed) setFlipVisualSeed (visualSeed)
return return
} }
@@ -2942,10 +2909,7 @@ const GekanatorBackdrop: FC<{
setFromThumbnails (currentThumbnails) setFromThumbnails (currentThumbnails)
setToThumbnails (nextThumbnails) setToThumbnails (nextThumbnails)
const shouldCrossfade = setIsFlippingTiles (true)
displayedBackdropMode === 'winning_run' && backdropMode === 'normal'
setIsCrossfading (shouldCrossfade)
setIsFlippingTiles (!(shouldCrossfade))
flipTimerRef.current = window.setTimeout (() => { flipTimerRef.current = window.setTimeout (() => {
setDisplayedBackdropMode (backdropMode) setDisplayedBackdropMode (backdropMode)
@@ -2954,11 +2918,10 @@ const GekanatorBackdrop: FC<{
setFromThumbnails (nextThumbnails) setFromThumbnails (nextThumbnails)
setToThumbnails (nextThumbnails) setToThumbnails (nextThumbnails)
setIsFlippingTiles (false) setIsFlippingTiles (false)
setIsCrossfading (false)
applyDirection () applyDirection ()
setFlipVisualSeed (visualSeed) setFlipVisualSeed (visualSeed)
flipTimerRef.current = null flipTimerRef.current = null
}, (shouldCrossfade ? crossfadeDuration : tileFlipDuration) * 1000) }, tileFlipDuration * 1000)
return () => { return () => {
if (flipTimerRef.current !== null) if (flipTimerRef.current !== null)
@@ -2978,126 +2941,10 @@ const GekanatorBackdrop: FC<{
visualSeed, visualSeed,
activeDirection, activeDirection,
winningRunQuestionCount, winningRunQuestionCount,
crossfadeDuration,
tileFlipDuration, tileFlipDuration,
x, x,
y]) y])
const renderTileSet = (
{ mode,
thumbnails,
settings,
tileCount,
scale,
opacity,
withFlip }: { mode: 'normal' | 'winning_run' | 'guess'
thumbnails: string[]
settings: { columns: number; rows: number; opacity: number }
tileCount: number
scale: number
opacity?: number
withFlip?: boolean }) => {
const guessModeFlg = mode === 'guess'
return (
<motion.div
className="absolute inset-0"
initial={guessModeFlg
? false
: (opacity == null
? undefined
: { opacity: opacity === 0 ? 1 : 0, scale, x: '0%', y: '0%' })}
animate={{ opacity: opacity ?? 1,
scale,
x: guessModeFlg ? guessFocusOffset.x : '0%',
y: guessModeFlg ? guessFocusOffset.y : '0%' }}
transition={guessModeFlg
? { duration: 0 }
: (mode === 'winning_run'
? { duration: crossfadeDuration, ease: [.16, 1, .3, 1] }
: { duration: .2 })}>
{Array.from ({ length: 9 }, (_, duplicate) => {
const column = duplicate % 3
const row = Math.floor (duplicate / 3)
return (
<motion.div
key={`${ mode }:${ duplicate }`}
className="absolute grid overflow-hidden"
layout={mode === 'winning_run'}
style={{
left: `${ column * 33.333333 }%`,
top: `${ row * 33.333333 }%`,
width: '33.333333%',
height: '33.333333%',
gridTemplateColumns:
`repeat(${ settings.columns }, minmax(0, 1fr))`,
gridTemplateRows:
`repeat(${ settings.rows }, minmax(0, 1fr))` }}
transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}>
{Array.from ({ length: tileCount }, (_, index) => {
const currentThumbnail =
thumbnails[index % Math.max (thumbnails.length, 1)]
const frontThumbnail =
withFlip
? fromThumbnails[index % Math.max (fromThumbnails.length, 1)]
: currentThumbnail
const backThumbnail =
withFlip
? toThumbnails[index % Math.max (toThumbnails.length, 1)]
: currentThumbnail
const thumbnail = currentThumbnail
if (!(thumbnail) || !(frontThumbnail) || !(backThumbnail))
return null
return (
<motion.div
key={`${ mode }:${ duplicate }:${ index }`}
className="relative overflow-hidden"
layout={mode === 'winning_run'}
transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}
style={{ perspective: 1600 }}>
{(mode !== 'normal' || !(withFlip))
? (
<img
src={['intro', 'end'].includes (phase)
? mascotAsset
: thumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{ opacity: settings.opacity }}/>)
: (
<motion.div
className="absolute inset-0"
initial={{ rotateY: 0 }}
animate={{ rotateY: 180 }}
transition={{
duration: tileFlipDuration,
ease: 'easeInOut' }}
style={{ transformStyle: 'preserve-3d' }}>
<img
src={backThumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{
backfaceVisibility: 'hidden',
opacity: settings.opacity,
transform: 'rotateY(180deg)' }}/>
<img
src={frontThumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{
backfaceVisibility: 'hidden',
opacity: settings.opacity }}/>
</motion.div>)}
</motion.div>)
})}
</motion.div>)
})}
</motion.div>)
}
if (motionMode === 'off' || nextThumbnails.length === 0) if (motionMode === 'off' || nextThumbnails.length === 0)
return ( return (
<div className="absolute inset-0 bg-gradient-to-br from-yellow-50 via-white <div className="absolute inset-0 bg-gradient-to-br from-yellow-50 via-white
@@ -3113,34 +2960,99 @@ const GekanatorBackdrop: FC<{
width: 'calc(max(100vw, 100vh) * 3)', width: 'calc(max(100vw, 100vh) * 3)',
height: 'calc(max(100vw, 100vh) * 3)' }}> height: 'calc(max(100vw, 100vh) * 3)' }}>
<motion.div <motion.div
className="relative h-full w-full"> className="relative h-full w-full"
{renderIsCrossfading animate={{ scale: renderedScale,
? ( x: displayedBackdropMode === 'guess' ? guessFocusOffset.x : '0%',
<> y: displayedBackdropMode === 'guess' ? guessFocusOffset.y : '0%' }}
{renderTileSet ({ transition={(displayedBackdropMode === 'winning_run'
mode: displayedBackdropMode, || displayedBackdropMode === 'guess')
thumbnails: fromThumbnails, ? { duration: motionMode === 'calm' ? .95 : .75,
settings: settingsForMode (displayedBackdropMode), ease: [.16, 1, .3, 1] }
tileCount: (settingsForMode (displayedBackdropMode).columns : { duration: .2 }}>
* settingsForMode (displayedBackdropMode).rows), {Array.from ({ length: 9 }, (_, duplicate) => {
scale: scaleForMode (displayedBackdropMode, displayedWinningRunCount), const column = duplicate % 3
opacity: 0 })} const row = Math.floor (duplicate / 3)
{renderTileSet ({
mode: backdropMode, return (
thumbnails: toThumbnails, <motion.div
settings: targetSettings, key={duplicate}
tileCount: targetTileCount, className="absolute grid overflow-hidden"
scale: scaleForMode (backdropMode, winningRunQuestionCount), layout={displayedBackdropMode !== 'normal'}
opacity: 1 })} style={{
</>) left: `${ column * 33.333333 }%`,
: ( top: `${ row * 33.333333 }%`,
renderTileSet ({ width: '33.333333%',
mode: renderBackdropMode, height: '33.333333%',
thumbnails: renderThumbnails, gridTemplateColumns:
settings: renderedSettings, `repeat(${ renderedSettings.columns }, minmax(0, 1fr))`,
tileCount: renderedTileCount, gridTemplateRows:
scale: renderedScale, `repeat(${ renderedSettings.rows }, minmax(0, 1fr))` }}
withFlip: isFlippingTiles && !(isLeavingGuessBackdrop) }))} transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}>
{Array.from ({ length: renderedTileCount }, (_, index) => {
const currentThumbnail =
displayedThumbnails[
index % Math.max (displayedThumbnails.length, 1)]
const frontThumbnail =
isFlippingTiles
? fromThumbnails[index % Math.max (fromThumbnails.length, 1)]
: currentThumbnail
const backThumbnail =
isFlippingTiles
? toThumbnails[index % Math.max (toThumbnails.length, 1)]
: currentThumbnail
const thumbnail =
displayedBackdropMode === 'winning_run'
|| displayedBackdropMode === 'guess'
? nextThumbnails[index % Math.max (nextThumbnails.length, 1)]
: currentThumbnail
if (!(thumbnail) || !(frontThumbnail) || !(backThumbnail))
return null
return (
<motion.div
key={`${ duplicate }:${ index }`}
className="relative overflow-hidden"
layout={displayedBackdropMode !== 'normal'}
transition={{ duration: tileFlipDuration, ease: 'easeInOut' }}
style={{ perspective: 1600 }}>
{(displayedBackdropMode !== 'normal' || !(isFlippingTiles))
? (
<img
src={['intro', 'end'].includes (phase)
? mascotAsset
: thumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{ opacity: renderedSettings.opacity }}/>)
: (
<motion.div
className="absolute inset-0"
initial={{ rotateY: 0 }}
animate={{ rotateY: 180 }}
transition={{
duration: tileFlipDuration,
ease: 'easeInOut' }}
style={{ transformStyle: 'preserve-3d' }}>
<img
src={backThumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{
backfaceVisibility: 'hidden',
opacity: renderedSettings.opacity,
transform: 'rotateY(180deg)' }}/>
<img
src={frontThumbnail}
alt=""
className="absolute inset-0 h-full w-full object-cover"
style={{
backfaceVisibility: 'hidden',
opacity: renderedSettings.opacity }}/>
</motion.div>)}
</motion.div>)
})}
</motion.div>)
})}
</motion.div> </motion.div>
</motion.div> </motion.div>
</div> </div>