首次上传
This commit is contained in:
1 parent
af9bda066c
commit
9b156a32c9
29 files changed
+14567
No files matched your search
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span>粒子系统 · 编辑器</span>
|
||||
<button class="mini-btn" @click="addSys">+ 系统</button>
|
||||
</div>
|
||||
|
||||
<!-- 系统列表 = 场景对象层级 -->
|
||||
<div class="sys-list">
|
||||
<div
|
||||
v-for="(sys, i) in store.systems"
|
||||
:key="sys.id"
|
||||
class="sys-item"
|
||||
:class="{ active: i === activeIdx }"
|
||||
@click="select(i)"
|
||||
>
|
||||
<span class="sys-name">{{ sys.config.name || '系统 ' + (i + 1) }}</span>
|
||||
<button class="mini-btn danger" @click.stop="store.removeSystem(sys.id)">✕</button>
|
||||
</div>
|
||||
<div v-if="!store.systems.length" class="empty">尚无粒子系统</div>
|
||||
</div>
|
||||
|
||||
<template v-if="sys">
|
||||
<!-- 1. 场景对象 -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('scene')">
|
||||
<span class="caret">{{ collapsed.scene ? '▸' : '▾' }}</span><span>场景对象</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.scene">
|
||||
<div class="tree">
|
||||
<div class="tree-item on"><span class="tdot">●</span><span class="tlabel">粒子系统</span></div>
|
||||
<div class="tree-item"><span class="tdot dim">◇</span><span class="tlabel">轨道路径</span><span class="tag">预留</span></div>
|
||||
<div class="tree-item"><span class="tdot dim">◇</span><span class="tlabel">碰撞体</span><span class="tag">预留</span></div>
|
||||
</div>
|
||||
<label class="row"><span>名称</span><input v-model="sys.config.name" class="inp" /></label>
|
||||
<NumSlider label="位置X" :min="-2000" :max="2000" :step="1" v-model="sys.config.centerX" />
|
||||
<NumSlider label="位置Y" :min="-2000" :max="2000" :step="1" v-model="sys.config.centerY" />
|
||||
<NumSlider label="偏移X" :min="-2000" :max="2000" :step="1" v-model="sys.config.offsetX" />
|
||||
<NumSlider label="偏移Y" :min="-2000" :max="2000" :step="1" v-model="sys.config.offsetY" />
|
||||
<NumSlider label="旋转" :min="-360" :max="360" :step="1" v-model="sys.config.rootRotation" />
|
||||
<NumSlider label="缩放X" :min="0.05" :max="10" :step="0.05" v-model="sys.config.rootScaleX" />
|
||||
<NumSlider label="缩放Y" :min="0.05" :max="10" :step="0.05" v-model="sys.config.rootScaleY" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 发射模式 -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('emitMode')">
|
||||
<span class="caret">{{ collapsed.emitMode ? '▸' : '▾' }}</span><span>发射模式</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.emitMode">
|
||||
<div class="segs">
|
||||
<button class="seg" :class="{ on: sys.config.mode === 'stream' }" @click="sys.config.mode = 'stream'">持续</button>
|
||||
<button class="seg" :class="{ on: sys.config.mode === 'burst' }" @click="sys.config.mode = 'burst'">爆发</button>
|
||||
</div>
|
||||
<NumSlider v-if="sys.config.mode === 'stream'" label="发射速率" :min="1" :max="300" :step="1" v-model="sys.config.rate" />
|
||||
<template v-else>
|
||||
<NumSlider label="单次数量" :min="1" :max="1000" :step="1" v-model="sys.config.burstCount" />
|
||||
</template>
|
||||
<NumSlider label="延迟时间" :min="0" :max="5" :step="0.1" v-model="sys.config.delay" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 发射器形状 -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('shape')">
|
||||
<span class="caret">{{ collapsed.shape ? '▸' : '▾' }}</span><span>发射器形状</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.shape">
|
||||
<label class="row"><span>形状</span>
|
||||
<select v-model="sys.config.shape" class="inp" @change="onShapeChange">
|
||||
<option value="point">点</option>
|
||||
<option value="circle">圆形</option>
|
||||
<option value="rect">矩形</option>
|
||||
<option value="cone">锥体</option>
|
||||
<option value="orbit">轨道</option>
|
||||
</select>
|
||||
</label>
|
||||
<template v-if="sys.config.shape === 'circle' || sys.config.shape === 'cone'">
|
||||
<NumSlider label="半径" :min="0" :max="300" :step="1" v-model="sys.config.radius" />
|
||||
</template>
|
||||
<template v-if="sys.config.shape === 'cone'">
|
||||
<NumSlider label="锥形开口角度" :min="1" :max="360" :step="1" v-model="sys.config.coneAngle" />
|
||||
</template>
|
||||
<template v-else-if="sys.config.shape === 'rect'">
|
||||
<NumSlider label="宽" :min="1" :max="400" :step="1" v-model="sys.config.rectW" />
|
||||
<NumSlider label="高" :min="1" :max="400" :step="1" v-model="sys.config.rectH" />
|
||||
</template>
|
||||
<div class="divider"></div>
|
||||
<NumSlider :label="sys.config.shape === 'cone' ? '锥形朝向' : '发射角度'" :min="-360" :max="360" :step="1" v-model="sys.config.direction" />
|
||||
<NumSlider v-if="sys.config.shape !== 'cone'" label="扩散角度" :min="0" :max="360" :step="1" v-model="sys.config.spread" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 粒子属性 -->
|
||||
<div class="group">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 5. 外观与资源 -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('look')">
|
||||
<span class="caret">{{ collapsed.look ? '▸' : '▾' }}</span><span>外观与资源</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.look">
|
||||
<!-- 透明度 -->
|
||||
<div class="look-title">透明度</div>
|
||||
<div class="seg">
|
||||
<button class="seg-btn" :class="{ active: sys.config.alphaMode === 'curve' }" @click="sys.config.alphaMode = 'curve'">曲线</button>
|
||||
<button class="seg-btn" :class="{ active: sys.config.alphaMode === 'fixed' }" @click="sys.config.alphaMode = 'fixed'">固定</button>
|
||||
</div>
|
||||
<template v-if="sys.config.alphaMode === 'fixed'">
|
||||
<NumSlider label="透明度" :min="0" :max="1" :step="0.01" v-model="sys.config.alpha" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="row"><span>曲线模式</span>
|
||||
<select v-model="sys.config.curveMode" class="inp">
|
||||
<option>跟随导出</option><option>线性</option><option>贝塞尔</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="curve-wrap">
|
||||
<CurveEditor v-model="sys.config.alphaCurve" />
|
||||
<div class="curve-labels"><span>0(透明)</span><span>生命周期 →</span><span>1(不透明)</span></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 图片资源 -->
|
||||
<div class="look-title">图片资源</div>
|
||||
<div class="imgrow">
|
||||
<div class="img-thumb"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#8a93bb" stroke-width="1.6"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.6"/><path d="M21 15l-5-5-9 9"/></svg></div>
|
||||
<div class="img-fields">
|
||||
<label class="row"><span>子文件夹</span><input type="text" v-model="sys.config.textureFolder" class="inp" placeholder="subfolder/" /></label>
|
||||
<label class="row"><span>名称</span><input type="text" v-model="sys.config.textureName" class="inp" placeholder="particle" /></label>
|
||||
</div>
|
||||
<div class="img-actions">
|
||||
<button class="mini-btn2" title="重置" @click="resetTexture()">⟳</button>
|
||||
<button class="mini-btn2" title="删除" @click="clearTexture()">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thumb-note">{{ sys.config.textureName || 'star' }}.png(默认 star.png)</div>
|
||||
|
||||
<!-- 颜色 -->
|
||||
<div class="look-title">颜色</div>
|
||||
<label class="row"><span>颜色类型</span>
|
||||
<select :value="sys.config.colorOverLife ? '渐变' : '固定色'" class="inp" @change="sys.config.colorOverLife = ($event.target as any).value === '渐变'">
|
||||
<option>固定色</option><option>渐变</option>
|
||||
</select>
|
||||
<select v-model="sys.config.blend" class="inp">
|
||||
<option value="normal">正常</option><option value="add">相加</option>
|
||||
<option value="multiply">相乘</option><option value="screen">滤色</option>
|
||||
</select>
|
||||
</label>
|
||||
<template v-if="sys.config.colorOverLife">
|
||||
<label class="row"><span>起始色</span><input type="color" v-model="sys.config.colorStart" class="colr" /></label>
|
||||
<label class="row"><span>结束色</span><input type="color" v-model="sys.config.colorEnd" class="colr" /></label>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="row"><span>颜色</span><input type="color" v-model="sys.config.colorStart" class="colr" />
|
||||
<span class="hex">{{ sys.config.colorStart }}</span></label>
|
||||
</template>
|
||||
|
||||
<!-- 占比 / 缩放 / 轴心 / 独立透明度 -->
|
||||
<div class="look-title">占比与缩放</div>
|
||||
<NumSlider label="占比" :min="0" :max="1" :step="0.01" v-model="sys.config.alphaScale" />
|
||||
<NumSlider label="缩放" :min="0.1" :max="3" :step="0.1" v-model="sys.config.scaleMax" />
|
||||
<label class="row"><span>轴心 x,y</span><input type="text" class="inp tiny" v-model.number="sys.config.anchorX" /><input type="text" class="inp tiny" v-model.number="sys.config.anchorY" /></label>
|
||||
<label class="row"><span>独立透明度</span>
|
||||
<select v-model="sys.config.independentAlpha" class="inp">
|
||||
<option value="off">关闭</option><option value="fixed">固定</option><option value="curve">曲线</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 修改器 (Modifiers) -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('mods')">
|
||||
<span class="caret">{{ collapsed.mods ? '▸' : '▾' }}</span><span>修改器 (Modifiers)</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.mods">
|
||||
<label class="row"><span>移动噪声</span><input type="checkbox" v-model="sys.config.noise" /></label>
|
||||
<NumSlider v-if="sys.config.noise" label="噪声强度" :min="0" :max="200" :step="5" v-model="sys.config.noiseAmt" />
|
||||
<label class="row"><span>重力</span><input type="checkbox" v-model="sys.config.useGravity" /></label>
|
||||
<NumSlider v-if="sys.config.useGravity" label="重力大小" :min="-500" :max="500" :step="10" v-model="sys.config.gravity" />
|
||||
<label class="row"><span>风力</span><input type="checkbox" v-model="sys.config.wind" /></label>
|
||||
<template v-if="sys.config.wind">
|
||||
<NumSlider label="风力X" :min="-300" :max="300" :step="1" v-model="sys.config.windX" />
|
||||
<NumSlider label="风力Y" :min="-300" :max="300" :step="1" v-model="sys.config.windY" />
|
||||
</template>
|
||||
<label class="row"><span>力场</span><input type="checkbox" v-model="sys.config.forceField" /></label>
|
||||
<template v-if="sys.config.forceField">
|
||||
<NumSlider label="力场强度" :min="-300" :max="300" :step="5" v-model="sys.config.forceStrength" />
|
||||
<NumSlider label="力场半径" :min="1" :max="500" :step="1" v-model="sys.config.forceRadius" />
|
||||
</template>
|
||||
<label class="row"><span>拖尾</span><input type="checkbox" v-model="sys.config.trail" /></label>
|
||||
<label class="row"><span>碰撞</span><input type="checkbox" v-model="sys.config.collision" /><span class="tag">预留</span></label>
|
||||
<label class="row"><span>路径跟随</span><input type="checkbox" v-model="sys.config.pathFollow" /><span class="tag">预留</span></label>
|
||||
<NumSlider label="阻尼" :min="0" :max="2" :step="0.05" v-model="sys.config.damping" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7. 导出设置(预留) -->
|
||||
<div class="group">
|
||||
<div class="group-head" @click="toggle('export')">
|
||||
<span class="caret">{{ collapsed.export ? '▸' : '▾' }}</span><span>导出设置</span><span class="tag">预留</span>
|
||||
</div>
|
||||
<div class="group-body" v-show="!collapsed.export">
|
||||
<div class="placeholder">导出功能后续实现,已预留位置</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!store.systems.length" class="panel-hint">点「+ 系统」创建粒子系统</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useParticleStore } from '../store/particleStore'
|
||||
import NumSlider from './NumSlider.vue'
|
||||
import CurveEditor from './CurveEditor.vue'
|
||||
|
||||
const store = useParticleStore()
|
||||
const activeIdx = computed(() => {
|
||||
const i = store.systems.findIndex((s) => s.id === store.activeId)
|
||||
return i >= 0 ? i : 0
|
||||
})
|
||||
const sys = computed(() => store.systems[activeIdx.value] || null)
|
||||
|
||||
const collapsed = reactive<Record<string, boolean>>({
|
||||
scene: false, emitMode: false, shape: false, attr: false, look: false, mods: false, export: false,
|
||||
})
|
||||
function toggle(key: string) { collapsed[key] = !collapsed[key] }
|
||||
|
||||
function select(i: number) { store.selectSystem(store.systems[i]?.id ?? 0) }
|
||||
function addSys() { store.addSystem() }
|
||||
|
||||
// 各发射器形状的默认参数:切换形状时应用(只影响形状相关字段,不牵连其他)
|
||||
const SHAPE_DEFAULTS: Record<string, Partial<any>> = {
|
||||
point: { direction: 0, spread: 360 },
|
||||
circle: { radius: 100, direction: -90, spread: 0 },
|
||||
rect: { rectW: 200, rectH: 100, direction: -90, spread: 0 },
|
||||
cone: { radius: 100, coneAngle: 90, direction: 90 },
|
||||
orbit: {},
|
||||
}
|
||||
function onShapeChange() {
|
||||
const cfg = sys.value?.config
|
||||
if (!cfg) return
|
||||
const d = SHAPE_DEFAULTS[cfg.shape]
|
||||
if (!d) return
|
||||
Object.assign(cfg, d)
|
||||
}
|
||||
// 重置/删除图片 → 回到默认 star.png
|
||||
function resetTexture() {
|
||||
const c = sys.value?.config
|
||||
if (c) { c.textureName = 'star'; c.textureFolder = '' }
|
||||
}
|
||||
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::-webkit-scrollbar { width: 6px; }
|
||||
.panel::-webkit-scrollbar-track { background: transparent; }
|
||||
.panel::-webkit-scrollbar-thumb { background: #2e2e44; border-radius: 3px; }
|
||||
.panel::-webkit-scrollbar-thumb:hover { background: #3a3a55; }
|
||||
.panel { scrollbar-width: thin; scrollbar-color: #2e2e44 transparent; }
|
||||
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-weight: 700; color: #ccd; }
|
||||
.sys-list { margin-bottom: 10px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.sys-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px; background: #1e1e2e; border-radius: 6px; cursor: pointer; }
|
||||
.sys-item.active { background: #2b3a5c; }
|
||||
.sys-name { font-size: 12px; }
|
||||
.empty, .panel-hint { color: #667; font-size: 12px; padding: 8px 0; }
|
||||
.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-body { padding: 8px 10px 12px; }
|
||||
.caret { display: inline-block; width: 12px; color: #667; font-size: 11px; }
|
||||
.divider { height: 1px; background: #2a2a3c; margin: 8px 0; }
|
||||
.tree { margin-bottom: 8px; }
|
||||
.tree-item { display: flex; align-items: center; gap: 6px; padding: 5px 6px; font-size: 12px; color: #bcd; border-radius: 6px; margin-bottom: 2px; }
|
||||
.tree-item.on { background: #26324a; color: #ffe; }
|
||||
.tree-item .tdot { color: #ffcc33; font-size: 10px; }
|
||||
.tree-item .tdot.dim { color: #556; }
|
||||
.tree-item .tlabel { flex: 1; }
|
||||
.tag { font-size: 10px; color: #667; background: #1c1c2a; border-radius: 4px; padding: 1px 5px; }
|
||||
.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; }
|
||||
.inp { flex: 1; min-width: 0; padding: 3px 5px; background: #1a1a28; border: 1px solid #2e2e44; border-radius: 5px; color: #dde; font-size: 12px; }
|
||||
.pair { display: flex; flex-direction: column; gap: 2px; flex: 1; }
|
||||
.colr { width: 28px; height: 22px; border: none; background: none; cursor: pointer; }
|
||||
.segs { display: flex; gap: 6px; margin: 6px 0; }
|
||||
.seg { flex: 1; padding: 5px; background: #1e1e2e; border: 1px solid #2e2e44; border-radius: 6px; color: #aab; font-size: 12px; cursor: pointer; }
|
||||
.seg.on { background: #3a5a8c; color: #fff; border-color: #5a7ab8; }
|
||||
.mini-btn { background: #2e3a55; border: 1px solid #3a4a6a; color: #cdf; border-radius: 5px; padding: 3px 8px; font-size: 11px; cursor: pointer; }
|
||||
.mini-btn.danger { background: #4a2a2e; color: #fbb; }
|
||||
.placeholder { color: #667; font-size: 11px; padding: 6px 0; }
|
||||
/* 外观与资源 参考版式 */
|
||||
.look-title { font-size: 12px; font-weight: 600; color: #ccd; margin: 10px 0 6px; padding-bottom: 4px; border-bottom: 1px solid #2a2a3c; }
|
||||
.look-title:first-child { margin-top: 2px; }
|
||||
.seg { display: flex; gap: 6px; margin: 6px 0; }
|
||||
.seg-btn { flex: 1; padding: 5px; background: #1e1e2e; border: 1px solid #2e2e44; border-radius: 6px; color: #aab; font-size: 12px; cursor: pointer; }
|
||||
.seg-btn.active { background: #3a5a8c; color: #fff; border-color: #5a7ab8; }
|
||||
.curve-hint { min-height: 54px; border: 1px dashed #2e2e44; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #556; font-size: 11px; margin: 6px 0; }
|
||||
.curve-wrap { margin: 6px 0; }
|
||||
.curve-labels { display: flex; justify-content: space-between; color: #566; font-size: 10px; margin-top: 2px; }
|
||||
.imgrow { display: flex; align-items: flex-start; gap: 8px; }
|
||||
.img-thumb { width: 48px; height: 48px; border: 1px solid #2e2e44; border-radius: 6px; background: #1a1a28; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.img-fields { flex: 1; min-width: 0; }
|
||||
.img-actions { display: flex; flex-direction: column; gap: 4px; }
|
||||
.mini-btn2 { width: 26px; height: 24px; background: #1e1e2e; border: 1px solid #2e2e44; border-radius: 5px; color: #aab; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
||||
.mini-btn2:hover { background: #2b3a5c; }
|
||||
.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; }
|
||||
</style>
|
||||
Reference in new issue
Block a user