重构扑克牌设计系统:修复后端渲染bug,重写前端编辑器
This commit is contained in:
@@ -1,128 +1,138 @@
|
||||
const LAYOUT_POSITIONS = {
|
||||
1: [
|
||||
{ x: 0.5, y: 0.5 }
|
||||
],
|
||||
2: [
|
||||
{ x: 0.5, y: 0.25 },
|
||||
{ x: 0.5, y: 0.75 }
|
||||
],
|
||||
3: [
|
||||
{ x: 0.5, y: 0.2 },
|
||||
{ x: 0.5, y: 0.5 },
|
||||
{ x: 0.5, y: 0.8 }
|
||||
],
|
||||
4: [
|
||||
{ x: 0.3, y: 0.25 },
|
||||
{ x: 0.7, y: 0.25 },
|
||||
{ x: 0.3, y: 0.75 },
|
||||
{ x: 0.7, y: 0.75 }
|
||||
],
|
||||
5: [
|
||||
{ x: 0.3, y: 0.2 },
|
||||
{ x: 0.7, y: 0.2 },
|
||||
{ x: 0.5, y: 0.5 },
|
||||
{ x: 0.3, y: 0.8 },
|
||||
{ x: 0.7, y: 0.8 }
|
||||
],
|
||||
6: [
|
||||
{ x: 0.3, y: 0.2 },
|
||||
{ x: 0.7, y: 0.2 },
|
||||
{ x: 0.3, y: 0.5 },
|
||||
{ x: 0.7, y: 0.5 },
|
||||
{ x: 0.3, y: 0.8 },
|
||||
{ x: 0.7, y: 0.8 }
|
||||
],
|
||||
7: [
|
||||
{ x: 0.3, y: 0.15 },
|
||||
{ x: 0.7, y: 0.15 },
|
||||
{ x: 0.5, y: 0.35 },
|
||||
{ x: 0.3, y: 0.55 },
|
||||
{ x: 0.7, y: 0.55 },
|
||||
{ x: 0.3, y: 0.85 },
|
||||
{ x: 0.7, y: 0.85 }
|
||||
],
|
||||
8: [
|
||||
{ x: 0.3, y: 0.15 },
|
||||
{ x: 0.7, y: 0.15 },
|
||||
{ x: 0.5, y: 0.35 },
|
||||
{ x: 0.3, y: 0.55 },
|
||||
{ x: 0.7, y: 0.55 },
|
||||
{ x: 0.5, y: 0.65 },
|
||||
{ x: 0.3, y: 0.85 },
|
||||
{ x: 0.7, y: 0.85 }
|
||||
],
|
||||
9: [
|
||||
{ x: 0.3, y: 0.15 },
|
||||
{ x: 0.7, y: 0.15 },
|
||||
{ x: 0.5, y: 0.35 },
|
||||
{ x: 0.2, y: 0.5 },
|
||||
{ x: 0.5, y: 0.5 },
|
||||
{ x: 0.8, y: 0.5 },
|
||||
{ x: 0.5, y: 0.65 },
|
||||
{ x: 0.3, y: 0.85 },
|
||||
{ x: 0.7, y: 0.85 }
|
||||
],
|
||||
10: [
|
||||
{ x: 0.3, y: 0.15 },
|
||||
{ x: 0.7, y: 0.15 },
|
||||
{ x: 0.3, y: 0.35 },
|
||||
{ x: 0.7, y: 0.35 },
|
||||
{ x: 0.5, y: 0.5 },
|
||||
{ x: 0.3, y: 0.65 },
|
||||
{ x: 0.7, y: 0.65 },
|
||||
{ x: 0.3, y: 0.85 },
|
||||
{ x: 0.7, y: 0.85 }
|
||||
]
|
||||
/**
|
||||
* 扑克牌布局数据 & 通用工具函数
|
||||
*
|
||||
* 这里和后端 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.50 }, { 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 function calculateSuitPositions(rank, cardWidth, cardHeight, symbolSize = 60) {
|
||||
const positions = LAYOUT_POSITIONS[rank] || LAYOUT_POSITIONS[1]
|
||||
|
||||
return positions.map(pos => ({
|
||||
x: pos.x * cardWidth - symbolSize / 2,
|
||||
y: pos.y * cardHeight - symbolSize / 2,
|
||||
width: symbolSize,
|
||||
height: symbolSize
|
||||
}))
|
||||
export const SUIT_TEXT = {
|
||||
spade: '♠',
|
||||
heart: '♥',
|
||||
club: '♣',
|
||||
diamond: '♦',
|
||||
}
|
||||
|
||||
export function getCornerPositions(cardWidth, cardHeight) {
|
||||
return {
|
||||
topLeft: { x: 50, y: 50 },
|
||||
topRight: { x: cardWidth - 100, y: 50 },
|
||||
bottomLeft: { x: 50, y: cardHeight - 100 },
|
||||
bottomRight: { x: cardWidth - 100, y: cardHeight - 100 }
|
||||
}
|
||||
export const SUIT_LABELS = {
|
||||
spade: '♠ 黑桃',
|
||||
heart: '♥ 红桃',
|
||||
club: '♣ 梅花',
|
||||
diamond: '♦ 方块',
|
||||
}
|
||||
|
||||
export function getSuitSymbol(suit) {
|
||||
const symbols = {
|
||||
spade: '♠',
|
||||
heart: '♥',
|
||||
club: '♣',
|
||||
diamond: '♦'
|
||||
}
|
||||
return symbols[suit] || '♠'
|
||||
export const SUIT_COLORS = {
|
||||
spade: '#000000',
|
||||
heart: '#E53935',
|
||||
club: '#000000',
|
||||
diamond: '#E53935',
|
||||
}
|
||||
|
||||
export function getSuitColor(suit, templateColors) {
|
||||
if (templateColors && templateColors[suit]) {
|
||||
return templateColors[suit]
|
||||
}
|
||||
export const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
|
||||
|
||||
const colors = {
|
||||
spade: '#000000',
|
||||
heart: '#FF0000',
|
||||
club: '#000000',
|
||||
diamond: '#FF0000'
|
||||
}
|
||||
return colors[suit] || '#000000'
|
||||
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 },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user