From c06d73fc6c56170fd02cdf5a0885fa690181fc33 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 14 Jun 2026 00:53:11 +0900 Subject: [PATCH] #361 --- frontend/src/pages/GekanatorPage.tsx | 131 ++++++++++++++++++--------- 1 file changed, 88 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/GekanatorPage.tsx b/frontend/src/pages/GekanatorPage.tsx index 8fa5bbd..12246b5 100644 --- a/frontend/src/pages/GekanatorPage.tsx +++ b/frontend/src/pages/GekanatorPage.tsx @@ -1,4 +1,4 @@ -import { motion, useMotionTemplate, useMotionValue } from 'framer-motion' +import { animate, motion, useMotionTemplate, useMotionValue } from 'framer-motion' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Helmet } from 'react-helmet-async' @@ -2660,6 +2660,21 @@ const GekanatorBackdrop: FC<{ winningRunTargetPost = null, winningRunQuestionCount = 0, }) => { + const guessFocusOffset = useMemo (() => { + const focusTiles = [ + { x: 'calc(max(100vw, 100vh) * 0.5)', + y: 'calc(max(100vw, 100vh) * 0.5)' }, + { x: 'calc(max(100vw, 100vh) * -0.5)', + y: 'calc(max(100vw, 100vh) * 0.5)' }, + { x: 'calc(max(100vw, 100vh) * 0.5)', + y: 'calc(max(100vw, 100vh) * -0.5)' }, + { x: 'calc(max(100vw, 100vh) * -0.5)', + y: 'calc(max(100vw, 100vh) * -0.5)' }] + + return (focusTiles[Math.abs (hashString (`${ visualSeed }:guess-focus`)) % focusTiles.length] + ?? focusTiles[0]) + }, [visualSeed]) + const directions = useMemo ( () => [ { x: 0, y: -33.333333 }, @@ -2686,6 +2701,7 @@ const GekanatorBackdrop: FC<{ : isWinningRunBackdrop ? 'winning_run' : 'normal' + const normalVisiblePosts = useMemo ( () => posts .filter (post => Boolean (backgroundThumbnailUrl (post))) @@ -2694,27 +2710,35 @@ const GekanatorBackdrop: FC<{ - hashString (`${ visualSeed }:${ right.id }`)) .slice (0, motionMode === 'calm' ? 24 : 36), [posts, visualSeed, motionMode]) - const settingsForMode = useCallback (( - mode: 'normal' | 'winning_run' | 'guess', - ): { columns: number; rows: number; opacity: number } => { - if (mode === 'winning_run') - return { columns: 8, rows: 8, opacity: motionMode === 'calm' ? .18 : .24 } - if (mode === 'guess') - return { columns: 1, rows: 1, opacity: motionMode === 'calm' ? .18 : .24 } - return motionMode === 'calm' - ? { columns: 7, rows: 7, opacity: .14 } - : { columns: 10, rows: 10, opacity: .2 } - }, [motionMode]) - const scaleForMode = useCallback (( - mode: 'normal' | 'winning_run' | 'guess', - displayedWinningCount: number, - ): number => { - if (mode === 'winning_run') - return [1, 8 / 6, 8 / 4, 8 / 2][ - Math.max (0, Math.min (3, displayedWinningCount))] - ?? 1 - return 1 - }, []) + + const settingsForMode = useCallback ( + ( + mode: 'normal' | 'winning_run' | 'guess', + ): { columns: number; rows: number; opacity: number } => { + if (mode === 'winning_run' || mode === 'guess') + return { columns: 8, rows: 8, opacity: motionMode === 'calm' ? .18 : .24 } + + return motionMode === 'calm' + ? { columns: 7, rows: 7, opacity: .14 } + : { columns: 10, rows: 10, opacity: .2 } + }, + [motionMode]) + + const scaleForMode = useCallback ( + ( + mode: 'normal' | 'winning_run' | 'guess', + displayedWinningCount: number, + ): number => { + if (mode === 'guess') + return 8 + + if (mode === 'winning_run') + return [1, 8 / 6, 8 / 4, 8 / 2][Math.max (0, Math.min (3, displayedWinningCount))] ?? 1 + + return 1 + }, + []) + const postsForMode = useCallback (( mode: 'normal' | 'winning_run' | 'guess', ): Post[] => { @@ -2724,6 +2748,7 @@ const GekanatorBackdrop: FC<{ return [winningRunTargetPost] return normalVisiblePosts }, [displayedGuess, winningRunTargetPost, normalVisiblePosts]) + const thumbnailsForMode = useCallback (( mode: 'normal' | 'winning_run' | 'guess', count: number, @@ -2737,16 +2762,20 @@ const GekanatorBackdrop: FC<{ return backgroundThumbnailUrl (post) ?? null }).filter ((thumbnail): thumbnail is string => Boolean (thumbnail)) }, [postsForMode]) + const targetSettings = settingsForMode (backdropMode) const targetTileCount = targetSettings.columns * targetSettings.rows + const nextThumbnails = useMemo ( () => thumbnailsForMode (backdropMode, targetTileCount), [backdropMode, targetTileCount, thumbnailsForMode]) + const nextDirection = useMemo ( () => directions[ Math.abs (hashString (`${ visualSeed }:direction`)) % directions.length] ?? directions[0], [visualSeed, directions]) + const marqueeDuration = backdropMode === 'winning_run' ? motionMode === 'calm' ? 28 : 20 @@ -2773,12 +2802,29 @@ const GekanatorBackdrop: FC<{ const renderedSettings = settingsForMode (displayedBackdropMode) const renderedTileCount = renderedSettings.columns * renderedSettings.rows - const renderedScale = scaleForMode ( - displayedBackdropMode, - displayedWinningRunCount) + const renderedScale = scaleForMode (displayedBackdropMode, displayedWinningRunCount) const isGuessPresentation = backdropMode === 'guess' || displayedBackdropMode === 'guess' + useEffect (() => { + if (motionMode === 'off') + return + + if (!(isGuessPresentation)) + return + + const duration = motionMode === 'calm' ? .95 : .75 + const ease = [0.16, 1, 0.3, 1] as const + + const controls = [ + animate (x, 0, { duration, ease }), + animate (y, 0, { duration, ease })] + + return () => { + controls.forEach (control => control.stop ()) + } + }, [isGuessPresentation, motionMode, visualSeed, x, y]) + useEffect (() => { activeDirectionRef.current = activeDirection }, [activeDirection]) @@ -2790,15 +2836,15 @@ const GekanatorBackdrop: FC<{ return wrapped > cell / 2 ? wrapped - cell : wrapped } - if ( - motionMode === 'off' - || isGuessPresentation - || nextThumbnails.length === 0 - ) { - x.set (0) - y.set (0) + if (motionMode === 'off' || nextThumbnails.length === 0) + { + x.set (0) + y.set (0) + return + } + + if (isGuessPresentation) return - } const speed = 33.333333 / marqueeDuration let animationFrame: number @@ -2843,8 +2889,6 @@ const GekanatorBackdrop: FC<{ } if (backdropMode === 'guess' && guessThumbnail) { - x.set (0) - y.set (0) setIsFlippingTiles (false) setDisplayedBackdropMode ('guess') setDisplayedWinningRunCount (winningRunQuestionCount) @@ -2942,18 +2986,19 @@ const GekanatorBackdrop: FC<{ + animate={{ scale: renderedScale, + x: displayedBackdropMode === 'guess' ? guessFocusOffset.x : '0%', + y: displayedBackdropMode === 'guess' ? guessFocusOffset.y : '0%' }} + transition={(displayedBackdropMode === 'winning_run' + || displayedBackdropMode === 'guess') + ? { duration: motionMode === 'calm' ? .95 : .75, + ease: [.16, 1, .3, 1] } + : { duration: .2 }}> {Array.from ({ length: 9 }, (_, duplicate) => { const column = duplicate % 3 const row = Math.floor (duplicate / 3)