// In place of the upstream's flying cards: its useCardMoveAnimation draws and gains with these, between the deck and the // seats, with 无名杀's cards and card back (a discard shows in the middle of the table instead, as a played card). And // in place of its createCardFlyAnimation (a card you play, from your hand): where the card left your hand, so the table // flies it to the middle from there. import { cardArt, CARD_BACK } from './art'; import { isRed } from './Card'; export interface FlyCardFace { name: string; suit: string; rank: string; } export type AnchorTarget = { kind: 'deck' } | { kind: 'discard' } | { kind: 'seat'; index: number } | { kind: 'offscreen' }; const DURATION = 620; function anchor(t: AnchorTarget): { x: number; y: number } { const sel = t.kind === 'seat' ? `[data-seat-index="${t.index}"]` : t.kind === 'offscreen' ? '' : `[data-zone-anchor="${t.kind}"]`; const el = sel ? document.querySelector(sel) : null; const r = el?.getBoundingClientRect(); if (r && r.width) return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; return { x: innerWidth / 2, y: t.kind === 'offscreen' ? -80 : innerHeight * 0.42 }; } /** The table's scale (the canvas is drawn at a design size and scaled to the frame): flying cards match its cards. */ export const scale = () => Number(getComputedStyle(document.documentElement).getPropertyValue('--nm-scale')) || 1; function face(f: FlyCardFace | null): HTMLElement { const el = document.createElement('div'); if (!f) { el.className = 'nm-card back'; el.style.backgroundImage = `url(${CARD_BACK})`; return el; } el.className = `nm-card${isRed(f.suit) ? ' red' : ''}`; const art = cardArt(f.name); const a = document.createElement('div'); a.className = art ? 'nm-card-art' : 'nm-card-big'; if (art) a.style.backgroundImage = `url(${art})`; else a.textContent = f.name.slice(0, 2); const n = document.createElement('div'); n.className = `nm-card-name${f.name.length > 3 ? ' long' : ''}`; n.textContent = f.name; el.append(a, n); if (f.suit) { const i = document.createElement('div'); i.className = `nm-card-info${isRed(f.suit) ? ' red' : ''}`; i.textContent = `${f.suit}${f.rank}`; el.append(i); } return el; } /** Where each card you played last left your hand (by card id), in the frame's pixels. */ export const handOrigin = new Map(); export function createCardFlyAnimation(cardEl: HTMLElement, card: { id: string }): void { const r = cardEl.getBoundingClientRect(); handOrigin.set(card.id, { x: r.left + r.width / 2, y: r.top + r.height / 2 }); if (handOrigin.size > 50) handOrigin.delete(handOrigin.keys().next().value!); } export function flyCards(from: AnchorTarget, to: AnchorTarget, faces: (FlyCardFace | null)[], count?: number): void { const total = Math.min(count ?? faces.length, 8); if (total <= 0 || to.kind === 'discard') return; const a = anchor(from); const b = anchor(to); const s = scale(); const box = document.createElement('div'); box.className = 'nm-fly'; 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`; for (let i = 0; i < total; i++) { const c = face(faces[i] ?? null); c.style.setProperty('--i', String(i)); box.append(c); } document.body.append(box); const done = () => box.remove(); box.addEventListener('animationend', done); setTimeout(done, DURATION + 300); }