Compare commits
6 Commits
1da35e4eab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944ea856d0 | ||
|
|
ebf0f51f94 | ||
|
|
aed831fc8c | ||
|
|
194e42a026 | ||
|
|
5e7eb5115d | ||
|
|
ffc5e3d0fc |
@@ -331,7 +331,7 @@ def draw_joker(canvas, design, which, project, card_key, asset):
|
|||||||
offset_y = int(body_h * image_dy)
|
offset_y = int(body_h * image_dy)
|
||||||
|
|
||||||
img_copy = asset.copy()
|
img_copy = asset.copy()
|
||||||
img_copy.thumbnail((body_w, half_h), Image.LANCZOS)
|
img_copy = img_copy.resize((body_w, half_h), Image.LANCZOS)
|
||||||
if image_scale != 1:
|
if image_scale != 1:
|
||||||
sw = max(1, int(img_copy.width * image_scale))
|
sw = max(1, int(img_copy.width * image_scale))
|
||||||
sh = max(1, int(img_copy.height * image_scale))
|
sh = max(1, int(img_copy.height * image_scale))
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from .views_library import library_list, library_themes, library_detail, library
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', project_list, name='project-list'),
|
path('', project_list, name='project-list'),
|
||||||
|
path('library/', library_list, name='library-list'),
|
||||||
|
path('library/themes/', library_themes, name='library-themes'),
|
||||||
|
path('library/<int:pk>/', library_detail, name='library-detail'),
|
||||||
path('<str:pk>/', project_detail, name='project-detail'),
|
path('<str:pk>/', project_detail, name='project-detail'),
|
||||||
path('<str:pk>/design/', project_save_design, name='project-save-design'),
|
path('<str:pk>/design/', project_save_design, name='project-save-design'),
|
||||||
path('<str:project_pk>/assets/', asset_list, name='asset-list'),
|
path('<str:project_pk>/assets/', asset_list, name='asset-list'),
|
||||||
path('<str:project_pk>/assets/<str:asset_pk>/', asset_detail, name='asset-detail'),
|
path('<str:project_pk>/assets/<str:asset_pk>/', asset_detail, name='asset-detail'),
|
||||||
path('library/', library_list, name='library-list'),
|
|
||||||
path('library/themes/', library_themes, name='library-themes'),
|
|
||||||
path('<str:project_pk>/library/<int:pk>/apply/', library_apply, name='library-apply'),
|
path('<str:project_pk>/library/<int:pk>/apply/', library_apply, name='library-apply'),
|
||||||
path('library/<int:pk>/', library_detail, name='library-detail'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,137 +1,201 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="design-panel">
|
<div class="design-panel">
|
||||||
<!-- 背景色 -->
|
<!-- ===== 背面设计面板 ===== -->
|
||||||
<section>
|
<template v-if="isBack">
|
||||||
<h4>背景色</h4>
|
<section>
|
||||||
<p class="hint">整副牌默认用这个颜色,个别牌可在下面单独覆盖</p>
|
<h4>背面背景色</h4>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input type="color" :value="design.background_color" @input="setBgColor($event.target.value)" />
|
<input type="color" :value="backDesign.background_color" @input="setBackBgColor($event.target.value)" />
|
||||||
<input type="text" :value="design.background_color" @change="setBgColor($event.target.value)" />
|
<input type="text" :value="backDesign.background_color" @change="setBackBgColor($event.target.value)" />
|
||||||
<button @click="uploadBg" class="mini">上传背景图</button>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<input ref="bgFileInput" type="file" accept="image/*" @change="onBgFile" hidden />
|
|
||||||
<div v-if="design.background_image" class="row">
|
|
||||||
<img :src="design.background_image" class="thumb" />
|
|
||||||
<button @click="clearBgImage" class="mini">清除</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 边框 -->
|
<section>
|
||||||
<section>
|
<h4>背面边框</h4>
|
||||||
<h4>边框</h4>
|
<div class="row">
|
||||||
<div class="row">
|
<label class="mini-label">颜色</label>
|
||||||
<label class="mini-label">颜色</label>
|
<input type="color" :value="backDesign.border_color" @input="setBackDesign('border_color', $event.target.value)" />
|
||||||
<input type="color" :value="design.border_color" @input="setBorder('border_color', $event.target.value)" />
|
<label class="mini-label">粗细</label>
|
||||||
<label class="mini-label">粗细</label>
|
<input type="number" min="0" max="40" :value="backDesign.border_width"
|
||||||
<input type="number" min="0" max="40" :value="design.border_width"
|
@change="setBackDesign('border_width', parseInt($event.target.value) || 0)" />
|
||||||
@change="setBorder('border_width', parseInt($event.target.value) || 0)" />
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 花色符号 -->
|
<section>
|
||||||
<section>
|
<h4>图案色调</h4>
|
||||||
<h4>花色符号</h4>
|
<p class="hint">为背面图案叠加一层半透明色调</p>
|
||||||
<p class="hint">用系统字体显示一个 Unicode 符号;想用图片可在右侧素材库上传后切换</p>
|
<div class="row">
|
||||||
<div v-for="s in suits" :key="s" class="suit-row">
|
<input type="color" :value="backDesign.pattern_color || '#C0A050'"
|
||||||
<div class="suit-label">{{ suitSymbol(s) }} {{ suitLabel(s) }}</div>
|
@input="setBackDesign('pattern_color', $event.target.value)" />
|
||||||
<input type="color" :value="design.suit_symbols?.[s]?.color || '#000000'"
|
<button v-if="backDesign.pattern_color" @click="setBackDesign('pattern_color', null)" class="mini ghost">清除色调</button>
|
||||||
@input="setSuit(s, 'color', $event.target.value)" />
|
</div>
|
||||||
<select :value="design.suit_symbols?.[s]?.type || 'text'"
|
</section>
|
||||||
@change="setSuit(s, 'type', $event.target.value)">
|
|
||||||
<option value="text">字体符号</option>
|
|
||||||
<option value="image" :disabled="!design.suit_symbols?.[s]?.asset_id">图片素材</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 尺寸 -->
|
<section>
|
||||||
<section>
|
<h4>背面图片</h4>
|
||||||
<h4>大小比例</h4>
|
<div class="row">
|
||||||
<label class="row">
|
<button @click="uploadBackImage" class="mini">上传背面图</button>
|
||||||
<span>角标</span>
|
</div>
|
||||||
<input type="range" min="0.08" max="0.20" step="0.01"
|
<input ref="backImageInput" type="file" accept="image/*" @change="onBackImageFile" hidden />
|
||||||
:value="design.corner_size_ratio"
|
<div v-if="backDesign.image" class="row">
|
||||||
@input="setDesign('corner_size_ratio', parseFloat($event.target.value))" />
|
<img :src="backDesign.image" class="thumb" />
|
||||||
<span class="val">{{ Math.round(design.corner_size_ratio * 100) }}%</span>
|
<button @click="setBackDesign('image', null)" class="mini ghost">清除</button>
|
||||||
</label>
|
</div>
|
||||||
<label class="row">
|
</section>
|
||||||
<span>中心花色</span>
|
|
||||||
<input type="range" min="0.08" max="0.24" step="0.01"
|
|
||||||
:value="design.pip_size_ratio"
|
|
||||||
@input="setDesign('pip_size_ratio', parseFloat($event.target.value))" />
|
|
||||||
<span class="val">{{ Math.round(design.pip_size_ratio * 100) }}%</span>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 单牌覆盖 -->
|
<section v-if="backDesign.image">
|
||||||
<section v-if="canHaveOverride">
|
<h4>图片位置微调</h4>
|
||||||
<h4>本牌特殊设置</h4>
|
<p class="hint">输入数值微调素材图片的位置与缩放(0 = 默认)</p>
|
||||||
<p class="hint">只对当前选中的牌生效</p>
|
<div class="pip-row">
|
||||||
<div class="row">
|
<label class="mini-label">dx</label>
|
||||||
<button @click="setOverrideBg" class="mini">设置独立背景色</button>
|
<input type="number" step="0.005" min="-0.05" max="0.05"
|
||||||
<button v-if="hasOverride" @click="clearOverride" class="mini ghost">清除</button>
|
:value="backImageOffsetVal('image_dx')" @change="setBackImageOffset('image_dx', $event.target.value)" />
|
||||||
</div>
|
<label class="mini-label">dy</label>
|
||||||
<div v-if="override?.background_color" class="row">
|
<input type="number" step="0.005" min="-0.05" max="0.05"
|
||||||
<input type="color" :value="override.background_color"
|
:value="backImageOffsetVal('image_dy')" @change="setBackImageOffset('image_dy', $event.target.value)" />
|
||||||
@input="patchOverride('background_color', $event.target.value)" />
|
<label class="mini-label">缩放</label>
|
||||||
<input type="text" :value="override.background_color"
|
<input type="number" step="0.05" min="0.6" max="1.4"
|
||||||
@change="patchOverride('background_color', $event.target.value)" />
|
:value="backImageOffsetVal('image_scale')" @change="setBackImageOffset('image_scale', $event.target.value)" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<button @click="resetBackImageOffset" class="mini ghost">重置图片位置</button>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 对称方式(JQK / 大小王) -->
|
<!-- ===== 正面设计面板 ===== -->
|
||||||
<section v-if="showSymmetryMode">
|
<template v-else>
|
||||||
<h4>对称方式</h4>
|
<!-- 背景色 -->
|
||||||
<p class="hint">下半身素材的生成方式</p>
|
<section>
|
||||||
<div class="row">
|
<h4>背景色</h4>
|
||||||
<select :value="symmetryMode" @change="setSymmetryMode($event.target.value)">
|
<p class="hint">整副牌默认用这个颜色,个别牌可在下面单独覆盖</p>
|
||||||
<option value="flip">垂直翻转(左右不变)</option>
|
<div class="row">
|
||||||
<option value="rotate">180° 旋转(左右镜像)</option>
|
<input type="color" :value="design.background_color" @input="setBgColor($event.target.value)" />
|
||||||
</select>
|
<input type="text" :value="design.background_color" @change="setBgColor($event.target.value)" />
|
||||||
</div>
|
<button @click="uploadBg" class="mini">上传背景图</button>
|
||||||
</section>
|
</div>
|
||||||
|
<input ref="bgFileInput" type="file" accept="image/*" @change="onBgFile" hidden />
|
||||||
|
<div v-if="design.background_image" class="row">
|
||||||
|
<img :src="design.background_image" class="thumb" />
|
||||||
|
<button @click="clearBgImage" class="mini">清除</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 图片位置微调(大小王 / 背面) -->
|
<!-- 边框 -->
|
||||||
<section v-if="showImageOffset">
|
<section>
|
||||||
<h4>图片位置微调</h4>
|
<h4>边框</h4>
|
||||||
<p class="hint">拖动滑块微调素材图片的位置与缩放(0 = 默认)</p>
|
<div class="row">
|
||||||
<div class="pip-row">
|
<label class="mini-label">颜色</label>
|
||||||
<label class="mini-label">dx</label>
|
<input type="color" :value="design.border_color" @input="setBorder('border_color', $event.target.value)" />
|
||||||
<input type="range" min="-0.05" max="0.05" step="0.005"
|
<label class="mini-label">粗细</label>
|
||||||
:value="imageOffsetVal('image_dx')" @input="setImageOffset('image_dx', $event.target.value)" />
|
<input type="number" min="0" max="40" :value="design.border_width"
|
||||||
<label class="mini-label">dy</label>
|
@change="setBorder('border_width', parseInt($event.target.value) || 0)" />
|
||||||
<input type="range" min="-0.05" max="0.05" step="0.005"
|
</div>
|
||||||
:value="imageOffsetVal('image_dy')" @input="setImageOffset('image_dy', $event.target.value)" />
|
</section>
|
||||||
<label class="mini-label">缩放</label>
|
|
||||||
<input type="range" min="0.6" max="1.4" step="0.05"
|
|
||||||
:value="imageOffsetVal('image_scale')" @input="setImageOffset('image_scale', $event.target.value)" />
|
|
||||||
</div>
|
|
||||||
<button @click="resetImageOffset" class="mini ghost">重置图片位置</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 数字牌花色位置微调 -->
|
<!-- 花色符号 -->
|
||||||
<section v-if="isNumberCard">
|
<section>
|
||||||
<h4>数字牌花色位置微调</h4>
|
<h4>花色符号</h4>
|
||||||
<p class="hint">拖动滑块微调每个花色的位置(占整张牌的比例,0 = 默认)</p>
|
<p class="hint">用系统字体显示一个 Unicode 符号;想用图片可在右侧素材库上传后切换</p>
|
||||||
<div class="rank-tabs">
|
<div v-for="s in suits" :key="s" class="suit-row">
|
||||||
<button v-for="r in [1,2,3,4,5,6,7,8,9,10]" :key="r"
|
<div class="suit-label">{{ suitSymbol(s) }} {{ suitLabel(s) }}</div>
|
||||||
:class="{ active: selectedRank === r }"
|
<input type="color" :value="design.suit_symbols?.[s]?.color || '#000000'"
|
||||||
@click="selectedRank = r">{{ r === 1 ? 'A' : r }}</button>
|
@input="setSuit(s, 'color', $event.target.value)" />
|
||||||
</div>
|
<select :value="design.suit_symbols?.[s]?.type || 'text'"
|
||||||
<div v-for="(pos, i) in positions" :key="i" class="pip-row">
|
@change="setSuit(s, 'type', $event.target.value)">
|
||||||
<div class="pip-label">#{{ i + 1 }}</div>
|
<option value="text">字体符号</option>
|
||||||
<label class="mini-label">dx</label>
|
<option value="image" :disabled="!design.suit_symbols?.[s]?.asset_id">图片素材</option>
|
||||||
<input type="range" min="-0.05" max="0.05" step="0.005"
|
</select>
|
||||||
:value="overrideFor(i).dx" @input="setOffset(i, 'dx', $event.target.value)" />
|
</div>
|
||||||
<label class="mini-label">dy</label>
|
</section>
|
||||||
<input type="range" min="-0.05" max="0.05" step="0.005"
|
|
||||||
:value="overrideFor(i).dy" @input="setOffset(i, 'dy', $event.target.value)" />
|
<!-- 尺寸 -->
|
||||||
<label class="mini-label">缩放</label>
|
<section>
|
||||||
<input type="range" min="0.6" max="1.4" step="0.05"
|
<h4>大小比例</h4>
|
||||||
:value="overrideFor(i).scale" @input="setOffset(i, 'scale', $event.target.value)" />
|
<label class="row">
|
||||||
</div>
|
<span>角标</span>
|
||||||
<button @click="resetLayout" class="mini ghost">重置本点数布局</button>
|
<input type="range" min="0.08" max="0.20" step="0.01"
|
||||||
</section>
|
:value="design.corner_size_ratio"
|
||||||
|
@input="setDesign('corner_size_ratio', parseFloat($event.target.value))" />
|
||||||
|
<span class="val">{{ Math.round(design.corner_size_ratio * 100) }}%</span>
|
||||||
|
</label>
|
||||||
|
<label class="row">
|
||||||
|
<span>中心花色</span>
|
||||||
|
<input type="range" min="0.08" max="0.24" step="0.01"
|
||||||
|
:value="design.pip_size_ratio"
|
||||||
|
@input="setDesign('pip_size_ratio', parseFloat($event.target.value))" />
|
||||||
|
<span class="val">{{ Math.round(design.pip_size_ratio * 100) }}%</span>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 单牌覆盖 -->
|
||||||
|
<section v-if="canHaveOverride">
|
||||||
|
<h4>本牌特殊设置</h4>
|
||||||
|
<p class="hint">只对当前选中的牌生效</p>
|
||||||
|
<div class="row">
|
||||||
|
<button @click="setOverrideBg" class="mini">设置独立背景色</button>
|
||||||
|
<button v-if="hasOverride" @click="clearOverride" class="mini ghost">清除</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="override?.background_color" class="row">
|
||||||
|
<input type="color" :value="override.background_color"
|
||||||
|
@input="patchOverride('background_color', $event.target.value)" />
|
||||||
|
<input type="text" :value="override.background_color"
|
||||||
|
@change="patchOverride('background_color', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 对称方式(JQK / 大小王) -->
|
||||||
|
<section v-if="showSymmetryMode">
|
||||||
|
<h4>对称方式</h4>
|
||||||
|
<p class="hint">下半身素材的生成方式</p>
|
||||||
|
<div class="row">
|
||||||
|
<select :value="symmetryMode" @change="setSymmetryMode($event.target.value)">
|
||||||
|
<option value="flip">垂直翻转(左右不变)</option>
|
||||||
|
<option value="rotate">180° 旋转(左右镜像)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 图片位置微调(大小王) -->
|
||||||
|
<section v-if="showImageOffset">
|
||||||
|
<h4>图片位置微调</h4>
|
||||||
|
<p class="hint">输入数值微调素材图片的位置与缩放(0 = 默认)</p>
|
||||||
|
<div class="pip-row">
|
||||||
|
<label class="mini-label">dx</label>
|
||||||
|
<input type="number" step="0.005" min="-0.05" max="0.05"
|
||||||
|
:value="imageOffsetVal('image_dx')" @change="setImageOffset('image_dx', $event.target.value)" />
|
||||||
|
<label class="mini-label">dy</label>
|
||||||
|
<input type="number" step="0.005" min="-0.05" max="0.05"
|
||||||
|
:value="imageOffsetVal('image_dy')" @change="setImageOffset('image_dy', $event.target.value)" />
|
||||||
|
<label class="mini-label">缩放</label>
|
||||||
|
<input type="number" step="0.05" min="0.6" max="1.4"
|
||||||
|
:value="imageOffsetVal('image_scale')" @change="setImageOffset('image_scale', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
<button @click="resetImageOffset" class="mini ghost">重置图片位置</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 数字牌花色位置微调 -->
|
||||||
|
<section v-if="isNumberCard">
|
||||||
|
<h4>数字牌花色位置微调</h4>
|
||||||
|
<p class="hint">输入数值微调每个花色的位置(占整张牌的比例,0 = 默认)</p>
|
||||||
|
<div class="rank-tabs">
|
||||||
|
<button v-for="r in [1,2,3,4,5,6,7,8,9,10]" :key="r"
|
||||||
|
:class="{ active: selectedRank === r }"
|
||||||
|
@click="selectedRank = r">{{ r === 1 ? 'A' : r }}</button>
|
||||||
|
</div>
|
||||||
|
<div v-for="(pos, i) in positions" :key="i" class="pip-row">
|
||||||
|
<div class="pip-label">#{{ i + 1 }}</div>
|
||||||
|
<label class="mini-label">dx</label>
|
||||||
|
<input type="number" step="0.005" min="-0.05" max="0.05"
|
||||||
|
:value="overrideFor(i).dx" @change="setOffset(i, 'dx', $event.target.value)" />
|
||||||
|
<label class="mini-label">dy</label>
|
||||||
|
<input type="number" step="0.005" min="-0.05" max="0.05"
|
||||||
|
:value="overrideFor(i).dy" @change="setOffset(i, 'dy', $event.target.value)" />
|
||||||
|
<label class="mini-label">缩放</label>
|
||||||
|
<input type="number" step="0.05" min="0.6" max="1.4"
|
||||||
|
:value="overrideFor(i).scale" @change="setOffset(i, 'scale', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
<button @click="resetLayout" class="mini ghost">重置本点数布局</button>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -142,6 +206,9 @@ import { SUITS, SUIT_TEXT, LAYOUT_POSITIONS, isJoker } from '@/utils/cardLayout'
|
|||||||
|
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const design = computed(() => store.effectiveDesign)
|
const design = computed(() => store.effectiveDesign)
|
||||||
|
const backDesign = computed(() => store.effectiveBackDesign)
|
||||||
|
const isBack = computed(() => store.currentCard === 'back')
|
||||||
|
|
||||||
const override = computed(() => {
|
const override = computed(() => {
|
||||||
if (!store.project || !store.currentCard) return null
|
if (!store.project || !store.currentCard) return null
|
||||||
return (store.project.card_overrides || {})[store.currentCard] || null
|
return (store.project.card_overrides || {})[store.currentCard] || null
|
||||||
@@ -152,6 +219,7 @@ const suits = SUITS
|
|||||||
const suitSymbol = (s) => SUIT_TEXT[s]
|
const suitSymbol = (s) => SUIT_TEXT[s]
|
||||||
const suitLabel = (s) => ({ spade: '黑桃', heart: '红桃', club: '梅花', diamond: '方块' })[s]
|
const suitLabel = (s) => ({ spade: '黑桃', heart: '红桃', club: '梅花', diamond: '方块' })[s]
|
||||||
|
|
||||||
|
// ===== 正面相关 =====
|
||||||
const bgFileInput = ref(null)
|
const bgFileInput = ref(null)
|
||||||
function uploadBg() { bgFileInput.value?.click() }
|
function uploadBg() { bgFileInput.value?.click() }
|
||||||
function onBgFile(e) {
|
function onBgFile(e) {
|
||||||
@@ -159,7 +227,6 @@ function onBgFile(e) {
|
|||||||
if (!f) return
|
if (!f) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (ev) => {
|
reader.onload = (ev) => {
|
||||||
// 这里只把 dataURL 临时存到 design;正式应上传到后端
|
|
||||||
store.patchDesign('background_image', ev.target.result)
|
store.patchDesign('background_image', ev.target.result)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(f)
|
reader.readAsDataURL(f)
|
||||||
@@ -190,7 +257,7 @@ const isNumberCard = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showImageOffset = computed(() => {
|
const showImageOffset = computed(() => {
|
||||||
return isJoker(store.currentCard) || store.currentCard === 'back'
|
return isJoker(store.currentCard)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFaceCard = computed(() => {
|
const isFaceCard = computed(() => {
|
||||||
@@ -206,8 +273,6 @@ const showSymmetryMode = computed(() => {
|
|||||||
const symmetryMode = computed(() => design.value.symmetry_mode || 'flip')
|
const symmetryMode = computed(() => design.value.symmetry_mode || 'flip')
|
||||||
|
|
||||||
function setSymmetryMode(v) {
|
function setSymmetryMode(v) {
|
||||||
// JQK 和大小王都存到 design 里(因为它们是单牌设置)
|
|
||||||
// 但 design 是合并后的,所以用 patchCardOverride 写到当前牌更干净
|
|
||||||
store.patchCardOverride(store.currentCard, 'symmetry_mode', v)
|
store.patchCardOverride(store.currentCard, 'symmetry_mode', v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +316,36 @@ function setOffset(i, key, val) {
|
|||||||
function resetLayout() {
|
function resetLayout() {
|
||||||
store.resetNumberLayout(String(selectedRank.value))
|
store.resetNumberLayout(String(selectedRank.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 背面相关 =====
|
||||||
|
const backImageInput = ref(null)
|
||||||
|
function uploadBackImage() { backImageInput.value?.click() }
|
||||||
|
function onBackImageFile(e) {
|
||||||
|
const f = e.target.files[0]
|
||||||
|
if (!f) return
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
store.patchBackDesign('image', ev.target.result)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBackBgColor(v) { store.patchBackDesign('background_color', v) }
|
||||||
|
function setBackDesign(path, v) { store.patchBackDesign(path, v) }
|
||||||
|
|
||||||
|
function backImageOffsetVal(key) {
|
||||||
|
const v = Number(backDesign.value[key])
|
||||||
|
if (Number.isNaN(v)) return key === 'image_scale' ? 1 : 0
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
function setBackImageOffset(key, val) {
|
||||||
|
store.patchBackDesign(key, parseFloat(val))
|
||||||
|
}
|
||||||
|
function resetBackImageOffset() {
|
||||||
|
store.patchBackDesign('image_dx', 0)
|
||||||
|
store.patchBackDesign('image_dy', 0)
|
||||||
|
store.patchBackDesign('image_scale', 1)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -280,19 +280,11 @@ async function drawJokerBody(ctx, w, h, which, design, project) {
|
|||||||
|
|
||||||
if (img && img.complete && img.naturalWidth) {
|
if (img && img.complete && img.naturalWidth) {
|
||||||
const halfH = bodyH / 2
|
const halfH = bodyH / 2
|
||||||
const imgRatio = img.naturalWidth / img.naturalHeight
|
|
||||||
const target = bodyW / halfH
|
|
||||||
let drawW, drawH
|
|
||||||
if (imgRatio > target) {
|
|
||||||
drawW = bodyW; drawH = bodyW / imgRatio
|
|
||||||
} else {
|
|
||||||
drawH = halfH; drawW = halfH * imgRatio
|
|
||||||
}
|
|
||||||
const imageDx = Number(design.image_dx) || 0
|
const imageDx = Number(design.image_dx) || 0
|
||||||
const imageDy = Number(design.image_dy) || 0
|
const imageDy = Number(design.image_dy) || 0
|
||||||
const imageScale = Number(design.image_scale) || 1
|
const imageScale = Number(design.image_scale) || 1
|
||||||
const finalW = Math.max(1, drawW * imageScale)
|
const finalW = Math.max(1, bodyW * imageScale)
|
||||||
const finalH = Math.max(1, drawH * imageScale)
|
const finalH = Math.max(1, halfH * imageScale)
|
||||||
const offsetX = bodyW * imageDx
|
const offsetX = bodyW * imageDx
|
||||||
const offsetY = bodyH * imageDy
|
const offsetY = bodyH * imageDy
|
||||||
const topX = padX + offsetX + (bodyW - finalW) / 2
|
const topX = padX + offsetX + (bodyW - finalW) / 2
|
||||||
@@ -300,15 +292,16 @@ async function drawJokerBody(ctx, w, h, which, design, project) {
|
|||||||
ctx.drawImage(img, topX, topY, finalW, finalH)
|
ctx.drawImage(img, topX, topY, finalW, finalH)
|
||||||
// symmetry_mode: 'flip' 垂直翻转,'rotate' 180° 旋转
|
// symmetry_mode: 'flip' 垂直翻转,'rotate' 180° 旋转
|
||||||
const symMode = design.symmetry_mode || 'flip'
|
const symMode = design.symmetry_mode || 'flip'
|
||||||
|
const botY = padTop + offsetY + halfH + (halfH - finalH) / 2
|
||||||
if (symMode === 'rotate') {
|
if (symMode === 'rotate') {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(topX + finalW, topY + finalH)
|
ctx.translate(topX + finalW, botY + finalH)
|
||||||
ctx.rotate(Math.PI)
|
ctx.rotate(Math.PI)
|
||||||
ctx.drawImage(img, 0, 0, finalW, finalH)
|
ctx.drawImage(img, 0, 0, finalW, finalH)
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
} else {
|
} else {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(topX, topY + finalH)
|
ctx.translate(topX, botY + finalH)
|
||||||
ctx.scale(1, -1)
|
ctx.scale(1, -1)
|
||||||
ctx.drawImage(img, 0, 0, finalW, finalH)
|
ctx.drawImage(img, 0, 0, finalW, finalH)
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ watch(() => project.value && project.value.assets, () => { drawAll() }, { deep:
|
|||||||
watch(() => project.value && project.value.design, () => { drawAll() }, { deep: true })
|
watch(() => project.value && project.value.design, () => { drawAll() }, { deep: true })
|
||||||
watch(() => project.value && project.value.card_overrides, () => { drawAll() }, { deep: true })
|
watch(() => project.value && project.value.card_overrides, () => { drawAll() }, { deep: true })
|
||||||
watch(() => project.value && project.value.number_layout, () => { drawAll() }, { deep: true })
|
watch(() => project.value && project.value.number_layout, () => { drawAll() }, { deep: true })
|
||||||
|
watch(() => project.value && project.value.back_design, () => { drawAll() }, { deep: true })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -202,7 +203,7 @@ onMounted(async () => {
|
|||||||
drawAll()
|
drawAll()
|
||||||
// 自动保存指示器
|
// 自动保存指示器
|
||||||
saveWatchStop = watch(
|
saveWatchStop = watch(
|
||||||
() => [project.value.design, project.value.card_overrides, project.value.number_layout],
|
() => [project.value.design, project.value.back_design, project.value.card_overrides, project.value.number_layout],
|
||||||
() => { markSaving() },
|
() => { markSaving() },
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user