2026-06-01 17:11:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 扑克牌布局数据 & 通用工具函数
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这里和后端 apps/exports/utils.py 保持一致。
|
|
|
|
|
|
* 实际渲染在前端用 canvas(drawCard),后端用 PIL(generate_card_png)。
|
|
|
|
|
|
*/
|
2026-05-31 15:33:50 +08:00
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
// 数字牌 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 }],
|
2026-06-01 21:48:51 +08:00
|
|
|
|
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 }],
|
2026-06-01 17:11:06 +08:00
|
|
|
|
}
|
2026-05-31 15:33:50 +08:00
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
export const SUIT_TEXT = {
|
|
|
|
|
|
spade: '♠',
|
|
|
|
|
|
heart: '♥',
|
|
|
|
|
|
club: '♣',
|
|
|
|
|
|
diamond: '♦',
|
2026-05-31 15:33:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
export const SUIT_LABELS = {
|
|
|
|
|
|
spade: '♠ 黑桃',
|
|
|
|
|
|
heart: '♥ 红桃',
|
|
|
|
|
|
club: '♣ 梅花',
|
|
|
|
|
|
diamond: '♦ 方块',
|
2026-05-31 15:33:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
export const SUIT_COLORS = {
|
|
|
|
|
|
spade: '#000000',
|
|
|
|
|
|
heart: '#E53935',
|
|
|
|
|
|
club: '#000000',
|
|
|
|
|
|
diamond: '#E53935',
|
2026-05-31 15:33:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
export const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
|
2026-05-31 15:33:50 +08:00
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
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 }
|
2026-05-31 15:33:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:11:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 计算数字牌 (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 是否为红色系
|
|
|
|
|
|
*/
|
2026-05-31 15:33:50 +08:00
|
|
|
|
export function isRedSuit(suit) {
|
|
|
|
|
|
return suit === 'heart' || suit === 'diamond'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isBlackSuit(suit) {
|
|
|
|
|
|
return suit === 'spade' || suit === 'club'
|
2026-06-01 17:11:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 },
|
|
|
|
|
|
}
|