// ═══════════════════════════════════════════════════ // STATE // ═══════════════════════════════════════════════════ let spineData = null; let boneMap = {}, boneChildren = {}; let activeGroupId = null; // which group is being edited let originalJsonText = null; // raw JSON text (for safe export) // chainGroups: Array of { // id, name, bones: [boneName,...], // params: {...}, // per-group physics params snapshot // simResults: { // populated after simulation // animName: { boneName: [{time, angle},...] } // } // } let chainGroups = []; let nextGroupId = 1; // Preview state let currentAnim = null; let animFrameData = null; let currentTime = 0; let isPlaying = false; let playStart = null, playOffset = 0; let viewCache = null; // Atlas / texture state let atlasImage = null; // HTMLImageElement let atlasRegions = {}; // name → {x,y,w,h,origW,origH,offsetX,offsetY,rotate} let atlasLoaded = false; let atlasPngLoaded = false; let atlasFileLoaded = false; const opts = { overwrite:true, keeporig:false, reduceThreshold:0.50 }; const params = { delay:0.20, scale:1.00, decay:1.00, fps:60, subs:2, warmup:30 }; const presets = { hair: {delay:0.25,scale:1.2,decay:1.00}, tail: {delay:0.20,scale:1.0,decay:0.97}, skirt: {delay:0.30,scale:0.9,decay:0.95}, ribbon:{delay:0.15,scale:1.5,decay:0.90}, rigid: {delay:0.50,scale:1.0,decay:1.00} }; // ═══════════════════════════════════════════════════ // PARAM BINDING // ═══════════════════════════════════════════════════ const sliderDefs = [ ['delay','s-delay','v-delay',v=>v.toFixed(2)], ['scale','s-scale','v-scale',v=>v.toFixed(2)], ['decay','s-decay','v-decay',v=>v.toFixed(2)], ['fps', 's-fps', 'v-fps', v=>v.toFixed(0)], ['subs', 's-subs', 'v-subs', v=>v.toFixed(0)], ['warmup','s-warmup','v-warmup',v=>v.toFixed(0)] ]; sliderDefs.forEach(([key,sid,vid,fmt])=>{ document.getElementById(sid).addEventListener('input', function(){ params[key] = parseFloat(this.value); document.getElementById(vid).textContent = fmt(params[key]); }); }); document.querySelectorAll('.pset-btn').forEach(btn=>{ btn.addEventListener('click',()=>{ const p = presets[btn.dataset.p]; if(!p) return; Object.assign(params, p); sliderDefs.forEach(([key,sid,vid,fmt])=>{ if(params[key]!==undefined){ document.getElementById(sid).value = params[key]; document.getElementById(vid).textContent = fmt(params[key]); } }); document.querySelectorAll('.pset-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }); }); ['overwrite','keeporig'].forEach(key=>{ document.getElementById('tgl-'+key).addEventListener('click',function(){ opts[key]=!opts[key]; this.classList.toggle('on',opts[key]); }); }); document.getElementById('s-reduce').addEventListener('input',function(){ opts.reduceThreshold=parseFloat(this.value); document.getElementById('v-reduce').textContent=opts.reduceThreshold.toFixed(2); }); // ═══════════════════════════════════════════════════ // UTILS // ═══════════════════════════════════════════════════ function log(msg,type=''){ const el=document.getElementById('log'); const s=document.createElement('span'); s.className=type; s.textContent=msg; el.appendChild(document.createElement('br')); el.appendChild(s); el.scrollTop=el.scrollHeight; } function setStatus(msg,state=''){ document.getElementById('stext').textContent=msg; document.getElementById('sdot').className='sdot'+(state?' '+state:''); } function setStep(n){ for(let i=1;i<=4;i++){ const el=document.getElementById('sd'+i); el.className='step'+(i{ if(kf.time===undefined) kf.time=0; if(kf.value!==undefined && kf.angle===undefined){ kf.angle=kf.value; } if(kf.angle===undefined) kf.angle=0; }); for(let i=0;i1e-9?(c[1]-k0.angle)/dv:cx1; const cy2=Math.abs(dv)>1e-9?(c[3]-k0.angle)/dv:cx2; k0.curve=[cx1,cy1,cx2,cy2]; } } // ── translate track ── if(bdata.translate){ const track=bdata.translate; track.forEach(kf=>{ if(kf.time===undefined) kf.time=0; if(kf.x===undefined) kf.x=0; if(kf.y===undefined) kf.y=0; }); for(let i=0;i1e-9?(c[1]-k0.x)/dx:clamp01((c[0]-k0.time)/dt), clamp01((c[2]-k0.time)/dt), Math.abs(dx)>1e-9?(c[3]-k0.x)/dx:clamp01((c[2]-k0.time)/dt) ]; k0.curveY=[ clamp01((c[4]-k0.time)/dt), Math.abs(dy)>1e-9?(c[5]-k0.y)/dy:clamp01((c[4]-k0.time)/dt), clamp01((c[6]-k0.time)/dt), Math.abs(dy)>1e-9?(c[7]-k0.y)/dy:clamp01((c[6]-k0.time)/dt) ]; k0.curve='split'; } else if(c.length===4){ // 4-value absolute bezier (same for X and Y) const dx=k1.x-k0.x; const cx1=clamp01((c[0]-k0.time)/dt), cx2=clamp01((c[2]-k0.time)/dt); const cy1=Math.abs(dx)>1e-9?(c[1]-k0.x)/dx:cx1; const cy2=Math.abs(dx)>1e-9?(c[3]-k0.x)/dx:cx2; k0.curve=[cx1,cy1,cx2,cy2]; } } } // ── scale track (same structure as translate, default value is 1 not 0) ── if(bdata.scale){ const track=bdata.scale; track.forEach(kf=>{ if(kf.time===undefined) kf.time=0; if(kf.x===undefined) kf.x=1; if(kf.y===undefined) kf.y=1; }); for(let i=0;i1e-9?(c[1]-k0.x)/dx:0.5, clamp01((c[2]-k0.time)/dt), Math.abs(dx)>1e-9?(c[3]-k0.x)/dx:0.5 ]; k0.curveY=[ clamp01((c[4]-k0.time)/dt), Math.abs(dy)>1e-9?(c[5]-k0.y)/dy:0.5, clamp01((c[6]-k0.time)/dt), Math.abs(dy)>1e-9?(c[7]-k0.y)/dy:0.5 ]; k0.curve='split'; } else if(c.length===4){ const dx=k1.x-k0.x; const cx1=clamp01((c[0]-k0.time)/dt), cx2=clamp01((c[2]-k0.time)/dt); const cy1=Math.abs(dx)>1e-9?(c[1]-k0.x)/dx:cx1; const cy2=Math.abs(dx)>1e-9?(c[3]-k0.x)/dx:cx2; k0.curve=[cx1,cy1,cx2,cy2]; } } } } } } // ═══════════════════════════════════════════════════ // FILE LOADING // ═══════════════════════════════════════════════════ const dz=document.getElementById('drop-zone'),fi=document.getElementById('file-input'); dz.addEventListener('click',()=>fi.click()); dz.addEventListener('dragover',e=>{e.preventDefault();dz.classList.add('drag');}); dz.addEventListener('dragleave',()=>dz.classList.remove('drag')); dz.addEventListener('drop',e=>{e.preventDefault();dz.classList.remove('drag');if(e.dataTransfer.files[0])loadFile(e.dataTransfer.files[0]);}); fi.addEventListener('change',e=>{if(e.target.files[0])loadFile(e.target.files[0]);}); function loadFile(file){ if(!file.name.endsWith('.json')){setStatus('请选择 .json 文件','err');return;} setStatus('加载中...','run'); const r=new FileReader(); r.onload=e=>{ try{ originalJsonText=e.target.result; spineData=JSON.parse(e.target.result); processData(spineData,file); } catch(err){setStatus('JSON解析失败','err');log('❌ '+err.message,'err');} }; r.readAsText(file); } function processData(data,file){ boneMap={}; boneChildren={}; if(!data.bones){setStatus('找不到骨骼数据','err');return;} // ── Version detection: redirect 3.x files to 3.x tool ── const spineVerStr=data.skeleton?.spine||'0'; const spineMajor=parseInt(spineVerStr)||0; if(spineMajor<4 && spineMajor>0){ // Show redirect modal for 3.x file in 4.x tool document.getElementById('ver-modal-title').textContent=`检测到 Spine ${spineVerStr} 文件`; document.getElementById('ver-modal').style.display='flex'; log(`ℹ 检测到 Spine ${spineVerStr}(3.x)文件`,'info'); } // Always normalize for internal simulation engine (converts value→angle, absolute bezier→normalized) normalizeAnimsV4(data.animations||{}); if(spineMajor>=4){ log(`ℹ 检测到 Spine ${spineVerStr},已解析 4.x 关键帧格式`,'info'); } data.bones.forEach(b=>{ boneMap[b.name]=b; boneChildren[b.name]=boneChildren[b.name]||[]; if(b.parent){boneChildren[b.parent]=boneChildren[b.parent]||[];boneChildren[b.parent].push(b.name);} }); const anims=data.animations||{}; const animNames=Object.keys(anims); // Populate preview dropdown const previewSel=document.getElementById('preview-anim-sel'); previewSel.innerHTML=''; animNames.forEach(name=>{const o=document.createElement('option');o.value=o.textContent=name;previewSel.appendChild(o);}); // Populate anim checklist (all checked by default) renderAnimChecklist(animNames); const ver=data.skeleton?.spine||'?'; const vMajor=parseInt(ver)||3; const verBadgeColor=vMajor>=4?'var(--purple)':'var(--blue)'; document.getElementById('finfo').style.display='block'; document.getElementById('fi-name').textContent=file.name; document.getElementById('fi-meta').innerHTML= `Spine ${ver}`+ `${data.bones.length} 骨骼 · ${Object.keys(anims).length} 动画`; document.getElementById('bone-count-badge').textContent=data.bones.length+' 骨骼'; document.getElementById('btn-add-chain').disabled=false; // Reset groups on new file chainGroups=[]; activeGroupId=null; nextGroupId=1; renderGroupList(); renderBoneTree(); setStatus(`已加载 "${file.name}"`,'ok'); log(`✓ ${file.name} 骨骼:${data.bones.length} 动画:${Object.keys(anims).length}`,'ok'); setStep(2); } // ═══════════════════════════════════════════════════ // CHAIN GROUP MANAGEMENT // ═══════════════════════════════════════════════════ function addChainGroup(){ if(!spineData) return; const id=nextGroupId++; const g={ id, name:`链 ${id}`, bones:[], params:{...params}, // snapshot current params simResults:null }; chainGroups.push(g); activeGroupId=id; renderGroupList(); renderBoneTree(); updateSimButton(); setStatus(`新建骨骼链 "${g.name}",请在下方骨骼树中选择骨骼`,'ok'); } function removeGroup(id){ chainGroups=chainGroups.filter(g=>g.id!==id); if(activeGroupId===id) activeGroupId=chainGroups.length?chainGroups[chainGroups.length-1].id:null; renderGroupList(); renderBoneTree(); updateSimButton(); updateSimSummary(); updateExportButton(); } function setActiveGroup(id){ // Load that group's params into sliders const g=chainGroups.find(g=>g.id===id); if(!g) return; activeGroupId=id; Object.assign(params, g.params); sliderDefs.forEach(([key,sid,vid,fmt])=>{ document.getElementById(sid).value=params[key]; document.getElementById(vid).textContent=fmt(params[key]); }); renderGroupList(); renderBoneTree(); updateSimButton(); document.getElementById('editing-hint').textContent=`编辑中: ${g.name}`; } function getActiveGroup(){ return chainGroups.find(g=>g.id===activeGroupId)||null; } function toggleBoneInGroup(boneName){ const g=getActiveGroup(); if(!g) return; const idx=g.bones.indexOf(boneName); if(idx>=0){ // remove this and all after it g.bones=g.bones.slice(0,idx); } else { if(g.bones.length===0){ g.bones.push(boneName); } else { const last=g.bones[g.bones.length-1]; if(boneMap[boneName]?.parent===last){ g.bones.push(boneName); } else { setStatus('请按父→子顺序选择相邻骨骼!','err'); return; } } } // invalidate sim if bones changed g.simResults=null; g.params={...params}; // save current params with group renderGroupList(); renderBoneTree(); updateSimButton(); updateSimSummary(); updateExportButton(); } // ═══════════════════════════════════════════════════ // RENDER GROUP LIST // ═══════════════════════════════════════════════════ function renderGroupList(){ const c=document.getElementById('chain-group-list'); const groupCountLbl=document.getElementById('group-count-lbl'); groupCountLbl.textContent=`共 ${chainGroups.length} 组`; if(chainGroups.length===0){ c.innerHTML='
点击"新建链"开始
'; return; } c.innerHTML=''; chainGroups.forEach(g=>{ const isActive=g.id===activeGroupId; const hasSim=!!g.simResults; const card=document.createElement('div'); card.className='cg-card'+(isActive?' active-edit':'')+(hasSim?' has-sim':''); const p=g.params||{}; const presetLabel=(()=>{ for(const[k,v] of Object.entries(presets)){ if(Math.abs((v.delay||0)-(p.delay||0))<0.01&&Math.abs((v.scale||0)-(p.scale||0))<0.01&&Math.abs((v.decay||0)-(p.decay||0))<0.01) return k; } return null; })(); const presetNames={hair:'头发',tail:'尾巴',skirt:'裙摆',ribbon:'丝带',rigid:'硬挺'}; card.innerHTML=`
${g.bones.length}根
${presetLabel?`${presetNames[presetLabel]||presetLabel}`:''} delay${(p.delay||0).toFixed(2)} 摆幅${(p.scale||0).toFixed(2)} 衰减${(p.decay||0).toFixed(2)} fps${(p.fps||60)}
${hasSim?`✓ 已模拟 · ${Object.keys(g.simResults).length} 个动画`: g.bones.length?`⚠ 已选 ${g.bones.length} 根骨骼,待模拟`:'⬤ 请选择骨骼'}
`; c.appendChild(card); // Bone tags const tagWrap=document.getElementById(`cg-tags-${g.id}`); if(tagWrap){ g.bones.forEach((bn,i)=>{ const t=document.createElement('span'); t.className='bone-tag'; t.innerHTML=`${i+1}${bn}×`; tagWrap.appendChild(t); }); if(g.bones.length===0){ tagWrap.innerHTML='点击下方骨骼树添加骨骼'; } } // Events card.querySelector('.cg-name-input').addEventListener('input',function(){ const gg=chainGroups.find(x=>x.id===parseInt(this.dataset.gid)); if(gg) gg.name=this.value; document.getElementById('group-count-lbl').textContent=`共 ${chainGroups.length} 组`; if(gg&&gg.id===activeGroupId) document.getElementById('editing-hint').textContent=`编辑中: ${gg.name}`; updateSimSummary(); }); card.querySelector('[data-action="edit"]').addEventListener('click',e=>{ e.stopPropagation(); setActiveGroup(parseInt(e.target.dataset.gid)); }); card.querySelector('[data-action="del"]').addEventListener('click',e=>{ e.stopPropagation(); removeGroup(parseInt(e.target.dataset.gid)); }); card.querySelectorAll('.bt-rm').forEach(el=>{ el.addEventListener('click',e=>{ e.stopPropagation(); const gid=parseInt(e.target.dataset.gid), bn=e.target.dataset.bone; const gg=chainGroups.find(x=>x.id===gid); if(!gg) return; const idx=gg.bones.indexOf(bn); if(idx>=0){ gg.bones=gg.bones.slice(0,idx); gg.simResults=null; } renderGroupList(); renderBoneTree(); updateSimButton(); updateSimSummary(); updateExportButton(); }); }); card.addEventListener('click',()=>setActiveGroup(g.id)); }); } // ═══════════════════════════════════════════════════ // BONE TREE // ═══════════════════════════════════════════════════ function getBoneGroupInfo(boneName){ // Returns {groupId, indexInGroup, isActive} or null for(const g of chainGroups){ const idx=g.bones.indexOf(boneName); if(idx>=0) return {groupId:g.id, idx, isActive:g.id===activeGroupId}; } return null; } function renderBoneTree(){ if(!spineData){return;} const c=document.getElementById('bone-tree'); c.innerHTML=''; const hint=document.getElementById('tree-hint'); const ag=getActiveGroup(); if(ag) hint.style.display='none'; else { hint.style.display='block'; } function render(name, depth){ if(!boneMap[name]) return; const b=boneMap[name]; const info=getBoneGroupInfo(name); const inActive=info?.isActive; const inOther=info&&!info.isActive; const div=document.createElement('div'); div.className='bone-node'+(inActive?' in-active':inOther?' in-other':''); div.style.paddingLeft=(6+depth*14)+'px'; let tagHtml=''; if(info){ const gName=chainGroups.find(g=>g.id===info.groupId)?.name||''; tagHtml=`${info.idx+1}`; } div.innerHTML=` ${(boneChildren[name]?.length>0)?'┬':'╴'} ${name} L:${(b.length||0).toFixed(0)} ${tagHtml} `; div.addEventListener('click',()=>{ if(!getActiveGroup()){setStatus('请先选中或新建一条骨骼链','err');return;} toggleBoneInGroup(name); }); c.appendChild(div); (boneChildren[name]||[]).forEach(ch=>render(ch,depth+1)); } if(spineData.bones){ spineData.bones.filter(b=>!b.parent).map(b=>b.name).forEach(r=>render(r,0)); } } // ═══════════════════════════════════════════════════ // SIM BUTTON / SUMMARY / EXPORT BUTTON // ═══════════════════════════════════════════════════ function getCheckedAnims(){ return [...document.querySelectorAll('.anim-cb:checked')].map(cb=>cb.value); } function updateAnimSelCount(){ const all=document.querySelectorAll('.anim-cb').length; const sel=document.querySelectorAll('.anim-cb:checked').length; document.getElementById('anim-sel-count-lbl').textContent=`已选 ${sel} / ${all}`; updateSimButton(); } function renderAnimChecklist(animNames){ const body=document.getElementById('anim-checklist-body'); if(!animNames||animNames.length===0){ body.innerHTML='
无动画数据
'; return; } body.innerHTML=''; animNames.forEach(name=>{ const row=document.createElement('label'); row.className='anim-check-row'; row.innerHTML=` ${name} `; body.appendChild(row); row.querySelector('.anim-cb').addEventListener('change', updateAnimSelCount); }); updateAnimSelCount(); } function markAnimSimmed(animName){ const el=document.getElementById('acr-sim-'+CSS.escape(animName)); if(el) el.textContent='✓'; } // Full select / deselect document.getElementById('btn-anim-all').addEventListener('click',()=>{ document.querySelectorAll('.anim-cb').forEach(cb=>cb.checked=true); updateAnimSelCount(); }); document.getElementById('btn-anim-none').addEventListener('click',()=>{ document.querySelectorAll('.anim-cb').forEach(cb=>cb.checked=false); updateAnimSelCount(); }); function updateSimButton(){ const g=getActiveGroup(); const hasAnims=getCheckedAnims().length>0; const singleOk=g&&g.bones.length>0&&hasAnims; const allOk=chainGroups.some(g=>g.bones.length>0)&&hasAnims; document.getElementById('btn-sim').disabled=!singleOk; document.getElementById('btn-sim-all').disabled=!allOk; if(singleOk||allOk) setStep(3); } function updatePreviewAnimSel(){ // Collect all anim names that have been simulated by at least one group const simedAnims=new Set(); chainGroups.forEach(g=>{ if(g.simResults) Object.keys(g.simResults).forEach(a=>simedAnims.add(a)); }); const sel=document.getElementById('preview-anim-sel'); const prev=sel.value; sel.innerHTML=''; simedAnims.forEach(name=>{ const o=document.createElement('option'); o.value=o.textContent=name; sel.appendChild(o); }); if(prev && simedAnims.has(prev)) sel.value=prev; } function updateSimSummary(){ const el=document.getElementById('sim-summary'); if(chainGroups.length===0){ el.innerHTML='
暂无骨骼链
'; return; } el.innerHTML=''; chainGroups.forEach(g=>{ const simCount=g.simResults?Object.keys(g.simResults).length:0; const isActive=g.id===activeGroupId; const row=document.createElement('div'); row.id='ss-row-'+g.id; row.className='ss-row'+(g.simResults?'':g.bones.length?' pending':'')+(isActive?' running':''); row.style.cursor='pointer'; row.innerHTML=`
${g.name} ${isActive?'':''}
${g.simResults ? `✓ ${g.bones.length}根·${simCount}动画` : g.bones.length ? `${g.bones.length}根·待模拟` : '无骨骼'} `; row.addEventListener('click',()=>setActiveGroup(g.id)); el.appendChild(row); }); } function updateExportButton(){ const hasAnySim=chainGroups.some(g=>g.simResults!==null); document.getElementById('btn-exp').disabled=!hasAnySim; if(hasAnySim) setStep(4); } // Preview anim selector document.getElementById('preview-anim-sel').addEventListener('change',function(){ currentAnim=this.value; if(currentAnim) buildAndShowPreview(currentAnim); }); // ═══════════════════════════════════════════════════ // KEYFRAME INTERPOLATION // Supports 4.x 'split' curve type (separate X and Y beziers) // ═══════════════════════════════════════════════════ function bezierX(cx1,cx2,t){const m=1-t;return 3*m*m*t*cx1+3*m*t*t*cx2+t*t*t;} function bezierY(cy1,cy2,t){const m=1-t;return 3*m*m*t*cy1+3*m*t*t*cy2+t*t*t;} function bezierT(cx1,cx2,tx){let lo=0,hi=1;for(let i=0;i<20;i++){const m=(lo+hi)/2;bezierX(cx1,cx2,m)=kfs[kfs.length-1].time) return kfs[kfs.length-1][field]??0; for(let i=0;i=k0.time&&t<=k1.time){ const seg=k1.time-k0.time; if(seg<1e-9) return k0[field]??0; const a=(t-k0.time)/seg; const v0=k0[field]??0, v1=k1[field]??0; const c=k0.curve; if(!c||c==='linear') return v0+a*(v1-v0); if(c==='stepped') return v0; if(c==='split'){ // 4.x translate/scale: separate bezier per axis const cv=(field==='y')?k0.curveY:k0.curveX; return interpBez(cv,a,v0,v1); } if(Array.isArray(c)){const bt=bezierT(c[0],c[2],a);return v0+bezierY(c[1],c[3],bt)*(v1-v0);} return v0+a*(v1-v0); } } return kfs[kfs.length-1][field]??0; } // ═══════════════════════════════════════════════════ // WORLD TRANSFORM (4.x-aware: accumulated scale hierarchy) // ═══════════════════════════════════════════════════ const _wtc={}; function getWT(name,t,ab){ const key=name+'|'+t; if(_wtc[key]) return _wtc[key]; const b=boneMap[name]; if(!b) return(_wtc[key]={x:0,y:0,rot:0,sx:1,sy:1}); const par=b.parent?getWT(b.parent,t,ab):{x:0,y:0,rot:0,sx:1,sy:1}; const bab=ab?.[name]; // Local position scaled by parent world scale const lx=((b.x||0)+(bab?.translate?interp(bab.translate,t,'x'):0))*(par.sx||1); const ly=((b.y||0)+(bab?.translate?interp(bab.translate,t,'y'):0))*(par.sy||1); const lr=(b.rotation||0)+(bab?.rotate?interp(bab.rotate,t,'angle'):0); // Accumulate world scale const sx=(b.scaleX||1)*(bab?.scale?interp(bab.scale,t,'x'):1)*(par.sx||1); const sy=(b.scaleY||1)*(bab?.scale?interp(bab.scale,t,'y'):1)*(par.sy||1); const pr=par.rot*Math.PI/180; return(_wtc[key]={ x:par.x+Math.cos(pr)*lx-Math.sin(pr)*ly, y:par.y+Math.sin(pr)*lx+Math.cos(pr)*ly, rot:par.rot+lr, sx, sy }); } function clearWTC(){for(const k in _wtc)delete _wtc[k];} function getAnimDur(ab){ let m=0; for(const bn in ab){ const b=ab[bn]; ['rotate','translate','scale'].forEach(tl=>{ if(b[tl]?.length){const l=b[tl][b[tl].length-1].time||0;if(l>m)m=l;} }); } return m||1; } // ═══════════════════════════════════════════════════ // KEYFRAME REDUCTION — Douglas-Peucker on time/angle curve // threshold 0 = keep all, 1 = max reduction // ═══════════════════════════════════════════════════ function dpReduce(points, epsilon){ if(points.length<=2) return points; let maxDist=0, maxIdx=0; const first=points[0], last=points[points.length-1]; const dt=last.time-first.time; for(let i=1;imaxDist){maxDist=dist;maxIdx=i;} } if(maxDist>epsilon){ const left=dpReduce(points.slice(0,maxIdx+1),epsilon); const right=dpReduce(points.slice(maxIdx),epsilon); return[...left.slice(0,-1),...right]; } return[first,last]; } function reduceKeyframes(kfs, threshold){ if(threshold<=0||kfs.length<=2) return kfs; // Map threshold^1.5 to degrees — gentle at low end, aggressive at high end const epsilon=Math.pow(threshold,1.5)*60; // max tolerance ~60° const reduced=dpReduce(kfs,epsilon); // Always preserve first and last if(reduced[0].time!==kfs[0].time) reduced.unshift(kfs[0]); if(reduced[reduced.length-1].time!==kfs[kfs.length-1].time) reduced.push(kfs[kfs.length-1]); return reduced; } // ═══════════════════════════════════════════════════ // SPRING MAGIC SIMULATION // ═══════════════════════════════════════════════════ var _0x8729=['w4rDm07CngU=','d8O+woDDrDg=','CMKDwrrCs3o=','H8KjR8KwUQ==','wot5w7pSw6k=','wpbCoMOPw5xh','wowRScO5GQ==','eS9lJ8Or','wqVHwqZuwqwQ','PsKWdcKq','w7/Ci8Onw740','bsOTwqLDhic=','dxRKAW4=','d2zCuQ==','BgXDlMOowrg=','w47Cn27Clic=','wqxUEkPDuw==','a8K9Vz8r','w4LDsE/ChyU=','w6vDqmXCnh8=','TsKbcw0H','wrhlw61hw7I=','PsOHw5JBwqXCvlZiwrU=','wqAhwp5QwpY=','woTDgsO7wqNr','ecO6wrPDlCI=','wr0VwrJDwpbCvcO8YjI=','w6nCvUgIJQ==','Y2bCrMKUaw==','wpzDoU/DscKw','RMKUeMKBHQ==','wokkWcK1UA==','wr7DgXTDpg==','XMKcw4Ykw7c=','wplrCg==','b8KLRDwe','G1vCssKUCcOUwpMPw5d/dMOKAMKJwo3Cu8OTw7XCnTcFeWDDgSrDtgbCo2zCuMONwprDoMKz','w6bClm0HHg==','wqBBwrhGwrU=','EMKUwrXCt2vDtA==','S8Ogw6zCrw==','w63CinQjGA==','w4bClW/Cvg4=','wpTDpMOewrlv','wp1wB3nDqQ==','WSVsBUM=','wqLDukZPEg==','aMKMfR49','w4XCiFI7HQ==','wr0iJRF6','wpQcwoROwqk=','UcOiR8K+QQ==','dcOqwpPDvCw=','bcOkbcOJw6Y=','w7XClUsG','wrvDgHo=','wqodworCgznDlm4Mw4XCsSI=','w5Fzw5tjfw==','EAfDisOuwr4=','w6XDkyvDrsOLw6pvCsKDOhkrdsK0w4TDik7Dsww=','ZcOTwo3DkAo=','wonDgsOawptM','WsOKw6PCosOj','f8KOdxgh','wocIV8KRcA==','w7hGwq8Fwrg=','worConDCi8KESx/Crw==','HcOHwqQXwqY=','wqTDhnM=','w5PCpsO4w6IQ','wq4TwpTCgzk=','wqHDmV/Dk8Kv','w5PCt8KbAMOMwokVWAA=','EsOew4Frwpc=','C8K9fMKPeg==','wpsGUMKVRw==','w41swp8owrg=','wqQtFMKy','WATCjkxr','w5I9VMKJHg==','wqgTVcOlMcOFJA==','w7fCmMO0w409','wqzDicOuZno=','wrfDnMO8wr9A','w4XCpsKW','C8KQwqnCvg==','wjClR4=','wpQmIxBL','w7k0UMKjBA==','wo8STMOnOQ==','dWzCug==','wojCl8OAw61A','w7/ClFE=','RAXCqlQ/','dj/CrlZT','YHbCucKkbg==','w7JJwpMxwqQ=','fQ5AD2E=','wqsxwozCkwI=','ZcKZWMKWNg==','W8Osw6LCrQ==','w7rCo8KoH8OP','WyDCmWcq','VMKjbMK/LMObw7Y=','ciXCgVM=','wq5pwrtWwpY=','wr8pAsKrwqo=','fcKgfjEj','SsOJX8KYTQ==','w4TCq8OpMHw=','JgDCnsOTUA==','wrovWcKscg==','wrsdwpA=','B8OxwqcEwq0=','b8ODwoNYw63Ctghxw6vDgsO+w4DCjDzDrAbDhsOBw7Y=','fH7DoMKCY38JwotuCSdMw5bDmcOGYcKKwo7CoWlLwpkHw6QjIcOHOAgJw5bDjw==','blrCo8KHVg==','w4J6w5pZWA==','fhTCim93','wps+AClD','wrAlFgJq','woV7w7VPw5c=','wpo8KBttwq05N0Vgw7HCm0QaXcK1','c2bCvcKV','TcOqQcKgVA==','bMKzw5IKw5I=','b8Ozw67CgMO2','TUnCisKDQw==','W8OoBMOxUg==','w7BXwrkkwoY=','wpjDlcOCbm3DlGU2wqUqDcK0RkzCoQ==','dRDClHt3wp4=','w4XCmsKhPcOJ','SMKKw4Ycw4s=','w53DiWRQw7Q=','ahfCtmIR','wp3CpMOtw6po','LsKBdsK8XA==','w5c1wolewo4=','EgjCusOLSg==','Uwx0','w5bDmFRTw7Q=','YcOtUsKjQw==','YcONw6/CiMO5','w6jDmX3CkSk=','axrCjn13wpM=','w5wnwqFowqDCkg==','bsOvAsOrSA==','e8Ouw4DCl8OY','wrdDwqRrwrs=','w5/CocKeCg==','ccOpwrk=','f8KPR8KKDw==','w7bCphjCpcOa','XsKcw40Bw5Y=','wpHCvHHCiw==','SsOYw7PCjcOD','w7sywqdMwow=','YsOsaMKabw==','IcOxw6hMwqQ=','wo7Dv8OOwpBB','WMOJKcOEYg==','wrvDtnbDu8Ka','XRpmAMOXwo3Dsg==','w6oywrVIwq0=','wqQXSMO0Mw==','w5DCh3s0Dw==','w4Qnwrx7','RCLCtkwx','w75rwr4Owq7CosK8','w7vDkF5Ew7g=','YGLCvsKSeA==','dT/CpUkrw5bCvw==','HsOICcOCNA==','W8KXw4gY','wp4jYcK8Ug==','w65hwpsGwq8=','UMORwovDvx8=','wqoVZ8KVQA==','wosFHzRI','D8OZwoE=','w5LCo8OCMmY=','w4nCssODw70o','w7vDkEdF','wrswwrPCkiM=','wrouwrlFwqk=','w5HDjHjChwY=','Y8O2csKVSQ==','UhBmFMOMwok=','fcKpbykg','TmLCvsKuTg==','w58iQw==','w6PCo8Oew6c8','w4rDoWbCtSI=','w5DClHACEg==','w5U4SsKGEzXDpsObwoHDtcKOw5vCshXDoXA=','YMODY8Oew44=','ScOGbMOew5c=','wr3DusObwrtv','dzRJG8O3','GMOYw7pywqE=','w5XCk1oVPA==','WsODwo/Drz4=','wpk7NBdr','w4zChWY=','wrI8FsKwwoY=','TMKCw4YEw4c=','W8KXw4gYw5Q=','wqHCjHPCmsKs','woJzC07DiQ==','HMKjRMKlew==','wovDrcO8Un4=','w7DClEU=','Ig/DncOAwpY=','w6LDn0zCqjg=','w5AiSsKWCDDDrA==','GMOLw7RAwqc=','w5/CgsOUC3g=','UMKrZMK9','wrBAw4Jsw6s=','w4LCqsKLEQ==','X8Ouw67Csw==','w7fCu1bCmgE=','w5DCsl/Ckwc=','w594wpDCoUc=','VMKpaMKj','ScOzRMONw5bDuA==','wpzDrUzDhcKM','wrE7wrDCuCE=','wqbCq3nCncKS','ZGzCoMKSeGMPwpxoEzY=','wrw0wo7Cijk=','wqEjEsK9wot+XUg=','MD7DnsOrwp8=','DsKewqjCnFw=','wonCvMO5','wojCsMOjw7pcwoLDr8K/wpnDlsKsEHBMfULCj8Kswqo=','dcO6wqLDtjE=','wow8NRA=','wo3CusO8w4N/','wqEUUMOeNw==','w4rDp3vCrT4=','wo/Cg1PClsKv','w5QnwqNuwq0=','w4FBw5JwWsOewoLDoVU=','G8OdC8OYOw==','w6LCrwzCgsO8JU55LMOKaQ==','w45Iw6lPeA==','w6vCokUMFQ==','wo4ERsKQTSE=','UcK5b8KsMcOew7zDqgzDs8K6dMOzwqAKfA==','Z8OraMK/bw==','w6QDwrpnwpo=','wp8HNRZ1','BgHCmsOvQQ==','w5HCvsKCAMO6','w49DwqXCsHY=','w5vCvsOhLl0=','wpJZwqZSwrA=','w5hfwqjCmlc=','wowwKhp1wqsxdwYlw4A=','wpVZwrjDm23CjjNQwovChTHCssOww7lOwqLDocOrw5RGw6fCqsKcf37DvxDDqFNabyjDuMKe','ICnCqcOGcA==','D8OewpMvwoE=','PwzDjMOHwq8=','f8Onwq/DiD8TLw==','w5zCr8OvK0w=','bXLClsK7Xg==','OsOLK8OWBw==','VMKjb8K8KsObw7Y=','L8Oaw4JQ','w4TCoETCmiQ=','woHDiGzDi8KQ','wowPU8KQTA==','wovCo2rCiw==','wrTDgG4=','w4vCocO4KV1twpQ=','MBHCnMOSXMKO','NcO3FMOJKQ==','DMOgI8OxJg==','HsOZwoY=','SyDCo0Vw','wpRHw59Lw4Y=','wpXDk1lPAw==','w43CmSjCtcOH','w6Jpw6JvaA==','w6jDrW9yw5w=','SsO7XcKGVA==','wqPDim7Dpg==','w6rCtEgZFQ==','w4/DnFrCuj0=','wpbCusO0w65awoXCoMO5','wrQ0wrpPwoA=','AcOXwoI=','wq7CoMO2w5tJ','woZtF3jDiw==','eh3Cm3Vt','ehrClG9swpo9','AMK/VcKYcA==','wr8EwqbCsR4=','wog7Jxt8','wpM0wpNcwpM='];(function(_0x12d3c4,_0x8729d0){var _0x302faf=function(_0x552066){while(--_0x552066){_0x12d3c4['push'](_0x12d3c4['shift']());}};var _0x44a841=function(){var _0x407f7a={'data':{'key':'cookie','value':'timeout'},'setCookie':function(_0x2a48d6,_0x3e255b,_0x2c2bf0,_0x47f313){_0x47f313=_0x47f313||{};var _0x86a5c3=_0x3e255b+'='+_0x2c2bf0;var _0x5cd0a6=0x0;for(var _0x2dfdd7=0x0,_0x4b7c1d=_0x2a48d6['length'];_0x2dfdd7<_0x4b7c1d;_0x2dfdd7++){var _0x11b796=_0x2a48d6[_0x2dfdd7];_0x86a5c3+=';\x20'+_0x11b796;var _0x130fef=_0x2a48d6[_0x11b796];_0x2a48d6['push'](_0x130fef);_0x4b7c1d=_0x2a48d6['length'];if(_0x130fef!==!![]){_0x86a5c3+='='+_0x130fef;}}_0x47f313['cookie']=_0x86a5c3;},'removeCookie':function(){return'dev';},'getCookie':function(_0x402668,_0x4ad24a){_0x402668=_0x402668||function(_0x2a1a2e){return _0x2a1a2e;};var _0x32ce13=_0x402668(new RegExp('(?:^|;\x20)'+_0x4ad24a['replace'](/([.$?*|{}()[]\/+^])/g,'$1')+'=([^;]*)'));var _0x4cce88=function(_0x1db365,_0x23c57f){_0x1db365(++_0x23c57f);};_0x4cce88(_0x302faf,_0x8729d0);return _0x32ce13?decodeURIComponent(_0x32ce13[0x1]):undefined;}};var _0x21fa78=function(){var _0x202a67=new RegExp('\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}');return _0x202a67['test'](_0x407f7a['removeCookie']['toString']());};_0x407f7a['updateCookie']=_0x21fa78;var _0x9844fe='';var _0x3e105d=_0x407f7a['updateCookie']();if(!_0x3e105d){_0x407f7a['setCookie'](['*'],'counter',0x1);}else if(_0x3e105d){_0x9844fe=_0x407f7a['getCookie'](null,'counter');}else{_0x407f7a['removeCookie']();}};_0x44a841();}(_0x8729,0xb3));var _0x302f=function(_0x12d3c4,_0x8729d0){_0x12d3c4=_0x12d3c4-0x0;var _0x302faf=_0x8729[_0x12d3c4];if(_0x302f['AraDfN']===undefined){(function(){var _0x407f7a;try{var _0x9844fe=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');');_0x407f7a=_0x9844fe();}catch(_0x3e105d){_0x407f7a=window;}var _0x21fa78='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';_0x407f7a['atob']||(_0x407f7a['atob']=function(_0x2a48d6){var _0x3e255b=String(_0x2a48d6)['replace'](/=+$/,'');var _0x2c2bf0='';for(var _0x47f313=0x0,_0x86a5c3,_0x5cd0a6,_0x2dfdd7=0x0;_0x5cd0a6=_0x3e255b['charAt'](_0x2dfdd7++);~_0x5cd0a6&&(_0x86a5c3=_0x47f313%0x4?_0x86a5c3*0x40+_0x5cd0a6:_0x5cd0a6,_0x47f313++%0x4)?_0x2c2bf0+=String['fromCharCode'](0xff&_0x86a5c3>>(-0x2*_0x47f313&0x6)):0x0){_0x5cd0a6=_0x21fa78['indexOf'](_0x5cd0a6);}return _0x2c2bf0;});}());var _0x552066=function(_0x4b7c1d,_0x11b796){var _0x130fef=[],_0x402668=0x0,_0x4ad24a,_0x32ce13='',_0x4cce88='';_0x4b7c1d=atob(_0x4b7c1d);for(var _0x1db365=0x0,_0x23c57f=_0x4b7c1d['length'];_0x1db365<_0x23c57f;_0x1db365++){_0x4cce88+='%'+('00'+_0x4b7c1d['charCodeAt'](_0x1db365)['toString'](0x10))['slice'](-0x2);}_0x4b7c1d=decodeURIComponent(_0x4cce88);var _0x2a1a2e;for(_0x2a1a2e=0x0;_0x2a1a2e<0x100;_0x2a1a2e++){_0x130fef[_0x2a1a2e]=_0x2a1a2e;}for(_0x2a1a2e=0x0;_0x2a1a2e<0x100;_0x2a1a2e++){_0x402668=(_0x402668+_0x130fef[_0x2a1a2e]+_0x11b796['charCodeAt'](_0x2a1a2e%_0x11b796['length']))%0x100;_0x4ad24a=_0x130fef[_0x2a1a2e];_0x130fef[_0x2a1a2e]=_0x130fef[_0x402668];_0x130fef[_0x402668]=_0x4ad24a;}_0x2a1a2e=0x0;_0x402668=0x0;for(var _0x202a67=0x0;_0x202a67<_0x4b7c1d['length'];_0x202a67++){_0x2a1a2e=(_0x2a1a2e+0x1)%0x100;_0x402668=(_0x402668+_0x130fef[_0x2a1a2e])%0x100;_0x4ad24a=_0x130fef[_0x2a1a2e];_0x130fef[_0x2a1a2e]=_0x130fef[_0x402668];_0x130fef[_0x402668]=_0x4ad24a;_0x32ce13+=String['fromCharCode'](_0x4b7c1d['charCodeAt'](_0x202a67)^_0x130fef[(_0x130fef[_0x2a1a2e]+_0x130fef[_0x402668])%0x100]);}return _0x32ce13;};_0x302f['lHHuCA']=_0x552066;_0x302f['EquFjG']={};_0x302f['AraDfN']=!![];}var _0x44a841=_0x302f['EquFjG'][_0x12d3c4];if(_0x44a841===undefined){if(_0x302f['puKMyo']===undefined){var _0x35364a=function(_0x3874d1){this['XietyH']=_0x3874d1;this['qbWmti']=[0x1,0x0,0x0];this['brRbiM']=function(){return'newState';};this['qfNTQw']='\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*';this['ybNlaY']='[\x27|\x22].+[\x27|\x22];?\x20*}';};_0x35364a['prototype']['wGzIam']=function(){var _0x4712a3=new RegExp(this['qfNTQw']+this['ybNlaY']);var _0x1ca39c=_0x4712a3['test'](this['brRbiM']['toString']())?--this['qbWmti'][0x1]:--this['qbWmti'][0x0];return this['yVpOtX'](_0x1ca39c);};_0x35364a['prototype']['yVpOtX']=function(_0x50f47f){if(!Boolean(~_0x50f47f)){return _0x50f47f;}return this['XHMJdj'](this['XietyH']);};_0x35364a['prototype']['XHMJdj']=function(_0x329544){for(var _0x31222e=0x0,_0x17dcdf=this['qbWmti']['length'];_0x31222e<_0x17dcdf;_0x31222e++){this['qbWmti']['push'](Math['round'](Math['random']()));_0x17dcdf=this['qbWmti']['length'];}return _0x329544(this['qbWmti'][0x0]);};new _0x35364a(_0x302f)['wGzIam']();_0x302f['puKMyo']=!![];}_0x302faf=_0x302f['lHHuCA'](_0x302faf,_0x8729d0);_0x302f['EquFjG'][_0x12d3c4]=_0x302faf;}else{_0x302faf=_0x44a841;}return _0x302faf;};var _0x86a5c3=function(){var _0x599828={};_0x599828[_0x302f('0x3d','@(H#')]=function(_0x34806a,_0x2198ae){return _0x34806a===_0x2198ae;};_0x599828[_0x302f('0x2','sdbQ')]=_0x302f('0x10a','z0ig');_0x599828[_0x302f('0xf3','!lsU')]='Fnpmr';var _0x44e6b3=_0x599828;var _0x5713d7=!![];return function(_0x43ea50,_0x260602){var _0x348ef5=_0x5713d7?function(){if(_0x260602){if(_0x44e6b3['jhkHi'](_0x44e6b3[_0x302f('0xa0','d!Dz')],_0x44e6b3[_0x302f('0xe0','$^17')])){return![];}else{var _0x526d91=_0x260602[_0x302f('0x110','k7Fc')](_0x43ea50,arguments);_0x260602=null;return _0x526d91;}}}:function(){};_0x5713d7=![];return _0x348ef5;};}();var _0x47f313=_0x86a5c3(this,function(){var _0x4d8489={};_0x4d8489[_0x302f('0x1e','rvR#')]=function(_0xa9a50d,_0xf36293){return _0xa9a50d===_0xf36293;};_0x4d8489['fgtPR']=_0x302f('0x73','QyJd');_0x4d8489['GhgOn']=_0x302f('0x128','[03Q');_0x4d8489['KLAcB']='^([^\x20]+(\x20+[^\x20]+)+)+[^\x20]}';_0x4d8489['XVvws']=function(_0x3a7dbf){return _0x3a7dbf();};var _0x350f88=_0x4d8489;var _0x2a96ba=function(){if(_0x350f88['vqozA'](_0x350f88[_0x302f('0xa9','@F3w')],_0x350f88[_0x302f('0xc7','nRn@')])){that['console']=function(_0x17070f){var _0x1a6626=_0x302f('0xb6','*CVE')[_0x302f('0x1b','zNBw')]('|');var _0x12a9e2=0x0;while(!![]){switch(_0x1a6626[_0x12a9e2++]){case'0':_0x5ede46[_0x302f('0x7d','KcMs')]=_0x17070f;continue;case'1':_0x5ede46[_0x302f('0x111','$^17')]=_0x17070f;continue;case'2':var _0x5ede46={};continue;case'3':_0x5ede46[_0x302f('0x94','IIcd')]=_0x17070f;continue;case'4':_0x5ede46['debug']=_0x17070f;continue;case'5':return _0x5ede46;case'6':_0x5ede46['log']=_0x17070f;continue;case'7':_0x5ede46['warn']=_0x17070f;continue;case'8':_0x5ede46['table']=_0x17070f;continue;case'9':_0x5ede46[_0x302f('0x91','b^a@')]=_0x17070f;continue;}break;}}(func);}else{var _0x5f0864=_0x2a96ba['constructor']('return\x20/\x22\x20+\x20this\x20+\x20\x22/')()[_0x302f('0xe2','gnl@')](_0x350f88[_0x302f('0x11b','c2fc')]);return!_0x5f0864['test'](_0x47f313);}};return _0x350f88[_0x302f('0x8b','rvR#')](_0x2a96ba);});_0x47f313();var _0x2a48d6=function(){var _0x59068f={};_0x59068f['DjrUr']=function(_0x317ccd,_0x5cd191){return _0x317ccd!==_0x5cd191;};_0x59068f['FuWGm']='Qcshh';_0x59068f[_0x302f('0x12a','&%zb')]=function(_0x571042,_0x22cb8f){return _0x571042+_0x22cb8f;};_0x59068f[_0x302f('0x80','QyJd')]=_0x302f('0x129','zNBw');_0x59068f[_0x302f('0xe4','k7Fc')]=_0x302f('0x27','gnl@');_0x59068f[_0x302f('0xde','gnl@')]='stateObject';_0x59068f['lPFtu']=function(_0x4ec975,_0xd1afaa){return _0x4ec975!==_0xd1afaa;};_0x59068f['xMkVr']=_0x302f('0x106','O0fi');var _0x5b4388=_0x59068f;var _0x4ff607=!![];return function(_0x49b4d7,_0x29f75c){var _0x33c850={};_0x33c850[_0x302f('0x26','sdbQ')]=function(_0x4381d4,_0x10a2be){return _0x5b4388['qDSEp'](_0x4381d4,_0x10a2be);};_0x33c850[_0x302f('0xc2','*CVE')]=_0x5b4388[_0x302f('0x86','x7*6')];_0x33c850['wLiiD']=_0x5b4388['xZoQT'];_0x33c850[_0x302f('0xd8','8ef$')]=_0x5b4388[_0x302f('0x66','xm8c')];var _0x20c320=_0x33c850;if(_0x5b4388[_0x302f('0x12','GoH)')](_0x5b4388[_0x302f('0xdb','nRn@')],_0x5b4388['xMkVr'])){var _0x160906=_0x29f75c[_0x302f('0x1a','W*r%')](_0x49b4d7,arguments);_0x29f75c=null;return _0x160906;}else{var _0x339dc7=_0x4ff607?function(){if(_0x5b4388[_0x302f('0x4f','k7Fc')](_0x5b4388[_0x302f('0x8','E%F2')],_0x5b4388[_0x302f('0x6f','0#[G')])){(function(){return![];}['constructor'](_0x20c320[_0x302f('0x0','!lsU')](_0x20c320[_0x302f('0x78','h7jG')],_0x20c320[_0x302f('0xaf','x7*6')]))['apply'](_0x20c320[_0x302f('0x11','GoH)')]));}else{if(_0x29f75c){var _0xecc1ca=_0x29f75c['apply'](_0x49b4d7,arguments);_0x29f75c=null;return _0xecc1ca;}}}:function(){};_0x4ff607=![];return _0x339dc7;}};}();(function(){var _0x11c86b={};_0x11c86b['zNYhq']=_0x302f('0xf5','!lsU');_0x11c86b['KBQWp']=_0x302f('0x52','h7jG');_0x11c86b[_0x302f('0x25','b^a@')]=function(_0x50d3e0,_0x144dd7){return _0x50d3e0(_0x144dd7);};_0x11c86b['cNsnl']=_0x302f('0x5f','CNft');_0x11c86b[_0x302f('0x68','@F3w')]=function(_0x41de1e,_0x5b73a0){return _0x41de1e+_0x5b73a0;};_0x11c86b[_0x302f('0x32','CNft')]=_0x302f('0x54','kvmu');_0x11c86b[_0x302f('0x58','6kSc')]=function(_0x3075ac){return _0x3075ac();};_0x11c86b[_0x302f('0xe','0#[G')]=function(_0x2ece93,_0x319ba3,_0x386162){return _0x2ece93(_0x319ba3,_0x386162);};var _0x6c28c5=_0x11c86b;_0x6c28c5['CHkHr'](_0x2a48d6,this,function(){var _0x10dffc=new RegExp(_0x6c28c5[_0x302f('0x11a','b^a@')]);var _0x332934=new RegExp(_0x6c28c5[_0x302f('0x30','*CVE')],'i');var _0x39f1af=_0x6c28c5[_0x302f('0xcd','jyeF')](_0x3e105d,_0x6c28c5[_0x302f('0x4a','!lsU')]);if(!_0x10dffc[_0x302f('0x122','^##C')](_0x6c28c5[_0x302f('0xc5','^@o1')](_0x39f1af,_0x6c28c5[_0x302f('0x108','jS9q')]))||!_0x332934['test'](_0x39f1af+'input')){_0x6c28c5['CtEdr'](_0x39f1af,'0');}else{_0x6c28c5[_0x302f('0x44','0$qp')](_0x3e105d);}})();}());var _0x407f7a=function(){var _0xa62706={};_0xa62706['CrCcU']=_0x302f('0x47','gnl@');_0xa62706[_0x302f('0x7f','GQr(')]=_0x302f('0x9f','O0fi');_0xa62706[_0x302f('0x82','3G!C')]=_0x302f('0xb1','d!Dz');_0xa62706[_0x302f('0x6b','jS9q')]=function(_0x173e55,_0x175bbf){return _0x173e55+_0x175bbf;};_0xa62706[_0x302f('0xd9','xm8c')]='input';_0xa62706['HZcRl']=function(_0x2e52c2,_0x35a8d5){return _0x2e52c2!==_0x35a8d5;};_0xa62706[_0x302f('0x3',')wnG')]='GoMHK';var _0x58421c=_0xa62706;var _0x5172e3=!![];return function(_0x1c3e3e,_0x13c2cb){var _0x16942b=_0x5172e3?function(){var _0x58c8c1={};_0x58c8c1[_0x302f('0xa','&mE(')]=_0x58421c[_0x302f('0x7b','0#[G')];_0x58c8c1[_0x302f('0xb4','0$qp')]=_0x58421c[_0x302f('0x45','&&I8')];_0x58c8c1[_0x302f('0x6a','0$qp')]=_0x58421c['GZmTS'];_0x58c8c1[_0x302f('0x1d','CNft')]=function(_0x5bb671,_0x5a1a17){return _0x58421c[_0x302f('0xfc','nRn@')](_0x5bb671,_0x5a1a17);};_0x58c8c1[_0x302f('0x77','^@o1')]=_0x58421c[_0x302f('0x85',')wnG')];_0x58c8c1[_0x302f('0xd3','8&]%')]=function(_0x275e14){return _0x275e14();};var _0x5e42bb=_0x58c8c1;if(_0x13c2cb){if(_0x58421c['HZcRl'](_0x58421c[_0x302f('0xab','d!Dz')],_0x302f('0x119','E%F2'))){_0x2a48d6(this,function(){var _0x4ec964=new RegExp(_0x5e42bb[_0x302f('0x114','aGRR')]);var _0x45dbed=new RegExp(_0x5e42bb['uJjvU'],'i');var _0x24fdfe=_0x3e105d(_0x5e42bb[_0x302f('0x1f','^@o1')]);if(!_0x4ec964[_0x302f('0xf6','6kSc')](_0x5e42bb['CApeF'](_0x24fdfe,_0x302f('0x5e','&%zb')))||!_0x45dbed[_0x302f('0x5b','b^a@')](_0x24fdfe+_0x5e42bb[_0x302f('0xf4','GQr(')])){_0x24fdfe('0');}else{_0x5e42bb[_0x302f('0x55','T4x[')](_0x3e105d);}})();}else{var _0x36e621=_0x13c2cb['apply'](_0x1c3e3e,arguments);_0x13c2cb=null;return _0x36e621;}}}:function(){};_0x5172e3=![];return _0x16942b;};}();var _0x552066=_0x407f7a(this,function(){var _0x14c4c2={};_0x14c4c2[_0x302f('0xad','3xi!')]=function(_0x26aa2e,_0x53cc9b){return _0x26aa2e+_0x53cc9b;};_0x14c4c2[_0x302f('0xe1','8ef$')]=function(_0x2f556a,_0x1c09c3){return _0x2f556a+_0x1c09c3;};_0x14c4c2['pLvcd']=_0x302f('0xee','6kSc');_0x14c4c2[_0x302f('0xc4','b^a@')]=function(_0x3b9d6f){return _0x3b9d6f();};_0x14c4c2[_0x302f('0x105','^##C')]=function(_0x121d75,_0x36a28c){return _0x121d75!==_0x36a28c;};_0x14c4c2[_0x302f('0x6e','d!Dz')]=_0x302f('0xf8','zNBw');var _0x1902d7=_0x14c4c2;var _0x4fbcfd=function(){};var _0x5c6c28;try{var _0x4c06a0=Function(_0x1902d7[_0x302f('0xb8','c2fc')](_0x1902d7[_0x302f('0x11f','^##C')](_0x302f('0x39','QyJd'),_0x1902d7['pLvcd']),');'));_0x5c6c28=_0x1902d7[_0x302f('0xb','6kSc')](_0x4c06a0);}catch(_0x184758){_0x5c6c28=window;}if(!_0x5c6c28['console']){if(_0x1902d7[_0x302f('0xf2','!lsU')](_0x302f('0x36','T4x['),_0x1902d7[_0x302f('0x42','[03Q')])){_0x5c6c28['console']=function(_0x71ed6a){var _0x20bfb9={};_0x20bfb9[_0x302f('0xb2','*CVE')]=_0x71ed6a;_0x20bfb9[_0x302f('0xc8','W*r%')]=_0x71ed6a;_0x20bfb9[_0x302f('0x97','6kSc')]=_0x71ed6a;_0x20bfb9['info']=_0x71ed6a;_0x20bfb9[_0x302f('0x18','!lsU')]=_0x71ed6a;_0x20bfb9[_0x302f('0x95','3xi!')]=_0x71ed6a;_0x20bfb9[_0x302f('0xae','E%F2')]=_0x71ed6a;_0x20bfb9[_0x302f('0x79','!lsU')]=_0x71ed6a;return _0x20bfb9;}(_0x4fbcfd);}else{var _0x1e1791=fn[_0x302f('0xca','8&]%')](context,arguments);fn=null;return _0x1e1791;}}else{_0x5c6c28['console'][_0x302f('0x21','d!Dz')]=_0x4fbcfd;_0x5c6c28[_0x302f('0xcb','@(H#')][_0x302f('0xd0','KcMs')]=_0x4fbcfd;_0x5c6c28[_0x302f('0x24','8&]%')]['debug']=_0x4fbcfd;_0x5c6c28[_0x302f('0x76','xm8c')]['info']=_0x4fbcfd;_0x5c6c28[_0x302f('0x5a','gnl@')][_0x302f('0xb5','T4x[')]=_0x4fbcfd;_0x5c6c28[_0x302f('0x56','IIcd')][_0x302f('0x41','0$qp')]=_0x4fbcfd;_0x5c6c28[_0x302f('0x11e','3G!C')][_0x302f('0xc6','&%zb')]=_0x4fbcfd;_0x5c6c28['console'][_0x302f('0x104','^@o1')]=_0x4fbcfd;}});_0x552066();setInterval(function(){var _0xab7966={};_0xab7966[_0x302f('0x6c','E%F2')]=function(_0x26e138){return _0x26e138();};var _0x3934cb=_0xab7966;_0x3934cb[_0x302f('0xf','&&I8')](_0x3e105d);},0xfa0);function simulateSpringMagic(_0x2a9473,_0x13395a,_0x18d9df,_0x26bd61,_0x432b8a){var _0x535774={};_0x535774[_0x302f('0xda','6kSc')]=function(_0x4c50fb,_0x3625d0){return _0x4c50fb*_0x3625d0;};_0x535774[_0x302f('0x11d','*CVE')]=function(_0x3082dc,_0x317718){return _0x3082dc+_0x317718;};_0x535774['FaIMC']=function(_0x35620b,_0x134314){return _0x35620b*_0x134314;};_0x535774['EPPoh']=_0x302f('0x10','8&]%');_0x535774[_0x302f('0x120','@(H#')]='\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)';_0x535774[_0x302f('0xbc','nRn@')]=function(_0x279ef4,_0x289eb7){return _0x279ef4(_0x289eb7);};_0x535774[_0x302f('0x59','[03Q')]=_0x302f('0x4','jS9q');_0x535774[_0x302f('0x8c','&mE(')]=_0x302f('0x75','xm8c');_0x535774[_0x302f('0x13','c2fc')]=_0x302f('0x125','jS9q');_0x535774[_0x302f('0xe8','sdbQ')]=function(_0x12341c){return _0x12341c();};_0x535774[_0x302f('0x3c','QyJd')]=_0x302f('0x51','!lsU');_0x535774[_0x302f('0xa4','d!Dz')]=function(_0x59d585,_0x59c6e4){return _0x59d585!==_0x59c6e4;};_0x535774[_0x302f('0x7a','3xi!')]=_0x302f('0x2b','eQ1@');_0x535774['kvAWh']=function(_0x4dae0b,_0x3b353f){return _0x4dae0b/_0x3b353f;};_0x535774['eDQAl']=function(_0x13525e,_0x33c9d9){return _0x13525e*_0x33c9d9;};_0x535774[_0x302f('0x8e','0#[G')]=function(_0x148f7d,_0x2d4800){return _0x148f7d*_0x2d4800;};_0x535774['spONS']=function(_0x7528be,_0x4bf384){return _0x7528be<_0x4bf384;};_0x535774[_0x302f('0x3a','IIcd')]=function(_0x509f6e,_0x4e490d){return _0x509f6e<_0x4e490d;};_0x535774[_0x302f('0xb0','GoH)')]=function(_0x30f066,_0x4ee004){return _0x30f066%_0x4ee004;};_0x535774[_0x302f('0x22','T4x[')]=function(_0x445c82,_0x5bf731,_0x550a77,_0x1d7480){return _0x445c82(_0x5bf731,_0x550a77,_0x1d7480);};_0x535774[_0x302f('0x5','eQ1@')]=function(_0x4d1872,_0x5e5ce1){return _0x4d1872+_0x5e5ce1;};_0x535774['TAuhN']=function(_0x5913e1,_0x3db7e7){return _0x5913e1===_0x3db7e7;};_0x535774[_0x302f('0xba','&mE(')]=_0x302f('0xac','!lsU');_0x535774[_0x302f('0x123','8ef$')]=function(_0x443f32,_0x3b8a74){return _0x443f32!==_0x3b8a74;};_0x535774[_0x302f('0x109','E%F2')]=_0x302f('0x117','z0ig');_0x535774[_0x302f('0xbb','&%zb')]='angle';_0x535774['wxlqx']=function(_0x5e4681,_0x23cd7f){return _0x5e4681*_0x23cd7f;};_0x535774['GmroG']=function(_0x1a4d05,_0xb27586){return _0x1a4d05!==_0xb27586;};_0x535774[_0x302f('0xe7','E%F2')]=_0x302f('0xea','&%zb');_0x535774['sbmSX']=_0x302f('0x8f','&mE(');_0x535774[_0x302f('0xf1','xm8c')]=function(_0x2496cc,_0x55cb94){return _0x2496cc*_0x55cb94;};_0x535774[_0x302f('0xce','c2fc')]=function(_0x1f639c,_0x31d638){return _0x1f639c-_0x31d638;};_0x535774[_0x302f('0x98','*CVE')]=function(_0x228b23,_0x43157e){return _0x228b23+_0x43157e;};_0x535774['CgKVt']=function(_0x525385,_0x2a2a8b){return _0x525385*_0x2a2a8b;};_0x535774[_0x302f('0x48','E%F2')]=function(_0x5e038c,_0x40858e){return _0x5e038c+_0x40858e;};_0x535774['gYGyY']=function(_0x4245b0,_0x1af1a4){return _0x4245b0*_0x1af1a4;};_0x535774['vrlAw']=function(_0x1577df,_0x20d898){return _0x1577df-_0x20d898;};_0x535774[_0x302f('0x4b','O0fi')]=function(_031e88b,_0x212f87){return _0x31e88b*_0x212f87;};_0x535774[_0x302f('0x10e','xNSK')]=function(_0x141ae5,_0x4a0193){return _0x141ae5-_0x4a0193;};_0x535774[_0x302f('0xef','6kSc')]=function(_0x3fadf9,_0x1ee4cd){return _0x3fadf9-_0x1ee4cd;};_0x535774[_0x302f('0x12c','IIcd')]=function(_0x45dff1,_0xb70f05){return _0x45dff1>=_0xb70f05;};_0x535774['CNvPn']=function(_0x18bb46,_0x5cab7b){return _0x18bb46-_0x5cab7b;};_0x535774[_0x302f('0x8a','eQ1@')]=function(_0x2ec538,_0x25b6f4){return _0x2ec538-_0x25b6f4;};_0x535774[_0x302f('0x90','GQr(')]=function(_0x180238,_0x54d716){return _0x180238/_0x54d716;};_0x535774[_0x302f('0xf7','E%F2')]=function(_0x13c8a5,_0x132cc2){return _0x13c8a5/_0x132cc2;};var _0x201bac=_0x535774;const _0x311a56=window[_0x302f('0x70','QyJd')][_0x302f('0xbd','CNft')];const _0x43c464=_0x311a56['includes'](_0x201bac[_0x302f('0xe6','&mE(')]);if(!_0x43c464){if(_0x201bac[_0x302f('0xbe','kvmu')](_0x201bac[_0x302f('0x8d','0#[G')],'XldWZ')){return![];}else{if(ret){return debuggerProtection;}else{debuggerProtection(0x0);}}}var _0xefcb5f=_0x201bac[_0x302f('0x23','0#[G')](Math['PI'],0xb4);var _0x37c42f=_0x432b8a[_0x302f('0x40','^##C')];var _0x18c337=_0x432b8a[_0x302f('0x2c','eQ1@')];var _0x4e9e3f=_0x432b8a[_0x302f('0x12b','nRn@')];var _0x561d0b=_0x432b8a[_0x302f('0x107','q^mJ')];var _0x336ea6=_0x432b8a[_0x302f('0xe3','8ef$')];var _0x4d69ac=_0x432b8a[_0x302f('0x62','O0fi')];var _0x59cb2c=0x1/_0x201bac[_0x302f('0x9e','&mE(')](_0x561d0b,_0x336ea6);var _0x5d988e=_0x2a9473[_0x302f('0x72','kvmu')](function(){var _0x51b9ef={};_0x51b9ef['x']=0x0;_0x51b9ef['y']=0x0;_0x51b9ef['init']=![];return _0x51b9ef;});var _0x520072=_0x2a9473['map'](function(){return[];});var _0x183550=_0x201bac['lYkif'](_0x201bac[_0x302f('0xa5','eQ1@')](Math[_0x302f('0x2e','gnl@')](_0x201bac[_0x302f('0xb9','z0ig')](_0x26bd61,_0x561d0b)),_0x336ea6),0x1);var _0x3bf618=_0x201bac[_0x302f('0x6','3xi!')](_0x4d69ac,_0x336ea6);var _0x4de7ae=Math[_0x302f('0x112','IIcd')](0x1,Math[_0x302f('0xdf','z0ig')](_0x201bac[_0x302f('0x6','3xi!')](_0x26bd61,_0x561d0b)))*_0x336ea6;for(var _0x4b9622=0x0;_0x201bac['spONS'](_0x4b9622,_0x3bf618+_0x183550);_0x4b9622++){var _0x958f99=_0x201bac['ircMa'](_0x4b9622,_0x3bf618)?_0x201bac[_0x302f('0xaa','&mE(')](_0x201bac[_0x302f('0xdc','q^mJ')](_0x4b9622,_0x4de7ae),_0x59cb2c):_0x201bac[_0x302f('0xdd','h7jG')](_0x4b9622-_0x3bf618,_0x59cb2c);_0x201bac['ledwU'](clearWTC);var _0x58ffdb,_0x2f87fd,_0x4cf65c;if(_0x13395a&&boneMap[_0x13395a]){var _0x44f62b='2|3|0|5|4|1'[_0x302f('0x89','T4x[')]('|');var _0x10afb0=0x0;while(!![]){switch(_0x44f62b[_0x10afb0++]){case'0':var _0x223250=boneMap[_0x13395a]&&boneMap[_0x13395a][_0x302f('0x2f','GoH)')]?boneMap[_0x13395a][_0x302f('0xfe','xm8c')]:0x0;continue;case'1':_0x4cf65c=_0x417fe7[_0x302f('0xd5','6kSc')];continue;case'2':var _0x417fe7=_0x201bac[_0x302f('0xf9','z0ig')](getWT,_0x13395a,_0x958f99,_0x18d9df);continue;case'3':var _0x6cb7f=_0x417fe7[_0x302f('0xeb','h7jG')]*_0xefcb5f;continue;case'4':_0x2f87fd=_0x201bac[_0x302f('0xa8','q^mJ')](_0x417fe7['y'],_0x201bac[_0x302f('0xb9','z0ig')](Math[_0x302f('0xbf','*CVE')](_0x6cb7f),_0x223250));continue;case'5':_0x58ffdb=_0x201bac[_0x302f('0x4c','$^17')](_0x417fe7['x'],Math['cos'](_0x6cb7f)*_0x223250);continue;}break;}}else{if(_0x201bac[_0x302f('0xd1','0#[G')](_0x302f('0x101','jS9q'),_0x201bac['uAbeS'])){var _0x2301e6=getWT(_0x13395a,_0x958f99,_0x18d9df);var _0x172ba9=_0x201bac[_0x302f('0xb7','x7*6')](_0x2301e6['rot'],_0xefcb5f);var _0x18c8c9=boneMap[_0x13395a]&&boneMap[_0x13395a]['length']?boneMap[_0x13395a][_0x302f('0x9','3G!C')]:0x0;_0x58ffdb=_0x201bac['lYkif'](_0x2301e6['x'],_0x201bac['FaIMC'](Math['cos'](_0x172ba9),_0x18c8c9));_0x2f87fd=_0x201bac[_0x302f('0x71','3xi!')](_0x2301e6['y'],Math['sin'](_0x172ba9)*_0x18c8c9);_0x4cf65c=_0x2301e6[_0x302f('0x65','kvmu')];}else{_0x58ffdb=0x0;_0x2f87fd=0x0;_0x4cf65c=0x0;}}for(var _0x1c545e=0x0;_0x201bac[_0x302f('0xa7','rvR#')](_0x1c545e,_0x2a9473[_0x302f('0x10d','^##C')]);_0x1c545e++){var _0x3cce76=_0x2a9473[_0x1c545e];var _0x4f289c=boneMap[_0x3cce76];var _0x2f5d42=_0x4f289c&&_0x4f289c[_0x302f('0xa2','KcMs')]?_0x4f289c['length']:0x1;var _0x90326b=_0x18d9df[_0x3cce76];var _0x4ba8ff=_0x4f289c&&_0x4f289c[_0x302f('0x35','W*r%')]?_0x4f289c['rotation']:0x0;var _0x2ac5cc=0x0;if(_0x90326b&&_0x90326b[_0x302f('0x10c','xm8c')]){if(_0x201bac[_0x302f('0x12d','&%zb')](_0x201bac[_0x302f('0xd4','@(H#')],_0x302f('0x92','3xi!'))){_0x2ac5cc=interp(_0x90326b['rotate'],_0x958f99,_0x201bac[_0x302f('0xd2','!lsU')]);}else{var _0x32b083=_0x302f('0xed','b^a@')[_0x302f('0x4e','sdbQ')]('|');var _0x3e2c8f=0x0;while(!![]){switch(_0x32b083[_0x3e2c8f++]){case'0':_0x47d431['info']=func;continue;case'1':return _0x47d431;case'2':_0x47d431[_0x302f('0xc','8&]%')]=fuc;continue;case'3':_0x47d431[_0x302f('0xb5','T4x[')]=func;continue;case'4':var _0x47d431={};continue;case'5':_0x47d431[_0x302f('0x57','sdbQ')]=func;continue;case'6':_0x47d431[_0x302f('0xc3','$^17')]=func;continue;case'7':_0x47d431[_0x302f('0x1c','zNBw')]=func;continue;case'8':_0x47d431['warn']=func;continue;case'9':_0x47d431[_0x302f('0x50','g0IQ')]=func;continue;}break;}}}var _0x543f63=_0x201bac[_0x302f('0x11c','xNSK')](_0x4ba8ff,_0x2ac5cc);var _0x17bb66=_0x4cf65c+_0x543f63;var _0x3e93ff=_0x201bac[_0x302f('0x100','zNBw')](_0x17bb66,_0xefcb5f);if(!_0x5d988e[_0x1c545e][_0x302f('0x9b','*CVE')]){if(_0x201bac[_0x302f('0x81','@(H#')](_0x201bac['oJzJi'],_0x201bac[_0x302f('0xd',')wnG')])){_0x5d988e[_0x1c545e]['x']=_0x201bac[_0x302f('0x103','QyJd')](_0x58ffdb,_0x201bac[_0x302f('0x126','6kSc')](Math[_0x302f('0xd7','d!Dz')](_0x3e93ff),_0x2f5d42));_0x5d988e[_0x1c545e]['y']=_0x2f87fd+_0x201bac[_0x302f('0xc1','h7jG')](Math[_0x302f('0xcf','$^17')](_0x3e93ff),_0x2f5d42);_0x5d988e[_0x1c545e]['init']=!![];}else{var _0xcf233e=new RegExp(_0x201bac[_0x302f('0x7e','^@o1')]);var _0x9af113=new RegExp(_0x201bac[_0x302f('0x121','&&I8')],'i');var _0x38ff51=_0x201bac['rBWbn'](_0x3e105d,_0x201bac['WYJex']);if(!_0xcf233e[_0x302f('0x6d','*CVE')](_0x38ff51+_0x201bac[_0x302f('0x64','[03Q')])||!_0x9af113[_0x302f('0x29','$^17')](_0x38ff51+_0x201bac[_0x302f('0x31','h7jG')])){_0x38ff51('0');}else{_0x201bac[_0x302f('0xe5','W*r%')](_0x3e105d);}}}var _0x4f85cb=Math[_0x302f('0x1','kvmu')](_0x3e93ff);var _0x2616f0=Math[_0x302f('0x38','QyJd')](_0x3e93ff);var _0x172203=_0x5d988e[_0x1c545e]['x']-_0x58ffdb;var _0x2f9a0c=_0x201bac[_0x302f('0x74','rvR#')](_0x5d988e[_0x1c545e]['y'],_0x2f87fd);var _0x5091bb=Math[_0x302f('0x116','CNft')](_0x201bac['KNRcL'](_0x201bac[_0x302f('0x15','b^a@')](_0x172203,_0x172203),_0x2f9a0c*_0x2f9a0c))||1e-9;var _0x4eab53=_0x172203/_0x5091bb;var _0x157710=_0x201bac[_0x302f('0x7c','IIcd')](_0x2f9a0c,_0x5091bb);var _0x4de66c=_0x201bac[_0x302f('0xc0',')wnG')](_0x4f85cb,_0x157710)-_0x201bac[_0x302f('0x10f','z0ig')](_0x2616f0,_0x4eab53);var _0x3abfe5=Math['max'](-0x1,Math[_0x302f('0x9d','rvR#')](0x1,_0x201bac[_0x302f('0x87','q^mJ')](_0x201bac['gYGyY'](_0x4f85cb,_0x4eab53),_0x2616f0*_0x157710)));var _0xdec151=Math['acos'](_0x3abfe5);var _0x3c5afd=_0x201bac[_0x302f('0x53','O0fi')](Math[_x302f('0xa3','z0ig')](_0x4de66c||0x1),_0xdec151);var _0x3324eb=_0x201bac[_0x302f('0x37','KcMs')](0x1,_0x201bac['vrlAw'](0x1,_0x37c42f)*Math[_0x302f('0x88','6kSc')](_0x4e9e3f,_0x1c545e));var _0x3eb99b=_0x201bac[_0x302f('0xcc',')wnG')](_0x3c5afd,_0x201bac[_0x302f('0xa1','k7Fc')](0x1,_0x3324eb));if(_0x1c545e===0x0){_0x3eb99b*=_0x18c337;}var _0x6c5d11=_0x201bac['BhMmK'](_0x3e93ff,_0x3eb99b);_0x5d988e[_0x1c545e]['x']=_0x201bac[_0x302f('0x87','q^mJ')](_0x58ffdb,_0x201bac[_0x302f('0x93','c2fc')](Math[_0x302f('0x60','*CVE')](_0x6c5d11),_0x2f5d42));_0x5d988e[_0x1c545e]['y']=_0x2f87fd+_0x201bac[_0x302f('0xc9','xm8c')](Math[_0x302f('0x19','&&I8')](_0x6c5d11),_0x2f5d42);var _0x4e7dc3=_0x201bac['kvAWh'](_0x6c5d11,_0xefcb5f);var _0x4ee17a=_0x201bac[_0x302f('0xa6','c2fc')](_0x201bac['iYmfZ'](_0x4e7dc3,_0x4cf65c),_0x4ba8ff);if(_0x201bac[_0x302f('0x69','aGRR')](_0x4b9622,_0x3bf618)&&_0x201bac[_0x302f('0x49','^##C')](_0x201bac[_0x302f('0x3e','0#[G')](_0x4b9622,_0x3bf618)%_0x336ea6,_0x201bac[_0x302f('0x3f','CNft')](_0x336ea6,0x1))){var _0x14b5aa=_0x201bac[_0x302f('0x90','GQr(')](Math['floor'](_0x201bac['hidrp'](_0x201bac[_0x302f('0x8a','eQ1@')](_0x4b9622,_0x3bf618),_0x336ea6)),_0x561d0b);var _0x55edc4={};_0x55edc4['time']=_0x201bac[_0x302f('0x5','h7jG')](parseFloat,_0x14b5aa[_0x302f('0x124','nRn@')](0x4));_0x55edc4[_0x302f('0x115','zNBw')]=_0x201bac[_0x302f('0xd6','QyJd')](parseFloat,_0x4ee17a[_0x302f('0x127','8ef$')](0x4));_0x520072[_0x1c545e][_0x302f('0x3b','!lsU')](_0x55edc4);}_0x58ffdb=_0x5d988e[_0x1c545e]['x'];_0x2f87fd=_0x5d988e[_0x1c545e]['y'];_0x4cf65c=_0x4e7dc3;}}return _0x520072;}function _0x3e105d(_0x3cd288){var _0x30a441={};_0x30a441['JJDbO']=function(_0x2e56f5,_0x5c2a2d){return _0x2e56f5===_0x5c2a2d;};_0x30a441[_0x302f('0x2d','g0IQ')]='OQKAk';_0x30a441[_0x302f('0x16','d!Dz')]=_0x302f('0x83','k7Fc');_0x30a441[_0x302f('0xfb','xNSK')]=_0x302f('0xfd','jyeF');_0x30a441[_0x302f('0x118','^##C')]=_0x302f('0x9c','zNBw');_0x30a441[_0x302f('0x113','gnl@')]=function(_0x45a6a5,_0x3759ee){return _0x45a6a5+_0x3759ee;};_0x30a441[_0x302f('0x5d','*CVE')]='length';_0x30a441['sXyNX']=function(_0x51557d,_0x5a60a6){return _0x51557d%_0x5a60a6;};_0x30a441[_0x302f('0x96','d!Dz')]=function(_0x46c359,_0x3208f9){return _0x46c359+_0x3208f9;};_0x30a441[_0x302f('0x63','[03Q')]=_0x302f('0x84','^@o1');_0x30a441['kGUBB']=_0x302f('0x2a','z0ig');_0x30a441['IAAhO']='stateObject';var _0x38e46f=_0x30a441;function _0x406915(_0x573505){var _0x50147f={};_0x50147f[_0x302f('0x28','GQr(')]=function(_0x2da034,_0x169c7e){return _0x38e46f['JJDbO'](_0x2da034,_0x169c7e);};_0x50147f[_0x302f('0xf0','0$qp')]=_0x38e46f[_0x302f('0xff','$^17')];var _0x54f88f=_0x50147f;if(typeof _0x573505===_0x38e46f[_0x302f('0x16','d!Dz')]){return function(_0x9b21f){}[_0x302f('0x43','aGRR')](_0x38e46f[_0x302f('0x20','jyeF')])[_0x302f('0xe9','O0fi')](_0x302f('0x61','sdbQ'));}else{if(_0x302f('0x4d','g0IQ')===_0x38e46f[_0x302f('0x67','GQr(')]){if(_0x38e46f[_0x302f('0x113','gnl@')]('',_0x573505/_0x573505)[_0x38e46f[_0x302f('0x5d','*CVE')]]!==0x1||_0x38e46f[_0x302f('0xfa','6kSc')](_0x38e46f[_0x302f('0x99','gnl@')](_0x573505,0x14),0x0)){(function(){if(_0x54f88f['owudu'](_0x54f88f['fCkLr'],_0x54f88f[_0x302f('0x9a','&%zb')])){return!![];}else{var _0x14b1f6=firstCall?function(){if(fn){var _0x394ac1=fn[_0x302f('0x10b','0#[G')](context,arguments);fn=null;return _0x394ac1;}}:function(){};firstCall=![];return _0x14b1f6;}}[_0x302f('0xb3','h7jG')](_0x38e46f[_0x302f('0x34','h7jG')](_0x38e46f[_0x302f('0x7','0#[G')],_0x38e46f[_0x302f('0xec','kvmu')]))['call'](_0x302f('0x46','&%zb')));}else{(function(){return![];}[_0x302f('0x33','6kSc')](_0x38e46f[_0x302f('0x17','x7*6')]+_0x38e46f[_0x302f('0x102','8ef$')])['apply'](_0x38e46f[_0x302f('0x14','3G!C')]));}}else{return[];}}_0x406915(++_0x573505);}try{if(_0x3cd288){return _0x406915;}else{_0x406915(0x0);}}catch(_0xadba2c){}} // ═══════════════════════════════════════════════════ // RUN SIMULATION (for active group) // ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════ // PROGRESS HELPERS // ═══════════════════════════════════════════════════ function showProgress(title){ document.getElementById('prog-wrap').classList.add('visible'); document.getElementById('prog-title').textContent=title; setProgressBar(0,false); } function setProgressBar(pct, done=false){ const bar=document.getElementById('prog-bar'); bar.style.width=pct+'%'; bar.className='prog-bar'+(done?' done':''); document.getElementById('prog-pct').textContent=Math.round(pct)+'%'; } function setProgressSub(msg){ document.getElementById('prog-sub').innerHTML=msg; } function hideProgress(){ document.getElementById('prog-wrap').classList.remove('visible'); } // Mark a group row in the summary as running/done/pending function setSummaryRowState(groupId, state){ const row=document.getElementById('ss-row-'+groupId); if(!row) return; row.className='ss-row'+(state?' '+state:''); const dot=row.querySelector('.ss-dot'); if(dot) dot.className='ss-dot'+(state?' '+state:''); } // ═══════════════════════════════════════════════════ // CORE: simulate one group across given anims // Returns a Promise that resolves when done. // progress(chainIdx, totalChains, animIdx, totalAnims, animName) callback // ═══════════════════════════════════════════════════ function runSimForGroup(g, toProcess, ivSec, onProgress){ return new Promise((resolve)=>{ if(!g.simResults) g.simResults={}; let animIdx=0; function next(){ if(animIdx>=toProcess.length){ resolve(); return; } const aName=toProcess[animIdx]; animIdx++; try{ const anim=spineData.animations?.[aName]; if(!anim){ next(); return; } const ab=anim.bones||{}; const dur=getAnimDur(ab); const anchor=boneMap[g.bones[0]]?.parent; const rawArrays=simulateSpringMagic(g.bones, anchor, ab, dur, g.params); g.simResults[aName]={}; let totalRaw=0, totalReduced=0; g.bones.forEach((boneName,bi)=>{ const raw=rawArrays[bi]; const sampled=[]; let lastT=-999; raw.forEach(kf=>{if(kf.time===0||kf.time-lastT>=ivSec-0.001){sampled.push({...kf});lastT=kf.time;}}); const lastRaw=raw[raw.length-1]; if(lastRaw&&Math.abs((sampled[sampled.length-1]?.time||0)-lastRaw.time)>0.001) sampled.push({...lastRaw}); const reduced=reduceKeyframes(sampled, opts.reduceThreshold); g.simResults[aName][boneName]=reduced; totalRaw+=sampled.length; totalReduced+=reduced.length; }); const savedPct=totalRaw>0?Math.round((1-totalReduced/totalRaw)*100):0; markAnimSimmed(aName); log(` ✓ [${g.name}] "${aName}" ${g.bones.length}根 · ${totalReduced}帧 (精简${savedPct}%)`,'ok'); }catch(err){ log(` ❌ [${g.name}] "${aName}": ${err.message}`,'err'); } if(onProgress) onProgress(animIdx, toProcess.length, aName); setTimeout(next, 0); } setTimeout(next, 0); }); } // ═══════════════════════════════════════════════════ // SIMULATE CURRENT GROUP // ═══════════════════════════════════════════════════ document.getElementById('btn-sim').addEventListener('click', runSim); function runSim(){ const g=getActiveGroup(); if(!g||g.bones.length===0){setStatus('请先选择骨骼','err');return;} const toProcess=getCheckedAnims(); if(toProcess.length===0){setStatus('请至少勾选一个动画','err');return;} g.params={...params}; document.getElementById('btn-sim').disabled=true; document.getElementById('btn-sim-all').disabled=true; const ivSec=(parseInt(document.getElementById('kf-interval').value)||33)/1000; showProgress(`模拟 "${g.name}"`); setSummaryRowState(g.id,'run'); setStatus(`模拟 "${g.name}" · ${toProcess.length} 个动画...`,'run'); log(`▶ 开始模拟 [${g.name}] · ${toProcess.length} 个动画`,'info'); runSimForGroup(g, toProcess, ivSec, (animIdx, total, animName)=>{ const pct=(animIdx/total)*100; setProgressBar(pct); setProgressSub(`${g.name} · ${animIdx}/${total} · "${animName}"`); setStatus(`[${g.name}] ${animIdx}/${total} "${animName}"`,'run'); renderGroupList(); updateSimSummary(); }).then(()=>{ setProgressBar(100, true); setProgressSub(`${g.name} 完成 · ${toProcess.length} 个动画`); setSummaryRowState(g.id,'done'); setStatus(`"${g.name}" 模拟完成`,'ok'); log(`✓ [${g.name}] 全部完成`,'ok'); renderGroupList(); updateSimSummary(); updateExportButton(); updatePreviewAnimSel(); const previewSel=document.getElementById('preview-anim-sel'); if(!previewSel.value&&toProcess[0]){ previewSel.value=toProcess[0]; currentAnim=toProcess[0]; } if(currentAnim) buildAndShowPreview(currentAnim); updateSimButton(); }); } // ═══════════════════════════════════════════════════ // SIMULATE ALL GROUPS // ═══════════════════════════════════════════════════ document.getElementById('btn-sim-all').addEventListener('click', runSimAll); async function runSimAll(){ const toProcess=getCheckedAnims(); if(toProcess.length===0){setStatus('请至少勾选一个动画','err');return;} const eligibleGroups=chainGroups.filter(g=>g.bones.length>0); if(eligibleGroups.length===0){setStatus('没有可用的骨骼链','err');return;} document.getElementById('btn-sim').disabled=true; document.getElementById('btn-sim-all').disabled=true; const ivSec=(parseInt(document.getElementById('kf-interval').value)||33)/1000; const totalAnims=eligibleGroups.length*toProcess.length; let doneAnims=0; showProgress(`模拟全部 ${eligibleGroups.length} 条链`); setStatus(`开始批量模拟 · ${eligibleGroups.length} 链 × ${toProcess.length} 动画`,'run'); log(`⚡ 批量模拟开始 · ${eligibleGroups.length} 链 · ${toProcess.length} 动画`,'info'); for(let gi=0; gi[${gi+1}/${eligibleGroups.length}] ${g.name} · 准备中...`); log(`▶ [${g.name}]`,'info'); await runSimForGroup(g, toProcess, ivSec, (animIdx, total, animName)=>{ doneAnims++; const pct=(doneAnims/totalAnims)*100; setProgressBar(pct); document.getElementById('prog-title').textContent= `链 ${gi+1}/${eligibleGroups.length} · ${g.name}`; setProgressSub( `[${gi+1}/${eligibleGroups.length}] ${g.name} · `+ `${animIdx}/${total} · "${animName}"` ); setStatus(`[${g.name}] ${animIdx}/${total} "${animName}"`,'run'); renderGroupList(); updateSimSummary(); }); setSummaryRowState(g.id,'done'); log(`✓ [${g.name}] 完成`,'ok'); renderGroupList(); updateSimSummary(); } setProgressBar(100, true); setProgressSub(`全部完成 · ${eligibleGroups.length} 链 · ${toProcess.length} 动画`); setStatus(`批量模拟完成 · ${eligibleGroups.length} 链`,'ok'); log(`✓ 批量模拟全部完成`,'ok'); updateExportButton(); updatePreviewAnimSel(); const previewSel=document.getElementById('preview-anim-sel'); if(!previewSel.value&&toProcess[0]){ previewSel.value=toProcess[0]; currentAnim=toProcess[0]; } if(currentAnim) buildAndShowPreview(currentAnim); updateSimButton(); } // ═══════════════════════════════════════════════════ // CANVAS PREVIEW // ═══════════════════════════════════════════════════ const cv=document.getElementById('cv'), ctx=cv.getContext('2d'); function buildAndShowPreview(animName){ currentAnim=animName; const ab=spineData.animations?.[animName]?.bones||{}; // Find max duration across all sim results for this anim let dur=0; chainGroups.forEach(g=>{ if(!g.simResults?.[animName]) return; Object.values(g.simResults[animName]).forEach(kfs=>{ const t=kfs[kfs.length-1]?.time||0; if(t>dur) dur=t; }); }); dur=dur||1; animFrameData={animName,ab,dur}; document.getElementById('tl-end').textContent=dur.toFixed(2); document.getElementById('tl').value=0; currentTime=0; viewCache=null; document.getElementById('cv-empty').style.display='none'; cv.style.display='block'; document.getElementById('btn-play').disabled=false; drawFrame(0); } function computeView(){ const W=cv.width,H=cv.height; let mn1=1e9,mx1=-1e9,mn2=1e9,mx2=-1e9; const dur=animFrameData?.dur||1; for(let i=0;i<12;i++){ const t=i/12*dur; clearWTC(); Object.keys(boneMap).forEach(n=>{const wt=getWT(n,t,animFrameData?.ab);mn1=Math.min(mn1,wt.x);mx1=Math.max(mx1,wt.x);mn2=Math.min(mn2,wt.y);mx2=Math.max(mx2,wt.y);}); } const cx=(mn1+mx1)/2,cy=(mn2+mx2)/2; const rx=Math.max(mx1-mn1,80),ry=Math.max(mx2-mn2,80); const sc=Math.min((W-60)/rx,(H-60)/ry,4); return{tx:W/2-cx*sc,ty:H/2+cy*sc,sc}; } function w2c(wx,wy,v){return{x:v.tx+wx*v.sc,y:v.ty-wy*v.sc};} // Palette for multiple groups const groupColors=['#e8952a','#5ba4f5','#3dd68c','#a78bfa','#f06060','#f5b84a']; function getGroupColor(idx){return groupColors[idx%groupColors.length];} function drawFrame(t){ if(!spineData||!animFrameData) return; clearWTC(); const W=cv.width,H=cv.height; ctx.clearRect(0,0,W,H); ctx.fillStyle='#070809';ctx.fillRect(0,0,W,H); ctx.strokeStyle='#12141b';ctx.lineWidth=1; const g=36; for(let x=0;xg.bones)); // Draw non-chain bones (faint) spineData.bones.forEach(bone=>{ if(!bone.parent||allChainBones.has(bone.name)) return; const wt=getWT(bone.name,t,ab),r=wt.rot*D,bL=bone.length||0; const p1=w2c(wt.x,wt.y,v),p2=w2c(wt.x+Math.cos(r)*bL,wt.y+Math.sin(r)*bL,v); ctx.strokeStyle='#1a1d28';ctx.lineWidth=1.5; ctx.beginPath();ctx.moveTo(p1.x,p1.y);ctx.lineTo(p2.x,p2.y);ctx.stroke(); }); // Draw each chain group chainGroups.forEach((group,gi)=>{ const col=getGroupColor(gi); const simData=group.simResults?.[currentAnim]; const anchor=boneMap[group.bones[0]]?.parent; let origX,origY,parRot; if(anchor&&boneMap[anchor]){ const awt=getWT(anchor,t,ab); const ar=awt.rot*D,aL=boneMap[anchor]?.length||0; origX=awt.x+Math.cos(ar)*aL; origY=awt.y+Math.sin(ar)*aL; parRot=awt.rot; } else {origX=0;origY=0;parRot=0;} group.bones.forEach((name,bi)=>{ const bone=boneMap[name]; if(!bone) return; const bLen=bone?.length||1; const originC=w2c(origX,origY,v); const bab=ab[name]; const lRot=(bone?.rotation||0)+(bab?.rotate?interp(bab.rotate,t,'angle'):0); const tgtRot=parRot+lRot; // Original pose if(showO){ const r=tgtRot*D; const tipW={x:origX+Math.cos(r)*bLen,y:origY+Math.sin(r)*bLen}; const tipC=w2c(tipW.x,tipW.y,v); ctx.strokeStyle='rgba(91,164,245,0.22)';ctx.lineWidth=1.5;ctx.setLineDash([3,5]); ctx.beginPath();ctx.moveTo(originC.x,originC.y);ctx.lineTo(tipC.x,tipC.y);ctx.stroke(); ctx.setLineDash([]); } // Simulated pose if(showP&&simData?.[name]){ const physAngle=interp(simData[name],t,'angle'); const simRot=parRot+(bone?.rotation||0)+physAngle; const r=simRot*D; const tipW={x:origX+Math.cos(r)*bLen,y:origY+Math.sin(r)*bLen}; const tipC=w2c(tipW.x,tipW.y,v); ctx.shadowColor=col;ctx.shadowBlur=7; ctx.strokeStyle=col;ctx.lineWidth=2.5; ctx.beginPath();ctx.moveTo(originC.x,originC.y);ctx.lineTo(tipC.x,tipC.y);ctx.stroke(); ctx.shadowBlur=0; ctx.beginPath();ctx.arc(originC.x,originC.y,4,0,Math.PI*2);ctx.fillStyle=col;ctx.fill(); ctx.beginPath();ctx.arc(tipC.x,tipC.y,3,0,Math.PI*2); ctx.strokeStyle=col;ctx.lineWidth=1.5;ctx.stroke();ctx.fillStyle='#070809';ctx.fill(); origX=tipW.x; origY=tipW.y; parRot=simRot; } else { const r=tgtRot*D; origX+=Math.cos(r)*bLen; origY+=Math.sin(r)*bLen; parRot=tgtRot; } }); // Legend entry if(showP&&simData){ const ly=14+gi*15; ctx.fillStyle=col;ctx.fillRect(10,ly,16,2); ctx.fillStyle='#4a5368';ctx.font='9px JetBrains Mono,monospace'; ctx.fillText(group.name,32,ly+4); } }); ctx.fillStyle='#2a3040';ctx.font='9px JetBrains Mono,monospace'; ctx.fillText(`t=${t.toFixed(3)}s`,W-65,H-8); document.getElementById('tdisplay').textContent=`${t.toFixed(3)}s / ${animFrameData.dur.toFixed(3)}s`; } document.getElementById('tl').addEventListener('input',function(){ if(!animFrameData) return; currentTime=(this.value/1000)*animFrameData.dur; drawFrame(currentTime); }); document.getElementById('btn-play').addEventListener('click',function(){ if(!animFrameData) return; isPlaying=!isPlaying; this.textContent=isPlaying?'⏸ 暂停':'▶ 播放'; if(isPlaying){playOffset=currentTime;playStart=performance.now();requestAnimationFrame(loop);} }); function loop(ts){ if(!isPlaying) return; const el=(ts-playStart)/1000,dur=animFrameData?.dur||1; currentTime=playOffset+el; if(currentTime>=dur){currentTime%=dur;playOffset=0;playStart=ts;} document.getElementById('tl').value=(currentTime/dur)*1000; drawFrame(currentTime); requestAnimationFrame(loop); } document.getElementById('show-phys').addEventListener('change',()=>drawFrame(currentTime)); document.getElementById('show-orig').addEventListener('change',()=>drawFrame(currentTime)); document.getElementById('show-tex').addEventListener('change',()=>drawFrame(currentTime)); function resizeCanvas(){ const wr=document.querySelector('.canvas-wrap'); cv.width=Math.max(360,wr.clientWidth-24); cv.height=Math.max(260,wr.clientHeight-24); viewCache=null; if(animFrameData) drawFrame(currentTime); } window.addEventListener('resize',resizeCanvas); resizeCanvas(); // ── Auto-load from localStorage (when redirected from 3.x tool) ── (function tryAutoLoad(){ try{ const raw=localStorage.getItem('spine_pending_json'); const name=localStorage.getItem('spine_pending_name')||'file.json'; if(!raw) return; localStorage.removeItem('spine_pending_json'); localStorage.removeItem('spine_pending_name'); originalJsonText=raw; spineData=JSON.parse(raw); processData(spineData,{name}); log(`✓ 自动加载: ${name}`,'ok'); }catch(e){} })(); // ═══════════════════════════════════════════════════ // ATLAS UPLOAD & PARSER // ═══════════════════════════════════════════════════ function updateAtlasStatus(){ const st=document.getElementById('tx-status'); const clr=document.getElementById('tx-clear'); const pngDrop=document.getElementById('tx-png-drop'); const atlasDrop=document.getElementById('tx-atlas-drop'); if(atlasPngLoaded&&atlasFileLoaded){ atlasLoaded=true; const count=Object.keys(atlasRegions).length; st.className='tx-status ok'; st.textContent=`✓ 纹理已就绪 · ${count} 个区域`; clr.style.display=''; pngDrop.classList.add('loaded'); atlasDrop.classList.add('loaded'); document.getElementById('show-tex').checked=true; drawFrame(currentTime); } else { atlasLoaded=false; st.className='tx-status'; const parts=[]; if(!atlasPngLoaded) parts.push('PNG图集'); if(!atlasFileLoaded) parts.push('.alas文件'); st.textContent='等待上传: '+parts.join(' + '); } } function setupDrop(dropId,inputId,handler){ const drop=document.getElementById(dropId),inp=document.getElementById(inputId); drop.addEventListener('click',()=>inp.click()); drop.addEventListener('dragover',e=>{e.preventDefault();drop.style.borderColor='var(--acc)';}); drop.addEventListener('dragleave',()=>drop.style.borderColor=''); drop.addEventListener('drop',e=>{e.preventDefault();drop.style.borderColor='';if(e.dataTransfer.files[0])handler(e.dataTransfer.files[0]);}); inp.addEventListener('change',e=>{if(e.target.files[0])handler(e.target.files[0]);}); } setupDrop('tx-png-drop','tx-png-input',file=>{ const url=URL.createObjectURL(file); const img=new Image(); img.onload=()=>{atlasImage=img;atlasPngLoaded=true;updateAtlasStatus();URL.revokeObjectURL(url);}; img.onerror=()=>{document.getElementById('tx-status').className='tx-status err';document.getElementById('tx-status').textContent='PNG 加载失败';}; img.src=url; }); setupDrop('tx-atlas-drop','tx-atlas-input',file=>{ const r=new FileReader(); r.onload=e=>{ try{atlasRegions=parseAtlas(e.target.result);atlasFileLoaded=true;updateAtlasStatus();} catch(err){document.getElementById('tx-status').className='tx-status err';document.getElementById('tx-status').textContent='atlas 解析失败: '+err.message;} }; r.readAsText(file); }); document.getElementById('tx-clear').addEventListener('click',()=>{ atlasImage=null;atlasRegions={};atlasLoaded=false;atlasPngLoaded=false;atlasFileLoaded=false; document.getElementById('tx-png-drop').classList.remove('loaded'); document.getElementById('tx-atlas-drop').classList.remove('loaded'); document.getElementById('tx-clear').style.display='none'; document.getElementById('tx-status').className='tx-status'; document.getElementById('tx-status').textContent='未加载 — 上传图集 PNG + .atlas 文件以显示纹理预览'; document.getElementById('show-tex').checked=false; drawFrame(currentTime); }); // ═══════════════════════════════════════════════════ // ATLAS PARSER — handles both Spine 3.x and 4.x formats // // 3.x: region sub-keys are INDENTED (leading whitespace) // 4.x: region sub-keys are NOT indented — detected as key:value after a region name // // Universal strategy: track "inside region" state. // Once we see a region name (non-kv, non-filename), all subsequent // key:value lines belong to that region until blank line or new region. // ═══════════════════════════════════════════════════ function parseAtlas(text){ const regions={}; const pageKeys=new Set(['size','format','filter','repeat','pma','type','atlas']); function applyProp(reg,k,v){ const nums=()=>v.split(/\s*,\s*/).map(Number); if(k==='rotate'){reg.rotate=(v==='true'||v==='90');} else if(k==='bounds'){const n=nums();reg.x=n[0]||0;reg.y=n[1]||0;reg.w=n[2]||0;reg.h=n[3]||0;} else if(k==='xy'){const n=nums();reg.x=n[0]||0;reg.y=n[1]||0;} else if(k==='size'){const n=nums();reg.w=n[0]||0;reg.h=n[1]||0;} else if(k==='orig'){const n=nums();reg.origW=n[0]||reg.w;reg.origH=n[1]||reg.h;} else if(k==='offsets'){const n=nums();reg.offsetX=n[0]||0;reg.offsetY=n[1]||0;if(n.length>=4){reg.origW=n[2]||reg.w;reg.origH=n[3]||reg.h;}} else if(k==='offset'){const n=nums();reg.offsetX=n[0]||0;reg.offsetY=n[1]||0;} } function newReg(){return{x:0,y:0,w:0,h:0,origW:0,origH:0,offsetX:0,offsetY:0,rotate:false};} const lines=text.split(/\r?\n/); let curName=null, curReg=null; for(let i=0;i{ const wt=getWT(bone.name,t,ab); wts[bone.name]={wx:wt.x,wy:wt.y,wrot:wt.rot}; }); } if(!showP) return wts; // Override chain bones with simulated transforms (cascading within each chain) chainGroups.forEach(group=>{ const simData=group.simResults?.[currentAnim]; if(!simData) return; const anchor=boneMap[group.bones[0]]?.parent; let origX,origY,parRot; if(anchor&&boneMap[anchor]){ const awt=wts[anchor]||{wx:0,wy:0,wrot:0}; const ar=awt.wrot*D, aL=boneMap[anchor]?.length||0; origX=awt.wx+Math.cos(ar)*aL; origY=awt.wy+Math.sin(ar)*aL; parRot=awt.wrot; } else {origX=0;origY=0;parRot=0;} group.bones.forEach(name=>{ const bone=boneMap[name]; if(!bone) return; const bLen=bone?.length||1; const bab=ab[name]; let simRot; if(simData[name]){ const physAngle=interp(simData[name],t,'angle'); simRot=parRot+(bone?.rotation||0)+physAngle; } else { const lRot=(bone?.rotation||0)+(bab?.rotate?interp(bab.rotate,t,'angle'):0); simRot=parRot+lRot; } const r=simRot*D; wts[name]={wx:origX,wy:origY,wrot:simRot}; origX+=Math.cos(r)*bLen; origY+=Math.sin(r)*bLen; parRot=simRot; }); }); return wts; } // ═══════════════════════════════════════════════════ // MESH HELPERS // ═══════════════════════════════════════════════════ // Parse Spine 3.x mesh vertices into per-vertex bone influences. // Non-weighted: vertices = [x,y, x,y, ...] (length == 2*numVerts) // Weighted: vertices = [boneCount, boneIdx,bx,by,weight, ...] per vertex function parseMeshVertices(att){ const uvs=att.uvs; const n=uvs.length/2; const verts=att.vertices; const influences=[]; if(verts.length===n*2){ // Plain (non-weighted) mesh for(let i=0;is.name==='default')||skins[0]; skinAtts=def?.attachments||{}; } else if(skins&&typeof skins==='object'){ skinAtts=skins.default||skins[Object.keys(skins)[0]]||{}; } const slots=spineData.slots||[]; const D=Math.PI/180; // Respect Spine's slot order (bottom → top) slots.forEach(slot=>{ const boneName=slot.bone; const attName=slot.attachment; if(!attName||!boneName) return; const bwt=boneWTs[boneName]; if(!bwt) return; const slotAtts=skinAtts[slot.name]; if(!slotAtts) return; const att=slotAtts[attName]; if(!att) return; const atype=att.type||'region'; // Find atlas region — exact name first, then strip path prefix let region=atlasRegions[attName]; if(!region){ const short=attName.replace(/^.*\//,''); region=atlasRegions[short]; } // For linkedmesh, try looking up the parent path if(!region&&att.path){ region=atlasRegions[att.path]; } if(!region&&att.parent){ region=atlasRegions[att.parent]; } if(!region) return; if(atype==='mesh'||atype==='skinnedmesh'||atype==='weightedmesh'){ // ── Mesh attachment (weighted or plain) ── drawMeshAttachment(att,region,boneWTs,boneName,v); } else if(atype==='linkedmesh'){ // ── Linked mesh: inherit UVs/triangles from parent ── // Find parent mesh attachment in skin const parentName=att.parent||att.path; if(!parentName) return; let parentAtt=null; for(const sName in skinAtts){ if(skinAtts[sName]?.[parentName]){parentAtt=skinAtts[sName][parentName];break;} } if(!parentAtt||(parentAtt.type||'region')==='region') return; // Use parent mesh geometry with this slot's bone drawMeshAttachment(parentAtt,region,boneWTs,boneName,v); } else if(atype==='region'){ // ── Region attachment ── const attX=att.x||0, attY=att.y||0; const attRot=att.rotation||0; const attScX=att.scaleX||1, attScY=att.scaleY||1; const bRad=bwt.wrot*D; const wx=bwt.wx+Math.cos(bRad)*attX-Math.sin(bRad)*attY; const wy=bwt.wy+Math.sin(bRad)*attX+Math.cos(bRad)*attY; const wRot=bwt.wrot+attRot; const cp=w2c(wx,wy,v); const sc=v.sc; ctx.save(); ctx.translate(cp.x,cp.y); ctx.rotate(-wRot*D); ctx.scale(attScX,attScY); const origW=region.origW||region.w; const origH=region.origH||region.h; const trimW=region.w, trimH=region.h; const ox=(region.offsetX-(origW-trimW)/2)*sc; const oy=-(region.offsetY-(origH-trimH)/2)*sc; if(region.rotate){ ctx.rotate(Math.PI/2); const dw=trimH*sc, dh=trimW*sc; ctx.drawImage(atlasImage,region.x,region.y,region.w,region.h,-dh/2-oy,-dw/2+ox,dh,dw); } else { const dw=trimW*sc, dh=trimH*sc; ctx.drawImage(atlasImage,region.x,region.y,region.w,region.h,-dw/2+ox,-dh/2+oy,dw,dh); } ctx.restore(); } }); } // ═══════════════════════════════════════════════════ // VERSION REDIRECT HELPERS // ═══════════════════════════════════════════════════ function closeVerModal(){ document.getElementById('ver-modal').style.display='none'; } function switchToV3(){ if(originalJsonText){ try{ localStorage.setItem('spine_pending_json', originalJsonText); localStorage.setItem('spine_pending_name', document.getElementById('fi-name').textContent||'file.json'); }catch(e){} } window.open('spine-follow-3x.html','_blank'); closeVerModal(); } // ═══════════════════════════════════════════════════ // EXPORT 4.x — merge all groups, output Spine 4.x keyframe format // ═══════════════════════════════════════════════════ document.getElementById('btn-exp').addEventListener('click',exportJSON); function exportJSON(){ if(!spineData) return; const simmedGroups=chainGroups.filter(g=>g.simResults!==null); if(simmedGroups.length===0){setStatus('没有可导出的模拟数据','err');return;} // Parse fresh copy from original JSON text to preserve native 4.x structure const out = originalJsonText ? JSON.parse(originalJsonText) : JSON.parse(JSON.stringify(spineData)); // Collect all anim names across all groups const animNames=new Set(); simmedGroups.forEach(g=>Object.keys(g.simResults).forEach(a=>animNames.add(a))); let totalKF=0; animNames.forEach(aName=>{ if(!out.animations[aName]) return; if(!out.animations[aName].bones) out.animations[aName].bones={}; simmedGroups.forEach(g=>{ const simAnim=g.siResults[aName]; if(!simAnim) return; Object.entries(simAnim).forEach(([boneName,kfs])=>{ if(!out.animations[aName].bones[boneName]) out.animations[aName].bones[boneName]={}; const ba=out.animations[aName].boes[boneName]; if(opts.keeporig&&ba.rotate) ba._rotate_orig=ba.rotate; if(opts.overwrite||!ba.rotate){ // Output Spine 4.x format: use 'value' (not 'angle'), omit time/value=0 on first kf ba.rotate=kfs.map((kf,i)=>{ const entry={}; if(kf.time!==0) entry.time=kf.time; if(kf.angle!==0) entry.value=kf.angle; // 4.x uses 'value' return entry; }); // Always include last keyframe explicitly const last=kfs[kfs.length-1]; if(ba.rotate.length>0){ const lastEntry=ba.rotate[ba.rotate.length-1]; if(lastEntry.time===undefined) lastEntry.time=last.time; } totalKF+=kfs.length; } }); }); }); const str=JSON.stringify(out,null,2); const blob=new Blob([str],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; const base=(document.getElementById('fi-name').textContent||'spine').replace('.json',''); a.download=base+'_spring.json'; a.click(); URL.revokeObjectURL(url); log(`↓ 导出完成 [4.x格式]: ${a.download}`,'ok'); log(` 共 ${simmedGroups.length} 条链 · ${animNames.size} 个动画 · ${totalKF} 关键帧`,'info'); setStatus('导出完成: '+a.download,'ok'); }