Play liveChoose your opponent
Join
1b3bc26
89 lines · 3.5 KB Raw Permalink History
1// The middle of the table: the cards just played, discarded, judged or shown, each flown in from its seat and captioned
2// (谁 → 谁), fading out as 无名杀's do; and the lines from a card's user to its targets.
3import { useLayoutEffect, useRef, useState, useEffect } from 'react';
4import type { CenterCard, Line, Word } from './useFx';
5import { Card, type Face } from './Card';
6import { scale, handOrigin } from './flyCards';
7
8function center(sel: string): { x: number; y: number } | null {
9 const r = document.querySelector(sel)?.getBoundingClientRect();
10 return r && r.width ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null;
11}
12
13function Played({ item }: { item: CenterCard }) {
14 const ref = useRef<HTMLDivElement>(null);
15 useLayoutEffect(() => {
16 const el = ref.current;
17 const from = (item.cardId && handOrigin.get(item.cardId)) || (item.from === null ? center('[data-zone-anchor="deck"]') : center(`[data-seat-index="${item.from}"]`));
18 if (!el || !from || !el.animate) return;
19 const r = el.getBoundingClientRect();
20 const s = scale();
21 const dx = (from.x - (r.left + r.width / 2)) / s;
22 const dy = (from.y - (r.top + r.height / 2)) / s;
23 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)' });
24 }, []); // eslint-disable-line react-hooks/exhaustive-deps
25 return (
26 <div className="nm-played" ref={ref}>
27 <Card face={item.face} />
28 <div className="nm-played-caption">{item.caption}</div>
29 </div>
30 );
31}
32
33export function Center({ items, reveal }: { items: CenterCard[]; reveal: { face: Face; caption: string } | null }) {
34 return (
35 <div className="nm-center">
36 <div className="nm-played-row">
37 {items.map((it) => (
38 <Played key={it.id} item={it} />
39 ))}
40 {reveal && (
41 <div className="nm-played shown">
42 <Card face={reveal.face} />
43 <div className="nm-played-caption">{reveal.caption}</div>
44 </div>
45 )}
46 </div>
47 <div className="nm-discard" data-zone-anchor="discard" />
48 </div>
49 );
50}
51
52/** Lines between seats and the words on them, drawn in the canvas's own coordinates. */
53export function Lines({ lines, words }: { lines: Line[]; words: Word[] }) {
54 const ref = useRef<SVGSVGElement>(null);
55 const [, redraw] = useState(0);
56 useEffect(() => redraw((x) => x + 1), [lines, words]);
57 const svg = ref.current?.getBoundingClientRect();
58 const s = scale();
59 const at = (i: number) => {
60 const c = center(`[data-seat-index="${i}"] .nm-avatar`) ?? center(`[data-seat-index="${i}"]`);
61 return c && svg ? { x: (c.x - svg.left) / s, y: (c.y - svg.top) / s } : null;
62 };
63 return (
64 <>
65 {words.map((w) => {
66 const c = at(w.seat);
67 return c ? (
68 <div key={w.id} className={`nm-cardeffect ${w.text === '杀' || w.text.endsWith('杀') ? 'sha' : ''}`} style={{ left: c.x, top: c.y }}>
69 {w.text}
70 </div>
71 ) : null;
72 })}
73 <svg className="nm-lines" ref={ref}>
74 {lines.map((l) => {
75 const a = at(l.from);
76 const b = at(l.to);
77 if (!a || !b) return null;
78 const len = Math.hypot(b.x - a.x, b.y - a.y);
79 return (
80 <g key={l.id} className={`nm-line ${l.kind}`} style={{ ['--len' as string]: `${len}` }}>
81 <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} strokeDasharray={len} />
82 <circle cx={b.x} cy={b.y} r="6" />
83 </g>
84 );
85 })}
86 </svg>
87 </>
88 );
89}