feat: 对称方式可选 - 垂直翻转 / 180° 旋转
为 JQK 人物图和大小王素材添加 symmetry_mode 字段: - 'flip'(默认):下半身用垂直镜像(沿水平中线翻转,左右不变) - 'rotate':下半身用 180° 旋转(上下颠倒 + 左右镜像) 改动: - 后端 draw_face_card / draw_joker:根据 symmetry_mode 选不同的 PIL transpose - 前端 cardRenderer.js:face card 和 joker 都用统一的分支: - flip: translate + scale(1, -1) - rotate: translate + rotate(Math.PI) - DesignPanel.vue:JQK/大小王面板加下拉选择器,存到 card_overrides
This commit is contained in:
@@ -212,27 +212,32 @@ async function drawFaceCardBody(ctx, w, h, suit, rank, design, project) {
|
||||
const drawX = padX + (bodyW - drawW) / 2
|
||||
const drawY = padTop + (bodyH - drawH) / 2
|
||||
|
||||
// 上下对称:先画上半,再把上半翻转贴到下半
|
||||
// 上下对称:先画上半,再把下半用指定方式生成
|
||||
const halfH = drawH / 2
|
||||
// 上半:用 clip 限定为上半
|
||||
ctx.save()
|
||||
// 上半(先画完整图,用 clip 限定为上半)
|
||||
ctx.beginPath()
|
||||
ctx.rect(drawX, drawY, drawW, halfH)
|
||||
ctx.clip()
|
||||
ctx.drawImage(img, drawX, drawY, drawW, drawH)
|
||||
ctx.restore()
|
||||
// 下半:把原图翻转 180° 贴到下半区域
|
||||
ctx.save()
|
||||
ctx.translate(drawX + drawW, drawY + drawH)
|
||||
ctx.rotate(Math.PI)
|
||||
// 现在画完整图(经过旋转坐标系),让它 fill bodyW x halfH
|
||||
const tmpW = drawW
|
||||
const tmpH = halfH
|
||||
// 由于旋转,drawImage 时 x/y 是反向的;目标 = (0,0) 到 (drawW, halfH)
|
||||
// 经过 180° 旋转,相当于在 (drawX, drawY) + (drawW, drawH) 处映射为 (-drawW, -drawH) 到 (0, 0)
|
||||
// 所以让图片左上角对应 (0,0) 即可
|
||||
ctx.drawImage(img, 0, 0, tmpW, tmpH * 2) // 0,0 -> drawW,halfH (after 180 rotation)
|
||||
ctx.restore()
|
||||
// 下半:symmetry_mode = 'flip' 垂直翻转,'rotate' 180° 旋转
|
||||
const symMode = design.symmetry_mode || 'flip'
|
||||
if (symMode === 'rotate') {
|
||||
// 180° 旋转:translate 到 (x+w, y+h) + 旋转 PI
|
||||
ctx.save()
|
||||
ctx.translate(drawX + drawW, drawY + drawH)
|
||||
ctx.rotate(Math.PI)
|
||||
ctx.drawImage(img, 0, 0, drawW, drawH) // 旋转后图片左上角在 (0,0),覆盖 (-drawW,-drawH)~(0,0)
|
||||
ctx.restore()
|
||||
} else {
|
||||
// 垂直翻转:translate 到 (x, y+halfH) + scale(1, -1)
|
||||
ctx.save()
|
||||
ctx.translate(drawX, drawY + halfH)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(img, 0, 0, drawW, halfH)
|
||||
ctx.restore()
|
||||
}
|
||||
} else {
|
||||
// 退化:绘制大花色 + 字母
|
||||
const big = Math.round(h * 0.30)
|
||||
@@ -290,11 +295,21 @@ async function drawJokerBody(ctx, w, h, which, design, project) {
|
||||
const topX = padX + offsetX + (bodyW - finalW) / 2
|
||||
const topY = padTop + offsetY + (halfH - finalH) / 2
|
||||
ctx.drawImage(img, topX, topY, finalW, finalH)
|
||||
ctx.save()
|
||||
ctx.translate(0, padTop + bodyH)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(img, topX, (halfH - finalH) / 2 - offsetY, finalW, finalH)
|
||||
ctx.restore()
|
||||
// symmetry_mode: 'flip' 垂直翻转,'rotate' 180° 旋转
|
||||
const symMode = design.symmetry_mode || 'flip'
|
||||
if (symMode === 'rotate') {
|
||||
ctx.save()
|
||||
ctx.translate(topX + finalW, topY + finalH)
|
||||
ctx.rotate(Math.PI)
|
||||
ctx.drawImage(img, 0, 0, finalW, finalH)
|
||||
ctx.restore()
|
||||
} else {
|
||||
ctx.save()
|
||||
ctx.translate(topX, topY + finalH)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(img, 0, 0, finalW, finalH)
|
||||
ctx.restore()
|
||||
}
|
||||
} else {
|
||||
// 退化
|
||||
const big = Math.round(h * 0.25)
|
||||
|
||||
Reference in New Issue
Block a user