发射器形状新增

This commit is contained in:
tianmo committed 2026-09-05 17:31:32 +08:00
1 parent 0bde824c88
commit 026ee74d48
11 files changed
+1657 -1336

No files matched your search

+1239
View File
File diff suppressed because it is too large. Load diff
-1239
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-C87f76bX.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-C87f76bX.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-DQa0oOKH.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-DQa0oOKH.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-B-Do_aFq.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-B-Do_aFq.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-Di3Av-MX.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-Di3Av-MX.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 aVertexPosition;
attribute vec2 aTextureCoord; attribute vec2 aTextureCoord;
@@ -1 +1 @@
.stage-wrap[data-v-6a919d1f]{position:relative;width:100%;height:100%;overflow:hidden}.stage-holder[data-v-6a919d1f]{width:100%;height:100%;cursor:grab;touch-action:none;background-color:#0e1219;outline:none}.stage-holder.grid[data-v-6a919d1f]{background:conic-gradient(#141a26 90deg,transparent 90deg 180deg,#141a26 180deg 270deg,transparent 270deg);background-size:28px 28px}.stage-holder[data-v-6a919d1f]:active{cursor:grabbing}.topbar[data-v-6a919d1f]{position:absolute;top:10px;left:10px;right:10px;display:flex;align-items:center;gap:10px;padding:8px 12px;background:#141420d9;border-radius:10px;border:1px solid rgba(255,255,255,.08);font-size:12px;color:#aab;z-index:5;flex-wrap:wrap}.topbar input[type=range][data-v-6a919d1f]{background:#1a1a28;border:1px solid #2e2e44;color:#dde;border-radius:5px}.tool-group[data-v-6a919d1f]{display:flex;gap:6px;margin-left:6px;padding-left:12px;border-left:1px solid #2a2a3c}.tool-btn[data-v-6a919d1f]{width:30px;height:26px;background:#1e1e2e;border:1px solid #2e2e44;border-radius:6px;color:#aab;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}.tool-btn[data-v-6a919d1f]:hover{background:#2b3a5c;color:#fff}.tool-btn.on[data-v-6a919d1f]{background:#3a5a8c;color:#fff;border-color:#5a7ab8}.background-menu[data-v-6a919d1f]{position:relative;flex:0 0 auto}.background-btn[data-v-6a919d1f]{height:28px;padding:0 11px;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;cursor:pointer}.background-btn[data-v-6a919d1f]:hover,.background-btn.on[data-v-6a919d1f]{border-color:#7774ff;background:#343b63;color:#fff}.background-panel[data-v-6a919d1f]{position:absolute;top:calc(100% + 9px);right:0;width:248px;box-sizing:border-box;padding:10px;border:1px solid #343b50;border-radius:9px;background:#141420fa;color:#aeb8cc;box-shadow:0 8px 24px #0000007a;z-index:12}.background-upload[data-v-6a919d1f]{width:100%;height:30px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;cursor:pointer}.background-upload[data-v-6a919d1f]:hover{border-color:#8982ff;background:#554dc2}.background-clear[data-v-6a919d1f]{width:100%;height:28px;margin-top:6px;border:1px solid #70404a;border-radius:6px;background:#512a33;color:#efb5bd;cursor:pointer}.background-clear[data-v-6a919d1f]:hover:not(:disabled){border-color:#b65b6a;background:#6a303b;color:#fff}.background-clear[data-v-6a919d1f]:disabled{opacity:.42;cursor:not-allowed}.background-file-input[data-v-6a919d1f]{display:none}.background-file-name[data-v-6a919d1f]{margin:7px 2px 9px;overflow:hidden;color:#737f98;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.background-position-row[data-v-6a919d1f]{display:grid;grid-template-columns:1fr 1fr;gap:8px}.background-position-row label[data-v-6a919d1f]{display:flex;align-items:center;gap:5px;color:#8995ad;font-size:11px}.background-position-row input[data-v-6a919d1f]{width:0;min-width:0;flex:1;box-sizing:border-box;height:28px;padding:0 6px;border:1px solid #343e55;border-radius:5px;background:#101522;color:#e1e6f2}.background-opacity-row[data-v-6a919d1f]{display:grid;grid-template-columns:46px 1fr 34px;align-items:center;gap:7px;margin-top:10px;font-size:11px}.background-opacity-row input[data-v-6a919d1f]{min-width:0;accent-color:#675de0}.background-opacity-row b[data-v-6a919d1f]{color:#aeb8cc;font-size:10px;text-align:right;font-variant-numeric:tabular-nums}.help-btn[data-v-6a919d1f]{height:28px;margin-left:auto;padding:0 11px;flex:0 0 auto;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;font-weight:700;cursor:pointer}.help-btn[data-v-6a919d1f]:hover,.help-btn.on[data-v-6a919d1f]{border-color:#7774ff;background:#343b63;color:#fff}.zoom-pct[data-v-6a919d1f]{margin-left:0;font-variant-numeric:tabular-nums;color:#9aa;min-width:44px;text-align:center}.help-overlay[data-v-6a919d1f]{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-6a919d1f]{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-6a919d1f]{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-6a919d1f]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-6a919d1f]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-6a919d1f]{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-6a919d1f]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-6a919d1f]{min-height:0;overflowLine truncated .stage-wrap[data-v-b460affb]{position:relative;width:100%;height:100%;overflow:hidden}.stage-holder[data-v-b460affb]{width:100%;height:100%;cursor:grab;touch-action:none;background-color:#0e1219;outline:none}.stage-holder.grid[data-v-b460affb]{background:conic-gradient(#141a26 90deg,transparent 90deg 180deg,#141a26 180deg 270deg,transparent 270deg);background-size:28px 28px}.stage-holder[data-v-b460affb]:active{cursor:grabbing}.topbar[data-v-b460affb]{position:absolute;top:10px;left:10px;right:10px;display:flex;align-items:center;gap:10px;padding:8px 12px;background:#141420d9;border-radius:10px;border:1px solid rgba(255,255,255,.08);font-size:12px;color:#aab;z-index:5;flex-wrap:wrap}.topbar input[type=range][data-v-b460affb]{background:#1a1a28;border:1px solid #2e2e44;color:#dde;border-radius:5px}.tool-group[data-v-b460affb]{display:flex;gap:6px;margin-left:6px;padding-left:12px;border-left:1px solid #2a2a3c}.tool-btn[data-v-b460affb]{width:30px;height:26px;background:#1e1e2e;border:1px solid #2e2e44;border-radius:6px;color:#aab;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}.tool-btn[data-v-b460affb]:hover{background:#2b3a5c;color:#fff}.tool-btn.on[data-v-b460affb]{background:#3a5a8c;color:#fff;border-color:#5a7ab8}.background-menu[data-v-b460affb]{position:relative;flex:0 0 auto}.background-btn[data-v-b460affb]{height:28px;padding:0 11px;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;cursor:pointer}.background-btn[data-v-b460affb]:hover,.background-btn.on[data-v-b460affb]{border-color:#7774ff;background:#343b63;color:#fff}.background-panel[data-v-b460affb]{position:absolute;top:calc(100% + 9px);right:0;width:248px;box-sizing:border-box;padding:10px;border:1px solid #343b50;border-radius:9px;background:#141420fa;color:#aeb8cc;box-shadow:0 8px 24px #0000007a;z-index:12}.background-upload[data-v-b460affb]{width:100%;height:30px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;cursor:pointer}.background-upload[data-v-b460affb]:hover{border-color:#8982ff;background:#554dc2}.background-clear[data-v-b460affb]{width:100%;height:28px;margin-top:6px;border:1px solid #70404a;border-radius:6px;background:#512a33;color:#efb5bd;cursor:pointer}.background-clear[data-v-b460affb]:hover:not(:disabled){border-color:#b65b6a;background:#6a303b;color:#fff}.background-clear[data-v-b460affb]:disabled{opacity:.42;cursor:not-allowed}.background-file-input[data-v-b460affb]{display:none}.background-file-name[data-v-b460affb]{margin:7px 2px 9px;overflow:hidden;color:#737f98;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.background-position-row[data-v-b460affb]{display:grid;grid-template-columns:1fr 1fr;gap:8px}.background-position-row label[data-v-b460affb]{display:flex;align-items:center;gap:5px;color:#8995ad;font-size:11px}.background-position-row input[data-v-b460affb]{width:0;min-width:0;flex:1;box-sizing:border-box;height:28px;padding:0 6px;border:1px solid #343e55;border-radius:5px;background:#101522;color:#e1e6f2}.background-opacity-row[data-v-b460affb]{display:grid;grid-template-columns:46px 1fr 34px;align-items:center;gap:7px;margin-top:10px;font-size:11px}.background-opacity-row input[data-v-b460affb]{min-width:0;accent-color:#675de0}.background-opacity-row b[data-v-b460affb]{color:#aeb8cc;font-size:10px;text-align:right;font-variant-numeric:tabular-nums}.help-btn[data-v-b460affb]{height:28px;margin-left:auto;padding:0 11px;flex:0 0 auto;border:1px solid #3a4963;border-radius:6px;background:#202a3b;color:#cbd7ed;font-size:12px;font-weight:700;cursor:pointer}.help-btn[data-v-b460affb]:hover,.help-btn.on[data-v-b460affb]{border-color:#7774ff;background:#343b63;color:#fff}.zoom-pct[data-v-b460affb]{margin-left:0;font-variant-numeric:tabular-nums;color:#9aa;min-width:44px;text-align:center}.help-overlay[data-v-b460affb]{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-b460affb]{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-b460affb]{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-b460affb]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-b460affb]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-b460affb]{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-b460affb]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-b460affb]{min-height:0;overflowLine 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-C87f76bX.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-C87f76bX.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-B-Do_aFq.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-B-Do_aFq.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 ${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
+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; } 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; } #app { display: flex; flex-direction: column; height: 100vh; }
</style> </style>
<script type="module" crossorigin src="/assets/index-C87f76bX.js"></script> <script type="module" crossorigin src="/assets/index-B-Do_aFq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CJwt9TyR.css"> <link rel="stylesheet" crossorigin href="/assets/index-CjobLXfp.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+150
View File
@@ -0,0 +1,150 @@
export type EmissionAngleMode = 'fixed' | 'random' | 'radial' | 'path-normal' | 'path-tangent'
export interface EmitterShapeSettings {
shape: 'point' | 'circle' | 'rect' | 'cone' | 'path' | 'orbit'
innerRadius: number
outerRadius: number
innerRectW: number
innerRectH: number
outerRectW: number
outerRectH: number
coneAngle: number
coneDirection: number
direction: number
spread: number
emissionAngleMode: EmissionAngleMode
}
export interface EmitterShapeSample {
/** 屏幕局部坐标,Y 向下。 */
x: number
y: number
/** 从形状中心指向出生点的屏幕角度。 */
radialAngle: number
/** 路径在出生点处的屏幕局部切线角度。 */
pathAngle?: number
}
export function normalizedRadialRange(innerValue: number, outerValue: number) {
const first = Math.max(0, Number(innerValue) || 0)
const second = Math.max(0, Number(outerValue) || 0)
return { inner: Math.min(first, second), outer: Math.max(first, second) }
}
export function normalizedRectRange(config: Pick<EmitterShapeSettings, 'innerRectW' | 'innerRectH' | 'outerRectW' | 'outerRectH'>) {
const outerW = Math.max(0, Number(config.outerRectW) || 0)
const outerH = Math.max(0, Number(config.outerRectH) || 0)
return {
innerW: Math.min(outerW, Math.max(0, Number(config.innerRectW) || 0)),
innerH: Math.min(outerH, Math.max(0, Number(config.innerRectH) || 0)),
outerW,
outerH,
}
}
export function pathDistributionProgress(
mode: 'fixed' | 'random',
spawnSerial: number,
particleCount: number,
closed: boolean,
random: () => number,
) {
if (mode === 'random') return random()
const count = Math.max(1, Math.round(particleCount))
const index = Math.max(0, Math.round(spawnSerial)) % count
if (closed) return index / count
return count <= 1 ? 0.5 : index / (count - 1)
}
function sampleAnnulus(inner: number, outer: number, angle: number, random: () => number): EmitterShapeSample {
const radius = Math.sqrt(inner * inner + random() * (outer * outer - inner * inner))
return {
x: Math.cos(angle) * radius,
y: -Math.sin(angle) * radius,
radialAngle: -angle * 180 / Math.PI,
}
}
function sampleRectRing(config: EmitterShapeSettings, random: () => number): EmitterShapeSample {
const { innerW, innerH, outerW, outerH } = normalizedRectRange(config)
const sideWidth = Math.max(0, (outerW - innerW) / 2)
const capHeight = Math.max(0, (outerH - innerH) / 2)
const sideArea = sideWidth * outerH
const capArea = innerW * capHeight
const totalArea = sideArea * 2 + capArea * 2
if (totalArea <= 0) {
const perimeter = outerW * 2 + outerH * 2
if (perimeter <= 0) return { x: 0, y: 0, radialAngle: 0 }
let edge = random() * perimeter
let x: number
let y: number
if (edge < outerW) {
x = -outerW / 2 + edge
y = -outerH / 2
} else if ((edge -= outerW) < outerH) {
x = outerW / 2
y = -outerH / 2 + edge
} else if ((edge -= outerH) < outerW) {
x = outerW / 2 - edge
y = outerH / 2
} else {
edge -= outerW
x = -outerW / 2
y = outerH / 2 - edge
}
return { x, y, radialAngle: Math.atan2(y, x) * 180 / Math.PI }
}
let cursor = random() * totalArea
let x = 0
let y = 0
if (cursor < sideArea) {
x = -outerW / 2 + random() * sideWidth
y = (random() - 0.5) * outerH
} else if ((cursor -= sideArea) < sideArea) {
x = innerW / 2 + random() * sideWidth
y = (random() - 0.5) * outerH
} else if ((cursor -= sideArea) < capArea) {
x = (random() - 0.5) * innerW
y = -outerH / 2 + random() * capHeight
} else {
x = (random() - 0.5) * innerW
y = innerH / 2 + random() * capHeight
}
return { x, y, radialAngle: Math.atan2(y, x) * 180 / Math.PI }
}
export function sampleEmitterShape(config: EmitterShapeSettings, random: () => number): EmitterShapeSample {
if (config.shape === 'circle') {
const { inner, outer } = normalizedRadialRange(config.innerRadius, config.outerRadius)
return sampleAnnulus(inner, outer, random() * Math.PI * 2, random)
}
if (config.shape === 'rect') return sampleRectRing(config, random)
if (config.shape === 'cone') {
const { inner, outer } = normalizedRadialRange(config.innerRadius, config.outerRadius)
const halfAngle = Math.max(0, Math.min(360, Number(config.coneAngle) || 0)) / 2
const angleDegrees = (Number(config.coneDirection) || 0) + (random() - 0.5) * halfAngle * 2
return sampleAnnulus(inner, outer, angleDegrees * Math.PI / 180, random)
}
return { x: 0, y: 0, radialAngle: 0 }
}
export function sampleEmissionVelocityAngle(config: EmitterShapeSettings, sample: EmitterShapeSample, followAngle: number, random: () => number) {
if (config.shape === 'path' || config.shape === 'orbit') {
if (config.emissionAngleMode === 'path-tangent' && Number.isFinite(sample.pathAngle)) {
return (sample.pathAngle! + followAngle) * Math.PI / 180
}
if (config.emissionAngleMode === 'path-normal' && Number.isFinite(sample.pathAngle)) {
return (sample.pathAngle! - 90 + followAngle) * Math.PI / 180
}
}
if (config.emissionAngleMode === 'radial' && config.shape !== 'point' && config.shape !== 'orbit') {
return (sample.radialAngle + followAngle) * Math.PI / 180
}
const randomOffset = config.emissionAngleMode === 'random'
? (random() - 0.5) * Math.max(0, Number(config.spread) || 0)
: 0
const mathematicalAngle = ((Number(config.direction) || 0) + followAngle + randomOffset) * Math.PI / 180
// 旧配置中锥形使用数学角度,其余形状使用屏幕角度;保留这一方向约定以兼容既有工程。
return config.shape === 'cone' ? -mathematicalAngle : mathematicalAngle
}
+91 -21
View File
@@ -1,9 +1,10 @@
import { Container, Sprite, Texture, IPointData, SimpleMesh, Graphics } from 'pixi.js' import { Container, Sprite, Texture, IPointData, SimpleMesh, Graphics } from 'pixi.js'
import { pathDistributionProgress, sampleEmitterShape, sampleEmissionVelocityAngle, type EmissionAngleMode, type EmitterShapeSample } from './emitterShape'
/** 发射模式 */ /** 发射模式 */
export type EmitMode = 'stream' | 'burst' export type EmitMode = 'stream' | 'burst'
/** 发射形状 */ /** 发射形状 */
export type EmitShape = 'point' | 'circle' | 'rect' | 'cone' | 'orbit' export type EmitShape = 'point' | 'circle' | 'rect' | 'cone' | 'path' | 'orbit'
/** 混合模式 */ /** 混合模式 */
export type Blend = 'normal' | 'add' | 'multiply' | 'screen' export type Blend = 'normal' | 'add' | 'multiply' | 'screen'
/** 粒子属性取值方式 */ /** 粒子属性取值方式 */
@@ -140,6 +141,20 @@ export interface EmitterConfig {
/** 持续发射时长(秒)= 时间轴绿色段;爆发模式不使用该时长 */ /** 持续发射时长(秒)= 时间轴绿色段;爆发模式不使用该时长 */
duration: number duration: number
shape: EmitShape // 发射形状 shape: EmitShape // 发射形状
/** 发射范围结构版本,用于旧配置迁移。 */
shapeRangeVersion: number
/** 圆形和锥形发射范围的内外半径。 */
innerRadius: number
outerRadius: number
/** 矩形发射范围:外矩形减去内矩形。 */
innerRectW: number
innerRectH: number
outerRectW: number
outerRectH: number
/** 粒子出生路径;与修改器中的发射器整体跟随路径完全独立。 */
spawnPathId: number
spawnPathDistribution: 'fixed' | 'random'
/** 旧版形状尺寸字段,保留用于配置迁移。 */
radius: number radius: number
rectW: number rectW: number
rectH: number rectH: number
@@ -151,8 +166,11 @@ export interface EmitterConfig {
rootScaleX: number rootScaleX: number
rootScaleY: number rootScaleY: number
// 方向 // 方向
emissionAngleMode: EmissionAngleMode
direction: number direction: number
spread: number spread: number
/** 锥形区域朝向;与粒子发射角度独立。 */
coneDirection: number
/** 锥形开口角度(发电器形状=锥体时,粒子在锥形扇区内均匀散开) */ /** 锥形开口角度(发电器形状=锥体时,粒子在锥形扇区内均匀散开) */
coneAngle: number coneAngle: number
// 属性 // 属性
@@ -423,6 +441,15 @@ export function defaultConfig(): EmitterConfig {
delay: 0, delay: 0,
duration: 1, // 持续发射时长;默认 1 秒(30f)。爆发模式运行时按 0 处理。 duration: 1, // 持续发射时长;默认 1 秒(30f)。爆发模式运行时按 0 处理。
shape: 'point', shape: 'point',
shapeRangeVersion: 1,
innerRadius: 0,
outerRadius: 30,
innerRectW: 0,
innerRectH: 0,
outerRectW: 60,
outerRectH: 40,
spawnPathId: 0,
spawnPathDistribution: 'fixed',
radius: 30, radius: 30,
rectW: 60, rectW: 60,
rectH: 40, rectH: 40,
@@ -431,8 +458,10 @@ export function defaultConfig(): EmitterConfig {
rootRotation: 0, rootRotation: 0,
rootScaleX: 1, rootScaleX: 1,
rootScaleY: 1, rootScaleY: 1,
emissionAngleMode: 'random',
direction: 0, direction: 0,
spread: 360, spread: 360,
coneDirection: 90,
coneAngle: 60, coneAngle: 60,
lifeMin: 0.6, lifeMin: 0.6,
lifeMax: 1.8, lifeMax: 1.8,
@@ -604,6 +633,13 @@ export function defaultConfig(): EmitterConfig {
export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig { export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
const defaults = defaultConfig() as unknown as Record<string, unknown> const defaults = defaultConfig() as unknown as Record<string, unknown>
const target = config as unknown as Record<string, unknown> const target = config as unknown as Record<string, unknown>
const needsShapeRangeMigration = target.shapeRangeVersion !== 1
const legacyRadius = Number(target.radius)
const legacyRectW = Number(target.rectW)
const legacyRectH = Number(target.rectH)
const legacyDirection = Number(target.direction)
const legacySpread = Number(target.spread)
const legacyShape = target.shape
const needsImageWeightMigration = target.imageWeightVersion !== 1 const needsImageWeightMigration = target.imageWeightVersion !== 1
const needsLoopDurationMigration = target.loopDurationFrames == null const needsLoopDurationMigration = target.loopDurationFrames == null
const needsGravityMigration = target.gravityMode == null const needsGravityMigration = target.gravityMode == null
@@ -625,6 +661,30 @@ export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
? value.map((item) => typeof item === 'object' && item !== null ? { ...item } : item) ? value.map((item) => typeof item === 'object' && item !== null ? { ...item } : item)
: value : value
} }
if (needsShapeRangeMigration) {
config.innerRadius = 0
config.outerRadius = Math.max(0, Number.isFinite(legacyRadius) ? legacyRadius : defaults.outerRadius as number)
config.innerRectW = 0
config.innerRectH = 0
config.outerRectW = Math.max(0, Number.isFinite(legacyRectW) ? legacyRectW : defaults.outerRectW as number)
config.outerRectH = Math.max(0, Number.isFinite(legacyRectH) ? legacyRectH : defaults.outerRectH as number)
config.coneDirection = Number.isFinite(legacyDirection) ? legacyDirection : 90
config.emissionAngleMode = Number.isFinite(legacySpread) && legacySpread > 0 ? 'random' : 'fixed'
if (legacyShape === 'cone') config.spread = Math.max(0, Number(config.coneAngle) || 0)
config.shapeRangeVersion = 1
}
if (config.shape === 'orbit') config.shape = 'path'
if (!['fixed', 'random', 'radial', 'path-normal', 'path-tangent'].includes(config.emissionAngleMode)) config.emissionAngleMode = 'fixed'
if (!['circle', 'rect', 'cone'].includes(config.shape) && config.emissionAngleMode === 'radial') config.emissionAngleMode = 'fixed'
if (config.shape !== 'path' && (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent')) config.emissionAngleMode = 'fixed'
config.spawnPathId = Math.max(0, Math.round(Number(config.spawnPathId) || 0))
if (config.spawnPathDistribution !== 'fixed' && config.spawnPathDistribution !== 'random') config.spawnPathDistribution = 'fixed'
config.innerRadius = Math.max(0, Number(config.innerRadius) || 0)
config.outerRadius = Math.max(config.innerRadius, Number(config.outerRadius) || 0)
config.innerRectW = Math.max(0, Number(config.innerRectW) || 0)
config.innerRectH = Math.max(0, Number(config.innerRectH) || 0)
config.outerRectW = Math.max(config.innerRectW, Number(config.outerRectW) || 0)
config.outerRectH = Math.max(config.innerRectH, Number(config.outerRectH) || 0)
if (config.streamBehavior !== 'normal' && config.streamBehavior !== 'loop') config.streamBehavior = 'normal' if (config.streamBehavior !== 'normal' && config.streamBehavior !== 'loop') config.streamBehavior = 'normal'
if (needsLoopDurationMigration || !Number.isFinite(config.loopDurationFrames)) config.loopDurationFrames = Math.ceil(Math.max(1 / 30, Number(config.duration) || 1) * 30) if (needsLoopDurationMigration || !Number.isFinite(config.loopDurationFrames)) config.loopDurationFrames = Math.ceil(Math.max(1 / 30, Number(config.duration) || 1) * 30)
config.loopDurationFrames = Math.min(1800, Math.max(1, Math.ceil(config.loopDurationFrames))) config.loopDurationFrames = Math.min(1800, Math.max(1, Math.ceil(config.loopDurationFrames)))
@@ -869,6 +929,8 @@ export class ParticleEmitter extends Container {
private followSpawnX = 0 private followSpawnX = 0
private followSpawnY = 0 private followSpawnY = 0
private followSpawnAngle = 0 private followSpawnAngle = 0
private spawnPathSampler: ((progress: number) => { x: number; y: number; angle: number } | null) | null = null
private spawnPathClosed = false
private followExportCurveMode: '线性' | '贝塞尔' = '线性' private followExportCurveMode: '线性' | '贝塞尔' = '线性'
private simulationTransform: { centerX: number; centerY: number; rotation: number; scaleX?: number; scaleY?: number } | null = null private simulationTransform: { centerX: number; centerY: number; rotation: number; scaleX?: number; scaleY?: number } | null = null
@@ -936,6 +998,10 @@ export class ParticleEmitter extends Container {
} }
setEmitterPos(fn: () => IPointData) { this._emitterPos = fn } setEmitterPos(fn: () => IPointData) { this._emitterPos = fn }
setSpawnPathSampler(sampler: ((progress: number) => { x: number; y: number; angle: number } | null) | null, closed = false) {
this.spawnPathSampler = sampler
this.spawnPathClosed = closed
}
setColliders(colliders: SceneCollider[]) { this.colliders = colliders } setColliders(colliders: SceneCollider[]) { this.colliders = colliders }
setEmitterFollowSpawn(x: number, y: number, angleDegrees: number) { setEmitterFollowSpawn(x: number, y: number, angleDegrees: number) {
this.followSpawnX = x this.followSpawnX = x
@@ -989,6 +1055,7 @@ export class ParticleEmitter extends Container {
const cfg = this.cfg const cfg = this.cfg
if ( if (
cfg.lifeMode == null || cfg.speedMode == null || cfg.scaleMode == null || cfg.lifeMode == null || cfg.speedMode == null || cfg.scaleMode == null ||
cfg.shapeRangeVersion !== 1 ||
cfg.initialRotationMode == null || cfg.speedOverLifeEnabled == null || cfg.initialRotationMode == null || cfg.speedOverLifeEnabled == null ||
cfg.scaleOverLifeEnabled == null || cfg.rotationOverLifeEnabled == null || cfg.scaleOverLifeEnabled == null || cfg.rotationOverLifeEnabled == null ||
!Array.isArray(cfg.scaleOverLifeCurve) || cfg.attraction == null || cfg.trailBoneCount == null !Array.isArray(cfg.scaleOverLifeCurve) || cfg.attraction == null || cfg.trailBoneCount == null
@@ -1484,23 +1551,31 @@ export class ParticleEmitter extends Container {
for (let i = 0; i < this.cfg.burstCount; i++) this.spawnOne() for (let i = 0; i < this.cfg.burstCount; i++) this.spawnOne()
} }
private spawnOrigin(): { x: number; y: number } { private spawnOrigin(): EmitterShapeSample {
const cfg = this.cfg const cfg = this.cfg
// 局部原点 = 0(root 中心,centerX 由发射器容器 position 补回);粒子相对 root 中心 = 偏移X/Y // 局部原点 = 0(root 中心,centerX 由发射器容器 position 补回);粒子相对 root 中心 = 偏移X/Y
let ox = cfg.offsetX, oy = cfg.offsetY let ox = cfg.offsetX, oy = cfg.offsetY
// 采用数学坐标系(Y 向上为正):world 为屏幕坐标(向下为正),故 y 取负 // 采用数学坐标系(Y 向上为正):world 为屏幕坐标(向下为正),故 y 取负
oy = -oy oy = -oy
const s = cfg.shape if (cfg.shape === 'path' && cfg.spawnPathId > 0 && this.spawnPathSampler) {
if (s === 'circle') { const count = this.estimatedEmissionCount()
const a = this.rng() * Math.PI * 2, r = this.rng() * cfg.radius const progress = pathDistributionProgress(cfg.spawnPathDistribution, this._spawnSerial, count, this.spawnPathClosed, this.rng)
return this.applyFollowToOrigin(ox + Math.cos(a) * r, oy + Math.sin(a) * r) const pathSample = this.spawnPathSampler(progress)
} else if (s === 'rect') { if (pathSample) {
return this.applyFollowToOrigin(ox + (this.rng() - 0.5) * cfg.rectW, oy + (this.rng() - 0.5) * cfg.rectH) const origin = this.applyFollowToOrigin(pathSample.x + ox, pathSample.y + oy)
} else if (s === 'cone') { return { ...origin, radialAngle: 0, pathAngle: pathSample.angle }
// 锥体:粒子从中心(锥形顶点)发射,方向由 spawnOne 在 [direction ± coneAngle/2] 扇形内随机 }
return this.applyFollowToOrigin(ox, oy)
} }
return this.applyFollowToOrigin(ox, oy) const sample = sampleEmitterShape(cfg, this.rng)
const origin = this.applyFollowToOrigin(ox + sample.x, oy + sample.y)
return { ...origin, radialAngle: sample.radialAngle }
}
private estimatedEmissionCount() {
const cfg = this.cfg
return cfg.mode === 'burst'
? Math.max(1, Math.round(cfg.burstCount))
: Math.max(1, Math.floor(cfg.rate * (cfg.streamBehavior === 'loop' ? Math.max(1, cfg.loopDurationFrames) / 30 : Math.max(0, cfg.duration))))
} }
private applyFollowToOrigin(x: number, y: number) { private applyFollowToOrigin(x: number, y: number) {
@@ -1516,16 +1591,11 @@ export class ParticleEmitter extends Container {
const cfg = this.cfg const cfg = this.cfg
let idx = this.pool.findIndex((p) => !p.active) let idx = this.pool.findIndex((p) => !p.active)
if (idx === -1) return // 池满 if (idx === -1) return // 池满
const { x: ox, y: oy } = this.spawnOrigin() const origin = this.spawnOrigin()
const { x: ox, y: oy } = origin
const life = initialValue(cfg.lifeMode, cfg.lifeMin, cfg.lifeMax, cfg.lifeCurve, this.rng()) const life = initialValue(cfg.lifeMode, cfg.lifeMin, cfg.lifeMax, cfg.lifeCurve, this.rng())
const speed = initialValue(cfg.speedMode, cfg.speedMin, cfg.speedMax, cfg.speedCurve, this.rng()) const speed = initialValue(cfg.speedMode, cfg.speedMin, cfg.speedMax, cfg.speedCurve, this.rng())
// 锥体:发射方向用锥形开口角度(coneAngle)散布,且 y 朝数学上正(与扇区绘制一致);其余形状沿用 spread + 屏幕坐标 const dirAng = sampleEmissionVelocityAngle(cfg, origin, this.followSpawnAngle, this.rng)
let dirAng: number
if (cfg.shape === 'cone') {
dirAng = (cfg.direction + this.followSpawnAngle + lerp(-cfg.coneAngle / 2, cfg.coneAngle / 2, this.rng())) * Math.PI / 180
} else {
dirAng = (cfg.direction + this.followSpawnAngle + lerp(-cfg.spread / 2, cfg.spread / 2, this.rng())) * Math.PI / 180
}
const p = this.pool[idx] const p = this.pool[idx]
const resource = this.pickImageResource() const resource = this.pickImageResource()
const spawnSerial = this._spawnSerial++ const spawnSerial = this._spawnSerial++
@@ -1572,7 +1642,7 @@ export class ParticleEmitter extends Container {
// 从出生点开始积累轨迹,使第一次更新时整组拖尾骨骼就能在首段轨迹上等距插值。 // 从出生点开始积累轨迹,使第一次更新时整组拖尾骨骼就能在首段轨迹上等距插值。
if (p.trailEnabled) p.trailHistory.push({ x: ox, y: oy }) if (p.trailEnabled) p.trailHistory.push({ x: ox, y: oy })
p.vx = Math.cos(dirAng) * speed p.vx = Math.cos(dirAng) * speed
p.vy = (cfg.shape === 'cone' ? -Math.sin(dirAng) : Math.sin(dirAng)) * speed p.vy = Math.sin(dirAng) * speed
p.scaleStartX = initialValue(cfg.scaleMode, cfg.scaleMin, cfg.scaleMax, cfg.scaleCurve, this.rng()) * resource.scale p.scaleStartX = initialValue(cfg.scaleMode, cfg.scaleMin, cfg.scaleMax, cfg.scaleCurve, this.rng()) * resource.scale
p.scaleStartY = cfg.scaleAxisMode === 'separate' p.scaleStartY = cfg.scaleAxisMode === 'separate'
? initialValue(cfg.scaleMode, cfg.scaleYMin, cfg.scaleYMax, cfg.scaleYCurve, this.rng()) * resource.scale ? initialValue(cfg.scaleMode, cfg.scaleYMin, cfg.scaleYMax, cfg.scaleYCurve, this.rng()) * resource.scale
+59
View File
@@ -0,0 +1,59 @@
export interface ScenePathSampleSource {
x: number
y: number
rotation: number
scaleX: number
scaleY: number
closed: boolean
points: Array<{ x: number; y: number; inX: number; inY: number; outX: number; outY: number }>
}
function cubicPoint(a: number, b: number, c: number, d: number, t: number) {
const u = 1 - t
return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d
}
function pathLocalToWorld(path: ScenePathSampleSource, x: number, y: number) {
const angle = path.rotation * Math.PI / 180
const cos = Math.cos(angle), sin = Math.sin(angle)
const scaleX = Math.max(0.0001, Math.abs(path.scaleX ?? 1))
const scaleY = Math.max(0.0001, Math.abs(path.scaleY ?? 1))
return { x: path.x + cos * x * scaleX - sin * y * scaleY, y: path.y + sin * x * scaleX + cos * y * scaleY }
}
/** 按近似弧长采样贝塞尔场景路径,并返回数学世界坐标和切线角度。 */
export function sampleScenePath(path: ScenePathSampleSource, progress: number) {
if (!path.points.length) return null
if (path.points.length === 1) {
const point = pathLocalToWorld(path, path.points[0].x, path.points[0].y)
return { ...point, angle: path.rotation }
}
const segmentCount = path.closed ? path.points.length : path.points.length - 1
const samples: Array<{ x: number; y: number; distance: number }> = []
let totalDistance = 0
for (let segment = 0; segment < segmentCount; segment++) {
const a = path.points[segment]
const b = path.points[(segment + 1) % path.points.length]
for (let step = 0; step <= 20; step++) {
if (segment > 0 && step === 0) continue
const t = step / 20
const localX = cubicPoint(a.x, a.x + a.outX, b.x + b.inX, b.x, t)
const localY = cubicPoint(a.y, a.y + a.outY, b.y + b.inY, b.y, t)
const worldPoint = pathLocalToWorld(path, localX, localY)
const previous = samples[samples.length - 1]
if (previous) totalDistance += Math.hypot(worldPoint.x - previous.x, worldPoint.y - previous.y)
samples.push({ ...worldPoint, distance: totalDistance })
}
}
const target = Math.min(1, Math.max(0, progress)) * totalDistance
let index = samples.findIndex((sample) => sample.distance >= target)
if (index < 0) index = samples.length - 1
const current = samples[index]
const previous = samples[Math.max(0, index - 1)]
const next = samples[Math.min(samples.length - 1, index + 1)]
const beforeDistance = previous.distance
const amount = index > 0 ? (target - beforeDistance) / Math.max(0.0001, current.distance - beforeDistance) : 0
const x = previous.x + (current.x - previous.x) * Math.min(1, Math.max(0, amount))
const y = previous.y + (current.y - previous.y) * Math.min(1, Math.max(0, amount))
return { x, y, angle: Math.atan2(next.y - previous.y, next.x - previous.x) * 180 / Math.PI }
}
+56 -13
View File
@@ -287,23 +287,53 @@
<option value="point">点</option> <option value="point">点</option>
<option value="circle">圆形</option> <option value="circle">圆形</option>
<option value="rect">矩形</option> <option value="rect">矩形</option>
<option value="cone">锥体</option> <option value="cone">锥形</option>
<option value="orbit">轨道</option> <option value="path">路径</option>
</select> </select>
</label> </label>
<template v-if="sys.config.shape === 'circle' || sys.config.shape === 'cone'"> <template v-if="sys.config.shape === 'circle' || sys.config.shape === 'cone'">
<NumSlider label="半径" :min="0" :max="300" :step="1" v-model="sys.config.radius" /> <NumSlider :label="sys.config.shape === 'cone' ? '内径' : '内半径'" :min="0" :max="sys.config.outerRadius" :step="1" v-model="sys.config.innerRadius" />
<NumSlider :label="sys.config.shape === 'cone' ? '外径' : '外半径'" :min="sys.config.innerRadius" :max="300" :step="1" v-model="sys.config.outerRadius" />
</template> </template>
<template v-if="sys.config.shape === 'cone'"> <template v-if="sys.config.shape === 'cone'">
<NumSlider label="锥形朝向" :min="-360" :max="360" :step="1" v-model="sys.config.coneDirection" />
<NumSlider label="锥形开口角度" :min="1" :max="360" :step="1" v-model="sys.config.coneAngle" /> <NumSlider label="锥形开口角度" :min="1" :max="360" :step="1" v-model="sys.config.coneAngle" />
</template> </template>
<template v-else-if="sys.config.shape === 'rect'"> <template v-else-if="sys.config.shape === 'rect'">
<NumSlider label="宽" :min="1" :max="400" :step="1" v-model="sys.config.rectW" /> <NumSlider label="内宽" :min="0" :max="sys.config.outerRectW" :step="1" v-model="sys.config.innerRectW" />
<NumSlider label="高" :min="1" :max="400" :step="1" v-model="sys.config.rectH" /> <NumSlider label="内高" :min="0" :max="sys.config.outerRectH" :step="1" v-model="sys.config.innerRectH" />
<NumSlider label="外宽" :min="sys.config.innerRectW" :max="400" :step="1" v-model="sys.config.outerRectW" />
<NumSlider label="外高" :min="sys.config.innerRectH" :max="400" :step="1" v-model="sys.config.outerRectH" />
</template>
<template v-else-if="sys.config.shape === 'path'">
<label class="row"><span>选择路径</span>
<select v-model.number="sys.config.spawnPathId" class="inp" @change="onSpawnPathChange">
<option :value="0">无路径(点发射)</option>
<option v-for="path in store.paths" :key="path.id" :value="path.id" :disabled="!path.enabled">{{ path.name }}</option>
</select>
</label>
<label v-if="sys.config.spawnPathId > 0" class="row"><span>出生分布</span>
<select v-model="sys.config.spawnPathDistribution" class="inp">
<option value="fixed">等间距固定分布</option>
<option value="random">随机分布</option>
</select>
</label>
</template> </template>
<div class="divider"></div> <div class="divider"></div>
<NumSlider :label="sys.config.shape === 'cone' ? '锥形朝向' : '发射角度'" :min="-360" :max="360" :step="1" v-model="sys.config.direction" /> <label class="row"><span>发射角度</span>
<NumSlider v-if="sys.config.shape !== 'cone'" label="扩散角度" :min="0" :max="360" :step="1" v-model="sys.config.spread" /> <select v-model="sys.config.emissionAngleMode" class="inp">
<option value="fixed">固定</option>
<option value="random">随机</option>
<option v-if="sys.config.shape === 'circle' || sys.config.shape === 'rect' || sys.config.shape === 'cone'" value="radial">径向(中心向外)</option>
<option v-if="sys.config.shape === 'path' && sys.config.spawnPathId > 0" value="path-normal">垂直于路径</option>
<option v-if="sys.config.shape === 'path' && sys.config.spawnPathId > 0" value="path-tangent">沿路径方向</option>
</select>
</label>
<NumSlider v-if="sys.config.emissionAngleMode === 'fixed'" label="固定角度" :min="-360" :max="360" :step="1" v-model="sys.config.direction" />
<template v-else-if="sys.config.emissionAngleMode === 'random'">
<NumSlider label="基准角度" :min="-360" :max="360" :step="1" v-model="sys.config.direction" />
<NumSlider label="随机范围" :min="0" :max="360" :step="1" v-model="sys.config.spread" />
</template>
</div> </div>
</div> </div>
@@ -1245,6 +1275,10 @@ watchEffect(() => {
watchEffect(() => { watchEffect(() => {
for (const system of store.systems) { for (const system of store.systems) {
const config = ensureEmitterConfig(system.config as any) const config = ensureEmitterConfig(system.config as any)
if (config.spawnPathId > 0 && !store.paths.some((path) => path.id === config.spawnPathId)) {
config.spawnPathId = 0
if (config.emissionAngleMode === 'path-normal' || config.emissionAngleMode === 'path-tangent') config.emissionAngleMode = 'fixed'
}
if (config.emitterFollowMode !== 'bone') continue if (config.emitterFollowMode !== 'bone') continue
const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId) const spine = store.spines.find((item) => item.id === config.emitterFollowSpineId)
const targetExists = !!spine?.bones.some((bone) => bone.name === config.emitterFollowBoneName) const targetExists = !!spine?.bones.some((bone) => bone.name === config.emitterFollowBoneName)
@@ -1404,7 +1438,11 @@ const MODULE_RESET_KEYS: Record<Exclude<ResetModule, 'export'>, string[]> = {
'generateLoopEndAnimation', 'loopEndUseCustomDuration', 'loopEndDurationFrames', 'generateLoopEndAnimation', 'loopEndUseCustomDuration', 'loopEndDurationFrames',
'burstCount', 'burstLoop', 'delay', 'duration', 'burstCount', 'burstLoop', 'delay', 'duration',
], ],
shape: ['shape', 'radius', 'rectW', 'rectH', 'direction', 'spread', 'coneAngle'], shape: [
'shape', 'shapeRangeVersion',
'innerRadius', 'outerRadius', 'innerRectW', 'innerRectH', 'outerRectW', 'outerRectH', 'spawnPathId', 'spawnPathDistribution',
'radius', 'rectW', 'rectH', 'emissionAngleMode', 'direction', 'spread', 'coneDirection', 'coneAngle',
],
attr: [ attr: [
'lifeMin', 'lifeMax', 'lifeMode', 'lifeCurve', 'lifeMin', 'lifeMax', 'lifeMode', 'lifeCurve',
'speedMin', 'speedMax', 'speedMode', 'speedCurve', 'speedMin', 'speedMax', 'speedMode', 'speedCurve',
@@ -1545,11 +1583,11 @@ function setScaleOverLifeMode(mode: 'fixed' | 'random' | 'curve' | 'direction')
// 各发射器形状的默认参数:切换形状时应用(只影响形状相关字段,不牵连其他) // 各发射器形状的默认参数:切换形状时应用(只影响形状相关字段,不牵连其他)
const SHAPE_DEFAULTS: Record<string, Partial<any>> = { const SHAPE_DEFAULTS: Record<string, Partial<any>> = {
point: { direction: 0, spread: 360 }, point: { emissionAngleMode: 'random', direction: 0, spread: 360 },
circle: { radius: 100, direction: -90, spread: 0 }, circle: { innerRadius: 0, outerRadius: 100, emissionAngleMode: 'fixed', direction: -90, spread: 0 },
rect: { rectW: 200, rectH: 100, direction: -90, spread: 0 }, rect: { innerRectW: 0, innerRectH: 0, outerRectW: 200, outerRectH: 100, emissionAngleMode: 'fixed', direction: -90, spread: 0 },
cone: { radius: 100, coneAngle: 90, direction: 90 }, cone: { innerRadius: 0, outerRadius: 100, coneAngle: 90, coneDirection: 90, emissionAngleMode: 'random', direction: 90, spread: 90 },
orbit: {}, path: { spawnPathId: 0, spawnPathDistribution: 'fixed', emissionAngleMode: 'fixed', direction: 0, spread: 0 },
} }
function onShapeChange() { function onShapeChange() {
const cfg = sys.value?.config const cfg = sys.value?.config
@@ -1558,6 +1596,11 @@ function onShapeChange() {
if (!d) return if (!d) return
Object.assign(cfg, d) Object.assign(cfg, d)
} }
function onSpawnPathChange() {
const cfg = sys.value?.config
if (!cfg || cfg.spawnPathId > 0) return
if (cfg.emissionAngleMode === 'path-normal' || cfg.emissionAngleMode === 'path-tangent') cfg.emissionAngleMode = 'fixed'
}
// 重置/删除图片 → 回到默认 star.png // 重置/删除图片 → 回到默认 star.png
const textureFileInput = ref<HTMLInputElement | null>(null) const textureFileInput = ref<HTMLInputElement | null>(null)
const sequenceFileInput = ref<HTMLInputElement | null>(null) const sequenceFileInput = ref<HTMLInputElement | null>(null)
+57 -58
View File
@@ -188,6 +188,8 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { Application, Container, Graphics, Sprite, Text, Texture } from 'pixi.js' import { Application, Container, Graphics, Sprite, Text, Texture } from 'pixi.js'
import { ParticleEmitter, ensureEmitterConfig, type ParticleState } from '../core/particleEmitter' import { ParticleEmitter, ensureEmitterConfig, type ParticleState } from '../core/particleEmitter'
import { normalizedRadialRange, normalizedRectRange } from '../core/emitterShape'
import { sampleScenePath } from '../core/scenePathSampler'
import { bakeLoopFrames } from '../core/loopFrameBaker' import { bakeLoopFrames } from '../core/loopFrameBaker'
import { loopPlaybackRange } from '../core/loopSegmentTiming' import { loopPlaybackRange } from '../core/loopSegmentTiming'
import { useParticleStore, type CollisionBody, type ParticleSystem, type ScenePath } from '../store/particleStore' import { useParticleStore, type CollisionBody, type ParticleSystem, type ScenePath } from '../store/particleStore'
@@ -985,7 +987,7 @@ function drawBoneFollowTargets() {
} }
} }
// 绘制当前选中系统的发射器形状范围(点/轨道不绘制),随参数实时响应 // 绘制当前选中系统的发射器形状范围(点/路径不额外绘制),随参数实时响应。
function drawShapeRange() { function drawShapeRange() {
if (!shapeGfx) return if (!shapeGfx) return
const g = shapeGfx const g = shapeGfx
@@ -999,7 +1001,7 @@ function drawShapeRange() {
const cx = preview?.x ?? c.centerX ?? 0 const cx = preview?.x ?? c.centerX ?? 0
const cy = -(preview?.y ?? c.centerY ?? 0) const cy = -(preview?.y ?? c.centerY ?? 0)
const shape = c.shape const shape = c.shape
if (shape === 'point' || shape === 'orbit') return // 点不绘制,轨道保留空位 if (shape === 'point' || shape === 'path') return
// 相对 root 中心的局部点(lx,ly,数学坐标)先加偏移(发射源 = root 中心 + offset),再应用 root 缩放+旋转,加 root 中心 → 世界坐标 // 相对 root 中心的局部点(lx,ly,数学坐标)先加偏移(发射源 = root 中心 + offset),再应用 root 缩放+旋转,加 root 中心 → 世界坐标
const sxr = c.rootScaleX ?? 1, syr = c.rootScaleY ?? 1 const sxr = c.rootScaleX ?? 1, syr = c.rootScaleY ?? 1
const rot = -(preview?.rotation ?? c.rootRotation ?? 0) * Math.PI / 180 // 数学上正 → 屏幕:逆时针为正 const rot = -(preview?.rotation ?? c.rootRotation ?? 0) * Math.PI / 180 // 数学上正 → 屏幕:逆时针为正
@@ -1012,36 +1014,48 @@ function drawShapeRange() {
return [cx + x * cs - y * sn, cy + (x * sn + y * cs)] return [cx + x * cs - y * sn, cy + (x * sn + y * cs)]
} }
if (shape === 'circle') { if (shape === 'circle') {
const range = normalizedRadialRange(c.innerRadius, c.outerRadius)
g.lineStyle(2, 0x3ce86a, 0.7) g.lineStyle(2, 0x3ce86a, 0.7)
// 圆:以 root 变换后的椭圆近似(逐点) // 圆环:以 root 变换后的椭圆近似(逐点)
const steps = 48 const steps = 48
for (let i = 0; i <= steps; i++) { for (const radius of [range.outer, range.inner]) {
const a = i / steps * Math.PI * 2 if (radius <= 0) continue
const [px, py] = pt(Math.cos(a) * c.radius, Math.sin(a) * c.radius) for (let i = 0; i <= steps; i++) {
if (i === 0) g.moveTo(px, py); else g.lineTo(px, py) const a = i / steps * Math.PI * 2
const [px, py] = pt(Math.cos(a) * radius, Math.sin(a) * radius)
if (i === 0) g.moveTo(px, py); else g.lineTo(px, py)
}
} }
} else if (shape === 'rect') { } else if (shape === 'rect') {
const hw = c.rectW / 2, hh = c.rectH / 2 const range = normalizedRectRange(c)
const p1 = pt(-hw, -hh), p2 = pt(hw, -hh), p3 = pt(hw, hh), p4 = pt(-hw, hh)
g.lineStyle(2, 0x4a8cff, 0.7) g.lineStyle(2, 0x4a8cff, 0.7)
g.moveTo(p1[0], p1[1]); g.lineTo(p2[0], p2[1]); g.lineTo(p3[0], p3[1]); g.lineTo(p4[0], p4[1]); g.lineTo(p1[0], p1[1]) for (const [width, height] of [[range.outerW, range.outerH], [range.innerW, range.innerH]]) {
if (width <= 0 || height <= 0) continue
const hw = width / 2, hh = height / 2
const p1 = pt(-hw, -hh), p2 = pt(hw, -hh), p3 = pt(hw, hh), p4 = pt(-hw, hh)
g.moveTo(p1[0], p1[1]); g.lineTo(p2[0], p2[1]); g.lineTo(p3[0], p3[1]); g.lineTo(p4[0], p4[1]); g.lineTo(p1[0], p1[1])
}
} else if (shape === 'cone') { } else if (shape === 'cone') {
const r = c.radius const range = normalizedRadialRange(c.innerRadius, c.outerRadius)
const half = (c.coneAngle / 2) * Math.PI / 180 const half = (c.coneAngle / 2) * Math.PI / 180
const base = c.direction * Math.PI / 180 const base = c.coneDirection * Math.PI / 180
// 锥形两条边 + 弧 // 锥形环带的两条边、内弧和外弧。
const edge1 = pt(Math.cos(base - half) * r, Math.sin(base - half) * r) const innerEdge1 = pt(Math.cos(base - half) * range.inner, Math.sin(base - half) * range.inner)
const edge2 = pt(Math.cos(base + half) * r, Math.sin(base + half) * r) const innerEdge2 = pt(Math.cos(base + half) * range.inner, Math.sin(base + half) * range.inner)
const outerEdge1 = pt(Math.cos(base - half) * range.outer, Math.sin(base - half) * range.outer)
const outerEdge2 = pt(Math.cos(base + half) * range.outer, Math.sin(base + half) * range.outer)
g.lineStyle(2, 0x3ce86a, 0.7) g.lineStyle(2, 0x3ce86a, 0.7)
g.moveTo(cx, cy); g.lineTo(edge1[0], edge1[1]) g.moveTo(innerEdge1[0], innerEdge1[1]); g.lineTo(outerEdge1[0], outerEdge1[1])
g.moveTo(cx, cy); g.lineTo(edge2[0], edge2[1]) g.moveTo(innerEdge2[0], innerEdge2[1]); g.lineTo(outerEdge2[0], outerEdge2[1])
// 弧
g.lineStyle(1, 0x3ce86a, 0.5) g.lineStyle(1, 0x3ce86a, 0.5)
const steps = 24 const steps = 24
for (let i = 0; i <= steps; i++) { for (const radius of [range.outer, range.inner]) {
const a = base - half + (2 * half * i / steps) if (radius <= 0) continue
const [px, py] = pt(Math.cos(a) * r, Math.sin(a) * r) for (let i = 0; i <= steps; i++) {
if (i === 0) g.moveTo(px, py); else g.lineTo(px, py) const a = base - half + (2 * half * i / steps)
const [px, py] = pt(Math.cos(a) * radius, Math.sin(a) * radius)
if (i === 0) g.moveTo(px, py); else g.lineTo(px, py)
}
} }
} }
} }
@@ -1343,42 +1357,6 @@ function cubicPoint(a: number, b: number, c: number, d: number, t: number) {
return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d
} }
function sampleScenePath(path: ScenePath, progress: number) {
if (!path.points.length) return null
if (path.points.length === 1) {
const point = pathLocalToWorld(path, path.points[0].x, path.points[0].y)
return { ...point, angle: path.rotation }
}
const segmentCount = path.closed ? path.points.length : path.points.length - 1
const samples: Array<{ x: number; y: number; distance: number }> = []
let totalDistance = 0
for (let segment = 0; segment < segmentCount; segment++) {
const a = path.points[segment]
const b = path.points[(segment + 1) % path.points.length]
for (let step = 0; step <= 20; step++) {
if (segment > 0 && step === 0) continue
const t = step / 20
const localX = cubicPoint(a.x, a.x + a.outX, b.x + b.inX, b.x, t)
const localY = cubicPoint(a.y, a.y + a.outY, b.y + b.inY, b.y, t)
const worldPoint = pathLocalToWorld(path, localX, localY)
const previous = samples[samples.length - 1]
if (previous) totalDistance += Math.hypot(worldPoint.x - previous.x, worldPoint.y - previous.y)
samples.push({ ...worldPoint, distance: totalDistance })
}
}
const target = Math.min(1, Math.max(0, progress)) * totalDistance
let index = samples.findIndex((sample) => sample.distance >= target)
if (index < 0) index = samples.length - 1
const current = samples[index]
const previous = samples[Math.max(0, index - 1)]
const next = samples[Math.min(samples.length - 1, index + 1)]
const beforeDistance = previous.distance
const amount = index > 0 ? (target - beforeDistance) / Math.max(0.0001, current.distance - beforeDistance) : 0
const x = previous.x + (current.x - previous.x) * Math.min(1, Math.max(0, amount))
const y = previous.y + (current.y - previous.y) * Math.min(1, Math.max(0, amount))
return { x, y, angle: Math.atan2(next.y - previous.y, next.x - previous.x) * 180 / Math.PI }
}
function buildPreviewColliders(timeSeconds: number) { function buildPreviewColliders(timeSeconds: number) {
previewColliders = store.colliders.map((collider) => { previewColliders = store.colliders.map((collider) => {
if (!collider.followPath || !collider.followPathId) return collider if (!collider.followPath || !collider.followPathId) return collider
@@ -1417,6 +1395,20 @@ function worldPointToEmitterLocal(
return { x: (cos * screenX + sin * screenY) / scaleX, y: (-sin * screenX + cos * screenY) / scaleY } return { x: (cos * screenX + sin * screenY) / scaleX, y: (-sin * screenX + cos * screenY) / scaleY }
} }
function sampleSpawnPathForEmitter(
config: { centerX: number; centerY: number; rootRotation: number; rootScaleX: number; rootScaleY: number },
path: ScenePath,
progress: number,
) {
const sample = sampleScenePath(path, progress)
if (!sample) return null
const local = worldPointToEmitterLocal(config, sample.x, sample.y)
const radians = sample.angle * Math.PI / 180
const tangentWorld = worldPointToEmitterLocal(config, sample.x + Math.cos(radians), sample.y + Math.sin(radians))
const angle = Math.atan2(tangentWorld.y - local.y, tangentWorld.x - local.x) * 180 / Math.PI
return { ...local, angle }
}
function loop() { function loop() {
raf = requestAnimationFrame(loop) raf = requestAnimationFrame(loop)
const now = performance.now() / 1000 const now = performance.now() / 1000
@@ -1517,6 +1509,13 @@ function loop() {
em.rotation = -c.rootRotation * Math.PI / 180 em.rotation = -c.rootRotation * Math.PI / 180
em.setSimulationTransform(c.centerX, c.centerY, c.rootRotation) em.setSimulationTransform(c.centerX, c.centerY, c.rootRotation)
} }
const spawnPath = c.shape === 'path' && c.spawnPathId > 0
? store.paths.find((path) => path.id === c.spawnPathId && path.enabled && path.points.length > 0) || null
: null
em.setSpawnPathSampler(
spawnPath ? (progress) => sampleSpawnPathForEmitter(c, spawnPath, progress) : null,
spawnPath?.closed === true,
)
emitterPreviewTransform.set(sys.id, preview) emitterPreviewTransform.set(sys.id, preview)
} }
let total = 0 let total = 0