悬浮导航

This commit is contained in:
tianmo committed 2026-10-24 18:52:25 +08:00
1 parent cffbd49eff
commit a417c9119d
31 files changed
+1577 -1326

No files matched your search

+1
View File
@@ -0,0 +1 @@
.help-overlay[data-v-4bb9bc76]{position:absolute;top:0;right:0;bottom:0;left:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:72px 24px 28px;box-sizing:border-box;background:#05080fb8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.help-dialog[data-v-4bb9bc76]{width:min(820px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;border:1px solid #3a4864;border-radius:14px;background:#141a27;color:#b8c3d8;box-shadow:0 22px 70px #0000009e}.help-head[data-v-4bb9bc76]{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:18px 20px 15px;border-bottom:1px solid #2b354a;background:linear-gradient(135deg,#202942,#181e2d)}.help-head h2[data-v-4bb9bc76]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-4bb9bc76]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-4bb9bc76]{width:28px;height:28px;flex:0 0 28px;border:1px solid #43516b;border-radius:6px;background:#273249;color:#ccd7ed;cursor:pointer}.help-close[data-v-4bb9bc76]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-4bb9bc76]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-4bb9bc76]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-4bb9bc76]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-4bb9bc76]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-4bb9bc76]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-4bb9bc76]{color:#dce4f7}.help-grid[data-v-4bb9bc76]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-4bb9bc76]{display:grid;gap:0;margin:0}.help-list>div[data-v-4bb9bc76]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-4bb9bc76]:last-child{border-bottom:0}.help-list dt[data-v-4bb9bc76]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-4bb9bc76]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-4bb9bc76]{display:grid;gap:7px}.shortcut-list>div[data-v-4bb9bc76]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-4bb9bc76]{min-width:24px;box-sizing:border-box;padding:3px 6px;border:1px solid #465470;border-bottom-width:2px;border-radius:5px;background:#101623;color:#e0e7f6;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center}.shortcut-list span[data-v-4bb9bc76]{color:#65728b;font-size:10px}.shortcut-list em[data-v-4bb9bc76]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-4bb9bc76]{margin:11px 0 0;padding-top:9px;border-top:1px solid #2a3549;color:#697791;font-size:10px;line-height:1.55}.help-export[data-v-4bb9bc76]{margin-top:12px}.help-export p[data-v-4bb9bc76]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-4bb9bc76]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-4bb9bc76]{margin-top:12px}.help-module-head[data-v-4bb9bc76]{display:flex;align-items:center;gap:8px;width:100%;padding:0;border:0;background:transparent;color:#9fb2ff;font:inherit;font-size:13px;font-weight:700;text-align:left;cursor:pointer}.help-module-head[data-v-4bb9bc76]:hover{color:#fff}.help-module-head .mtitle[data-v-4bb9bc76]{flex:1;min-width:0}.help-module-head .caret[data-v-4bb9bc76]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-4bb9bc76]{color:#fff}.help-module.open .help-module-head .caret[data-v-4bb9bc76]{color:#9fb2ff}.help-module .mnum[data-v-4bb9bc76]{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:linear-gradient(135deg,#4a53b8,#6a5fd0);color:#fff;font-size:12px;font-weight:700}.help-module.help-connector[data-v-4bb9bc76]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-4bb9bc76]{color:#b8c2ff}.help-module .connector-intro[data-v-4bb9bc76]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-4bb9bc76]{color:#dce4f7}.help-module .dl-link[data-v-4bb9bc76]{display:inline-flex;flex-direction:column;align-items:flex-start;gap:2px;margin:0 10px 6px 0;padding:6px 12px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;font-size:12px;font-weight:700;text-decoration:none}.help-module .dl-link[data-v-4bb9bc76]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-4bb9bc76]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-4bb9bc76]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-4bb9bc76]{padding:66px 12px 14px}.help-grid[data-v-4bb9bc76]{grid-template-columns:1fr}}.topbar[data-v-6411b316]{position:absolute;top:10px;left:10px;right:10px;dispLine truncated
-1
View File
@@ -1 +0,0 @@
.help-overlay[data-v-4bb9bc76]{position:absolute;top:0;right:0;bottom:0;left:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:72px 24px 28px;box-sizing:border-box;background:#05080fb8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.help-dialog[data-v-4bb9bc76]{width:min(820px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;border:1px solid #3a4864;border-radius:14px;background:#141a27;color:#b8c3d8;box-shadow:0 22px 70px #0000009e}.help-head[data-v-4bb9bc76]{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:18px 20px 15px;border-bottom:1px solid #2b354a;background:linear-gradient(135deg,#202942,#181e2d)}.help-head h2[data-v-4bb9bc76]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-4bb9bc76]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-4bb9bc76]{width:28px;height:28px;flex:0 0 28px;border:1px solid #43516b;border-radius:6px;background:#273249;color:#ccd7ed;cursor:pointer}.help-close[data-v-4bb9bc76]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-4bb9bc76]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-4bb9bc76]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-4bb9bc76]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-4bb9bc76]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-4bb9bc76]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-4bb9bc76]{color:#dce4f7}.help-grid[data-v-4bb9bc76]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-4bb9bc76]{display:grid;gap:0;margin:0}.help-list>div[data-v-4bb9bc76]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-4bb9bc76]:last-child{border-bottom:0}.help-list dt[data-v-4bb9bc76]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-4bb9bc76]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-4bb9bc76]{display:grid;gap:7px}.shortcut-list>div[data-v-4bb9bc76]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-4bb9bc76]{min-width:24px;box-sizing:border-box;padding:3px 6px;border:1px solid #465470;border-bottom-width:2px;border-radius:5px;background:#101623;color:#e0e7f6;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center}.shortcut-list span[data-v-4bb9bc76]{color:#65728b;font-size:10px}.shortcut-list em[data-v-4bb9bc76]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-4bb9bc76]{margin:11px 0 0;padding-top:9px;border-top:1px solid #2a3549;color:#697791;font-size:10px;line-height:1.55}.help-export[data-v-4bb9bc76]{margin-top:12px}.help-export p[data-v-4bb9bc76]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-4bb9bc76]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-4bb9bc76]{margin-top:12px}.help-module-head[data-v-4bb9bc76]{display:flex;align-items:center;gap:8px;width:100%;padding:0;border:0;background:transparent;color:#9fb2ff;font:inherit;font-size:13px;font-weight:700;text-align:left;cursor:pointer}.help-module-head[data-v-4bb9bc76]:hover{color:#fff}.help-module-head .mtitle[data-v-4bb9bc76]{flex:1;min-width:0}.help-module-head .caret[data-v-4bb9bc76]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-4bb9bc76]{color:#fff}.help-module.open .help-module-head .caret[data-v-4bb9bc76]{color:#9fb2ff}.help-module .mnum[data-v-4bb9bc76]{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:linear-gradient(135deg,#4a53b8,#6a5fd0);color:#fff;font-size:12px;font-weight:700}.help-module.help-connector[data-v-4bb9bc76]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-4bb9bc76]{color:#b8c2ff}.help-module .connector-intro[data-v-4bb9bc76]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-4bb9bc76]{color:#dce4f7}.help-module .dl-link[data-v-4bb9bc76]{display:inline-flex;flex-direction:column;align-items:flex-start;gap:2px;margin:0 10px 6px 0;padding:6px 12px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;font-size:12px;font-weight:700;text-decoration:none}.help-module .dl-link[data-v-4bb9bc76]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-4bb9bc76]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-4bb9bc76]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-4bb9bc76]{padding:66px 12px 14px}.help-grid[data-v-4bb9bc76]{grid-template-columns:1fr}}.topbar[data-v-6411b316]{position:absolute;top:10px;left:10px;right:10px;dispLine truncated
@@ -1,2 +1,2 @@
var ft=Object.defineProperty;var gt=(W,t,e)=>t in W?ft(W,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):W[t]=e;var T=(W,t,e)=>gt(W,typeof t!="symbol"?t+"":t,e);import{f as ht,R as pt,N as dt,ac as ut,Z as mt,b4 as yt,O as rt,b5 as at,U as D,B as Tt}from"./index-f-s7HL-r.js";import{$ as Rt,a0 as Yt,a1 as Ft,a2 as Et,y as _t,a3 as Ot,a4 as kt,a5 as Xt,a6 as Bt,a7 as Nt,a8 as Dt,a9 as $t,aa as zt,ab as Wt,ad as qt,H as Gt,ae as Jt,af as Ht,ag as Zt,ah as Kt,ai as Qt,aj as jt,ak as te,al as ee,am as se,an as ne,ao as ie,ap as oe,aq as re,ar as ae,as as le,at as ce,au as he,av as pe,aw as de,ax as ue,ay as fe,az as ge,aA as me,aB as ye,aC as Te,aD as be,aE as we,aF as xe,aG as Ae,aH as Pe,aI as Ie,aJ as Ce,aK as Ve,aL as Le,aM as Se,aN as Me,aO as ve,aP as Ue,aQ as Re,aR as Ye,aS as Fe,aT as Ee,x as _e,aU as Oe,aV as ke,aW as Xe,aX as Be,aY as Ne,aZ as De,a_ as $e,a$ as ze,b0 as We,b1 as qe,b2 as Ge,b3 as Je,b6 as He,b7 as Ze,b8 as Ke,b9 as Qe,ba as je,bb as ts,bc as es,bd as ss,be as ns,bf as is,bg as os,bh as rs,bi as as,bj as ls,bk as cs,bl as hs,bm as ps,bn as ds,bo as us,bp as fs,bq as gs,br as ms,bs as ys,bt as Ts,bu as bs,w as ws,X as xs,J as As,Y as Ps,z as Is,bv as Cs,bw as Vs,bx as Ls,by as Ss,bz as Ms,bA as vs,bB as Us,bC as Rs,bD as Ys,bE as Fs,bF as Es,bG as _s,T as Os,bH as ks,bI as Xs,b as Bs,bJ as Ns,c as Ds,bK as $s,bL as zs,bM as Ws,bN as qs,bO as Gs,bP as Js,bQ as Hs,bR as Zs,bS as Ks,bT as Qs,bU as js,bV as tn,bW as en,bX as sn,bY as nn,bZ as on,b_ as rn,V as an,b$ as ln,c0 as cn,c1 as hn,c2 as pn,c3 as dn}from"./index-f-s7HL-r.js";class It{constructor(t){T(this,"skeletonData");T(this,"animationToMixTime",{});T(this,"defaultMix",0);if(!t)throw new Error("skeletonData cannot be null.");this.skeletonData=t}setMix(t,e,n){return typeof t=="string"?this.setMix1(t,e,n):this.setMix2(t,e,n)}setMix1(t,e,n){const s=this.skeletonData.findAnimation(t);if(!s)throw new Error(`Animation not found: ${t}`);const i=this.skeletonData.findAnimation(e);if(!i)throw new Error(`Animation not found: ${e}`);this.setMix2(s,i,n)}setMix2(t,e,n){if(!t)throw new Error("from cannot be null.");if(!e)throw new Error("to cannot be null.");const s=`${t.name}.${e.name}`;this.animationToMixTime[s]=n}getMix(t,e){const n=`${t.name}.${e.name}`,s=this.animationToMixTime[n];return s===void 0?this.defaultMix:s}}class Ct{constructor(t,e="",n=new bt,s=new lt){T(this,"textureLoader");T(this,"pathPrefix");T(this,"downloader");T(this,"cache");T(this,"errors",{});T(this,"toLoad",0);T(this,"loaded",0);T(this,"texturePmaInfo",{});this.textureLoader=t,this.pathPrefix=e,this.downloader=n,this.cache=s}start(t){return this.toLoad++,this.pathPrefix+t}success(t,e,n){this.toLoad--,this.loaded++,this.cache.assets[e]=n,this.cache.assetsRefCount[e]=(this.cache.assetsRefCount[e]||0)+1,t&&t(e,n)}error(t,e,n){this.toLoad--,this.loaded++,this.errors[e]=n,t&&t(e,n)}loadAll(){return new Promise((e,n)=>{const s=()=>{if(this.isLoadingComplete()){this.hasErrors()?n(this.errors):e(this);return}requestAnimationFrame(s)};requestAnimationFrame(s)})}setRawDataURI(t,e){this.downloader.rawDataUris[this.pathPrefix+t]=e}loadBinary(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadBinary(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load binary ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}loadText(t,e=()=>{},n=()=>{}){t=this.start(t),this.downloader.downloadText(t,s=>{this.success(e,t,s)},(s,i)=>{this.error(n,t,`Couldn't load text ${t}: status ${s}, ${i}`)})}loadJson(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadJson(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load JSON ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}reuseAssets(t,e=()=>{},n=()=>{}){const s=this.cache.getAsset(t),i=s!==void 0;return i&&(this.cache.assetsLoaded[t]=s.then(o=>(o=o instanceof Image||o instanceof ImageBitmap?this.textureLoader(o):o,this.success(e,t,o),o)).catch(o=>{this.error(n,t,o)})),i}loadTexture(t,e=()=>{},n=()=>{}){if(t=this.start(t),this.reuseAssets(t,e,n))return;const s=this.texturePmaInfo[t];this.cache.assetsLoaded[t]=new Promise((i,o)=>{if(!!!(typeof window<"u"&&typeof navigator<"u"&&window.document))fetch(t,{mode:"cors"}).then(c=>{if(c.ok)return c.blob();const a=`Couldn't load image: ${t}`;this.error(n,t,`Couldn't load image: ${t}`),o(a)}).then(c=>c?createImageBitmap(c,{premultiplyAlpha:"none",colorSpaceConversion:"none"}):null).then(c=>{if(c){const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)}});else{const c=new Image;c.crossOrigin="anonymous",c.onload=()=>{const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)},c.onerror=()=>{const a=`Couldn't load image: ${t}`;this.error(n,t,a),o(a)},this.downloader.rawDataUris[t]&&(t=this.downloader.rawDataUris[t]),c.src=t}})}loadTextureAtlas(t,e=()=>{},n=()=>{},s){const i=t.lastIndexOf("/"),o=i>=0?t.substring(0,i+1)Line truncated
var ft=Object.defineProperty;var gt=(W,t,e)=>t in W?ft(W,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):W[t]=e;var T=(W,t,e)=>gt(W,typeof t!="symbol"?t+"":t,e);import{f as ht,R as pt,N as dt,ac as ut,Z as mt,b4 as yt,O as rt,b5 as at,U as D,B as Tt}from"./index-CcSqUk8V.js";import{$ as Rt,a0 as Yt,a1 as Ft,a2 as Et,y as _t,a3 as Ot,a4 as kt,a5 as Xt,a6 as Bt,a7 as Nt,a8 as Dt,a9 as $t,aa as zt,ab as Wt,ad as qt,H as Gt,ae as Jt,af as Ht,ag as Zt,ah as Kt,ai as Qt,aj as jt,ak as te,al as ee,am as se,an as ne,ao as ie,ap as oe,aq as re,ar as ae,as as le,at as ce,au as he,av as pe,aw as de,ax as ue,ay as fe,az as ge,aA as me,aB as ye,aC as Te,aD as be,aE as we,aF as xe,aG as Ae,aH as Pe,aI as Ie,aJ as Ce,aK as Ve,aL as Le,aM as Se,aN as Me,aO as ve,aP as Ue,aQ as Re,aR as Ye,aS as Fe,aT as Ee,x as _e,aU as Oe,aV as ke,aW as Xe,aX as Be,aY as Ne,aZ as De,a_ as $e,a$ as ze,b0 as We,b1 as qe,b2 as Ge,b3 as Je,b6 as He,b7 as Ze,b8 as Ke,b9 as Qe,ba as je,bb as ts,bc as es,bd as ss,be as ns,bf as is,bg as os,bh as rs,bi as as,bj as ls,bk as cs,bl as hs,bm as ps,bn as ds,bo as us,bp as fs,bq as gs,br as ms,bs as ys,bt as Ts,bu as bs,w as ws,X as xs,J as As,Y as Ps,z as Is,bv as Cs,bw as Vs,bx as Ls,by as Ss,bz as Ms,bA as vs,bB as Us,bC as Rs,bD as Ys,bE as Fs,bF as Es,bG as _s,T as Os,bH as ks,bI as Xs,b as Bs,bJ as Ns,c as Ds,bK as $s,bL as zs,bM as Ws,bN as qs,bO as Gs,bP as Js,bQ as Hs,bR as Zs,bS as Ks,bT as Qs,bU as js,bV as tn,bW as en,bX as sn,bY as nn,bZ as on,b_ as rn,V as an,b$ as ln,c0 as cn,c1 as hn,c2 as pn,c3 as dn}from"./index-CcSqUk8V.js";class It{constructor(t){T(this,"skeletonData");T(this,"animationToMixTime",{});T(this,"defaultMix",0);if(!t)throw new Error("skeletonData cannot be null.");this.skeletonData=t}setMix(t,e,n){return typeof t=="string"?this.setMix1(t,e,n):this.setMix2(t,e,n)}setMix1(t,e,n){const s=this.skeletonData.findAnimation(t);if(!s)throw new Error(`Animation not found: ${t}`);const i=this.skeletonData.findAnimation(e);if(!i)throw new Error(`Animation not found: ${e}`);this.setMix2(s,i,n)}setMix2(t,e,n){if(!t)throw new Error("from cannot be null.");if(!e)throw new Error("to cannot be null.");const s=`${t.name}.${e.name}`;this.animationToMixTime[s]=n}getMix(t,e){const n=`${t.name}.${e.name}`,s=this.animationToMixTime[n];return s===void 0?this.defaultMix:s}}class Ct{constructor(t,e="",n=new bt,s=new lt){T(this,"textureLoader");T(this,"pathPrefix");T(this,"downloader");T(this,"cache");T(this,"errors",{});T(this,"toLoad",0);T(this,"loaded",0);T(this,"texturePmaInfo",{});this.textureLoader=t,this.pathPrefix=e,this.downloader=n,this.cache=s}start(t){return this.toLoad++,this.pathPrefix+t}success(t,e,n){this.toLoad--,this.loaded++,this.cache.assets[e]=n,this.cache.assetsRefCount[e]=(this.cache.assetsRefCount[e]||0)+1,t&&t(e,n)}error(t,e,n){this.toLoad--,this.loaded++,this.errors[e]=n,t&&t(e,n)}loadAll(){return new Promise((e,n)=>{const s=()=>{if(this.isLoadingComplete()){this.hasErrors()?n(this.errors):e(this);return}requestAnimationFrame(s)};requestAnimationFrame(s)})}setRawDataURI(t,e){this.downloader.rawDataUris[this.pathPrefix+t]=e}loadBinary(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadBinary(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load binary ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}loadText(t,e=()=>{},n=()=>{}){t=this.start(t),this.downloader.downloadText(t,s=>{this.success(e,t,s)},(s,i)=>{this.error(n,t,`Couldn't load text ${t}: status ${s}, ${i}`)})}loadJson(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadJson(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load JSON ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}reuseAssets(t,e=()=>{},n=()=>{}){const s=this.cache.getAsset(t),i=s!==void 0;return i&&(this.cache.assetsLoaded[t]=s.then(o=>(o=o instanceof Image||o instanceof ImageBitmap?this.textureLoader(o):o,this.success(e,t,o),o)).catch(o=>{this.error(n,t,o)})),i}loadTexture(t,e=()=>{},n=()=>{}){if(t=this.start(t),this.reuseAssets(t,e,n))return;const s=this.texturePmaInfo[t];this.cache.assetsLoaded[t]=new Promise((i,o)=>{if(!!!(typeof window<"u"&&typeof navigator<"u"&&window.document))fetch(t,{mode:"cors"}).then(c=>{if(c.ok)return c.blob();const a=`Couldn't load image: ${t}`;this.error(n,t,`Couldn't load image: ${t}`),o(a)}).then(c=>c?createImageBitmap(c,{premultiplyAlpha:"none",colorSpaceConversion:"none"}):null).then(c=>{if(c){const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)}});else{const c=new Image;c.crossOrigin="anonymous",c.onload=()=>{const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)},c.onerror=()=>{const a=`Couldn't load image: ${t}`;this.error(n,t,a),o(a)},this.downloader.rawDataUris[t]&&(t=this.downloader.rawDataUris[t]),c.src=t}})}loadTextureAtlas(t,e=()=>{},n=()=>{},s){const i=t.lastIndexOf("/"),o=i>=0?t.substring(0,i+1)Line truncated
${n}`:""}`)}remove(t){t=this.pathPrefix+t;const e=this.cache.assets[t];return e.dispose&&e.dispose(),delete this.cache.assets[t],delete this.cache.assetsRefCount[t],delete this.cache.assetsLoaded[t],e}removeAll(){for(const t in this.cache.assets){const e=this.cache.assets[t];e.dispose&&e.dispose()}this.cache.assets={},this.cache.assetsLoaded={},this.cache.assetsRefCount={}}isLoadingComplete(){return this.toLoad===0}getToLoad(){return this.toLoad}getLoaded(){return this.loaded}dispose(){this.removeAll()}disposeAsset(t){const e=this.cache.assets[t];if(e instanceof ht){e.dispose();return}this.disposeAssetInternal(t)}hasErrors(){return Object.keys(this.errors).length>0}getErrors(){return this.errors}disposeAssetInternal(t){if(this.cache.assetsRefCount[t]>0&&--this.cache.assetsRefCount[t]===0)return this.remove(t)}createTextureAtlas(t,e,n,s){const i=new ht(t);i.dispose=()=>{var o;if(!(this.cache.assetsRefCount[n]<=0)){this.disposeAssetInternal(n);for(const r of i.pages)(o=r.texture)==null||o.dispose()}};for(const o of i.pages){const r=this.texturePath(e,o.name,s);this.texturePmaInfo[this.pathPrefix+r]=o.pma}return i}createTexture(t,e,n){const s=this.textureLoader(n,e),i=s.dispose.bind(s);return s.dispose=()=>{this.disposeAssetInternal(t)&&i()},s}texturePath(t,e,n){return n?n[e]:t+e}}const nt=class nt{constructor(){T(this,"assets",{});T(this,"assetsRefCount",{});T(this,"assetsLoaded",{})}static getCache(t){const e=nt.AVAILABLE_CACHES.get(t);if(e)return e;const n=new nt;return nt.AVAILABLE_CACHES.set(t,n),n}async addAsset(t,e){return this.assetsLoaded[t]=Promise.resolve(e),this.assets[t]=e,e}getAsset(t){return this.assetsLoaded[t]}};T(nt,"AVAILABLE_CACHES",new Map);let lt=nt;class bt{constructor(){T(this,"callbacks",{});T(this,"rawDataUris",{})}dataUriToString(t){if(!t.startsWith("data:"))throw new Error("Not a data URI.");let e=t.indexOf("base64,");return e!==-1?(e+=7,atob(t.substr(e))):t.substr(t.indexOf(",")+1)}base64ToUint8Array(t){var e=window.atob(t),n=e.length,s=new Uint8Array(n);for(let i=0;i<n;i++)s[i]=e.charCodeAt(i);return s}dataUriToUint8Array(t){if(!t.startsWith("data:"))throw new Error("Not a data URI.");let e=t.indexOf("base64,");if(e===-1)throw new Error("Not a binary data URI.");return e+=7,this.base64ToUint8Array(t.substr(e))}downloadText(t,e,n){if(this.start(t,e,n))return;const s=this.rawDataUris[t];if(s&&!s.includes(".")){try{this.finish(t,200,this.dataUriToString(s))}catch(r){this.finish(t,400,JSON.stringify(r))}return}const i=new XMLHttpRequest;i.overrideMimeType("text/html"),i.open("GET",s||t,!0);const o=()=>{this.finish(t,i.status,i.responseText)};i.onload=o,i.onerror=o,i.send()}downloadJson(t,e,n){this.downloadText(t,s=>{e(JSON.parse(s))},n)}downloadBinary(t,e,n){if(this.start(t,e,n))return;const s=this.rawDataUris[t];if(s&&!s.includes(".")){try{this.finish(t,200,this.dataUriToUint8Array(s))}catch(r){this.finish(t,400,JSON.stringify(r))}return}const i=new XMLHttpRequest;i.open("GET",s||t,!0),i.responseType="arraybuffer";const o=()=>{this.finish(t,i.status,i.response)};i.onload=()=>{i.status===200||i.status===0?this.finish(t,200,new Uint8Array(i.response)):o()},i.onerror=o,i.send()}start(t,e,n){let s=this.callbacks[t];try{if(s)return!0;this.callbacks[t]=s=[]}finally{s.push(e,n)}}finish(t,e,n){const s=this.callbacks[t];if(delete this.callbacks[t],e===200||e===0)for(let i=0,o=s.length;i<o;i+=2)s[i](n);else for(let i=1,o=s.length;i<o;i+=2)s[i](e,n)}}class Vt{constructor(t,e=!1){T(this,"atlas");T(this,"allowMissingRegions");this.atlas=t,this.allowMissingRegions=e}findRegions(t,e,n){const s=n.regions;for(let i=0,o=s.length;i<o;i++)s[i]=this.findRegion(t,n.getPath(e,i))}findRegion(t,e){const n=this.atlas.findRegion(e);if(!n&&!this.allowMissingRegions)throw new Error(`Region not found in atlas: ${e} (attachment: ${t})`);return n}newRegionAttachment(t,e,n,s,i){return this.findRegions(n,s,i),new pt(n,i)}newMeshAttachment(t,e,n,s,i){return this.findRegions(n,s,i),new dt(n,i)}newBoundingBoxAttachment(t,e,n){return new ut(n)}newPathAttachment(t,e,n){return new mt(n)}newPointAttachment(t,e,n){return new yt(n)}newClippingAttachment(t,e,n){return new rt(n)}}class Lt{constructor(){T(this,"minX",0);T(this,"minY",0);T(this,"maxX",0);T(this,"maxY",0);T(this,"boundingBoxes",[]);T(this,"polygons",[]);T(this,"polygonPool",new at(()=>D.newFloatArray(16)))}update(t,e){if(!t)throw new Error("skeleton cannot be null.");const n=this.boundingBoxes,s=this.polygons,i=this.polygonPool,o=t.slots,r=o.length;n.length=0,i.freeAll(s),s.length=0;for(let l=0;l<r;l++){const c=o[l];if(!c.bone.active)continue;const a=c.appliedPose.attachment;if(a instanceof ut){n.push(a);let h=i.obtain();h.length!==a.worldVerticesLength&&(h=D.newFloatArray(a.worldVerticesLength)),s.push(h),a.computeWorldVertices(t,c,0,a.worldVerticesLength,h,0,2)}}e?this.aabbCompute():(this.minX=Number.POSITIVE_INFINITY,this.minY=Number.POSITIVE_INFINITY,this.maxX=Number.NEGATIVE_INFINITY,this.maxY=Number.NEGATIVE_INFINITY)}aabbCompute(){let t=Number.POSITIVE_INFLine truncated
+1257
View File
File diff suppressed because it is too large. Load diff
@@ -1,4 +1,4 @@
var Ae=Object.defineProperty;var Be=(m,o,e)=>o in m?Ae(m,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[o]=e;var a=(m,o,e)=>Be(m,typeof o!="symbol"?o+"":o,e);import{T as Fe,a as he,b as F,S as ue,M as pe,c as oe,W as ne,B as H,d as z,L as Te,E as O,e as ce,p as q,f as Ee,g as fe,A as Q,h as me,i as De,j as V,s as re,G as we,k as $,l as Y,m as Le,P as Ie,n as Re,o as Ne,C as X,q as be,r as _e,t as Ue,u as Ye,v as Xe,w as ee,x as j,y as ke,z as Ve,D as J,F as ge,V as Ge,H as ae,I as We,J as $e,U as xe,K as E,R as ve,N as ye,O as Pe,Q as Oe,X as He,Y as ze,Z as qe,_ as Ke}from"./index-f-s7HL-r.js";import{$ as bt,a0 as kt,a1 as vt,a2 as yt,a3 as Pt,a4 as St,a5 as Mt,a6 as At,a7 as Bt,a8 as Ft,a9 as Et,aa as Dt,ab as Lt,ac as It,ad as Rt,ae as Nt,af as _t,ag as Ut,ah as Yt,ai as Xt,aj as Vt,ak as Gt,al as Wt,am as $t,an as Ot,ao as Ht,ap as zt,aq as qt,ar as Kt,as as Zt,at as Jt,au as Qt,av as jt,aw as es,ax as ts,ay as ss,az as os,aA as ns,aB as as,aC as rs,aD as is,aE as ls,aF as hs,aG as cs,aH as ds,aI as us,aJ as ps,aK as fs,aL as ms,aM as gs,aN as xs,aO as Cs,aP as Ts,aQ as ws,aR as bs,aS as ks,aT as vs,aU as ys,aV as Ps,aW as Ss,aX as Ms,aY as As,aZ as Bs,a_ as Fs,a$ as Es,b0 as Ds,b1 as Ls,b2 as Is,b3 as Rs,b4 as Ns,b5 as _s,b6 as Us,b7 as Ys,b8 as Xs,b9 as Vs,ba as Gs,bb as Ws,bc as $s,bd as Os,be as Hs,bf as zs,bg as qs,bh as Ks,bi as Zs,bj as Js,bk as Qs,bl as js,bm as eo,bn as to,bo as so,bp as oo,bq as no,br as ao,bs as ro,bt as io,bu as lo,bv as ho,bw as co,bx as uo,by as po,bz as fo,bA as mo,bB as go,bC as xo,bD as Co,bE as To,bF as wo,bG as bo,bH as ko,bI as vo,bJ as yo,bK as Po,bL as So,bM as Mo,bN as Ao,bO as Bo,bP as Fo,bQ as Eo,bR as Do,bS as Lo,bT as Io,bU as Ro,bV as No,bW as _o,bX as Uo,bY as Yo,bZ as Xo,b_ as Vo,b$ as Go,c0 as Wo,c1 as $o,c2 as Oo,c3 as Ho}from"./index-f-s7HL-r.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-DqWLLav-.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-DqWLLav-.js";if(typeof window<"u"&&window.PIXI){const m=window.require;window.require=o=>{if(m)return m(o);if(o.startsWith("@pixi/"))return window.PIXI}}const _=class _ extends Fe{constructor(e){super(e.resource.source);a(this,"texture");this.texture=he.from(e)}static from(e){return _.textureMap.has(e)?_.textureMap.get(e):new _(e)}setFilters(e,t){this.texture.baseTexture.scaleMode=_.toPixiTextureFilter(e),this.texture.baseTexture.mipmap=_.toPixiMipMap(e)}setWraps(e,t){this.texture.baseTexture.wrapMode=_.toPixiTextureWrap(e)}dispose(){this.texture.destroy()}static toPixiTextureFilter(e){switch(e){case F.Nearest:case F.MipMapNearestLinear:case F.MipMapNearestNearest:return ue.NEAREST;case F.Linear:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return ue.LINEAR;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiMipMap(e){switch(e){case F.Nearest:case F.Linear:return pe.OFF;case F.MipMapNearestLinear:case F.MipMapNearestNearest:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return pe.ON;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiTextureWrap(e){switch(e){case oe.ClampToEdge:return ne.CLAMP;case oe.MirroredRepeat:return ne.MIRRORED_REPEAT;case oe.Repeat:return ne.REPEAT;default:throw new Error(`Unknown texture wrap: ${String(e)}`)}}static toPixiBlending(e){switch(e){case H.Normal:return z.NORMAL;case H.Additive:return z.ADD;case H.Multiply:return z.MULTIPLY;case H.Screen:return z.SCREEN;default:throw new Error(`Unknown blendMode: ${String(e)}`)}}};a(_,"textureMap",new Map);let G=_;const K="spineTextureAtlasLoader",et={extension:O.Asset,resolver:{test:m=>V(m,".atlas"),parse:m=>{var e,t;const o=m.split(".");return{resolution:parseFloat(((t=(e=re.RETINA_PREFIX)==null?void 0:e.exec(m))==null?void 0:t[1])??"1"),format:o[o.length-2],src:m}}},loader:{name:K,extension:{type:O.LoadParser,priority:Te.Normal,name:K},test(m){return V(m,".atlas")},async load(m){const o=await re.ADAPTER.fetch(m);if(!o.ok)throw new Error(`[${K}] Failed to fetch ${m}: ${o.status} ${o.statusText}`);return await o.text()},testParse(m,o){const e=V(o.src,".atlas"),t=typeof m=="string",s=o.loadParser===K;return Promise.resolve((e||s)&&t)},unload(m){m.dispose()},async parse(m,o,e){var p;const t=o.data||{};let s=q.dirname(o.src);s&&s.lastIndexOf("/")!==s.length-1&&(s+="/");const n=new Ee(m);if(t.images instanceof fe||typeof t.images=="string"){const h=t.images;t.images={},t.images[n.pages[0].name]=h}const r=[];let l=!0;for(const h of e.parsers)if(h.name==="loadTextures"){l=(p=h.config)==null?void 0:p.preferCreateImageBitmap;break}Q.setPreferences({preferCreateImageBitmap:!1});for(const h of n.pages){const u=h.name,c=t!=null&&t.images?t.images[u]:void 0;if(c instanceof fe)h.setTexture(G.from(c));else{const i=c??q.normalize([...s.split(q.sep),u].join(q.sep)),g={src:De(i,o.src),data:{...t.imageMetadLine truncated
var Ae=Object.defineProperty;var Be=(m,o,e)=>o in m?Ae(m,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[o]=e;var a=(m,o,e)=>Be(m,typeof o!="symbol"?o+"":o,e);import{T as Fe,a as he,b as F,S as ue,M as pe,c as oe,W as ne,B as H,d as z,L as Te,E as O,e as ce,p as q,f as Ee,g as fe,A as Q,h as me,i as De,j as V,s as re,G as we,k as $,l as Y,m as Le,P as Ie,n as Re,o as Ne,C as X,q as be,r as _e,t as Ue,u as Ye,v as Xe,w as ee,x as j,y as ke,z as Ve,D as J,F as ge,V as Ge,H as ae,I as We,J as $e,U as xe,K as E,R as ve,N as ye,O as Pe,Q as Oe,X as He,Y as ze,Z as qe,_ as Ke}from"./index-CcSqUk8V.js";import{$ as bt,a0 as kt,a1 as vt,a2 as yt,a3 as Pt,a4 as St,a5 as Mt,a6 as At,a7 as Bt,a8 as Ft,a9 as Et,aa as Dt,ab as Lt,ac as It,ad as Rt,ae as Nt,af as _t,ag as Ut,ah as Yt,ai as Xt,aj as Vt,ak as Gt,al as Wt,am as $t,an as Ot,ao as Ht,ap as zt,aq as qt,ar as Kt,as as Zt,at as Jt,au as Qt,av as jt,aw as es,ax as ts,ay as ss,az as os,aA as ns,aB as as,aC as rs,aD as is,aE as ls,aF as hs,aG as cs,aH as ds,aI as us,aJ as ps,aK as fs,aL as ms,aM as gs,aN as xs,aO as Cs,aP as Ts,aQ as ws,aR as bs,aS as ks,aT as vs,aU as ys,aV as Ps,aW as Ss,aX as Ms,aY as As,aZ as Bs,a_ as Fs,a$ as Es,b0 as Ds,b1 as Ls,b2 as Is,b3 as Rs,b4 as Ns,b5 as _s,b6 as Us,b7 as Ys,b8 as Xs,b9 as Vs,ba as Gs,bb as Ws,bc as $s,bd as Os,be as Hs,bf as zs,bg as qs,bh as Ks,bi as Zs,bj as Js,bk as Qs,bl as js,bm as eo,bn as to,bo as so,bp as oo,bq as no,br as ao,bs as ro,bt as io,bu as lo,bv as ho,bw as co,bx as uo,by as po,bz as fo,bA as mo,bB as go,bC as xo,bD as Co,bE as To,bF as wo,bG as bo,bH as ko,bI as vo,bJ as yo,bK as Po,bL as So,bM as Mo,bN as Ao,bO as Bo,bP as Fo,bQ as Eo,bR as Do,bS as Lo,bT as Io,bU as Ro,bV as No,bW as _o,bX as Uo,bY as Yo,bZ as Xo,b_ as Vo,b$ as Go,c0 as Wo,c1 as $o,c2 as Oo,c3 as Ho}from"./index-CcSqUk8V.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-CMg-dxKo.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-CMg-dxKo.js";if(typeof window<"u"&&window.PIXI){const m=window.require;window.require=o=>{if(m)return m(o);if(o.startsWith("@pixi/"))return window.PIXI}}const _=class _ extends Fe{constructor(e){super(e.resource.source);a(this,"texture");this.texture=he.from(e)}static from(e){return _.textureMap.has(e)?_.textureMap.get(e):new _(e)}setFilters(e,t){this.texture.baseTexture.scaleMode=_.toPixiTextureFilter(e),this.texture.baseTexture.mipmap=_.toPixiMipMap(e)}setWraps(e,t){this.texture.baseTexture.wrapMode=_.toPixiTextureWrap(e)}dispose(){this.texture.destroy()}static toPixiTextureFilter(e){switch(e){case F.Nearest:case F.MipMapNearestLinear:case F.MipMapNearestNearest:return ue.NEAREST;case F.Linear:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return ue.LINEAR;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiMipMap(e){switch(e){case F.Nearest:case F.Linear:return pe.OFF;case F.MipMapNearestLinear:case F.MipMapNearestNearest:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return pe.ON;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiTextureWrap(e){switch(e){case oe.ClampToEdge:return ne.CLAMP;case oe.MirroredRepeat:return ne.MIRRORED_REPEAT;case oe.Repeat:return ne.REPEAT;default:throw new Error(`Unknown texture wrap: ${String(e)}`)}}static toPixiBlending(e){switch(e){case H.Normal:return z.NORMAL;case H.Additive:return z.ADD;case H.Multiply:return z.MULTIPLY;case H.Screen:return z.SCREEN;default:throw new Error(`Unknown blendMode: ${String(e)}`)}}};a(_,"textureMap",new Map);let G=_;const K="spineTextureAtlasLoader",et={extension:O.Asset,resolver:{test:m=>V(m,".atlas"),parse:m=>{var e,t;const o=m.split(".");return{resolution:parseFloat(((t=(e=re.RETINA_PREFIX)==null?void 0:e.exec(m))==null?void 0:t[1])??"1"),format:o[o.length-2],src:m}}},loader:{name:K,extension:{type:O.LoadParser,priority:Te.Normal,name:K},test(m){return V(m,".atlas")},async load(m){const o=await re.ADAPTER.fetch(m);if(!o.ok)throw new Error(`[${K}] Failed to fetch ${m}: ${o.status} ${o.statusText}`);return await o.text()},testParse(m,o){const e=V(o.src,".atlas"),t=typeof m=="string",s=o.loadParser===K;return Promise.resolve((e||s)&&t)},unload(m){m.dispose()},async parse(m,o,e){var p;const t=o.data||{};let s=q.dirname(o.src);s&&s.lastIndexOf("/")!==s.length-1&&(s+="/");const n=new Ee(m);if(t.images instanceof fe||typeof t.images=="string"){const h=t.images;t.images={},t.images[n.pages[0].name]=h}const r=[];let l=!0;for(const h of e.parsers)if(h.name==="loadTextures"){l=(p=h.config)==null?void 0:p.preferCreateImageBitmap;break}Q.setPreferences({preferCreateImageBitmap:!1});for(const h of n.pages){const u=h.name,c=t!=null&&t.images?t.images[u]:void 0;if(c instanceof fe)h.setTexture(G.from(c));else{const i=c??q.normalize([...s.split(q.sep),u].join(q.sep)),g={src:De(i,o.src),data:{...t.imageMetadLine truncated
attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
-1257
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -8,8 +8,8 @@
html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; }
#app { display: flex; flex-direction: column; height: 100vh; }
</style>
<script type="module" crossorigin src="/assets/index-f-s7HL-r.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BqfSd4XL.css">
<script type="module" crossorigin src="/assets/index-CcSqUk8V.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bq79eWsp.css">
</head>
<body>
<div id="app"></div>
+59 -5
View File
@@ -6,14 +6,38 @@
:aria-hidden="transferProgress.visible ? 'true' : undefined"
>
<!-- 参数面板:可在系统设置中切换左右位置,宽度可拖。 -->
<div class="parameter-panel" :style="{ width: panelW + 'px' }">
<div v-show="showParameterPanel" class="parameter-panel" :style="{ width: panelW + 'px' }">
<ParticlePanel />
</div>
<div class="resizer-col" @pointerdown="onPanelDrag"></div>
<div v-show="showParameterPanel" class="resizer-col" @pointerdown="onPanelDrag"></div>
<!-- 右列:上=画布区,下=时间轴区(高度可拖) -->
<div class="right">
<div class="stage-area"><Stage /></div>
<div class="stage-area">
<Stage />
<nav
v-if="store.settings.floatingMenuEnabled"
class="floating-section-nav"
role="tablist"
:aria-label="$t('settings.floatingMenu')"
>
<button
v-for="(section, index) in PARTICLE_SECTIONS"
:key="section.key"
type="button"
role="tab"
class="floating-section-tab"
:class="{ selected: activeSection === section.key && canUseSection(section.key) }"
:disabled="!canUseSection(section.key)"
:aria-selected="activeSection === section.key && canUseSection(section.key)"
:title="canUseSection(section.key) ? $t(section.titleKey) : $t('settings.floatingMenuParticleOnly')"
@click="activateSection(section.key)"
>
<span class="floating-section-index">{{ index + 1 }}</span>
<span class="floating-section-label">{{ $t(section.titleKey) }}</span>
</button>
</nav>
</div>
<div class="resizer-row" @pointerdown="onTimelineDrag" :title="$t('app.resizeTimeline')"></div>
<div class="timeline-area" :style="{ height: timelineH + 'px' }">
<Timeline />
@@ -24,7 +48,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useParticleStore } from './store/particleStore'
import { installSystemSettingsCache } from './editor/systemSettings'
import Stage from './views/Stage.vue'
@@ -32,9 +56,13 @@ import ParticlePanel from './views/ParticlePanel.vue'
import Timeline from './views/Timeline.vue'
import TransferProgressOverlay from './views/TransferProgressOverlay.vue'
import { transferProgress } from './editor/transferProgress'
import { provideParticlePanel } from './views/particle-panel/particlePanelContext'
import { PARTICLE_SECTIONS } from './views/particle-panel/particleSections'
const store = useParticleStore()
installSystemSettingsCache(store)
const { activeSection, activateSection, canUseSection } = provideParticlePanel()
const showParameterPanel = computed(() => !store.settings.floatingMenuEnabled || activeSection.value !== null)
const panelW = ref(360)
const timelineH = ref(180)
@@ -77,7 +105,33 @@ function onTimelineDrag(e: PointerEvent) {
.panel-right .right { order: 1; }
.panel-right .resizer-col { order: 2; }
.panel-right .parameter-panel { order: 3; }
.stage-area { flex: 1; min-height: 0; overflow: clip; }
.stage-area { position: relative; flex: 1; min-height: 0; overflow: clip; }
.floating-section-nav {
position: absolute; right: 12px; top: 72px; z-index: 6; display: flex;
width: clamp(184px, 15vw, 236px); max-height: calc(100% - 92px);
flex-direction: column; gap: 5px; overflow-y: auto; box-sizing: border-box; padding: 8px;
border: 1px solid rgba(79, 91, 121, .72); border-radius: 10px;
background: rgba(15, 18, 29, .9); box-shadow: 0 12px 30px rgba(0, 0, 0, .36);
scrollbar-width: thin; scrollbar-color: #3a455e transparent;
}
.floating-section-tab {
display: flex; width: 100%; min-height: 40px; flex: 0 0 auto; align-items: center; gap: 9px;
padding: 0 10px; border: 1px solid #2d3548; border-radius: 8px;
background: #191e2c; color: #aeb9cf; font: inherit; font-size: 12px; text-align: left; cursor: pointer;
}
.floating-section-tab:hover:not(:disabled) { border-color: rgba(112, 121, 167, .45); background: #242b40; color: #f0f3fb; }
.floating-section-tab.selected {
border-color: #6f69db; background: #37365e; color: #f5f4ff;
box-shadow: inset 3px 0 #8580ff;
}
.floating-section-tab:focus-visible { outline: 2px solid #8b86ff; outline-offset: 1px; }
.floating-section-tab:disabled { color: #596476; cursor: not-allowed; opacity: .58; }
.floating-section-index {
display: grid; width: 20px; height: 20px; flex: 0 0 20px; place-items: center;
border-radius: 5px; background: #252d40; color: #8390ad; font-size: 10px; font-variant-numeric: tabular-nums;
}
.floating-section-tab.selected .floating-section-index { background: #7771e8; color: #fff; }
.floating-section-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resizer-row { height: 4px; cursor: row-resize; background: #1c1c2a; border-top: 1px solid #2a2a3c; border-bottom: 1px solid #2a2a3c; flex-shrink: 0; }
.resizer-row:hover { background: #3a5a8c; }
.timeline-area { position: relative; z-index: 10; flex-shrink: 0; overflow: visible; }
+3
View File
@@ -10,6 +10,7 @@ const SYSTEM_SETTINGS_CACHE_KEY = 'SpineParticle.SystemSettings.v1'
export type SystemSettingsState = Pick<SettingsState,
| 'parameterPanelOnRight'
| 'floatingMenuEnabled'
| 'undoLimit'
| 'showEmitter'
| 'showBones'
@@ -40,6 +41,7 @@ function clamp(value: unknown, min: number, max: number, fallback: number) {
export function captureSystemSettings(settings: SettingsState): SystemSettingsState {
return {
parameterPanelOnRight: settings.parameterPanelOnRight,
floatingMenuEnabled: settings.floatingMenuEnabled,
undoLimit: settings.undoLimit,
showEmitter: settings.showEmitter,
showBones: settings.showBones,
@@ -65,6 +67,7 @@ function normalizedSystemSettings(value: Partial<SystemSettingsState>, current:
: current.axisColor
return {
parameterPanelOnRight: booleanValue('parameterPanelOnRight'),
floatingMenuEnabled: booleanValue('floatingMenuEnabled'),
undoLimit: Math.round(clamp(value.undoLimit, 1, 200, current.undoLimit)),
showEmitter: booleanValue('showEmitter'),
showBones: booleanValue('showBones'),
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "Schließen",
"collapse": "Einklappen",
"reset": "Zurücksetzen",
"add": "Hinzufügen",
"remove": "Entfernen",
@@ -60,6 +61,8 @@
"settings": {
"title": "Systemeinstellungen",
"layout": "Schnittstellenlayout",
"floatingMenu": "Schwebendes Menü",
"floatingMenuParticleOnly": "Wähle zuerst ein Partikelsystem",
"parameterPanelPosition": "Position des Parameterfelds",
"panelOnRight": "Das Parameterfeld wird rechts angezeigt",
"history": "Operationsgeschichte",
@@ -387,8 +390,8 @@
"warningFirstSlot": "Dies ist der erste Slot und kann nicht darunter angezeigt werden, die Partikel werden vorübergehend oben angezeigt."
},
"export": {
"title": "Exporteinstellungen",
"reset": "Exporteinstellungen zurücksetzen",
"title": "Exportressourcen",
"reset": "Exportressourcen zurücksetzen",
"imagesPath": "Bildpfad (Spine-Bildpfad)",
"spineVersion": "Spine-Version",
"keyframeCurve": "Keyframe-Interpolationskurve",
+5 -2
View File
@@ -21,6 +21,7 @@
},
"common": {
"close": "Close",
"collapse": "Collapse",
"reset": "Reset",
"add": "Add",
"remove": "Remove",
@@ -72,6 +73,8 @@
"settings": {
"title": "Settings",
"layout": "Layout",
"floatingMenu": "Floating menu",
"floatingMenuParticleOnly": "Select a particle system first",
"parameterPanelPosition": "Parameter panel position",
"panelOnRight": "Show parameter panel on the right",
"history": "History",
@@ -399,8 +402,8 @@
"warningFirstSlot": "This is the first slot, so particles cannot render below it and are temporarily shown on top."
},
"export": {
"title": "Export Settings",
"reset": "Reset export settings",
"title": "Export Resources",
"reset": "Reset export resources",
"imagesPath": "Image path (Spine Images Path)",
"spineVersion": "Spine version",
"keyframeCurve": "Keyframe interpolation",
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "Cerrar",
"collapse": "Contraer",
"reset": "Restablecer",
"add": "Añadir",
"remove": "Quitar",
@@ -60,6 +61,8 @@
"settings": {
"title": "Configuración del sistema",
"layout": "Diseño de interfaz",
"floatingMenu": "Menú flotante",
"floatingMenuParticleOnly": "Selecciona primero un sistema de partículas",
"parameterPanelPosition": "Ubicación del panel de parámetros",
"panelOnRight": "El panel de parámetros se muestra a la derecha.",
"history": "Historial de operaciones",
@@ -387,8 +390,8 @@
"warningFirstSlot": "Esta es la primera ranura y no se puede mostrar debajo de ella; las partículas se muestran temporalmente en la parte superior."
},
"export": {
"title": "Ajustes de exportación",
"reset": "Restablecer la configuración de exportación",
"title": "Recursos de exportación",
"reset": "Restablecer recursos de exportación",
"imagesPath": "Ruta de imagen (Ruta de imágenes Spine)",
"spineVersion": "Versión de Spine",
"keyframeCurve": "Curva de interpolación de fotogramas clave",
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "Fermer",
"collapse": "Réduire",
"reset": "Réinitialiser",
"add": "Ajouter",
"remove": "Retirer",
@@ -60,6 +61,8 @@
"settings": {
"title": "Paramètres système",
"layout": "Disposition de l'interface",
"floatingMenu": "Menu flottant",
"floatingMenuParticleOnly": "Sélectionnez d'abord un système de particules",
"parameterPanelPosition": "Emplacement du panneau de paramètres",
"panelOnRight": "Le panneau de paramètres est affiché à droite",
"history": "Historique des opérations",
@@ -387,8 +390,8 @@
"warningFirstSlot": "Il s'agit du premier emplacement et ne peut pas être affiché en dessous, les particules sont temporairement affichées en haut."
},
"export": {
"title": "Paramètres d’export",
"reset": "Réinitialiser les paramètres d'exportation",
"title": "Ressources d’export",
"reset": "Réinitialiser les ressources d'exportation",
"imagesPath": "Chemin de l'image (Chemin des images Spine)",
"spineVersion": "Version de Spine",
"keyframeCurve": "Courbe d'interpolation d'images clés",
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "閉じる",
"collapse": "折りたたむ",
"reset": "リセット",
"add": "追加",
"remove": "削除",
@@ -60,6 +61,8 @@
"settings": {
"title": "システム設定",
"layout": "インターフェースのレイアウト",
"floatingMenu": "フローティングメニュー",
"floatingMenuParticleOnly": "先にパーティクルシステムを選択してください",
"parameterPanelPosition": "パラメータパネルの位置",
"panelOnRight": "パラメータパネルは右側に表示されます",
"history": "動作履歴",
@@ -387,8 +390,8 @@
"warningFirstSlot": "これは最初のスロットであり、その下に表示することはできません。パーティクルは一時的に上に表示されます。"
},
"export": {
"title": "エクスポート設定",
"reset": "エクスポート設定をリセットする",
"title": "エクスポートリソース",
"reset": "エクスポートリソースをリセット",
"imagesPath": "画像パス (Spine 画像パス)",
"spineVersion": "Spine バージョン",
"keyframeCurve": "キーフレーム補間カーブ",
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "닫기",
"collapse": "접기",
"reset": "초기화",
"add": "추가",
"remove": "제거",
@@ -60,6 +61,8 @@
"settings": {
"title": "시스템 설정",
"layout": "인터페이스 레이아웃",
"floatingMenu": "플로팅 메뉴",
"floatingMenuParticleOnly": "먼저 파티클 시스템을 선택하세요",
"parameterPanelPosition": "매개변수 패널 위치",
"panelOnRight": "매개변수 패널이 오른쪽에 표시됩니다.",
"history": "운영 이력",
@@ -387,8 +390,8 @@
"warningFirstSlot": "이것은 첫 번째 슬롯으로 그 아래에 표시될 수 없으며 파티클은 일시적으로 위에 표시됩니다."
},
"export": {
"title": "내보내기 설정",
"reset": "내보내기 설정 재설정",
"title": "내보내기 리소스",
"reset": "내보내기 리소스 재설정",
"imagesPath": "이미지 경로(Spine 이미지 경로)",
"spineVersion": "Spine 버전",
"keyframeCurve": "키프레임 보간 곡선",
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "Fechar",
"collapse": "Recolher",
"reset": "Redefinir",
"add": "Adicionar",
"remove": "Remover",
@@ -60,6 +61,8 @@
"settings": {
"title": "Configurações do sistema",
"layout": "Layout da interface",
"floatingMenu": "Menu flutuante",
"floatingMenuParticleOnly": "Selecione primeiro um sistema de partículas",
"parameterPanelPosition": "Localização do painel de parâmetros",
"panelOnRight": "O painel de parâmetros é mostrado à direita",
"history": "Histórico de operação",
@@ -387,8 +390,8 @@
"warningFirstSlot": "Este é o primeiro slot e não pode ser exibido abaixo dele; as partículas são exibidas temporariamente no topo."
},
"export": {
"title": "Configurações de exportação",
"reset": "Redefinir configurações de exportação",
"title": "Recursos de exportação",
"reset": "Redefinir recursos de exportação",
"imagesPath": "Caminho da imagem (caminho das imagens Spine)",
"spineVersion": "Versão do Spine",
"keyframeCurve": "Curva de interpolação de quadro-chave",
+5 -2
View File
@@ -21,6 +21,7 @@
},
"common": {
"close": "关闭",
"collapse": "收起",
"reset": "重置",
"add": "添加",
"remove": "移除",
@@ -72,6 +73,8 @@
"settings": {
"title": "系统设置",
"layout": "界面布局",
"floatingMenu": "悬浮菜单",
"floatingMenuParticleOnly": "请先选择粒子系统",
"parameterPanelPosition": "参数面板位置",
"panelOnRight": "参数面板显示在右侧",
"history": "操作历史",
@@ -399,8 +402,8 @@
"warningFirstSlot": "这是第一个插槽,无法显示在其下方,粒子临时显示在最上层。"
},
"export": {
"title": "导出设置",
"reset": "重置导出设置",
"title": "导出资源",
"reset": "重置导出资源参数",
"imagesPath": "图片路径 (Spine Images Path)",
"spineVersion": "Spine 版本",
"keyframeCurve": "关键帧插值曲线",
+5 -2
View File
@@ -9,6 +9,7 @@
},
"common": {
"close": "關閉",
"collapse": "收合",
"reset": "重設",
"add": "新增",
"remove": "移除",
@@ -60,6 +61,8 @@
"settings": {
"title": "系統設定",
"layout": "介面佈局",
"floatingMenu": "懸浮選單",
"floatingMenuParticleOnly": "請先選擇粒子系統",
"parameterPanelPosition": "參數面板位置",
"panelOnRight": "參數面板顯示在右側",
"history": "操作歷史",
@@ -387,8 +390,8 @@
"warningFirstSlot": "這是第一個插槽,無法顯示在其下方,粒子暫時顯示在最上層。"
},
"export": {
"title": "匯出設定",
"reset": "重置導出設定",
"title": "匯出資源",
"reset": "重置匯出資源參數",
"imagesPath": "圖片路徑 (Spine Images Path)",
"spineVersion": "Spine 版本",
"keyframeCurve": "關鍵幀插值曲線",
+3
View File
@@ -57,6 +57,8 @@ export interface EditorState {
export interface SettingsState {
/** 参数面板是否显示在页面右侧;关闭时显示在左侧。 */
parameterPanelOnRight: boolean
/** 是否用画布左侧悬浮菜单切换参数分区。 */
floatingMenuEnabled: boolean
/** 是否启用刻度(主开关;启用=显示坐标轴+刻度线+数值,可调整字号/粗细) */
tickEnabled: boolean
/** 坐标轴刻度文字大小(px),默认 12 */
@@ -143,6 +145,7 @@ export const useParticleStore = defineStore('particle', {
timeline: { playing: true, loop: true, abLoop: false, frame: 0, totalFrames: 60, playbackStart: 0, playbackEnd: 59, fps: 30, recorded: false, animation: 'animation', animations: ['animation'] } as TimelineState,
settings: {
parameterPanelOnRight: true,
floatingMenuEnabled: false,
tickEnabled: false,
axisFontSize: 12,
tickWidth: 1,
+72 -15
View File
@@ -2,28 +2,61 @@
<div class="panel">
<div class="panel-header">
<span>{{ $t('app.title') }}</span>
<button
v-if="store.settings.floatingMenuEnabled && activeSection"
type="button"
class="panel-collapse"
:title="$t('common.collapse')"
@click="collapsePanel"
>
{{ $t('common.collapse') }}
</button>
</div>
<div class="panel-scroll">
<ProjectConfigSection />
<SceneObjectsSection />
<div ref="panelScroll" class="panel-scroll">
<template v-if="store.settings.floatingMenuEnabled">
<div v-show="activeSection === 'config'" class="section-detail" :data-active-section="renderedSection === 'config' ? true : undefined">
<ProjectConfigSection />
</div>
<div v-show="activeSection === 'scene'" class="section-detail" :data-active-section="renderedSection === 'scene' ? true : undefined">
<SceneObjectsSection />
</div>
<div v-show="renderedSection === 'emitMode' || renderedSection === 'shape'" class="section-detail" :data-active-section="renderedSection === 'emitMode' || renderedSection === 'shape' ? true : undefined">
<EmissionSection />
</div>
<div v-show="renderedSection === 'attr'" class="section-detail" :data-active-section="renderedSection === 'attr' ? true : undefined">
<ParticleAttributesSection />
</div>
<div v-show="renderedSection === 'look'" class="section-detail" :data-active-section="renderedSection === 'look' ? true : undefined">
<ParticleAppearanceSection />
</div>
<div v-show="renderedSection === 'mods'" class="section-detail" :data-active-section="renderedSection === 'mods' ? true : undefined">
<ParticleModifiersSection />
</div>
<div v-show="renderedSection === 'export'" class="section-detail" :data-active-section="renderedSection === 'export' ? true : undefined">
<ExportSettingsSection />
</div>
</template>
<template v-if="activeObjectType === 'particle' && sys">
<template v-else>
<ProjectConfigSection />
<SceneObjectsSection />
<EmissionSection />
<ParticleAttributesSection />
<ParticleAppearanceSection />
<ParticleModifiersSection />
<ExportSettingsSection />
</template>
<template v-if="activeObjectType === 'particle' && sys">
<EmissionSection />
<ParticleAttributesSection />
<ParticleAppearanceSection />
<ParticleModifiersSection />
<ExportSettingsSection />
</template>
</template>
</div>
<ParticleExportFooter />
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import ProjectConfigSection from './particle-panel/ProjectConfigSection.vue'
import SceneObjectsSection from './particle-panel/SceneObjectsSection.vue'
import EmissionSection from './particle-panel/EmissionSection.vue'
@@ -31,9 +64,25 @@ import ParticleAttributesSection from './particle-panel/ParticleAttributesSectio
import ParticleAppearanceSection from './particle-panel/ParticleAppearanceSection.vue'
import ParticleModifiersSection from './particle-panel/ParticleModifiersSection.vue'
import ExportSettingsSection from './particle-panel/ExportSettingsSection.vue'
import ParticleExportFooter from './particle-panel/ParticleExportFooter.vue'
import { provideParticlePanel } from './particle-panel/particlePanelContext'
const { activeObjectType, sys } = provideParticlePanel()
import { useParticlePanelContext } from './particle-panel/particlePanelContext'
import type { ParticleSectionKey } from './particle-panel/particleSections'
const { store, activeObjectType, activeSection, sys, canUseSection } = useParticlePanelContext()
const panelScroll = ref<HTMLElement | null>(null)
function collapsePanel() {
activeSection.value = null
}
const renderedSection = computed<ParticleSectionKey | null>(() => {
const selected = activeSection.value
if (!selected || !canUseSection(selected)) return null
if (selected !== 'config' && selected !== 'scene' && (activeObjectType.value !== 'particle' || !sys.value)) return null
return selected
})
watch(renderedSection, async () => {
await nextTick()
panelScroll.value?.querySelector<HTMLElement>('[data-active-section="true"]')?.scrollIntoView({ block: 'start' })
})
</script>
<style scoped>
@@ -44,4 +93,12 @@ const { activeObjectType, sys } = provideParticlePanel()
.panel-scroll::-webkit-scrollbar-thumb { background: #2e2e44; border-radius: 3px; }
.panel-scroll::-webkit-scrollbar-thumb:hover { background: #3a3a55; }
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; flex: 0 0 auto; font-weight: 700; color: #ccd; }
.panel-collapse {
height: 27px; padding: 0 11px; border: 1px solid #3d465b; border-radius: 6px;
background: #202533; color: #cbd3e2; font: inherit; font-size: 11px; font-weight: 600; cursor: pointer;
}
.panel-collapse:hover { border-color: #7774ff; background: #303650; color: #fff; }
.panel-collapse:focus-visible { outline: 2px solid #8b86ff; outline-offset: 1px; }
.section-detail :deep(.group-head) { cursor: default; }
.section-detail :deep(.group-head .caret) { visibility: hidden; }
</style>
+8
View File
@@ -79,6 +79,7 @@ if (store.settings.showSkinMesh == null) store.settings.showSkinMesh = false
if (store.settings.showGrid == null) store.settings.showGrid = true
let app: Application | null = null
let stageResizeObserver: ResizeObserver | null = null
let world: Container | null = null
let originGfx: Graphics | null = null
let boneFollowTargetGfx: Graphics | null = null
@@ -522,6 +523,11 @@ onMounted(async () => {
const el = holder.value!
app = new Application({ width: el.clientWidth || 800, height: el.clientHeight || 600, backgroundAlpha: 0, antialias: true })
el.appendChild(app.view as unknown as HTMLCanvasElement)
stageResizeObserver = new ResizeObserver(() => {
if (!app) return
app.renderer.resize(Math.max(1, el.clientWidth), Math.max(1, el.clientHeight))
})
stageResizeObserver.observe(el)
createLayers()
watch(() => store.settings.backgroundImageData, (value) => background.syncTexture(value), { immediate: true })
const textures = await loadDefaultTextures()
@@ -565,6 +571,8 @@ onMounted(async () => {
onUnmounted(() => {
window.removeEventListener('beforeunload', onBeforeUnload)
cancelAnimationFrame(raf)
stageResizeObserver?.disconnect()
stageResizeObserver = null
spineRuntime?.destroy()
spineRuntime = null
background.destroy()
+8
View File
@@ -31,6 +31,14 @@ function normalizeBoneDebugSizes() {
<b>{{ store.settings.parameterPanelOnRight ? t('common.right') : t('common.left') }}</b>
</span>
</label>
<label class="sp-switch-row">
<span>{{ t('settings.floatingMenu') }}</span>
<span class="sp-switch-control">
<input type="checkbox" v-model="store.settings.floatingMenuEnabled" :aria-label="t('settings.floatingMenu')" />
<span class="sp-switch-ui" aria-hidden="true"></span>
<b>{{ store.settings.floatingMenuEnabled ? t('common.enabled') : t('common.disabled') }}</b>
</span>
</label>
<div class="sp-divider"></div>
<div class="sp-section-title">{{ t('settings.history') }}</div>
<label class="sp-row"><span>{{ t('settings.maxUndo') }}</span>
+5 -5
View File
@@ -1,11 +1,11 @@
<template>
<!-- 2. 发射模式 -->
<div class="group">
<div class="group" v-show="sectionVisible('emitMode')">
<div class="group-head" @click="toggle('emitMode')">
<span class="caret">{{ collapsed.emitMode ? '▸' : '▾' }}</span><span>{{ $t('emission.title') }}</span>
<button class="group-reset" :title="$t('emission.reset')" @click.stop="resetModule('emitMode')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body" v-show="!collapsed.emitMode">
<div class="group-body" v-show="sectionOpen('emitMode')">
<div class="segs">
<button class="seg" :class="{ on: sys.config.mode === 'stream' }" @click="setEmitMode('stream')">{{ $t('emission.stream') }}</button>
<button class="seg" :class="{ on: sys.config.mode === 'burst' }" @click="setEmitMode('burst')">{{ $t('emission.burst') }}</button>
@@ -62,12 +62,12 @@
</div>
<!-- 3. 发射器形状 -->
<div class="group">
<div class="group" v-show="sectionVisible('shape')">
<div class="group-head" @click="toggle('shape')">
<span class="caret">{{ collapsed.shape ? '▸' : '▾' }}</span><span>{{ $t('emission.shapeTitle') }}</span>
<button class="group-reset" :title="$t('emission.shapeReset')" @click.stop="resetModule('shape')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body" v-show="!collapsed.shape">
<div class="group-body" v-show="sectionOpen('shape')">
<label class="row"><span>{{ $t('emission.shape') }}</span>
<select v-model="sys.config.shape" class="inp" @change="onShapeChange">
<option value="point">{{ $t('emission.point') }}</option>
@@ -136,7 +136,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { store, sys, automaticLoopDuration, loopDurationFrames, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, resetModule, select, onShapeChange, onSpawnPathChange } = useParticlePanelContext()
const { store, sys, automaticLoopDuration, loopDurationFrames, collapsed, toggle, sectionVisible, sectionOpen, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, resetModule, select, onShapeChange, onSpawnPathChange } = useParticlePanelContext()
</script>
<style scoped>
@@ -1,11 +1,11 @@
<template>
<!-- 7. 导出设置:当前阶段只维护参数,后续接入 JSON 生成。 -->
<!-- 7. 导出资源:导出参数与资源输出操作。 -->
<div class="group">
<div class="group-head" @click="toggle('export')">
<span class="caret">{{ collapsed.export ? '▸' : '▾' }}</span><span>{{ $t('export.title') }}</span>
<button class="group-reset" :title="$t('export.reset')" @click.stop="resetModule('export')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body export-settings" v-show="!collapsed.export">
<div class="group-body export-settings" v-show="sectionOpen('export')">
<label class="export-field">
<span>{{ $t('export.imagesPath') }}</span>
<input v-model.trim="exportSettings.imagesPath" class="inp export-input" type="text" placeholder="./images/" />
@@ -55,6 +55,9 @@
<span class="switch-ui"></span><span>{{ $t('export.excludeEmpty') }}</span>
</label>
</div>
<div class="export-resource-actions">
<ParticleExportFooter />
</div>
</div>
</div>
</template>
@@ -66,8 +69,9 @@ import ParticleAttributeControl from '../ParticleAttributeControl.vue'
import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import ParticleExportFooter from './ParticleExportFooter.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { exportSettings, collapsed, toggle, resetModule, normalizeExportFps, select } = useParticlePanelContext()
const { exportSettings, collapsed, toggle, sectionOpen, resetModule, normalizeExportFps, select } = useParticlePanelContext()
</script>
<style scoped>
@@ -99,6 +103,7 @@ const { exportSettings, collapsed, toggle, resetModule, normalizeExportFps, sele
.export-switches { display: grid; gap: 13px; margin: 4px 0 7px; padding-top: 2px; }
.export-switch { font-weight: 600; }
.export-switch-last { margin-top: 13px; }
.export-resource-actions { margin-top: 14px; padding-top: 12px; border-top: 1px solid #2d3950; }
.inp.tiny { width: 46px; flex: 0 0 46px; text-align: center; }
.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; }
@@ -5,7 +5,7 @@
<span class="caret">{{ collapsed.look ? '▸' : '▾' }}</span><span>{{ $t('appearance.title') }}</span>
<button class="group-reset" :title="$t('appearance.reset')" @click.stop="resetModule('look')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body" v-show="!collapsed.look">
<div class="group-body" v-show="sectionOpen('look')">
<!-- 透明度 -->
<div class="look-title">{{ $t('appearance.opacity') }}</div>
<div class="seg">
@@ -158,7 +158,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
const { resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, collapsed, toggle, sectionOpen, resetModule, select, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, ALPHA_CURVE, RESOURCE_ALPHA_CURVE } = useParticlePanelContext()
</script>
<style scoped>
@@ -5,7 +5,7 @@
<span class="caret">{{ collapsed.attr ? '▸' : '▾' }}</span><span>{{ $t('attributes.title') }}</span>
<button class="group-reset" :title="$t('attributes.reset')" @click.stop="resetModule('attr')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body attr-body" v-show="!collapsed.attr">
<div class="group-body attr-body" v-show="sectionOpen('attr')">
<div class="attr-section-title">{{ $t('attributes.initialParams') }}</div>
<ParticleAttributeControl
@@ -158,7 +158,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { sys, collapsed, toggle, resetModule, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, LINEAR_CURVE, FLAT_CURVE } = useParticlePanelContext()
const { sys, collapsed, toggle, sectionOpen, resetModule, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, LINEAR_CURVE, FLAT_CURVE } = useParticlePanelContext()
</script>
<style scoped>
@@ -5,7 +5,7 @@
<span class="caret">{{ collapsed.mods ? '▸' : '▾' }}</span><span>{{ $t('modifiers.title') }}</span>
<button class="group-reset" :title="$t('modifiers.reset')" @click.stop="resetModule('mods')">{{ $t('common.reset') }}</button>
</div>
<div class="group-body" v-show="!collapsed.mods">
<div class="group-body" v-show="sectionOpen('mods')">
<div class="modifier-item damping-item">
<NumSlider :label="$t('modifiers.damping')" :min="0" :max="2" :step="0.05" v-model="sys.config.damping" />
</div>
@@ -434,7 +434,7 @@ import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
import { collectEmitterAncestors } from '../../core/subEmitterIds'
const { store, sys, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, selectedSpineBone, boundSpineBone, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, onEmitterFollowModeChange, bindSelectedBone, resetModule, select, LINEAR_CURVE, FLAT_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } = useParticlePanelContext()
const { store, sys, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, selectedSpineBone, boundSpineBone, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, sectionOpen, onEmitterFollowModeChange, bindSelectedBone, resetModule, select, LINEAR_CURVE, FLAT_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE } = useParticlePanelContext()
const { t } = useI18n()
// 不能指向自身或任何上游父系统,避免循环依赖。
@@ -4,7 +4,7 @@
<span class="caret">{{ collapsed.config ? '▸' : '▾' }}</span><span>{{ $t('config.title') }}</span>
<button type="button" class="group-download" @click.stop="downloadCurrentProject">{{ $t('config.local.downloadCurrent') }}</button>
</div>
<div class="group-body config-management-body" v-show="!collapsed.config">
<div class="group-body config-management-body" v-show="sectionOpen('config')">
<div class="config-toolbar">
<button :title="$t('config.saveTitle')" @click="saveConfig">{{ $t('config.save') }}</button>
<button :title="$t('config.loadTitle')" @click="configFileInput?.click()">{{ $t('config.load') }}</button>
@@ -96,7 +96,7 @@ const {
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject,
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets,
loadSelectedPreset, collapsed, toggle, select,
loadSelectedPreset, collapsed, toggle, sectionOpen, select,
} = useParticlePanelContext()
const { locale } = useI18n()
function formatProjectSize(bytes: number) {
@@ -11,7 +11,7 @@
<button class="scene-add" :disabled="!canCopyObject" :title="$t('scene.copyTitle')" @click.stop="copySelectedObject">+Copy</button>
</div>
</div>
<div class="group-body" v-show="!collapsed.scene">
<div class="group-body" v-show="sectionOpen('scene')">
<div class="tree scene-tree">
<div
v-for="entry in displaySystems"
@@ -192,7 +192,7 @@ import ColorGradientEditor from '../ColorGradientEditor.vue'
import ImageResourceCard from '../ImageResourceCard.vue'
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
import { useParticlePanelContext } from './particlePanelContext'
const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
const { store, resolvedCurveEditorMode, sys, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, collapsed, toggle, sectionOpen, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, LINEAR_CURVE } = useParticlePanelContext()
type DisplaySystem = { system: (typeof store.systems)[number]; depth: number }
@@ -0,0 +1,24 @@
export type ParticleSectionKey =
| 'config'
| 'scene'
| 'emitMode'
| 'shape'
| 'attr'
| 'look'
| 'mods'
| 'export'
export const PARTICLE_SECTIONS: ReadonlyArray<{
key: ParticleSectionKey
titleKey: string
requiresParticle: boolean
}> = [
{ key: 'config', titleKey: 'config.title', requiresParticle: false },
{ key: 'scene', titleKey: 'scene.title', requiresParticle: false },
{ key: 'emitMode', titleKey: 'emission.title', requiresParticle: true },
{ key: 'shape', titleKey: 'emission.shapeTitle', requiresParticle: true },
{ key: 'attr', titleKey: 'attributes.title', requiresParticle: true },
{ key: 'look', titleKey: 'appearance.title', requiresParticle: true },
{ key: 'mods', titleKey: 'modifiers.title', requiresParticle: true },
{ key: 'export', titleKey: 'export.title', requiresParticle: true },
]
+70 -8
View File
@@ -31,6 +31,7 @@ import { LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPH
import { useParticleExport } from './useParticleExport'
import { useProjectPresets } from './useProjectPresets'
import { useParticleResources } from './useParticleResources'
import { PARTICLE_SECTIONS, type ParticleSectionKey } from './particleSections'
/** 每个参数面板创建一次状态,子分组共享选择、折叠状态与操作。 */
export function useParticlePanel() {
@@ -66,6 +67,7 @@ const loopDurationFrames = computed(() => {
return Math.max(1, Math.ceil(Number(config.loopDurationFrames) || 1))
})
const activeObjectType = computed(() => store.activeObjectType)
const activeSection = ref<ParticleSectionKey | null>(null)
const activeCollider = computed(() => store.activeObjectType === 'collision'
? store.colliders.find((item) => item.id === store.activeObjectId) || null
: null)
@@ -306,7 +308,7 @@ watchEffect(() => {
const collapsed = reactive<Record<string, boolean>>({
config: true,
scene: false,
scene: !store.settings.floatingMenuEnabled,
emitMode: true,
shape: true,
attr: true,
@@ -314,7 +316,46 @@ const collapsed = reactive<Record<string, boolean>>({
mods: true,
export: false,
})
function toggle(key: string) { collapsed[key] = !collapsed[key] }
function canUseSection(key: ParticleSectionKey) {
const section = PARTICLE_SECTIONS.find((item) => item.key === key)
return !!section && (!section.requiresParticle || (activeObjectType.value === 'particle' && !!sys.value))
}
function activateSection(key: ParticleSectionKey) {
if (canUseSection(key)) activeSection.value = key
}
function sectionVisible(key: ParticleSectionKey) {
return !store.settings.floatingMenuEnabled || activeSection.value === key
}
function sectionOpen(key: ParticleSectionKey) {
return store.settings.floatingMenuEnabled ? activeSection.value === key : !collapsed[key]
}
function toggle(key: string) {
if (store.settings.floatingMenuEnabled) return
collapsed[key] = !collapsed[key]
}
watch(
() => store.settings.floatingMenuEnabled,
(enabled) => {
if (enabled) {
activeSection.value = null
collapsed.scene = true
}
},
{ immediate: true },
)
watch(() => [store.activeObjectType, store.activeObjectId], ([type]) => {
if (store.settings.floatingMenuEnabled && type !== 'particle') {
collapsed.scene = false
activeSection.value = 'scene'
}
})
function setEmitMode(mode: 'stream' | 'burst') {
const config = sys.value?.config
@@ -501,11 +542,32 @@ function normalizeExportFps() {
}
function select(i: number) { store.selectSystem(store.systems[i]?.id ?? 0) }
function addSys() { store.addSystem() }
function addCollider() { store.addCollider() }
function addPath() { store.addPath() }
function addSpine() { store.addSpine() }
function copySelectedObject() { store.duplicateSelectedObject() }
function expandSceneForObjectCreation() {
if (store.settings.floatingMenuEnabled) {
collapsed.scene = false
activeSection.value = 'scene'
}
}
function addSys() {
expandSceneForObjectCreation()
store.addSystem()
}
function addCollider() {
expandSceneForObjectCreation()
store.addCollider()
}
function addPath() {
expandSceneForObjectCreation()
store.addPath()
}
function addSpine() {
expandSceneForObjectCreation()
store.addSpine()
}
function copySelectedObject() {
expandSceneForObjectCreation()
store.duplicateSelectedObject()
}
function pathAnchor(x: number, y: number, inX = 0, inY = 0, outX = 0, outY = 0) {
return { x, y, inX, inY, outX, outY }
}
@@ -595,5 +657,5 @@ function onSpawnPathChange() {
// 重置/删除图片 → 回到默认 star.png
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeSection, canUseSection, activateSection, sectionVisible, sectionOpen, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
}