/** * 扑克牌布局数据 & 通用工具函数 * * 这里和后端 apps/exports/utils.py 保持一致。 * 实际渲染在前端用 canvas(drawCard),后端用 PIL(generate_card_png)。 */ // 数字牌 1-10 的花色位置(相对坐标 0~1) export const LAYOUT_POSITIONS = { 1: [{ x: 0.50, y: 0.50 }], 2: [{ x: 0.50, y: 0.25 }, { x: 0.50, y: 0.75 }], 3: [{ x: 0.50, y: 0.20 }, { x: 0.50, y: 0.50 }, { x: 0.50, y: 0.80 }], 4: [{ x: 0.30, y: 0.25 }, { x: 0.70, y: 0.25 }, { x: 0.30, y: 0.75 }, { x: 0.70, y: 0.75 }], 5: [{ x: 0.30, y: 0.20 }, { x: 0.70, y: 0.20 }, { x: 0.50, y: 0.50 }, { x: 0.30, y: 0.80 }, { x: 0.70, y: 0.80 }], 6: [{ x: 0.30, y: 0.20 }, { x: 0.70, y: 0.20 }, { x: 0.30, y: 0.50 }, { x: 0.70, y: 0.50 }, { x: 0.30, y: 0.80 }, { x: 0.70, y: 0.80 }], 7: [{ x: 0.30, y: 0.15 }, { x: 0.70, y: 0.15 }, { x: 0.50, y: 0.35 }, { x: 0.30, y: 0.55 }, { x: 0.70, y: 0.55 }, { x: 0.30, y: 0.85 }, { x: 0.70, y: 0.85 }], 8: [{ x: 0.30, y: 0.15 }, { x: 0.70, y: 0.15 }, { x: 0.50, y: 0.32 }, { x: 0.30, y: 0.50 }, { x: 0.70, y: 0.50 }, { x: 0.50, y: 0.68 }, { x: 0.30, y: 0.85 }, { x: 0.70, y: 0.85 }], 9: [{ x: 0.30, y: 0.15 }, { x: 0.70, y: 0.15 }, { x: 0.50, y: 0.30 }, { x: 0.22, y: 0.50 }, { x: 0.50, y: 0.50 }, { x: 0.78, y: 0.50 }, { x: 0.50, y: 0.70 }, { x: 0.30, y: 0.85 }, { x: 0.70, y: 0.85 }], 10: [{ x: 0.30, y: 0.15 }, { x: 0.70, y: 0.15 }, { x: 0.30, y: 0.35 }, { x: 0.70, y: 0.35 }, { x: 0.50, y: 0.45 }, { x: 0.50, y: 0.55 }, { x: 0.30, y: 0.65 }, { x: 0.70, y: 0.65 }, { x: 0.30, y: 0.85 }, { x: 0.70, y: 0.85 }], } export const SUIT_TEXT = { spade: '♠', heart: '♥', club: '♣', diamond: '♦', } export const SUIT_LABELS = { spade: '♠ 黑桃', heart: '♥ 红桃', club: '♣ 梅花', diamond: '♦ 方块', } export const SUIT_COLORS = { spade: '#000000', heart: '#E53935', club: '#000000', diamond: '#E53935', } export const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] export const SUITS = ['spade', 'heart', 'club', 'diamond'] export const JOKERS = [ { key: 'joker-big', label: '大王', defaultColor: '#1B5E20' }, { key: 'joker-small', label: '小王', defaultColor: '#B71C1C' }, ] /** * 合并项目级 design 与单牌覆盖 */ export function getEffectiveDesign(project, cardKey) { const base = JSON.parse(JSON.stringify(project?.design || {})) const overrides = (project?.card_overrides || {})[cardKey] || {} return { ...base, ...overrides } } /** * 计算数字牌 (1-10) 实际的花色位置(绝对像素) * - 默认按 LAYOUT_POSITIONS * - 项目可保存 number_layout 做微调 */ export function computeNumberPipPositions(rank, cardW, cardH, pipSize, numberLayout) { const rankInt = parseInt(rank, 10) || 1 const defaults = LAYOUT_POSITIONS[rankInt] || LAYOUT_POSITIONS[1] const userOverrides = (numberLayout || {})[String(rankInt)] || [] return defaults.map((p, i) => { const o = userOverrides[i] || {} const dx = Number(o.dx) || 0 const dy = Number(o.dy) || 0 const scale = Number(o.scale) || 1 return { x: (p.x + dx) * cardW, y: (p.y + dy) * cardH, size: Math.max(20, pipSize * scale), } }) } /** * 解析一个花色 key 是否为红色系 */ export function isRedSuit(suit) { return suit === 'heart' || suit === 'diamond' } export function isBlackSuit(suit) { return suit === 'spade' || suit === 'club' } export function isFace(rank) { return rank === 'J' || rank === 'Q' || rank === 'K' } export function isJoker(cardKey) { return typeof cardKey === 'string' && cardKey.startsWith('joker-') } export function isNumber(rank) { return RANKS.indexOf(rank) >= 0 && !isFace(rank) } /** * 列出全部 54 张牌 + 背面 */ export function listAllCards() { const out = [] for (const s of SUITS) { for (const r of RANKS) { out.push({ key: `${s}-${r}`, suit: s, rank: r, type: isFace(r) ? 'face' : 'number' }) } } out.push({ key: 'joker-big', suit: null, rank: null, type: 'joker' }) out.push({ key: 'joker-small', suit: null, rank: null, type: 'joker' }) out.push({ key: 'back', suit: null, rank: null, type: 'back' }) return out } export const DEFAULT_DESIGN = { background_color: '#FFFFFF', background_image: null, border_color: '#333333', border_width: 2, suit_symbols: { spade: { type: 'text', value: '♠', asset_id: null, color: '#000000' }, heart: { type: 'text', value: '♥', asset_id: null, color: '#E53935' }, club: { type: 'text', value: '♣', asset_id: null, color: '#000000' }, diamond: { type: 'text', value: '♦', asset_id: null, color: '#E53935' }, }, corner_size_ratio: 0.13, pip_size_ratio: 0.16, font_family: 'Times New Roman', font_color: '#000000', corner_offset: { x: 0, y: 0 }, }