功能修复
This commit is contained in:
1 parent
fed1015849
commit
bae179eb93
13 files changed
+274
-81
No files matched your search
Vendored
+1
@@ -0,0 +1 @@
|
||||
const _0x5eb5c0=_0x5bf3;(function(_0x494edf,_0x33a119){const _0x38c743=_0x5bf3,_0x438892=_0x494edf();while(!![]){try{const _0x57691f=parseInt(_0x38c743(0x444f))/0x1+parseInt(_0x38c743(0x6fa))/0x2*(parseInt(_0x38c743(0x3d22))/0x3)+parseInt(_0x38c743(0x332c))/0x4+-parseInt(_0x38c743(0x287d))/0x5*(parseInt(_0x38c743(0x4095))/0x6)+-parseInt(_0x38c743(0x374b))/0x7*(parseInt(_0x38c743(0x407))/0x8)+-parseInt(_0x38c743(0x118c))/0x9*(-parseInt(_0x38c743(0x2c23))/0xa)+-parseInt(_0x38c743(0x356b))/0xb;if(_0x57691f===_0x33a119)break;else _0x438892['push'](_0x438892['shift']());}catch(_0x4abcc5){_0x438892['push'](_0x438892['shift']());}}}(_0x53dc,0x7e158));var ms=Object[_0x5eb5c0(0x1e5a)+'operty'],fs=(_0x10be7a,_0x28ad76,_0x259ba7)=>_0x28ad76 in _0x10be7a?ms(_0x10be7a,_0x28ad76,{'enumerable':!0x0,'configurable':!0x0,'writable':!0x0,'value':_0x259ba7}):_0x10be7a[_0x28ad76]=_0x259ba7,ke=(_0x549db0,_0x57ec8e,_0x3a7a59)=>fs(_0x549db0,typeof _0x57ec8e!=_0x5eb5c0(0x841)?_0x57ec8e+'':_0x57ec8e,_0x3a7a59);import{T as _0x409cd4,S as _0x1b526b,C as _0x193bca,G as _0x5bbf64,a as _0x10cc69,d as _0x270c68,c as _0x4b949c,w as _0x30e5dc,b as _0x4b2a2f,M as _0x48ab1b,A as _0x599b96,e as _0x529eb,f as _0x43546c,g as _0x3c9b97,h as _0x2cb5ed,i as _0x1e04c8,j as _0x2aaa99,m as _0x193005,r as _0x5adbbf,o as _0x2c4d02,k as _0x49fa8c,l as _0x1468e9,n as _0x459d9e,p as _0x34b692,q as _0x48e703,s as _0x42bfa5,t as _0x3eec3a,u as _0x4da584,v as _0x8fce0f,x as _0x3d6255,y as _0xc27860,z as _0x308cc0,B as _0x1bf2c9,D as _0x1d55af,E as _0x254831,F as _0x16dd1c,H as _0x4ffd04,I as _0x37de60,J as _0x2608b3,K as _0x3251e8,L as _0x4108af,N as _0x4bd63c,O as _0x110d36,P as _0x280f41,Q as _0x4bfea3,R as _0x29023d,U as _0x16969a,V as _0x1f01a0,W as _0x45944a,X as _0x422ed5,Y as _0x556213,Z as _0x48badf,_ as _0x1cb7e5,$ as _0x60fb8e,a0 as _0x4936ea,a1 as _0x50416c,a2 as _0x33fc66,a3 as _0x27309a,a4 as _0xc31bd2,a5 as _0x2e0884,a6 as _0x331e02,a7 as _0x40bde4,a8 as _0x26c78a,a9 as _0x427dae,aa as _0x3cd815,ab as _0x1d8ab8,ac as _0x8981c0,ad as _0x356850,ae as _0x4678af,af as _0x399dfb,ag as _0x21369a,ah as _0x7bf1b3,ai as _0x158b54}from'./vendor-Cb_6CYmx.js';(function(){const _0x26605f=_0x5eb5c0,_0x1481af=document['createEl'+_0x26605f(0x46f9)](_0x26605f(0x4126))[_0x26605f(0x35d)];if(_0x1481af&&_0x1481af[_0x26605f(0x1b38)]&&_0x1481af[_0x26605f(0x1b38)](_0x26605f(0x3577)+'eload'))return;for(const _0x18abf8 of document['querySel'+_0x26605f(0x3579)]('link[rel'+_0x26605f(0x3f20)+'preload\x22'+']'))_0x3368a1(_0x18abf8);new MutationObserver(_0x36db09=>{const _0xe31c81=_0x26605f;for(const _0x49d81d of _0x36db09)if(_0x49d81d[_0xe31c81(0xa78)]==='childLis'+'t'){for(const _0x7ceeac of _0x49d81d['addedNod'+'es'])_0x7ceeac[_0xe31c81(0x2f45)]===_0xe31c81(0x207)&&_0x7ceeac[_0xe31c81(0x1c40)]==='modulepr'+'eload'&&_0x3368a1(_0x7ceeac);}})[_0x26605f(0x1918)](document,{'childList':!0x0,'subtree':!0x0});function _0x4321dc(_0x5c147e){const _0x2a6024=_0x26605f,_0x4dc157={};return _0x5c147e[_0x2a6024(0x2cdc)+'y']&&(_0x4dc157['integrit'+'y']=_0x5c147e[_0x2a6024(0x2cdc)+'y']),_0x5c147e['referrer'+_0x2a6024(0x45b5)]&&(_0x4dc157[_0x2a6024(0xc64)+_0x2a6024(0x45b5)]=_0x5c147e[_0x2a6024(0xc64)+'Policy']),_0x5c147e[_0x2a6024(0x1b5c)+_0x2a6024(0x4ac)]==='use-cred'+'entials'?_0x4dc157[_0x2a6024(0x412d)+_0x2a6024(0x938)]=_0x2a6024(0x3c18):_0x5c147e[_0x2a6024(0x1b5c)+_0x2a6024(0x4ac)]===_0x2a6024(0x259d)+'s'?_0x4dc157[_0x2a6024(0x412d)+_0x2a6024(0x938)]=_0x2a6024(0x1a9e):_0x4dc157['credenti'+'als']=_0x2a6024(0x118a)+'gin',_0x4dc157;}function _0x3368a1(_0xfca811){const _0x108cfd=_0x26605f;if(_0xfca811['ep'])return;_0xfca811['ep']=!0x0;const _0x506e0a=_0x4321dc(_0xfca811);fetch(_0xfca811[_0x108cfd(0x34ae)],_0x506e0a);}}());function In(_0x34f762,_0x576b43){const _0x19bf33=_0x5eb5c0,_0x3045c2=Math[_0x19bf33(0x46aa)](0x0,Number(_0x34f762)||0x0),_0x3c53bb=Math[_0x19bf33(0x46aa)](0x0,Number(_0x576b43)||0x0);return{'inner':Math[_0x19bf33(0x1418)](_0x3045c2,_0x3c53bb),'outer':Math[_0x19bf33(0x46aa)](_0x3045c2,_0x3c53bb)};}function Bi(_0x1542b2){const _0x8339bb=_0x5eb5c0,_0x2131e3=Math[_0x8339bb(0x46aa)](0x0,Number(_0x1542b2[_0x8339bb(0x166c)+'tW'])||0x0),_0x2da2bb=Math[_0x8339bb(0x46aa)](0x0,Number(_0x1542b2[_0x8339bb(0x166c)+'tH'])||0x0);return{'innerW':Math[_0x8339bb(0x1418)](_0x2131e3,Math['max'](0x0,Number(_0x1542b2[_0x8339bb(0x1bda)+'tW'])||0x0)),'innerH':Math[_0x8339bb(0x1418)](_0x2da2bb,Math[_0x8339bb(0x46aa)](0x0,Number(_0x1542b2[_0x8339bb(0x1bda)+'tH'])||0x0)),'outerW':_0x2131e3,'outerH':_0x2da2bb};}function qs(_0xe91d39,_0x252acb,_0x5ca127,_0x25cc02,_0x97e90){const _0x4f53dd=_0x5eb5c0;if(_0xe91d39==='random')return _0x97e90();const _0xe8b9ee=Math[_0x4f53dd(0x46aa)](0x1,Math['round'](_0x5ca127)),_0x11ec7a=Math['max'](0x0,Math['round'](_0x252acb))%_0xe8b9ee;return _0x25cc02?_0x11ec7a/_0xe8b9ee:_0xe8b9ee<=0x1?0.5:_0x11ec7a/(_0xe8b9ee-0x1);}function Ho(_0x1fca64,_0x153259,_0x1fb7c9,_0x749c4a){const _0x34ef4f=_0x5eb5c0,_0x321982=Math[_0x34ef4f(0x46af)](_0x1fca64*_0x1fca64+_0x749c4a()*(_0x1532Line truncated
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
const _0x202e38=_0x15bb;(function(_0x409791,_0x3ac34d){const _0x2a9db8=_0x15bb,_0x4476ba=_0x409791();while(!![]){try{const _0x1c64c1=-parseInt(_0x2a9db8(0x256f))/0x1*(parseInt(_0x2a9db8(0x42f0))/0x2)+-parseInt(_0x2a9db8(0x1d38))/0x3*(-parseInt(_0x2a9db8(0xf43))/0x4)+-parseInt(_0x2a9db8(0x3609))/0x5*(parseInt(_0x2a9db8(0x927))/0x6)+parseInt(_0x2a9db8(0x1cc6))/0x7*(parseInt(_0x2a9db8(0x45a2))/0x8)+parseInt(_0x2a9db8(0x43ba))/0x9*(parseInt(_0x2a9db8(0x1e06))/0xa)+-parseInt(_0x2a9db8(0x809))/0xb+parseInt(_0x2a9db8(0x1154))/0xc*(-parseInt(_0x2a9db8(0x31dd))/0xd);if(_0x1c64c1===_0x3ac34d)break;else _0x4476ba['push'](_0x4476ba['shift']());}catch(_0x4eb889){_0x4476ba['push'](_0x4476ba['shift']());}}}(_0x384f,0xb4407));var ps=Object[_0x202e38(0xc00)+_0x202e38(0x2770)],ms=(_0x5b1db1,_0x6ed7f5,_0x14260c)=>_0x6ed7f5 in _0x5b1db1?ps(_0x5b1db1,_0x6ed7f5,{'enumerable':!0x0,'configurable':!0x0,'writable':!0x0,'value':_0x14260c}):_0x5b1db1[_0x6ed7f5]=_0x14260c,ke=(_0x4c3a70,_0x2df07e,_0x209f42)=>ms(_0x4c3a70,typeof _0x2df07e!=_0x202e38(0x3de1)?_0x2df07e+'':_0x2df07e,_0x209f42);import{T as _0x2f45dd,S as _0x3b930a,C as _0x4fb826,G as _0x3e219d,a as _0x5d9c86,d as _0x329760,c as _0x28a8c4,w as _0x4e8ea3,b as _0x518d73,M as _0x38b11d,A as _0x4bbf18,e as _0x4e0b77,f as _0x1c12d8,g as _0x118ee6,h as _0x518087,i as _0x343be0,j as _0x3aea42,m as _0x4245ee,r as _0x345b8b,o as _0x5a1993,k as _0x55586f,l as _0x27fe2d,n as _0x233271,p as _0x3499a7,q as _0x4bfd2f,s as _0x2489db,t as _0xd56584,u as _0x418cd7,v as _0x49b4e4,x as _0x1dfcde,y as _0x6931fc,z as _0x433d94,B as _0x5e51e6,D as _0x2c42df,E as _0x2a7120,F as _0x56caf9,H as _0x92f02b,I as _0x531c0d,J as _0xea11ef,K as _0x45b8dd,L as _0x55e444,N as _0xf76111,O as _0x27fd42,P as _0x536523,Q as _0x1b2419,R as _0x178f0b,U as _0x2c79c0,V as _0xeabcda,W as _0x546e3f,X as _0x10c5d5,Y as _0x48ae11,Z as _0x50af4a,_ as _0x117110,$ as _0x3e4f2e,a0 as _0x497587,a1 as _0xb85fd5,a2 as _0x992e5a,a3 as _0x4ad220,a4 as _0x1cef85,a5 as _0x4f2607,a6 as _0x422693,a7 as _0x5c5ee4,a8 as _0x58d34a,a9 as _0x28f39d,aa as _0xce11ba,ab as _0x1c58f3,ac as _0x2fe175,ad as _0x403499,ae as _0x5607c6,af as _0x3cac7a,ag as _0x489ef5,ah as _0x22b07f,ai as _0x553394}from'./vendor-Cb_6CYmx.js';(function(){const _0x359d0a=_0x202e38,_0x2cdb2f=document[_0x359d0a(0x3230)+_0x359d0a(0xe72)](_0x359d0a(0x31cd))[_0x359d0a(0x1b7f)];if(_0x2cdb2f&&_0x2cdb2f['supports']&&_0x2cdb2f[_0x359d0a(0x3111)](_0x359d0a(0x747)+_0x359d0a(0x17f5)))return;for(const _0x11d7ef of document[_0x359d0a(0x5cf)+_0x359d0a(0x2685)](_0x359d0a(0xe2a)+_0x359d0a(0xa9b)+_0x359d0a(0x36d9)+']'))_0x155df4(_0x11d7ef);new MutationObserver(_0x443b71=>{const _0x514ef2=_0x359d0a;for(const _0x1d0590 of _0x443b71)if(_0x1d0590[_0x514ef2(0xab6)]===_0x514ef2(0x1971)+'t'){for(const _0x2a7962 of _0x1d0590[_0x514ef2(0x1300)+'es'])_0x2a7962[_0x514ef2(0x10c8)]===_0x514ef2(0x1161)&&_0x2a7962[_0x514ef2(0x254a)]===_0x514ef2(0x747)+_0x514ef2(0x17f5)&&_0x155df4(_0x2a7962);}})[_0x359d0a(0x1acc)](document,{'childList':!0x0,'subtree':!0x0});function _0x5ced3d(_0x5f3b8d){const _0x4e3b5b=_0x359d0a,_0x5d8c3d={};return _0x5f3b8d[_0x4e3b5b(0x2598)+'y']&&(_0x5d8c3d[_0x4e3b5b(0x2598)+'y']=_0x5f3b8d[_0x4e3b5b(0x2598)+'y']),_0x5f3b8d['referrer'+_0x4e3b5b(0x3607)]&&(_0x5d8c3d['referrer'+_0x4e3b5b(0x3607)]=_0x5f3b8d['referrer'+_0x4e3b5b(0x3607)]),_0x5f3b8d['crossOri'+_0x4e3b5b(0x2ac6)]==='use-cred'+_0x4e3b5b(0x2c35)?_0x5d8c3d[_0x4e3b5b(0x1ea5)+_0x4e3b5b(0x12ed)]=_0x4e3b5b(0x3e9c):_0x5f3b8d['crossOri'+_0x4e3b5b(0x2ac6)]===_0x4e3b5b(0x2b7e)+'s'?_0x5d8c3d[_0x4e3b5b(0x1ea5)+_0x4e3b5b(0x12ed)]=_0x4e3b5b(0x28f2):_0x5d8c3d['credenti'+_0x4e3b5b(0x12ed)]=_0x4e3b5b(0x9a6)+_0x4e3b5b(0x2ac6),_0x5d8c3d;}function _0x155df4(_0x10a173){const _0x2c4918=_0x359d0a;if(_0x10a173['ep'])return;_0x10a173['ep']=!0x0;const _0x2646b1=_0x5ced3d(_0x10a173);fetch(_0x10a173[_0x2c4918(0x345a)],_0x2646b1);}}());function zn(_0x649915,_0x4ff56f){const _0x3ed132=_0x202e38,_0x247a97=Math[_0x3ed132(0x2746)](0x0,Number(_0x649915)||0x0),_0x58cf91=Math['max'](0x0,Number(_0x4ff56f)||0x0);return{'inner':Math['min'](_0x247a97,_0x58cf91),'outer':Math[_0x3ed132(0x2746)](_0x247a97,_0x58cf91)};}function Vi(_0x328171){const _0xf6b96a=_0x202e38,_0xfa47b6=Math['max'](0x0,Number(_0x328171[_0xf6b96a(0x67a)+'tW'])||0x0),_0x22b8a6=Math[_0xf6b96a(0x2746)](0x0,Number(_0x328171[_0xf6b96a(0x67a)+'tH'])||0x0);return{'innerW':Math[_0xf6b96a(0x18b8)](_0xfa47b6,Math[_0xf6b96a(0x2746)](0x0,Number(_0x328171[_0xf6b96a(0x2b48)+'tW'])||0x0)),'innerH':Math[_0xf6b96a(0x18b8)](_0x22b8a6,Math[_0xf6b96a(0x2746)](0x0,Number(_0x328171[_0xf6b96a(0x2b48)+'tH'])||0x0)),'outerW':_0xfa47b6,'outerH':_0x22b8a6};}function Us(_0x5b615f,_0x4ef316,_0x7caf7f,_0x1d4f9d,_0x405420){const _0x1ec7ea=_0x202e38;if(_0x5b615f===_0x1ec7ea(0x12b9))return _0x405420();const _0x524f66=Math[_0x1ec7ea(0x2746)](0x1,Math['round'](_0x7caf7f)),_0x20d2aa=Math['max'](0x0,Math['round'](_0x4ef316))%_0x524f66;return _0x1d4f9d?_0x20d2aa/_0x524f66:_0x524f66<=0x1?0.5:_0x20d2aa/(_0x524f66-0x1);}function qLine truncated
|
||||
@@ -1 +1 @@
|
||||
.help-overlay[data-v-2ef0dcfc]{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-2ef0dcfc]{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-2ef0dcfc]{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-2ef0dcfc]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-2ef0dcfc]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-2ef0dcfc]{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-2ef0dcfc]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-2ef0dcfc]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-2ef0dcfc]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-2ef0dcfc]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-2ef0dcfc]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-2ef0dcfc]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-2ef0dcfc]{color:#dce4f7}.help-grid[data-v-2ef0dcfc]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-2ef0dcfc]{display:grid;gap:0;margin:0}.help-list>div[data-v-2ef0dcfc]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-2ef0dcfc]:last-child{border-bottom:0}.help-list dt[data-v-2ef0dcfc]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-2ef0dcfc]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-2ef0dcfc]{display:grid;gap:7px}.shortcut-list>div[data-v-2ef0dcfc]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-2ef0dcfc]{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-2ef0dcfc]{color:#65728b;font-size:10px}.shortcut-list em[data-v-2ef0dcfc]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-2ef0dcfc]{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-2ef0dcfc]{margin-top:12px}.help-export p[data-v-2ef0dcfc]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-2ef0dcfc]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-2ef0dcfc]{margin-top:12px}.help-module-head[data-v-2ef0dcfc]{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-2ef0dcfc]:hover{color:#fff}.help-module-head .mtitle[data-v-2ef0dcfc]{flex:1;min-width:0}.help-module-head .caret[data-v-2ef0dcfc]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-2ef0dcfc]{color:#fff}.help-module.open .help-module-head .caret[data-v-2ef0dcfc]{color:#9fb2ff}.help-module .mnum[data-v-2ef0dcfc]{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-2ef0dcfc]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-2ef0dcfc]{color:#b8c2ff}.help-module .connector-intro[data-v-2ef0dcfc]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-2ef0dcfc]{color:#dce4f7}.help-module .dl-link[data-v-2ef0dcfc]{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-2ef0dcfc]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-2ef0dcfc]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-2ef0dcfc]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-2ef0dcfc]{padding:66px 12px 14px}.help-grid[data-v-2ef0dcfc]{grid-template-columns:1fr}}.topbar[data-v-de1baa8a]{display:flex;align-items:center;gap:10px;flex-wrap:wLine truncated
|
||||
.help-overlay[data-v-2ef0dcfc]{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-2ef0dcfc]{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-2ef0dcfc]{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-2ef0dcfc]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-2ef0dcfc]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-2ef0dcfc]{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-2ef0dcfc]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-2ef0dcfc]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-2ef0dcfc]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-2ef0dcfc]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-2ef0dcfc]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-2ef0dcfc]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-2ef0dcfc]{color:#dce4f7}.help-grid[data-v-2ef0dcfc]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-2ef0dcfc]{display:grid;gap:0;margin:0}.help-list>div[data-v-2ef0dcfc]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-2ef0dcfc]:last-child{border-bottom:0}.help-list dt[data-v-2ef0dcfc]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-2ef0dcfc]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-2ef0dcfc]{display:grid;gap:7px}.shortcut-list>div[data-v-2ef0dcfc]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-2ef0dcfc]{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-2ef0dcfc]{color:#65728b;font-size:10px}.shortcut-list em[data-v-2ef0dcfc]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-2ef0dcfc]{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-2ef0dcfc]{margin-top:12px}.help-export p[data-v-2ef0dcfc]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-2ef0dcfc]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-2ef0dcfc]{margin-top:12px}.help-module-head[data-v-2ef0dcfc]{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-2ef0dcfc]:hover{color:#fff}.help-module-head .mtitle[data-v-2ef0dcfc]{flex:1;min-width:0}.help-module-head .caret[data-v-2ef0dcfc]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-2ef0dcfc]{color:#fff}.help-module.open .help-module-head .caret[data-v-2ef0dcfc]{color:#9fb2ff}.help-module .mnum[data-v-2ef0dcfc]{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-2ef0dcfc]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-2ef0dcfc]{color:#b8c2ff}.help-module .connector-intro[data-v-2ef0dcfc]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-2ef0dcfc]{color:#dce4f7}.help-module .dl-link[data-v-2ef0dcfc]{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-2ef0dcfc]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-2ef0dcfc]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-2ef0dcfc]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-2ef0dcfc]{padding:66px 12px 14px}.help-grid[data-v-2ef0dcfc]{grid-template-columns:1fr}}.topbar[data-v-de1baa8a]{display:flex;align-items:center;gap:10px;flex-wrap:wLine truncated
|
||||
Vendored
+2
-2
@@ -9,9 +9,9 @@
|
||||
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-D9evYJNa.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C0M19raZ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-Cb_6CYmx.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BiWG32s_.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-NY7UreRZ.css">
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
|
||||
@@ -122,6 +122,45 @@ if (delayedTarget.activeCount !== 0) throw new Error('子发射器延迟未到
|
||||
delayedTarget.update(0.1)
|
||||
if (delayedTarget.activeCount !== 2) throw new Error(`子发射器延迟后应生成 2 颗,实际 ${delayedTarget.activeCount} 颗`)
|
||||
|
||||
// 持续子发射器:每次父事件启动独立窗口,窗口内所有粒子复用该事件的继承快照。
|
||||
const streamConfig = makeConfig({
|
||||
mode: 'stream',
|
||||
streamBehavior: 'normal',
|
||||
rate: 30,
|
||||
duration: 0.1,
|
||||
lifeMin: 1,
|
||||
lifeMax: 1,
|
||||
scaleMode: 'fixed',
|
||||
scaleMin: 1,
|
||||
scaleMax: 1,
|
||||
initialRotationMode: 'fixed',
|
||||
initialRotationMin: 0,
|
||||
initialRotationMax: 0,
|
||||
})
|
||||
streamConfig.imageResources[0].colorMode = 'fixed'
|
||||
streamConfig.imageResources[0].colorStart = '#102030'
|
||||
const streamTarget = new ParticleEmitter(streamConfig)
|
||||
streamTarget.setAutoEmission(false)
|
||||
const streamInheritAttributes = ['color', 'scale', 'rotation', 'life']
|
||||
const streamInheritData = { colorHex: 0x778899, scaleX: 2, scaleY: 2, rotation: 90, life: 0.5 }
|
||||
streamTarget.spawnEventParticle(5, 6, 1, {
|
||||
attributes: streamInheritAttributes,
|
||||
colorMode: 'replace',
|
||||
data: streamInheritData,
|
||||
})
|
||||
streamInheritAttributes.length = 0
|
||||
streamInheritData.colorHex = 0
|
||||
for (let i = 0; i < 3; i++) streamTarget.update(step)
|
||||
const streamStates = streamTarget.capture()
|
||||
if (streamStates.length !== 3) throw new Error(`持续子发射器应按速率生成 3 颗,实际 ${streamStates.length} 颗`)
|
||||
for (const state of streamStates) {
|
||||
if (state.colorHex !== 0x778899 || state.scaleX !== 2 || state.scaleY !== 2 || Math.abs(state.rotation - 90) > 1e-6) {
|
||||
throw new Error('持续发射窗口没有复用父事件的继承快照')
|
||||
}
|
||||
}
|
||||
streamTarget.update(step)
|
||||
if (streamTarget.capture().length !== 3) throw new Error('持续发射窗口结束后仍继续生成粒子')
|
||||
|
||||
// 子发射器:被引用后不自动发射,只接收事件粒子;移除引用后恢复普通系统。
|
||||
const subEmitter = new ParticleEmitter(makeConfig({ burstCount: 5 }))
|
||||
subEmitter.setAutoEmission(false)
|
||||
@@ -224,4 +263,4 @@ if (Math.abs(scaledRectHit.worldX + 125.02) > 0.1 || Math.abs(scaledRectHit.worl
|
||||
throw new Error(`非等比矩形吸附面错误:实际命中 (${scaledRectHit.worldX}, ${scaledRectHit.worldY})`)
|
||||
}
|
||||
|
||||
console.log('粒子事件校验通过:暖场 / 出生 / 死亡 / 碰撞 / 非等比矩形碰撞面 / 子发射器生成与禁用自动发射 / 属性继承 / 深度上限')
|
||||
console.log('粒子事件校验通过:暖场 / 出生 / 死亡 / 碰撞 / 非等比矩形碰撞面 / 持续发射 / 子发射器生成与禁用自动发射 / 属性继承 / 深度上限')
|
||||
@@ -78,6 +78,13 @@ const source = `
|
||||
failures++
|
||||
console.error('骨骼池复用:死亡/复用边界应写阶跃关键帧,实际为 ' + JSON.stringify(poolStepped))
|
||||
}
|
||||
const limitedResult = buildSpineJson({ systems: [poolSystem], timeline, settings: poolSettings, playbackEnd: 4 })
|
||||
const limitedFramesAt = limitedResult.json.animations.animation.bones.verify_0.translate
|
||||
.map((key) => Math.round(key.time * 30))
|
||||
if (JSON.stringify(limitedFramesAt) !== JSON.stringify([0, 4])) {
|
||||
failures++
|
||||
console.error('导出范围:不应写出实际播放末端之后的关键帧,实际为 ' + JSON.stringify(limitedFramesAt))
|
||||
}
|
||||
|
||||
// 子发射器的帧从父事件时刻才开始,不能按自身生命周期从 0f 截断。
|
||||
const childFrames = Array.from({ length: 80 }, (_, frame) => (
|
||||
|
||||
@@ -28,35 +28,38 @@ const source = `
|
||||
probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add',
|
||||
}]
|
||||
|
||||
// 子发射器自身的 10s 发射时长不应算入时间轴。
|
||||
// 持续子发射器每次事件都会运行自身的 10s 发射窗口。
|
||||
child.config.mode = 'stream'
|
||||
child.config.streamBehavior = 'normal'
|
||||
child.config.delay = 0
|
||||
child.config.duration = 10
|
||||
setLife(child, 1.5)
|
||||
|
||||
// 死亡触发:父粒子最晚 3s 死亡,子发射器再存活 1.5s → 4.5s。
|
||||
// 死亡触发:父粒子最晚 3s 死亡,子发射器持续到 13s,粒子再存活 1.5s → 14.5s。
|
||||
let frames = store.recalcTotalFrames()
|
||||
assert(frames === Math.ceil(4.5 * 30) + 1, '死亡子发射器时间轴错误:' + frames)
|
||||
assert(frames === Math.ceil(14.5 * 30) + 1, '死亡子发射器时间轴错误:' + frames)
|
||||
const deathTiming = subEmitterTriggerTimings(store.systems, 30).get(child.id)
|
||||
assert(deathTiming && Math.abs(deathTiming.firstTrigger - 2) < 1e-6, '死亡子发射器起点应为第一次死亡 2s')
|
||||
assert(deathTiming && Math.abs(deathTiming.lastTrigger - 3) < 1e-6, '死亡子发射器最后触发应为 3s')
|
||||
assert(deathTiming && Math.abs(deathTiming.end - 4.5) < 1e-6, '死亡子发射器终点应为 4.5s')
|
||||
assert(deathTiming && Math.abs(deathTiming.end - 14.5) < 1e-6, '死亡子发射器终点应为 14.5s')
|
||||
|
||||
// 子发射器自身 delay 在父事件之后继续延后发射。
|
||||
child.config.delay = 0.2
|
||||
frames = store.recalcTotalFrames()
|
||||
assert(frames === Math.ceil(4.7 * 30) + 1, '子发射器延迟未计入时间轴:' + frames)
|
||||
assert(frames === Math.ceil(14.7 * 30) + 1, '子发射器延迟未计入时间轴:' + frames)
|
||||
const delayedTiming = subEmitterTriggerTimings(store.systems, 30).get(child.id)
|
||||
assert(delayedTiming && Math.abs(delayedTiming.end - delayedTiming.firstTrigger - 0.2 - 12.5) < 1e-6,
|
||||
'子发射器拖动延迟后,可见条长度不应包含 delay')
|
||||
child.config.delay = 0
|
||||
|
||||
// 出生触发:子发射器与父粒子同时开始,取较长者 2s → 3s。
|
||||
// 出生触发:子发射器持续到 11s,粒子存活的 1.5s 继续至 12.5s。
|
||||
parent.config.events[0].event = 'birth'
|
||||
frames = store.recalcTotalFrames()
|
||||
assert(frames === Math.ceil(3 * 30) + 1, '出生子发射器时间轴错误:' + frames)
|
||||
assert(frames === Math.ceil(12.5 * 30) + 1, '出生子发射器时间轴错误:' + frames)
|
||||
const birthTiming = subEmitterTriggerTimings(store.systems, 30).get(child.id)
|
||||
assert(birthTiming && Math.abs(birthTiming.firstTrigger - 0) < 1e-6, '出生子发射器应从父粒子首次出生开始')
|
||||
assert(birthTiming && Math.abs(birthTiming.lastTrigger - 1) < 1e-6, '出生子发射器最后触发应为发射结束 1s')
|
||||
assert(birthTiming && Math.abs(birthTiming.end - 2.5) < 1e-6, '出生子发射器终点应为 2.5s')
|
||||
assert(birthTiming && Math.abs(birthTiming.end - 12.5) < 1e-6, '出生子发射器终点应为 12.5s')
|
||||
|
||||
// 关闭“开启子发射器”后,保留的规则不应继续把目标标记为子发射器。
|
||||
parent.config.eventEnabled = false
|
||||
@@ -71,6 +74,28 @@ const source = `
|
||||
frames = store.recalcTotalFrames()
|
||||
assert(frames === Math.ceil(11.5 * 30) + 1, '移除引用后时间轴错误:' + frames)
|
||||
|
||||
// 理论总长度可以预留最坏情况,但完整播放只到最后一个可见时间条的末端。
|
||||
const grandchild = store.addSystem()
|
||||
parent.config.mode = 'burst'
|
||||
setLife(parent, 1)
|
||||
parent.config.eventEnabled = true
|
||||
parent.config.events = [{
|
||||
id: 1, event: 'death', targetSystemId: child.id,
|
||||
probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add',
|
||||
}]
|
||||
child.config.duration = 1
|
||||
setLife(child, 1)
|
||||
child.config.eventEnabled = true
|
||||
child.config.events = [{
|
||||
id: 1, event: 'collision', targetSystemId: grandchild.id,
|
||||
probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add',
|
||||
}]
|
||||
grandchild.config.mode = 'burst'
|
||||
setLife(grandchild, 1)
|
||||
frames = store.recalcTotalFrames()
|
||||
assert(frames === Math.ceil(4 * 30) + 1, '最坏情况总长度计算错误:' + frames)
|
||||
assert(store.playbackRange.start === 0 && store.playbackRange.end === 90, '播放范围没有停在时间条末端')
|
||||
|
||||
console.log('子发射器时间轴校验通过:死亡/出生触发与移除引用后的总长度')
|
||||
`
|
||||
|
||||
|
||||
+78
-42
@@ -3,7 +3,7 @@ import { pathDistributionProgress, sampleEmitterShape, sampleEmissionVelocityAng
|
||||
import { TrailController, createTrailState, resetTrailState, type ParticleTrailState } from './particleTrail'
|
||||
import { pickImageResource, pickTrailResource, imageFrameState, imageFrameTexture } from './particleResources'
|
||||
|
||||
import type { SceneCollider, EmitterConfig, ParticleState, ParticleInheritAttribute } from './particleTypes'
|
||||
import type { SceneCollider, EmitterConfig, EmitMode, ParticleState, ParticleInheritAttribute } from './particleTypes'
|
||||
import { defaultConfig, ensureEmitterConfig } from './particleConfig'
|
||||
import { BLEND_MAP, MAX_PARTICLES } from './particleConstants'
|
||||
import { lerp, finite, localToWorldMath, worldMathToLocal, localVelocityToWorldMath, worldVelocityToLocalMath, resolveColliderPoint, tFromLife, seededUnit, initialValue, overLifeValue, curveAt, mulberry32, hexToNumber, gradientColorAt, fieldFalloff, vortexAcceleration, addRgbColors } from './particleMath'
|
||||
@@ -90,6 +90,19 @@ export interface ParticleInheritOptions {
|
||||
data: ParticleInheritData
|
||||
}
|
||||
|
||||
interface EventEmission {
|
||||
mode: EmitMode
|
||||
worldX: number
|
||||
worldY: number
|
||||
depth: number
|
||||
inherit?: ParticleInheritOptions
|
||||
count: number
|
||||
rate: number
|
||||
remainingDelay: number
|
||||
remainingDuration: number
|
||||
accumulator: number
|
||||
}
|
||||
|
||||
export class ParticleEmitter extends Container {
|
||||
private pool: Particle[] = []
|
||||
private cfg: EmitterConfig
|
||||
@@ -103,15 +116,8 @@ export class ParticleEmitter extends Container {
|
||||
private _attractionWasEnabled = false
|
||||
/** 子发射器关闭自动发射,只接收父粒子事件生成的粒子。 */
|
||||
private autoEmission = true
|
||||
/** 事件粒子延迟队列:子发射器 delay 表示在父事件之后再延迟多久发射。 */
|
||||
private pendingEventSpawns: Array<{
|
||||
worldX: number
|
||||
worldY: number
|
||||
depth: number
|
||||
inherit?: ParticleInheritOptions
|
||||
count: number
|
||||
remaining: number
|
||||
}> = []
|
||||
/** 父事件启动的独立子发射窗口;持续模式会按 rate/duration 运行。 */
|
||||
private eventEmissions: EventEmission[] = []
|
||||
/** 活动粒子状态快照(供骨架 / 导出) */
|
||||
states: ParticleState[] = []
|
||||
/** 粒子出生/死亡/碰撞事件回调(由 EmitterSync 接入跨系统事件)。 */
|
||||
@@ -287,23 +293,8 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子发射器的延迟发射队列;新粒子在本帧就会参与后续更新与录制。
|
||||
if (this.pendingEventSpawns.length && dt > 0) {
|
||||
const ready: typeof this.pendingEventSpawns = []
|
||||
const waiting: typeof this.pendingEventSpawns = []
|
||||
for (const pending of this.pendingEventSpawns) {
|
||||
pending.remaining -= dt
|
||||
if (pending.remaining <= 0) ready.push(pending)
|
||||
else waiting.push(pending)
|
||||
}
|
||||
this.pendingEventSpawns = waiting
|
||||
for (const pending of ready) {
|
||||
const local = worldMathToLocal(this.cfg, pending.worldX, pending.worldY, this.simulationTransform)
|
||||
for (let index = 0; index < pending.count; index++) {
|
||||
if (!this.spawnOne({ x: local.x, y: local.y }, pending.depth, pending.inherit)) break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新粒子在本帧就会参与后续更新与录制。
|
||||
this.updateEventEmissions(dt)
|
||||
|
||||
const states: ParticleState[] = []
|
||||
for (let i = 0; i < this.pool.length; i++) {
|
||||
@@ -557,27 +548,72 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
/** 由跨系统事件触发:在给定的数学世界坐标处生成一颗目标系统的粒子。 */
|
||||
/** 由跨系统事件触发:爆发模式生成一次,持续模式启动一个独立发射窗口。 */
|
||||
spawnEventParticle(worldX: number, worldY: number, depth = 1, inherit?: ParticleInheritOptions, count?: number): boolean {
|
||||
if (depth > 2) return false
|
||||
// 子发射器按自己的发射模式决定每次事件生成多少颗:爆发=单次数量,持续=1。
|
||||
const requested = count ?? (this.cfg.mode === 'burst' ? this.cfg.burstCount : 1)
|
||||
const total = Math.max(1, Math.min(MAX_PARTICLES, Math.round(Number(requested) || 1)))
|
||||
// 子发射器 delay 表示在父事件之后再延迟多久发射。
|
||||
const delay = Math.max(0, Number(this.cfg.delay) || 0)
|
||||
if (delay > 0) {
|
||||
this.pendingEventSpawns.push({ worldX, worldY, depth, inherit, count: total, remaining: delay })
|
||||
return true
|
||||
const duration = this.cfg.streamBehavior === 'loop'
|
||||
? Math.max(1 / 30, Number(this.cfg.loopDurationFrames) / 30 || 1 / 30)
|
||||
: Math.max(1 / 30, Number(this.cfg.duration) || 0)
|
||||
const emission: EventEmission = {
|
||||
mode: this.cfg.mode,
|
||||
worldX,
|
||||
worldY,
|
||||
depth,
|
||||
inherit: inherit
|
||||
? { ...inherit, attributes: [...inherit.attributes], data: { ...inherit.data } }
|
||||
: undefined,
|
||||
count: Math.max(1, Math.min(MAX_PARTICLES, Math.round(Number(count ?? this.cfg.burstCount) || 1))),
|
||||
rate: Math.max(0, Number(this.cfg.rate) || 0),
|
||||
remainingDelay: Math.max(0, Number(this.cfg.delay) || 0),
|
||||
remainingDuration: duration,
|
||||
accumulator: 0,
|
||||
}
|
||||
const local = worldMathToLocal(this.cfg, worldX, worldY, this.simulationTransform)
|
||||
if (emission.mode === 'burst' && emission.remainingDelay <= 0) {
|
||||
return this.spawnEventBurst(emission)
|
||||
}
|
||||
this.eventEmissions.push(emission)
|
||||
return true
|
||||
}
|
||||
|
||||
private spawnEventBurst(emission: EventEmission) {
|
||||
const local = worldMathToLocal(this.cfg, emission.worldX, emission.worldY, this.simulationTransform)
|
||||
let spawned = false
|
||||
for (let index = 0; index < total; index++) {
|
||||
if (!this.spawnOne({ x: local.x, y: local.y }, depth, inherit)) break
|
||||
for (let index = 0; index < emission.count; index++) {
|
||||
if (!this.spawnOne({ x: local.x, y: local.y }, emission.depth, emission.inherit)) break
|
||||
spawned = true
|
||||
}
|
||||
return spawned
|
||||
}
|
||||
|
||||
private updateEventEmissions(dt: number) {
|
||||
if (!this.eventEmissions.length || dt <= 0) return
|
||||
const waiting: EventEmission[] = []
|
||||
for (const emission of this.eventEmissions) {
|
||||
if (emission.remainingDelay > 0) {
|
||||
if (emission.remainingDelay > dt) {
|
||||
emission.remainingDelay -= dt
|
||||
waiting.push(emission)
|
||||
continue
|
||||
}
|
||||
emission.remainingDelay = 0
|
||||
}
|
||||
if (emission.mode === 'burst') {
|
||||
this.spawnEventBurst(emission)
|
||||
continue
|
||||
}
|
||||
const activeTime = Math.min(dt, emission.remainingDuration)
|
||||
emission.remainingDuration -= activeTime
|
||||
emission.accumulator += activeTime * emission.rate
|
||||
const local = worldMathToLocal(this.cfg, emission.worldX, emission.worldY, this.simulationTransform)
|
||||
while (emission.accumulator >= 1) {
|
||||
emission.accumulator -= 1
|
||||
this.spawnOne({ x: local.x, y: local.y }, emission.depth, emission.inherit)
|
||||
}
|
||||
if (emission.remainingDuration > 1e-8) waiting.push(emission)
|
||||
}
|
||||
this.eventEmissions = waiting
|
||||
}
|
||||
|
||||
private emitWorldEvent(
|
||||
kind: ParticleEventKind,
|
||||
particle: Particle,
|
||||
@@ -738,7 +774,7 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.pendingEventSpawns.length = 0
|
||||
this.eventEmissions.length = 0
|
||||
for (const p of this.pool) {
|
||||
p.active = false
|
||||
p.sprite.visible = false
|
||||
@@ -830,7 +866,7 @@ export class ParticleEmitter extends Container {
|
||||
|
||||
/** 清空所有粒子并重置发射计时 */
|
||||
reset() {
|
||||
this.pendingEventSpawns.length = 0
|
||||
this.eventEmissions.length = 0
|
||||
for (const p of this.pool) {
|
||||
p.active = false
|
||||
p.sprite.visible = false
|
||||
@@ -852,7 +888,7 @@ export class ParticleEmitter extends Container {
|
||||
* 发射计时与随机序列回到周期起点,使每一周期产生完全相同的发射事件。
|
||||
*/
|
||||
restartEmissionCycle() {
|
||||
this.pendingEventSpawns.length = 0
|
||||
this.eventEmissions.length = 0
|
||||
this.accumulator = 0
|
||||
this.burstTimer = 0
|
||||
this._spawnSerial = 0
|
||||
|
||||
@@ -34,7 +34,7 @@ const numberOrZero = (value: unknown) => Math.max(0, Number(value) || 0)
|
||||
|
||||
/**
|
||||
* 计算子发射器在时间轴上的实际活动区间:
|
||||
* 起点取第一次可能被触发的时刻,终点取最后一次触发 + 子粒子最大生命。
|
||||
* 起点取第一次可能被触发的时刻,终点取最后一次触发 + 子发射持续时长 + 子粒子最大生命。
|
||||
* 触发时机:出生=父粒子出生时;死亡=最早/最晚死亡时;碰撞=出生到死亡之间。
|
||||
*/
|
||||
export function subEmitterTriggerTimings(
|
||||
@@ -66,13 +66,15 @@ export function subEmitterTriggerTimings(
|
||||
|
||||
const emissionStart = (config: SubEmitterTimingSource['config']) => numberOrZero(config.delay)
|
||||
const delayOf = (config: SubEmitterTimingSource['config']) => numberOrZero(config.delay)
|
||||
const emissionDuration = (config: SubEmitterTimingSource['config']) => {
|
||||
if (config.mode !== 'stream') return 0
|
||||
return config.streamBehavior === 'loop'
|
||||
? loopSegmentTiming(config as any, safeFps).total / safeFps
|
||||
: Math.max(1 / safeFps, Number(config.duration) || 0)
|
||||
}
|
||||
const emissionEnd = (config: SubEmitterTimingSource['config']) => {
|
||||
const delay = emissionStart(config)
|
||||
if (config.mode !== 'stream') return delay
|
||||
if (config.streamBehavior === 'loop') {
|
||||
return delay + loopSegmentTiming(config as any, safeFps).total / safeFps
|
||||
}
|
||||
return delay + Math.max(1 / safeFps, Number(config.duration) || 0)
|
||||
return delay + emissionDuration(config)
|
||||
}
|
||||
const lifeMinOf = (config: SubEmitterTimingSource['config']) => {
|
||||
const min = numberOrZero(config.lifeMin)
|
||||
@@ -135,7 +137,9 @@ export function subEmitterTriggerTimings(
|
||||
result.set(id, {
|
||||
firstTrigger: first,
|
||||
lastTrigger: last,
|
||||
end: Math.max(first, last) + lifeMaxOf(system.config),
|
||||
end: Math.max(first, last) + numberOrZero(system.config.delay)
|
||||
+ emissionDuration(system.config)
|
||||
+ lifeMaxOf(system.config),
|
||||
})
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -10,7 +10,7 @@ import { collectSubEmitterIds } from '../core/subEmitterIds'
|
||||
import { translate } from '../i18n'
|
||||
|
||||
type Track = { key: string; name: string; states: Array<ParticleState | null> }
|
||||
type ExportInput = { systems: ParticleSystem[]; timeline: TimelineState; settings: SpineExportSettings }
|
||||
type ExportInput = { systems: ParticleSystem[]; timeline: TimelineState; settings: SpineExportSettings; playbackEnd?: number }
|
||||
|
||||
const BLENDS = { normal: 'normal', add: 'additive', multiply: 'multiply', screen: 'screen' } as const
|
||||
const PARTICLE_POSITION_TOLERANCE = 0.1
|
||||
@@ -343,9 +343,12 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
|
||||
for (const system of visibleSystems) {
|
||||
const frameLength = system.frames?.length || 0
|
||||
const playbackFrameCount = Number.isFinite(input.playbackEnd)
|
||||
? Math.min(frameLength, Math.max(1, Math.floor(Number(input.playbackEnd)) + 1))
|
||||
: frameLength
|
||||
const sourceCount = subEmitterIds.has(system.id)
|
||||
? frameLength
|
||||
: Math.min(frameLength, particleDurationFrames(system, sourceFps))
|
||||
? playbackFrameCount
|
||||
: Math.min(playbackFrameCount, particleDurationFrames(system, sourceFps))
|
||||
const frames = (system.frames || []).slice(0, sourceCount)
|
||||
if (!frames.length) warnings.push({ code: 'empty-cache', message: translate('errors.emptyCacheWarning', { name: system.config.name }) })
|
||||
const prefix = uniqueName(safeName(system.config.name, `system_${system.id}`), usedNames)
|
||||
|
||||
+76
-11
@@ -6,6 +6,7 @@ import { cloneEditorValue } from '../editor/editorClone'
|
||||
import { defaultSpineExportSettings, type SpineExportSettings } from '../export/spineExportSettings'
|
||||
import { loopSegmentTiming } from '../core/loopSegmentTiming'
|
||||
import { collectSubEmitterIds } from '../core/subEmitterIds'
|
||||
import { subEmitterTriggerTimings } from '../core/subEmitterTiming'
|
||||
import { lastFrameOfDuration, resizePlaybackRange, resolvePlaybackRange } from '../timeline/playbackRange'
|
||||
|
||||
export interface ParticleSystem {
|
||||
@@ -117,6 +118,52 @@ export interface TimelineState {
|
||||
animations: string[]
|
||||
}
|
||||
|
||||
function timelineContentEndFrame(
|
||||
systems: readonly any[],
|
||||
spines: readonly any[],
|
||||
colliders: readonly any[],
|
||||
fps: number,
|
||||
) {
|
||||
const safeFps = Math.max(1, fps || 30)
|
||||
const subEmitterIds = collectSubEmitterIds(systems)
|
||||
const subEmitterTiming = subEmitterTriggerTimings(systems as any, safeFps)
|
||||
let endFrame = 1
|
||||
for (const system of systems) {
|
||||
const config = system.config
|
||||
if (subEmitterIds.has(system.id)) {
|
||||
const timing = subEmitterTiming.get(system.id)
|
||||
if (timing) endFrame = Math.max(endFrame, lastFrameOfDuration(timing.end, safeFps))
|
||||
continue
|
||||
}
|
||||
const delay = Math.max(0, Number(config.delay) || 0)
|
||||
if (config.mode === 'stream' && config.streamBehavior === 'loop') {
|
||||
endFrame = Math.max(endFrame, lastFrameOfDuration(delay + loopSegmentTiming(config as any, safeFps).total / safeFps, safeFps))
|
||||
continue
|
||||
}
|
||||
const emission = config.mode === 'stream' ? Math.max(1 / safeFps, Number(config.duration) || 0) : 0
|
||||
const life = config.lifeMode === 'fixed'
|
||||
? Math.max(0, Number(config.lifeMin) || 0)
|
||||
: Math.max(0, Number(config.lifeMin) || 0, Number(config.lifeMax) || 0)
|
||||
endFrame = Math.max(endFrame, lastFrameOfDuration(delay + emission + life, safeFps))
|
||||
}
|
||||
for (const spine of spines) {
|
||||
const animation = spine.animations.find((item: any) => item.name === spine.selectedAnimation)
|
||||
const duration = Math.max(0, Number(animation?.duration) || 0)
|
||||
if (duration > 0) {
|
||||
endFrame = Math.max(
|
||||
endFrame,
|
||||
lastFrameOfDuration(Math.max(0, Number(spine.timeOffset) || 0) + duration, safeFps),
|
||||
)
|
||||
}
|
||||
}
|
||||
for (const collider of colliders) {
|
||||
if (collider.followPath && collider.followPathId) {
|
||||
endFrame = Math.max(endFrame, lastFrameOfDuration(collider.followDuration, safeFps))
|
||||
}
|
||||
}
|
||||
return endFrame
|
||||
}
|
||||
|
||||
let seq = 1
|
||||
let colliderSeq = 1
|
||||
let pathSeq = 1
|
||||
@@ -170,12 +217,17 @@ export const useParticleStore = defineStore('particle', {
|
||||
}),
|
||||
getters: {
|
||||
activeSystemCount: (s) => s.systems.length,
|
||||
playbackRange: (s) => resolvePlaybackRange(
|
||||
s.timeline.abLoop,
|
||||
s.timeline.playbackStart,
|
||||
s.timeline.playbackEnd,
|
||||
s.timeline.totalFrames,
|
||||
),
|
||||
playbackRange: (s) => {
|
||||
const range = resolvePlaybackRange(
|
||||
s.timeline.abLoop,
|
||||
s.timeline.playbackStart,
|
||||
s.timeline.playbackEnd,
|
||||
s.timeline.totalFrames,
|
||||
)
|
||||
const contentEnd = timelineContentEndFrame(s.systems, s.spines, s.colliders, s.timeline.fps)
|
||||
const end = Math.max(1, Math.min(range.end, contentEnd))
|
||||
return { start: Math.min(range.start, end - 1), end }
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
syncSceneSequences() {
|
||||
@@ -511,7 +563,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
/** 依据所有粒子系统的"最晚消失时间"重算总帧数。
|
||||
* 固定生命周期使用 lifeMin;随机生命周期使用上下限中的较大值。
|
||||
* 单系统总时长 = delay + 持续模式 duration(爆发为 0)+ 最大粒子生命,
|
||||
* 并递归计入子发射器链(出生立即触发、死亡/碰撞在最晚生命结束时触发)。
|
||||
* 并递归计入子发射器的持续窗口与粒子链(出生立即触发、死亡/碰撞在最晚生命结束时触发)。
|
||||
* 多系统取最大;转帧 = ceil(sec * fps)。 */
|
||||
recalcTotalFrames() {
|
||||
const fps = Math.max(1, this.timeline.fps || 30)
|
||||
@@ -523,6 +575,13 @@ export const useParticleStore = defineStore('particle', {
|
||||
const byId = new Map(this.systems.map((system) => [system.id, system]))
|
||||
const tailCache = new Map<number, number>()
|
||||
const visiting = new Set<number>()
|
||||
const emissionDuration = (system: any) => {
|
||||
const config = system.config
|
||||
if (config.mode !== 'stream') return 0
|
||||
return config.streamBehavior === 'loop'
|
||||
? loopSegmentTiming(config, fps).total / fps
|
||||
: Math.max(1 / fps, Number(config.duration) || 0)
|
||||
}
|
||||
/** 从一颗粒子出生算起,它及其子发射器链最晚持续多久。 */
|
||||
const effectTail = (system: any): number => {
|
||||
const cached = tailCache.get(system.id)
|
||||
@@ -536,7 +595,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
for (const rule of config.events || []) {
|
||||
const target = byId.get(rule.targetSystemId)
|
||||
if (!target || target.id === system.id) continue
|
||||
const childTail = effectTail(target)
|
||||
const childTail = emissionDuration(target) + effectTail(target)
|
||||
// 出生时立即触发;死亡/碰撞最晚发生在父粒子生命结束时。
|
||||
const offset = rule.event === 'birth' ? 0 : life
|
||||
// 子发射器自身的 delay 表示父事件之后再延迟多久发射。
|
||||
@@ -559,10 +618,10 @@ export const useParticleStore = defineStore('particle', {
|
||||
const isLoop = c.mode === 'stream' && c.streamBehavior === 'loop'
|
||||
let end: number
|
||||
if (c.mode === 'stream') {
|
||||
const emissionDuration = isLoop
|
||||
const rootEmissionDuration = isLoop
|
||||
? loopSegmentTiming(c, fps).total / fps
|
||||
: Math.max(1 / fps, Number(c.duration) || 0)
|
||||
end = delay + emissionDuration
|
||||
end = delay + rootEmissionDuration
|
||||
if (isLoop) {
|
||||
// 循环结束段已包含父系统自身消亡,只追加子发射器链的尾部时长。
|
||||
let childTail = 0
|
||||
@@ -572,7 +631,13 @@ export const useParticleStore = defineStore('particle', {
|
||||
const target = byId.get(rule.targetSystemId)
|
||||
if (!target || target.id === system.id) continue
|
||||
const targetDelay = Math.max(0, Number(target.config.delay) || 0)
|
||||
childTail = Math.max(childTail, (rule.event === 'birth' ? 0 : life) + targetDelay + effectTail(target))
|
||||
childTail = Math.max(
|
||||
childTail,
|
||||
(rule.event === 'birth' ? 0 : life)
|
||||
+ targetDelay
|
||||
+ emissionDuration(target)
|
||||
+ effectTail(target),
|
||||
)
|
||||
}
|
||||
}
|
||||
end += childTail
|
||||
|
||||
@@ -238,6 +238,9 @@ const playbackEndDraft = ref(String(store.timeline.playbackEnd))
|
||||
|
||||
watch(() => store.timeline.playbackStart, (value) => { playbackStartDraft.value = String(value) })
|
||||
watch(() => store.timeline.playbackEnd, (value) => { playbackEndDraft.value = String(value) })
|
||||
watch(() => store.playbackRange.end, (end) => {
|
||||
if (store.timeline.recorded && store.timeline.frame > end) store.timeline.frame = end
|
||||
}, { immediate: true })
|
||||
|
||||
const totalFrames = computed(() => store.timeline.totalFrames)
|
||||
// 子发射器延长时间轴后自动扩大预览范围;用户手动放大后保持其缩放。
|
||||
@@ -335,7 +338,8 @@ function barWidthPx(sys: SysLike): number {
|
||||
const timing = subEmitterTiming.value.get(sys.id)
|
||||
const start = timing?.firstTrigger ?? 0
|
||||
const end = timing?.end ?? start
|
||||
return (Math.max(0, end - start) * fps.value) / pxPerF()
|
||||
const delay = draftDelays.value[sys.id] ?? Math.max(0, Number((sys.config as any).delay) || 0)
|
||||
return (Math.max(0, end - start - delay) * fps.value) / pxPerF()
|
||||
}
|
||||
return lifeWidthPx(sys)
|
||||
}
|
||||
@@ -398,7 +402,7 @@ function onWheel(e: WheelEvent) {
|
||||
viewFrames.value = Math.round(nv)
|
||||
}
|
||||
|
||||
// 拖动时间轴滑动(scrub):更新 frame(0..totalFrames-1)
|
||||
// 拖动时间轴滑动(scrub):只允许进入实际播放范围。
|
||||
function onScrubDown(e: PointerEvent) {
|
||||
const vp = vpRef.value!
|
||||
// 用户接管时间轴时立即暂停播放,否则每帧推进会和拖动抢播放头。
|
||||
@@ -406,7 +410,7 @@ function onScrubDown(e: PointerEvent) {
|
||||
const move = (ev: PointerEvent) => {
|
||||
const rect = vp.getBoundingClientRect()
|
||||
const f = Math.floor(((ev.clientX - rect.left) / rect.width) * viewFrames.value)
|
||||
store.timeline.frame = Math.max(0, Math.min(f, totalFrames.value - 1))
|
||||
store.timeline.frame = Math.max(0, Math.min(f, store.playbackRange.end))
|
||||
}
|
||||
move(e)
|
||||
const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up) }
|
||||
|
||||
@@ -137,7 +137,12 @@ async function exportSpineJson() {
|
||||
exportMessage.value = t('export.recalculating')
|
||||
try {
|
||||
await prepareExportFrames()
|
||||
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
|
||||
const result = buildSpineJson({
|
||||
systems: store.systems as unknown as ParticleSystem[],
|
||||
timeline: store.timeline,
|
||||
settings: exportSettings.value,
|
||||
playbackEnd: store.playbackRange.end,
|
||||
})
|
||||
validateSpineJson(result.text, exportSettings.value.spineVersion)
|
||||
downloadSpineJson(result, exportSettings.value.fileName)
|
||||
const warning = result.warnings.length ? t('export.resourceWarning', { count: result.warnings.length }) : ''
|
||||
@@ -170,7 +175,12 @@ async function exportSpineProject() {
|
||||
await transfer.paint()
|
||||
await prepareExportFrames((progress) => transfer.update(progress * 18, t('export.recalculating')))
|
||||
transfer.update(20, t('export.generatingJson'))
|
||||
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
|
||||
const result = buildSpineJson({
|
||||
systems: store.systems as unknown as ParticleSystem[],
|
||||
timeline: store.timeline,
|
||||
settings: exportSettings.value,
|
||||
playbackEnd: store.playbackRange.end,
|
||||
})
|
||||
validateSpineJson(result.text, exportSettings.value.spineVersion)
|
||||
exportMessage.value = t('export.preparingProject')
|
||||
const projectName = exportSettings.value.fileName.replace(/\.json$/i, '') || 'SpineParticle'
|
||||
|
||||
Reference in new issue
Block a user