cell flip animation

This commit is contained in:
2025-12-09 09:38:04 +02:00
parent 48b2e23f32
commit 570acf193a
+108
View File
@@ -0,0 +1,108 @@
import { useEffect, useRef, useState } from 'react'
import { useStore } from '../store'
interface FlipItem {
row: number
col: number
oldLetter: string
newLetter: string
clueNumber: number | undefined
id: number
}
let flipId = 0
export default function CellFlip({ svgRef }: { svgRef: React.RefObject<SVGSVGElement | null> }) {
const entries = useStore((s) => s.entries)
const puzzle = useStore((s) => s.puzzle)
const [flips, setFlips] = useState<FlipItem[]>([])
const prevEntries = useRef<string[][]>([])
useEffect(() => {
if (!puzzle) return
const prev = prevEntries.current
const curr = entries
const newFlips: FlipItem[] = []
for (let r = 0; r < curr.length; r++) {
for (let c = 0; c < (curr[r]?.length || 0); c++) {
const was = prev[r]?.[c] || ''
const now = curr[r]?.[c] || ''
if (now && now !== was) {
const num = puzzle.cell_numbers[`${r},${c}`]
newFlips.push({ row: r, col: c, oldLetter: was, newLetter: now, clueNumber: num, id: ++flipId })
}
}
}
prevEntries.current = curr.map(r => [...r])
// skip animation when bulk-loading (e.g. reopening a solved puzzle)
const totalWhiteCells = puzzle.cells.flat().filter(c => c !== 'black').length
if (newFlips.length > 0 && newFlips.length < totalWhiteCells * 0.5) {
// hide the SVG letter text for the flipping cells
if (svgRef.current) {
for (const flip of newFlips) {
const texts = svgRef.current.querySelectorAll<SVGElement>(`g[data-cell="${flip.row},${flip.col}"] .cell-letter`)
texts.forEach(t => { t.style.opacity = '0' })
}
}
setFlips(f => [...f, ...newFlips])
setTimeout(() => {
// restore SVG letter visibility
if (svgRef.current) {
for (const flip of newFlips) {
const texts = svgRef.current.querySelectorAll<SVGElement>(`g[data-cell="${flip.row},${flip.col}"] .cell-letter`)
texts.forEach(t => { t.style.opacity = '1' })
}
}
setFlips(f => f.filter(fl => !newFlips.some(n => n.id === fl.id)))
}, 600)
}
}, [entries, puzzle, svgRef])
if (!puzzle || !svgRef.current || flips.length === 0) return null
const svg = svgRef.current
const rect = svg.getBoundingClientRect()
const cellW = rect.width / puzzle.width
const cellH = rect.height / puzzle.height
const fontSize = Math.round(cellW * 0.52)
const numSize = Math.round(cellW * 0.26)
return (
<>
{flips.map(flip => (
<div
key={flip.id}
className="flip-scene"
style={{
position: 'absolute',
left: flip.col * cellW,
top: flip.row * cellH,
width: cellW,
height: cellH,
}}
>
<div className="flip-backing" />
<div className="flip-card flip-card-animate">
<div className="flip-face flip-front">
{flip.clueNumber && (
<span className="flip-num" style={{ fontSize: numSize }}>{flip.clueNumber}</span>
)}
{flip.oldLetter && <span className="flip-letter" style={{ fontSize }}>{flip.oldLetter}</span>}
</div>
<div className="flip-face flip-back">
{flip.clueNumber && (
<span className="flip-num" style={{ fontSize: numSize }}>{flip.clueNumber}</span>
)}
<span className="flip-letter" style={{ fontSize }}>{flip.newLetter}</span>
</div>
</div>
</div>
))}
</>
)
}