贴图轴点

This commit is contained in:
tianmo committed 2026-10-22 11:46:00 +08:00
1 parent c041503004
commit 4d54c87681
13 files changed
+2294 -1248

No files matched your search

+120 -1
View File
@@ -57,12 +57,64 @@
</button>
</div>
<slot />
<div v-if="supportsPivot" class="pivot-toggle-row">
<div class="pivot-toggle-icon">◎</div>
<div class="pivot-toggle-text">
<div class="pivot-toggle-title">贴图轴点</div>
<div class="pivot-toggle-sub" :title="pivotSummary">{{ pivotSummary }}</div>
</div>
<label class="pivot-switch" :title="pivotOpen ? '收起轴点调整' : '展开轴点调整'">
<input v-model="pivotOpen" type="checkbox" />
<span class="pivot-switch-ui"></span>
</label>
</div>
<PivotEditor
v-if="supportsPivot && pivotOpen"
:preview-url="displayPreviewUrl"
:anchor-x="resource.anchorX ?? 0.5"
:anchor-y="resource.anchorY ?? 0.5"
@update:anchorX="resource.anchorX = $event"
@update:anchorY="resource.anchorY = $event"
@fullscreen="openPivotFullscreen"
/>
</div>
<Teleport to="body">
<div v-if="pivotFullscreen" class="pivot-modal-backdrop" @pointerdown.self="closePivotFullscreen(false)">
<div class="pivot-modal" role="dialog" aria-modal="true" aria-label="贴图旋转轴点大图精修">
<div class="pivot-modal-head">
<div class="pivot-modal-icon">◎</div>
<div class="pivot-modal-info">
<div class="pivot-modal-title">
贴图旋转轴点大图精修
<span class="pivot-modal-badge">{{ resource.textureName || defaultFile }}</span>
</div>
<div class="pivot-modal-desc">在下方贴图上任意点击或拖拽,精确设置粒子的旋转/摆动中心</div>
</div>
<button type="button" class="pivot-modal-close" title="关闭" @click="closePivotFullscreen(false)">✕</button>
</div>
<div class="pivot-modal-hint">支持 100% ~ 400% 放大,精确对准尖端或根部:</div>
<PivotEditor
large
:preview-url="displayPreviewUrl"
:anchor-x="draftX"
:anchor-y="draftY"
@update:anchorX="draftX = $event"
@update:anchorY="draftY = $event"
/>
<div class="pivot-modal-actions">
<button type="button" class="pivot-modal-cancel" @click="closePivotFullscreen(false)">取消</button>
<button type="button" class="pivot-modal-confirm" @click="closePivotFullscreen(true)">确定并保存轴点</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import NumSlider from './NumSlider.vue'
import PivotEditor from './PivotEditor.vue'
type CommonImageResource = {
id: number
@@ -73,6 +125,8 @@ type CommonImageResource = {
sequenceFrames?: Array<{ name: string; previewUrl: string }>
textureName: string
textureFolder?: string
anchorX?: number
anchorY?: number
weight: number
locked: boolean
}
@@ -115,6 +169,43 @@ const displayPreviewUrl = computed(() => props.resource.imageMode === 'sequence'
? props.resource.sequenceFrames?.[0]?.previewUrl || ''
: props.resource.previewUrl)
const pivotOpen = ref(false)
const pivotFullscreen = ref(false)
const draftX = ref(0.5)
const draftY = ref(0.5)
const supportsPivot = computed(() => typeof props.resource.anchorX === 'number' && typeof props.resource.anchorY === 'number')
const pivotCentered = computed(() =>
Math.abs((props.resource.anchorX ?? 0.5) - 0.5) < 1e-4 && Math.abs((props.resource.anchorY ?? 0.5) - 0.5) < 1e-4,
)
const pivotSummary = computed(() => {
const x = (Math.min(1, Math.max(0, props.resource.anchorX ?? 0.5)) * 100).toFixed(1)
const y = (Math.min(1, Math.max(0, props.resource.anchorY ?? 0.5)) * 100).toFixed(1)
if (pivotOpen.value) return `已开启:X ${x}% | Y ${y}%`
if (pivotCentered.value) return '未开启,默认围绕图片正中心 (50%, 50%)'
return `轴点已自定义:X ${x}% | Y ${y}%`
})
function openPivotFullscreen() {
draftX.value = props.resource.anchorX ?? 0.5
draftY.value = props.resource.anchorY ?? 0.5
pivotFullscreen.value = true
}
function closePivotFullscreen(commit: boolean) {
if (commit) {
props.resource.anchorX = draftX.value
props.resource.anchorY = draftY.value
}
pivotFullscreen.value = false
}
function onKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && pivotFullscreen.value) closePivotFullscreen(false)
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
const sequenceOrderText = computed(() => {
const frames = props.resource.sequenceFrames || []
if (!frames.length) return '未选择序列帧'
@@ -156,4 +247,32 @@ function pickImage() {
.weight-lock:hover:not(:disabled) { border-color: #7774ff; color: #b8b6ff; }
.weight-lock.on { border-color: #7774ff; background: #514fd0; color: #fff; }
.weight-lock:disabled { opacity: 0.35; cursor: not-allowed; }
.pivot-toggle-row { display: flex; align-items: center; gap: 9px; margin-top: 9px; padding: 8px 9px; border: 1px solid #2c3a54; border-radius: 8px; background: #131b28; }
.pivot-toggle-icon { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex: 0 0 32px; border: 1px solid #35435c; border-radius: 7px; background: #1c2638; color: #8f8cff; font-size: 17px; }
.pivot-toggle-text { flex: 1; min-width: 0; }
.pivot-toggle-title { color: #d6deee; font-size: 12px; font-weight: 600; }
.pivot-toggle-sub { margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #7584a0; font-size: 9px; }
.pivot-switch { position: relative; flex: 0 0 auto; cursor: pointer; }
.pivot-switch input { position: absolute; opacity: 0; pointer-events: none; }
.pivot-switch-ui { position: relative; display: block; width: 34px; height: 19px; border-radius: 10px; background: #354258; transition: background .16s ease; }
.pivot-switch-ui::after { content: ''; position: absolute; left: 2px; top: 2px; width: 15px; height: 15px; border-radius: 50%; background: #fff; transition: transform .16s ease; }
.pivot-switch input:checked + .pivot-switch-ui { background: #625ff1; }
.pivot-switch input:checked + .pivot-switch-ui::after { transform: translateX(15px); }
.pivot-modal-backdrop { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(6, 9, 15, .78); backdrop-filter: blur(2px); }
.pivot-modal { display: flex; flex-direction: column; width: min(980px, 94vw); max-height: 92vh; overflow: auto; padding: 14px; border: 1px solid #2f3f5c; border-radius: 14px; background: #121927; box-shadow: 0 24px 70px rgba(0, 0, 0, .6); }
.pivot-modal-head { display: flex; align-items: flex-start; gap: 10px; padding-bottom: 10px; border-bottom: 1px solid #253148; }
.pivot-modal-icon { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; flex: 0 0 34px; border: 1px solid #3b4a66; border-radius: 8px; background: #1c2638; color: #8f8cff; font-size: 18px; }
.pivot-modal-info { flex: 1; min-width: 0; }
.pivot-modal-title { display: flex; align-items: center; gap: 7px; color: #e4e9f5; font-size: 15px; font-weight: 700; }
.pivot-modal-badge { padding: 2px 7px; border: 1px solid #4b4a8f; border-radius: 999px; background: #2c2b58; color: #b9b6ff; font-size: 10px; font-weight: 500; }
.pivot-modal-desc { margin-top: 3px; color: #7f8daa; font-size: 11px; }
.pivot-modal-close { width: 28px; height: 28px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: #8b98b3; font-size: 15px; cursor: pointer; }
.pivot-modal-close:hover { background: #253148; color: #fff; }
.pivot-modal-hint { margin: 10px 0 0; color: #8b98b3; font-size: 11px; }
.pivot-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; padding-top: 10px; border-top: 1px solid #253148; }
.pivot-modal-cancel { height: 34px; padding: 0 16px; border: 1px solid #34435e; border-radius: 7px; background: #1a2334; color: #a9b6d0; font-size: 12px; cursor: pointer; }
.pivot-modal-cancel:hover { border-color: #5d5aa8; color: #fff; }
.pivot-modal-confirm { height: 34px; padding: 0 22px; border: 1px solid #7774ff; border-radius: 7px; background: #5b57e8; color: #fff; font-size: 12px; font-weight: 700; cursor: pointer; }
.pivot-modal-confirm:hover { background: #6a66ff; }
</style>