粒子属性调整
This commit is contained in:
1 parent
0be0844f0f
commit
2ad0b76d11
6 files changed
+530
-49
No files matched your search
+224
-12
@@ -97,15 +97,153 @@
|
||||
<div class="group-head" @click="toggle('attr')">
|
||||
<span class="caret">{{ collapsed.attr ? '▸' : '▾' }}</span><span>粒子属性</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.attr">
|
||||
<NumSlider label="速度下限" :min="0" :max="400" :step="1" v-model="sys.config.speedMin" />
|
||||
<NumSlider label="速度上限" :min="0" :max="600" :step="1" v-model="sys.config.speedMax" />
|
||||
<NumSlider label="生命下限" :min="0.1" :max="5" :step="0.1" v-model="sys.config.lifeMin" />
|
||||
<NumSlider label="生命上限" :min="0.1" :max="6" :step="0.1" v-model="sys.config.lifeMax" />
|
||||
<NumSlider label="缩放下限" :min="0.05" :max="3" :step="0.05" v-model="sys.config.scaleMin" />
|
||||
<NumSlider label="缩放上限" :min="0.05" :max="5" :step="0.05" v-model="sys.config.scaleMax" />
|
||||
<NumSlider label="旋转速度" :min="-360" :max="360" :step="10" v-model="sys.config.rotationSpeedMin" />
|
||||
<NumSlider label="随机种子" :min="0" :max="9999" :step="1" v-model="sys.config.seed" />
|
||||
<div class="group-body attr-body" v-show="!collapsed.attr">
|
||||
<div class="attr-section-title">粒子初始参数</div>
|
||||
|
||||
<ParticleAttributeControl
|
||||
label="初始生命周期"
|
||||
: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="随机采样 →"
|
||||
/>
|
||||
|
||||
<ParticleAttributeControl
|
||||
label="初始速度"
|
||||
: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="随机采样 →"
|
||||
/>
|
||||
|
||||
<div class="attr-axis-head">
|
||||
<span>初始缩放</span>
|
||||
<div class="axis-modes">
|
||||
<button class="axis-mode" :class="{ on: sys.config.scaleAxisMode === 'uniform' }" @click="sys.config.scaleAxisMode = 'uniform'">统一</button>
|
||||
<button class="axis-mode" :class="{ on: sys.config.scaleAxisMode === 'separate' }" @click="sys.config.scaleAxisMode = 'separate'">XY分离</button>
|
||||
</div>
|
||||
</div>
|
||||
<ParticleAttributeControl
|
||||
:label="sys.config.scaleAxisMode === 'uniform' ? '初始缩放' : '缩放 X'"
|
||||
: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="随机采样 →"
|
||||
/>
|
||||
<ParticleAttributeControl
|
||||
v-if="sys.config.scaleAxisMode === 'separate'"
|
||||
label="缩放 Y"
|
||||
: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="随机采样 →"
|
||||
/>
|
||||
|
||||
<ParticleAttributeControl
|
||||
label="初始旋转"
|
||||
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="随机采样 →"
|
||||
/>
|
||||
|
||||
<div class="attr-section-title life-title">粒子生命周期变化参数</div>
|
||||
|
||||
<div class="life-item">
|
||||
<label class="switch-row">
|
||||
<input type="checkbox" v-model="sys.config.speedOverLifeEnabled" />
|
||||
<span class="switch-ui"></span><span>生命周期内速度</span>
|
||||
</label>
|
||||
<ParticleAttributeControl
|
||||
v-if="sys.config.speedOverLifeEnabled"
|
||||
label="速度倍率"
|
||||
: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="生命周期 →"
|
||||
/>
|
||||
</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>生命周期内缩放</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'">统一</button>
|
||||
<button class="axis-mode" :class="{ on: sys.config.scaleOverLifeAxisMode === 'separate' }" @click="sys.config.scaleOverLifeAxisMode = 'separate'">XY分离</button>
|
||||
</div>
|
||||
<ParticleAttributeControl
|
||||
:label="sys.config.scaleOverLifeAxisMode === 'uniform' ? '缩放倍率' : '倍率 X'"
|
||||
: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="生命周期 →"
|
||||
/>
|
||||
<ParticleAttributeControl
|
||||
v-if="sys.config.scaleOverLifeAxisMode === 'separate'"
|
||||
label="倍率 Y"
|
||||
: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="生命周期 →"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="life-item">
|
||||
<label class="switch-row">
|
||||
<input type="checkbox" v-model="sys.config.rotationOverLifeEnabled" />
|
||||
<span class="switch-ui"></span><span>生命周期内旋转</span>
|
||||
</label>
|
||||
<ParticleAttributeControl
|
||||
v-if="sys.config.rotationOverLifeEnabled"
|
||||
label="旋转速度"
|
||||
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="生命周期 →"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="seed-row">
|
||||
<label for="particle-seed">随机种子</label>
|
||||
<input id="particle-seed" class="seed-input" type="number" min="0" step="1" v-model.number="sys.config.seed" />
|
||||
<button class="seed-random" title="生成随机种子" @click="randomizeSeed">⚄</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +269,7 @@
|
||||
</select>
|
||||
</label>
|
||||
<div class="curve-wrap">
|
||||
<CurveEditor v-model="sys.config.alphaCurve" />
|
||||
<CurveEditor v-model="sys.config.alphaCurve" :default-value="ALPHA_CURVE" />
|
||||
<div class="curve-labels"><span>0(透明)</span><span>生命周期 →</span><span>1(不透明)</span></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -226,10 +364,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref, watchEffect } from 'vue'
|
||||
import { useParticleStore } from '../store/particleStore'
|
||||
import { ensureEmitterConfig } from '../core/particleEmitter'
|
||||
import NumSlider from './NumSlider.vue'
|
||||
import CurveEditor from './CurveEditor.vue'
|
||||
import ParticleAttributeControl from './ParticleAttributeControl.vue'
|
||||
|
||||
const store = useParticleStore()
|
||||
const activeIdx = computed(() => {
|
||||
@@ -238,6 +378,21 @@ const activeIdx = computed(() => {
|
||||
})
|
||||
const sys = computed(() => store.systems[activeIdx.value] || null)
|
||||
|
||||
// 开发热更新会保留旧的 Pinia 对象;渲染属性控件前先补齐新增字段。
|
||||
watchEffect(() => {
|
||||
if (!sys.value) return
|
||||
const config = ensureEmitterConfig(sys.value.config as any)
|
||||
// 初始属性不再提供曲线模式;旧配置自动回退为随机。
|
||||
if (config.lifeMode === 'curve') config.lifeMode = 'random'
|
||||
if (config.speedMode === 'curve') config.speedMode = 'random'
|
||||
if (config.scaleMode === 'curve') config.scaleMode = 'random'
|
||||
if (config.initialRotationMode === 'curve') config.initialRotationMode = 'random'
|
||||
})
|
||||
|
||||
const LINEAR_CURVE = [{ x: 0, y: 0 }, { x: 1, y: 1 }]
|
||||
const FLAT_CURVE = [{ x: 0, y: 1 }, { x: 1, y: 1 }]
|
||||
const ALPHA_CURVE = [{ x: 0, y: 0 }, { x: 0.2, y: 1 }, { x: 0.8, y: 1 }, { x: 1, y: 0 }]
|
||||
|
||||
const collapsed = reactive<Record<string, boolean>>({
|
||||
scene: false, emitMode: false, shape: false, attr: false, look: false, mods: false, export: false,
|
||||
})
|
||||
@@ -245,6 +400,40 @@ function toggle(key: string) { collapsed[key] = !collapsed[key] }
|
||||
|
||||
function select(i: number) { store.selectSystem(store.systems[i]?.id ?? 0) }
|
||||
function addSys() { store.addSystem() }
|
||||
function randomizeSeed() {
|
||||
if (sys.value) sys.value.config.seed = Math.floor(Math.random() * 999999) + 1
|
||||
}
|
||||
|
||||
function setSpeedOverLifeMode(mode: 'fixed' | 'random' | 'curve' | 'direction') {
|
||||
const config = sys.value?.config
|
||||
if (!config || mode === 'direction') return
|
||||
config.speedOverLifeMode = mode
|
||||
// 旧默认是 1~1,曲线映射后永远为 1;切换曲线时改为有效倍率范围。
|
||||
if (mode === 'curve' && config.speedOverLifeMin === config.speedOverLifeMax) {
|
||||
config.speedOverLifeMin = 0
|
||||
config.speedOverLifeMax = 1
|
||||
}
|
||||
}
|
||||
|
||||
function prepareScaleOverLifeCurve() {
|
||||
const config = sys.value?.config
|
||||
if (!config || config.scaleOverLifeMode !== 'curve') return
|
||||
if (config.scaleOverLifeMin === config.scaleOverLifeMax) {
|
||||
config.scaleOverLifeMin = 0
|
||||
config.scaleOverLifeMax = 1
|
||||
}
|
||||
if (config.scaleYOverLifeMin === config.scaleYOverLifeMax) {
|
||||
config.scaleYOverLifeMin = 0
|
||||
config.scaleYOverLifeMax = 1
|
||||
}
|
||||
}
|
||||
|
||||
function setScaleOverLifeMode(mode: 'fixed' | 'random' | 'curve' | 'direction') {
|
||||
const config = sys.value?.config
|
||||
if (!config || mode === 'direction') return
|
||||
config.scaleOverLifeMode = mode
|
||||
prepareScaleOverLifeCurve()
|
||||
}
|
||||
|
||||
// 各发射器形状的默认参数:切换形状时应用(只影响形状相关字段,不牵连其他)
|
||||
const SHAPE_DEFAULTS: Record<string, Partial<any>> = {
|
||||
@@ -270,7 +459,7 @@ function clearTexture() { resetTexture() }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel { width: 100%; height: 100%; overflow-y: auto; background: #14141f; border-left: 1px solid #2a2a3c; padding: 12px; box-sizing: border-box; }
|
||||
.panel { width: 100%; height: 100%; overflow-y: auto; overscroll-behavior: contain; background: #14141f; border-left: 1px solid #2a2a3c; padding: 12px; box-sizing: border-box; }
|
||||
/* 深色主题滚动条:细窄、圆角、暗色,避免默认白条突兀 */
|
||||
.panel::-webkit-scrollbar { width: 6px; }
|
||||
.panel::-webkit-scrollbar-track { background: transparent; }
|
||||
@@ -328,4 +517,27 @@ function clearTexture() { resetTexture() }
|
||||
.thumb-note { color: #667; font-size: 11px; margin: 4px 0 2px; }
|
||||
.hex { font-size: 11px; color: #889; margin-left: auto; font-variant-numeric: tabular-nums; }
|
||||
.inp.tiny { width: 46px; flex: 0 0 46px; text-align: center; }
|
||||
|
||||
/* 粒子属性面板 */
|
||||
.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; }
|
||||
.seed-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #2d3950; }
|
||||
.seed-row label { width: 64px; color: #8f9bb5; font-size: 12px; }
|
||||
.seed-input { min-width: 0; flex: 1; padding: 6px 8px; border: 1px solid #344158; border-radius: 6px; outline: none; background: #0f1625; color: #e2e7f2; font-size: 12px; }
|
||||
.seed-input:focus { border-color: #625ff1; }
|
||||
.seed-random { width: 34px; height: 30px; border: 1px solid #3a4860; border-radius: 6px; background: #2c3950; color: #d7def0; cursor: pointer; }
|
||||
.seed-random:hover { background: #394964; }
|
||||
</style>
|
||||
Reference in new issue
Block a user