feat: 背面图案支持 + 大小王素材位置微调 + 10号牌pip修复
This commit is contained in:
75
docs/superpowers/specs/2026-06-01-back-image-joker-offset.md
Normal file
75
docs/superpowers/specs/2026-06-01-back-image-joker-offset.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 背面图案渲染 + 大小王素材位置微调
|
||||
|
||||
## 背景
|
||||
|
||||
当前背面牌(back)虽有 `'back'` 素材类型的上传 UI,但渲染时完全不使用素材,仅显示纯色 + "CARD BACK" 文字。大小王素材已渲染但无位置微调功能。
|
||||
|
||||
## 需求
|
||||
|
||||
### 1. 背面图案渲染
|
||||
- 优先从 `project.assets` 中读取 `asset_type='back'` 的素材图片
|
||||
- 有图时:在躯干区域居中绘制(contain 模式等比适配)
|
||||
- 无图时:保持现有回退(纯色背景 + "CARD BACK" 文字 + 边框)
|
||||
- 支持位置微调
|
||||
|
||||
### 2. 大小王素材位置微调
|
||||
- 在 DesignPanel 选中大小王时,显示 dx/dy/缩放滑块
|
||||
- 数据存储在 `card_overrides[joker-key].image_dx, image_dy, image_scale`
|
||||
- `drawJokerBody()` 绘制时应用偏移
|
||||
|
||||
## 数据模型
|
||||
|
||||
扩展 `card_overrides` JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"joker-big": { "image_dx": 0, "image_dy": 0, "image_scale": 1 },
|
||||
"joker-small": { "image_dx": 0, "image_dy": 0, "image_scale": 1 },
|
||||
"back": { "image_dx": 0, "image_dy": 0, "image_scale": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
- `image_dx` / `image_dy`:相对躯体区域的比例偏移,范围 -0.05 ~ +0.05
|
||||
- `image_scale`:缩放系数,范围 0.6 ~ 1.4
|
||||
|
||||
## 前端实现
|
||||
|
||||
### cardRenderer.js
|
||||
|
||||
#### `drawBackSide()` 改动
|
||||
- 新增 `project` 参数以读取素材和 card_overrides
|
||||
- 调用 `assetByType(project, 'back', 'back')` 获取素材
|
||||
- 有素材时绘制图片(contain 适配 + 偏移应用)
|
||||
- `renderCard()` 调用处传入 `project`
|
||||
|
||||
#### `drawJokerBody()` 改动
|
||||
- 读取 `override = design.image_dx/dy/scale`(由 `getEffectiveDesign()` 合并后传入的 design 包含 card_overrides 字段)
|
||||
- 在图片绘制坐标上叠加 `bodyW * image_dx`、`bodyH * image_dy`
|
||||
- 缩放叠加 `drawW * image_scale`、`drawH * image_scale`
|
||||
|
||||
### projectStore.js
|
||||
- 开放 `canHaveOverride` 覆盖 joker 和 back
|
||||
- 添加图片偏移的 patch 方法(复用已有 `patchCardOverride` 或新增)
|
||||
|
||||
### DesignPanel.vue
|
||||
- 移除 `canHaveOverride` 对 joker/back 的限制
|
||||
- 当当前牌为 joker 或 back 时,显示图片位置微调滑块(dx/dy/缩放)
|
||||
|
||||
## 后端实现
|
||||
|
||||
### exports/utils.py
|
||||
|
||||
#### `generate_card_png()` back 分支
|
||||
- 查找 `project.assets.filter(asset_type='back')` 获取素材
|
||||
- `load_image_safe()` 加载图片
|
||||
- `draw_back()` 函数中用 `thumbnail` contain 适配 + 偏移
|
||||
|
||||
#### `draw_joker()` 改动
|
||||
- 读取 `card_overrides` 的 image_dx/dy/scale
|
||||
- 在坐标计算中叠加偏移和缩放
|
||||
|
||||
## UI 滑块参数
|
||||
- dx: range -0.05 ~ +0.05, step 0.005
|
||||
- dy: range -0.05 ~ +0.05, step 0.005
|
||||
- scale: range 0.6 ~ 1.4, step 0.05
|
||||
(与现有 number_layout 一致)
|
||||
@@ -0,0 +1,75 @@
|
||||
# 大小王对称渲染
|
||||
|
||||
## 背景
|
||||
|
||||
当前大小王在有素材图片时,将整张图居中适配绘制在躯体区域,无对称处理。需要改为:上半区显示原图,下半区显示原图的垂直镜像,实现类似 JQK 人像的对称效果。
|
||||
|
||||
## 需求
|
||||
|
||||
- 上传的素材图片作为**上半部分**的源图
|
||||
- 躯体区域纵向平分:上半区绘制原图(居中适配),下半区绘制垂直翻转后的同一张图
|
||||
- 角标 JOKER + BIG/SMALL 不变
|
||||
- 无素材时回退到白字 "JOKER" 居中(不变)
|
||||
|
||||
## 修改范围
|
||||
|
||||
| 文件 | 函数 |
|
||||
|------|------|
|
||||
| `frontend/src/utils/cardRenderer.js` | `drawJokerBody()` |
|
||||
| `backend/apps/exports/utils.py` | `draw_joker()` |
|
||||
|
||||
## 前端实现 (Canvas2D)
|
||||
|
||||
在 `drawJokerBody` 中,当有素材图片时:
|
||||
|
||||
```js
|
||||
// 躯体区域上下平分
|
||||
const halfH = bodyH / 2
|
||||
|
||||
// 上半区:原图居中适配
|
||||
const fit = fitSize(imgW, imgH, bodyW, halfH)
|
||||
ctx.drawImage(img, bodyX + (bodyW - fit.w) / 2, bodyY + (halfH - fit.h) / 2, fit.w, fit.h)
|
||||
|
||||
// 下半区:垂直翻转后居中适配
|
||||
ctx.save()
|
||||
ctx.translate(0, bodyY + halfH) // 移动到下半区中线
|
||||
ctx.scale(1, -1) // 垂直翻转
|
||||
ctx.drawImage(img, bodyX + (bodyW - fit.w) / 2, -(halfH - fit.h) / 2, fit.w, fit.h)
|
||||
ctx.restore()
|
||||
```
|
||||
|
||||
## 后端实现 (PIL)
|
||||
|
||||
在 `draw_joker` 中,当有素材图片时:
|
||||
|
||||
```python
|
||||
half_h = body_h // 2
|
||||
|
||||
# 上半区:原图居中适配
|
||||
img.thumbnail((body_w, half_h), Image.LANCZOS)
|
||||
x1 = body_x + (body_w - img.width) // 2
|
||||
y1 = body_y + (half_h - img.height) // 2
|
||||
canvas.paste(img, (x1, y1), img)
|
||||
|
||||
# 下半区:垂直翻转后居中适配
|
||||
img_flipped = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
y2 = body_y + half_h + (half_h - img_flipped.height) // 2
|
||||
canvas.paste(img_flipped, (x1, y2), img_flipped)
|
||||
```
|
||||
|
||||
Note: `thumbnail` 是原地修改,所以上半区粘贴完成后需要重新加载原图再做翻转,或者先复制一份。
|
||||
|
||||
修正:
|
||||
```python
|
||||
img_copy = img.copy()
|
||||
img_copy.thumbnail((body_w, half_h), Image.LANCZOS)
|
||||
# 上半区粘贴 img_copy
|
||||
img_flipped = img_copy.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
# 下半区粘贴 img_flipped
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 前端当前使用 `Math.max` (fill) 适配策略,改为对称后每个半区内图片尺寸一致
|
||||
- 后端当前使用 `thumbnail` (contain) 适配,保持一致即可
|
||||
- 躯体 padding 前端 18%/22%,后端 15%/20%,各自保持不变
|
||||
Reference in New Issue
Block a user