cell flip animation
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user