Files
SpineParticlesWeb/src/views/particle-panel/ParticleAttributesSection.vue
T
2026-10-28 14:32:52 +08:00

197 lines
10 KiB
Vue

<template>
<!-- 4. 粒子属性 -->
<div class="group">
<div class="group-head" @click="toggle('attr')">
<span class="caret">{{ collapsed.attr ? '▸' : '▾' }}</span><span>{{ $t('attributes.title') }}</span>
<button class="group-reset" data-matomo-event="attributes.reset" :title="$t('attributes.reset')" @click.stop="resetModule('attr')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body attr-body" v-show="sectionOpen('attr')">
<div class="attr-section-title">{{ $t('attributes.initialParams') }}</div>
<ParticleAttributeControl
:label="$t('attributes.initialLife')"
:allow-curve="false"
v-model:mode="sys.config.lifeMode"
v-model:value-min="sys.config.lifeMin"
v-model:value-max="sys.config.lifeMax"
v-model:curve="sys.config.lifeCurve"
:min="0.1" :max="10" :step="0.1"
:curve-label="$t('attributes.randomSample')"
/>
<ParticleAttributeControl
:label="$t('attributes.initialSpeed')"
:allow-curve="false"
v-model:mode="sys.config.speedMode"
v-model:value-min="sys.config.speedMin"
v-model:value-max="sys.config.speedMax"
v-model:curve="sys.config.speedCurve"
:min="0" :max="1000" :step="1"
:curve-label="$t('attributes.randomSample')"
/>
<div class="attr-axis-head">
<span>{{ $t('attributes.initialScale') }}</span>
<div class="axis-modes">
<button class="axis-mode" :class="{ on: sys.config.scaleAxisMode === 'uniform' }" @click="sys.config.scaleAxisMode = 'uniform'">{{ $t('attributes.uniform') }}</button>
<button class="axis-mode" :class="{ on: sys.config.scaleAxisMode === 'separate' }" @click="sys.config.scaleAxisMode = 'separate'">{{ $t('attributes.separateXY') }}</button>
</div>
</div>
<ParticleAttributeControl
:label="sys.config.scaleAxisMode === 'uniform' ? $t('attributes.initialScale') : $t('attributes.scaleX')"
:allow-curve="false"
v-model:mode="sys.config.scaleMode"
v-model:value-min="sys.config.scaleMin"
v-model:value-max="sys.config.scaleMax"
v-model:curve="sys.config.scaleCurve"
:min="0" :max="5" :step="0.05"
:curve-label="$t('attributes.randomSample')"
/>
<ParticleAttributeControl
v-if="sys.config.scaleAxisMode === 'separate'"
:label="$t('attributes.scaleY')"
:show-modes="false"
:allow-curve="false"
v-model:mode="sys.config.scaleMode"
v-model:value-min="sys.config.scaleYMin"
v-model:value-max="sys.config.scaleYMax"
v-model:curve="sys.config.scaleYCurve"
:min="0" :max="5" :step="0.05"
:curve-label="$t('attributes.randomSample')"
/>
<ParticleAttributeControl
:label="$t('attributes.initialRotation')"
allow-direction
:allow-curve="false"
v-model:mode="sys.config.initialRotationMode"
v-model:value-min="sys.config.initialRotationMin"
v-model:value-max="sys.config.initialRotationMax"
v-model:curve="sys.config.initialRotationCurve"
:min="-360" :max="360" :step="1"
:curve-label="$t('attributes.randomSample')"
/>
<div class="attr-section-title life-title">{{ $t('attributes.lifecycleTitle') }}</div>
<div class="life-item">
<label class="switch-row">
<input type="checkbox" v-model="sys.config.speedOverLifeEnabled" />
<span class="switch-ui"></span><span>{{ $t('attributes.speedOverLife') }}</span>
</label>
<ParticleAttributeControl
v-if="sys.config.speedOverLifeEnabled"
:label="$t('attributes.speedMultiplier')"
:mode="sys.config.speedOverLifeMode"
@update:mode="setSpeedOverLifeMode"
v-model:value-min="sys.config.speedOverLifeMin"
v-model:value-max="sys.config.speedOverLifeMax"
v-model:curve="sys.config.speedOverLifeCurve"
:default-curve="FLAT_CURVE"
:min="0" :max="3" :step="0.05"
:curve-label="$t('attributes.lifeTo')"
/>
</div>
<div class="life-item">
<label class="switch-row">
<input type="checkbox" v-model="sys.config.scaleOverLifeEnabled" @change="prepareScaleOverLifeCurve" />
<span class="switch-ui"></span><span>{{ $t('attributes.scaleOverLife') }}</span>
</label>
<template v-if="sys.config.scaleOverLifeEnabled">
<div class="axis-modes life-axis">
<button class="axis-mode" :class="{ on: sys.config.scaleOverLifeAxisMode === 'uniform' }" @click="sys.config.scaleOverLifeAxisMode = 'uniform'">{{ $t('attributes.uniform') }}</button>
<button class="axis-mode" :class="{ on: sys.config.scaleOverLifeAxisMode === 'separate' }" @click="sys.config.scaleOverLifeAxisMode = 'separate'">{{ $t('attributes.separateXY') }}</button>
</div>
<ParticleAttributeControl
:label="sys.config.scaleOverLifeAxisMode === 'uniform' ? $t('attributes.scaleMultiplier') : $t('attributes.multiplierX')"
:mode="sys.config.scaleOverLifeMode"
@update:mode="setScaleOverLifeMode"
v-model:value-min="sys.config.scaleOverLifeMin"
v-model:value-max="sys.config.scaleOverLifeMax"
v-model:curve="sys.config.scaleOverLifeCurve"
:default-curve="FLAT_CURVE"
:min="0" :max="3" :step="0.05"
:curve-label="$t('attributes.lifeTo')"
/>
<ParticleAttributeControl
v-if="sys.config.scaleOverLifeAxisMode === 'separate'"
:label="$t('attributes.multiplierY')"
:show-modes="false"
v-model:mode="sys.config.scaleOverLifeMode"
v-model:value-min="sys.config.scaleYOverLifeMin"
v-model:value-max="sys.config.scaleYOverLifeMax"
v-model:curve="sys.config.scaleYOverLifeCurve"
:default-curve="FLAT_CURVE"
:min="0" :max="3" :step="0.05"
:curve-label="$t('attributes.lifeTo')"
/>
</template>
</div>
<div class="life-item">
<label class="switch-row">
<input type="checkbox" v-model="sys.config.rotationOverLifeEnabled" />
<span class="switch-ui"></span><span>{{ $t('attributes.rotationOverLife') }}</span>
</label>
<ParticleAttributeControl
v-if="sys.config.rotationOverLifeEnabled"
:label="$t('attributes.rotationSpeed')"
v-model:mode="sys.config.rotationOverLifeMode"
v-model:value-min="sys.config.rotationSpeedMin"
v-model:value-max="sys.config.rotationSpeedMax"
v-model:curve="sys.config.rotationOverLifeCurve"
:default-curve="LINEAR_CURVE"
:min="-720" :max="720" :step="10"
:curve-label="$t('attributes.lifeTo')"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import NumSlider from '../NumSlider.vue'
import CurveEditor from '../CurveEditor.vue'
import ParticleAttributeControl from '../ParticleAttributeControl.vue'
import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { sys, collapsed, toggle, sectionOpen, resetModule, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, LINEAR_CURVE, FLAT_CURVE } = useParticlePanelContext()
</script>
<style scoped>
.group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
.group-head {
display: flex; align-items: center; gap: 6px; padding: 7px 10px; cursor: pointer;
font-size: 12px; font-weight: 600; color: #ccd; background: #191924; user-select: none;
}
.group-head:hover { background: #20202e; }
.group-reset {
margin-left: auto; height: 22px; padding: 0 7px; border: 1px solid #3a465f; border-radius: 5px;
background: #252d3d; color: #9eabc3; font-size: 10px; line-height: 20px; cursor: pointer;
}
.group-reset:hover { border-color: #7774ff; background: #45426f; color: #fff; }
.group-body { padding: 8px 10px 12px; }
.caret { display: inline-block; width: 12px; color: #667; font-size: 11px; }
.row { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-size: 12px; color: #aab; }
.row > span:first-child { width: 60px; flex-shrink: 0; color: #8a93bb; }
.attr-body { padding: 10px; background: #151b27; }
.attr-section-title { margin: 2px 0 8px; color: #9ca8c0; font-size: 12px; font-weight: 700; }
.attr-section-title.life-title { margin-top: 14px; padding-top: 12px; border-top: 1px solid #2d3950; }
.attr-axis-head { display: flex; align-items: center; justify-content: space-between; margin: 10px 1px -2px; color: #aeb8cf; font-size: 12px; font-weight: 600; }
.axis-modes { display: flex; gap: 4px; }
.axis-mode { padding: 4px 8px; border: 1px solid #344158; border-radius: 5px; background: #273246; color: #aeb8cf; font-size: 11px; cursor: pointer; }
.axis-mode.on { border-color: #7774ff; background: #605df0; color: #fff; }
.life-item { padding: 9px 0; border-bottom: 1px solid #2a3549; }
.switch-row { display: flex; align-items: center; gap: 9px; color: #b8c1d5; font-size: 12px; cursor: pointer; }
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
.switch-ui { position: relative; width: 32px; height: 18px; border-radius: 10px; background: #354258; transition: background .16s ease; }
.switch-ui::after { content: ''; position: absolute; left: 2px; top: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .16s ease; }
.switch-row input:checked + .switch-ui { background: #625ff1; }
.switch-row input:checked + .switch-ui::after { transform: translateX(14px); }
.life-axis { margin: 9px 0 2px 42px; }
.switch-row input:disabled + .switch-ui { opacity: 0.45; cursor: not-allowed; }
</style>