| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | import { cardArt, CARD_BACK } from './art'; |
| 6 | import { isRed } from './Card'; |
| 7 | |
| 8 | export interface FlyCardFace { |
| 9 | name: string; |
| 10 | suit: string; |
| 11 | rank: string; |
| 12 | } |
| 13 | export type AnchorTarget = { kind: 'deck' } | { kind: 'discard' } | { kind: 'seat'; index: number } | { kind: 'offscreen' }; |
| 14 | |
| 15 | const DURATION = 620; |
| 16 | |
| 17 | function anchor(t: AnchorTarget): { x: number; y: number } { |
| 18 | const sel = t.kind === 'seat' ? `[data-seat-index="${t.index}"]` : t.kind === 'offscreen' ? '' : `[data-zone-anchor="${t.kind}"]`; |
| 19 | const el = sel ? document.querySelector(sel) : null; |
| 20 | const r = el?.getBoundingClientRect(); |
| 21 | if (r && r.width) return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; |
| 22 | return { x: innerWidth / 2, y: t.kind === 'offscreen' ? -80 : innerHeight * 0.42 }; |
| 23 | } |
| 24 | |
| 25 | |
| 26 | export const scale = () => Number(getComputedStyle(document.documentElement).getPropertyValue('--nm-scale')) || 1; |
| 27 | |
| 28 | function face(f: FlyCardFace | null): HTMLElement { |
| 29 | const el = document.createElement('div'); |
| 30 | if (!f) { |
| 31 | el.className = 'nm-card back'; |
| 32 | el.style.backgroundImage = `url(${CARD_BACK})`; |
| 33 | return el; |
| 34 | } |
| 35 | el.className = `nm-card${isRed(f.suit) ? ' red' : ''}`; |
| 36 | const art = cardArt(f.name); |
| 37 | const a = document.createElement('div'); |
| 38 | a.className = art ? 'nm-card-art' : 'nm-card-big'; |
| 39 | if (art) a.style.backgroundImage = `url(${art})`; |
| 40 | else a.textContent = f.name.slice(0, 2); |
| 41 | const n = document.createElement('div'); |
| 42 | n.className = `nm-card-name${f.name.length > 3 ? ' long' : ''}`; |
| 43 | n.textContent = f.name; |
| 44 | el.append(a, n); |
| 45 | if (f.suit) { |
| 46 | const i = document.createElement('div'); |
| 47 | i.className = `nm-card-info${isRed(f.suit) ? ' red' : ''}`; |
| 48 | i.textContent = `${f.suit}${f.rank}`; |
| 49 | el.append(i); |
| 50 | } |
| 51 | return el; |
| 52 | } |
| 53 | |
| 54 | |
| 55 | export const handOrigin = new Map<string, { x: number; y: number }>(); |
| 56 | |
| 57 | export function createCardFlyAnimation(cardEl: HTMLElement, card: { id: string }): void { |
| 58 | const r = cardEl.getBoundingClientRect(); |
| 59 | handOrigin.set(card.id, { x: r.left + r.width / 2, y: r.top + r.height / 2 }); |
| 60 | if (handOrigin.size > 50) handOrigin.delete(handOrigin.keys().next().value!); |
| 61 | } |
| 62 | |
| 63 | export function flyCards(from: AnchorTarget, to: AnchorTarget, faces: (FlyCardFace | null)[], count?: number): void { |
| 64 | const total = Math.min(count ?? faces.length, 8); |
| 65 | if (total <= 0 || to.kind === 'discard') return; |
| 66 | const a = anchor(from); |
| 67 | const b = anchor(to); |
| 68 | const s = scale(); |
| 69 | const box = document.createElement('div'); |
| 70 | box.className = 'nm-fly'; |
| 71 | box.style.cssText = `left:${a.x}px;top:${a.y}px;--dx:${b.x - a.x}px;--dy:${b.y - a.y}px;--s:${s * 0.8};--n:${total};animation-duration:${DURATION}ms`; |
| 72 | for (let i = 0; i < total; i++) { |
| 73 | const c = face(faces[i] ?? null); |
| 74 | c.style.setProperty('--i', String(i)); |
| 75 | box.append(c); |
| 76 | } |
| 77 | document.body.append(box); |
| 78 | const done = () => box.remove(); |
| 79 | box.addEventListener('animationend', done); |
| 80 | setTimeout(done, DURATION + 300); |
| 81 | } |