// The middle of the table: the cards just played, discarded, judged or shown, each flown in from its seat and captioned // (谁 → 谁), fading out as 无名杀's do; and the lines from a card's user to its targets. import { useLayoutEffect, useRef, useState, useEffect } from 'react'; import type { CenterCard, Line, Word } from './useFx'; import { Card, type Face } from './Card'; import { scale, handOrigin } from './flyCards'; function center(sel: string): { x: number; y: number } | null { const r = document.querySelector(sel)?.getBoundingClientRect(); return r && r.width ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null; } function Played({ item }: { item: CenterCard }) { const ref = useRef(null); useLayoutEffect(() => { const el = ref.current; const from = (item.cardId && handOrigin.get(item.cardId)) || (item.from === null ? center('[data-zone-anchor="deck"]') : center(`[data-seat-index="${item.from}"]`)); if (!el || !from || !el.animate) return; const r = el.getBoundingClientRect(); const s = scale(); const dx = (from.x - (r.left + r.width / 2)) / s; const dy = (from.y - (r.top + r.height / 2)) / s; el.animate([{ transform: `translate(${dx}px, ${dy}px) scale(0.55)`, opacity: 0.4 }, { transform: 'none', opacity: 1 }], { duration: 420, easing: 'cubic-bezier(.2,.7,.3,1)' }); }, []); // eslint-disable-line react-hooks/exhaustive-deps return (
{item.caption}
); } export function Center({ items, reveal }: { items: CenterCard[]; reveal: { face: Face; caption: string } | null }) { return (
{items.map((it) => ( ))} {reveal && (
{reveal.caption}
)}
); } /** Lines between seats and the words on them, drawn in the canvas's own coordinates. */ export function Lines({ lines, words }: { lines: Line[]; words: Word[] }) { const ref = useRef(null); const [, redraw] = useState(0); useEffect(() => redraw((x) => x + 1), [lines, words]); const svg = ref.current?.getBoundingClientRect(); const s = scale(); const at = (i: number) => { const c = center(`[data-seat-index="${i}"] .nm-avatar`) ?? center(`[data-seat-index="${i}"]`); return c && svg ? { x: (c.x - svg.left) / s, y: (c.y - svg.top) / s } : null; }; return ( <> {words.map((w) => { const c = at(w.seat); return c ? (
{w.text}
) : null; })} {lines.map((l) => { const a = at(l.from); const b = at(l.to); if (!a || !b) return null; const len = Math.hypot(b.x - a.x, b.y - a.y); return ( ); })} ); }