Files
Dansori_Characters/LeeSori_Live2D/07_Viewer/index.html
T
2026-07-04 10:34:46 +09:00

141 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LeeSori Rig Viewer — dance_idle (full-canvas)</title>
<style>
:root { color-scheme: dark; }
body { margin:0; font-family: system-ui, "Segoe UI", sans-serif; background:#0e1014; color:#e6e8ee; }
header { padding:10px 16px; border-bottom:1px solid #232733; display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
h1 { font-size:15px; margin:0; font-weight:600; color:#9ff0e0; }
.wrap { display:flex; gap:16px; padding:16px; flex-wrap:wrap; }
.stage { background:
linear-gradient(45deg,#1a1d24 25%,transparent 25%,transparent 75%,#1a1d24 75%),
linear-gradient(45deg,#1a1d24 25%,#15171d 25%,#15171d 75%,#1a1d24 75%);
background-size:24px 24px; background-position:0 0,12px 12px; border:1px solid #232733; border-radius:8px; }
canvas { display:block; }
.panel { min-width:250px; max-width:340px; font-size:13px; line-height:1.6; }
.row { display:flex; align-items:center; gap:8px; margin:6px 0; flex-wrap:wrap; }
button { background:#1d2a3a; color:#cfe6ff; border:1px solid #2c4a63; border-radius:6px; padding:6px 12px; cursor:pointer; font-size:13px; }
button:hover { background:#26405a; }
button.on { background:#2e9e8b; color:#04231d; border-color:#2e9e8b; }
label.file { display:inline-block; background:#232733; border:1px solid #37414f; border-radius:6px; padding:6px 10px; cursor:pointer; }
input[type=file]{ display:none; } input[type=range]{ width:130px; }
.muted { color:#8a93a6; font-size:12px; } code { background:#191c23; padding:1px 5px; border-radius:4px; color:#9ff0e0; }
</style>
</head>
<body>
<header>
<h1>이소리 Rig Viewer — <span id="clipName">dance_idle</span> <span class="muted">(full-canvas)</span></h1>
<div class="row">
<button id="playBtn" class="on">⏸ 일시정지</button>
<span class="muted">속도</span><input type="range" id="speed" min="0" max="2" step="0.05" value="1"><span id="speedV" class="muted">1.0×</span>
<button id="boneBtn">🦴 스켈레톤</button>
</div>
<div class="row">
<label class="file">📂 rig.json<input type="file" id="rigFile" accept=".json"></label>
<label class="file">📂 animation.json<input type="file" id="animFile" accept=".json"></label>
<label class="file">🖼 파츠 PNG(다중)<input type="file" id="imgFiles" accept="image/png" multiple></label>
</div>
</header>
<div class="wrap">
<div class="stage"><canvas id="cv" width="520" height="900"></canvas></div>
<div class="panel">
<p id="status">풀캔버스 파츠를 <code>../03_Assets/Parts/Images/</code> 에서 자동 로드합니다.</p>
<p class="muted">• 파츠는 520×900 풀캔버스라 <b>원점에 그리고 관절 피벗을 중심으로 회전</b>합니다.<br>
• 상대경로 자동 로드가 막히면(브라우저 보안) <b>파츠 PNG(다중)</b>으로 직접 지정.<br>
<b>스켈레톤</b>: 분홍 점 = 자동 산출한 관절 피벗.</p>
<p class="muted" id="loadInfo"></p>
</div>
</div>
<script>
const DEFAULT_RIG = {
"name":"LeeSori","canvas":{"width":520,"height":900},"imageBase":"../03_Assets/Parts/Images/","mode":"fullcanvas",
"bones":[
{"name":"pelvis","parent":null,"pivot":[259.6,363.4],"z":6,"image":"sori_part_pelvis.png"},
{"name":"chest","parent":"pelvis","pivot":[259.6,363.4],"z":8,"image":"sori_part_chest.png"},
{"name":"neck","parent":"chest","pivot":[262.0,229.3],"z":9,"image":"sori_part_neck.png"},
{"name":"head","parent":"neck","pivot":[259.8,209.3],"z":10,"image":"sori_part_head.png"},
{"name":"upperarm_r","parent":"chest","pivot":[174.2,287.2],"z":5,"image":"sori_part_upperarm_r.png"},
{"name":"forearm_r","parent":"upperarm_r","pivot":[137.3,358.0],"z":5,"image":"sori_part_forearm_r.png"},
{"name":"hand_r","parent":"forearm_r","pivot":[134.2,400.8],"z":5,"image":"sori_part_hand_r.png"},
{"name":"upperarm_l","parent":"chest","pivot":[346.0,286.5],"z":12,"image":"sori_part_upperarm_l.png"},
{"name":"forearm_l","parent":"upperarm_l","pivot":[382.9,357.6],"z":12,"image":"sori_part_forearm_l.png"},
{"name":"hand_l","parent":"forearm_l","pivot":[389.6,400.6],"z":13,"image":"sori_part_hand_l.png"},
{"name":"thigh_r","parent":"pelvis","pivot":[226.3,455.0],"z":4,"image":"sori_part_thigh_r.png"},
{"name":"shin_r","parent":"thigh_r","pivot":[233.3,609.1],"z":3,"image":"sori_part_shin_r.png"},
{"name":"foot_r","parent":"shin_r","pivot":[236.5,729.4],"z":2,"image":"sori_part_foot_r.png"},
{"name":"thigh_l","parent":"pelvis","pivot":[294.1,455.0],"z":4,"image":"sori_part_thigh_l.png"},
{"name":"shin_l","parent":"thigh_l","pivot":[286.9,609.1],"z":3,"image":"sori_part_shin_l.png"},
{"name":"foot_l","parent":"shin_l","pivot":[283.9,729.5],"z":2,"image":"sori_part_foot_l.png"}
]
};
const DEFAULT_ANIM = {
"name":"dance_idle","duration":2.0,"loop":true,"defaultEase":"sine",
"tracks":{
"pelvis":{"ty":[{"t":0,"v":0},{"t":0.5,"v":10},{"t":1.0,"v":0},{"t":1.5,"v":10},{"t":2.0,"v":0}],"tx":[{"t":0,"v":0},{"t":0.5,"v":7},{"t":1.0,"v":0},{"t":1.5,"v":-7},{"t":2.0,"v":0}],"rot":[{"t":0,"v":0},{"t":0.5,"v":2},{"t":1.0,"v":0},{"t":1.5,"v":-2},{"t":2.0,"v":0}]},
"chest":{"ty":[{"t":0,"v":0},{"t":0.5,"v":-3},{"t":1.0,"v":0},{"t":1.5,"v":-3},{"t":2.0,"v":0}],"tx":[{"t":0,"v":0},{"t":0.5,"v":-3},{"t":1.0,"v":0},{"t":1.5,"v":3},{"t":2.0,"v":0}],"rot":[{"t":0,"v":0},{"t":0.5,"v":-3},{"t":1.0,"v":0},{"t":1.5,"v":3},{"t":2.0,"v":0}]},
"neck":{"rot":[{"t":0,"v":0},{"t":0.5,"v":3},{"t":1.0,"v":0},{"t":1.5,"v":-3},{"t":2.0,"v":0}]},
"head":{"rot":[{"t":0,"v":0},{"t":0.5,"v":7},{"t":1.0,"v":0},{"t":1.5,"v":-7},{"t":2.0,"v":0}],"ty":[{"t":0,"v":0},{"t":0.5,"v":-2},{"t":1.0,"v":0},{"t":1.5,"v":-2},{"t":2.0,"v":0}]},
"upperarm_r":{"rot":[{"t":0,"v":0},{"t":0.5,"v":8},{"t":1.0,"v":0},{"t":1.5,"v":-4},{"t":2.0,"v":0}]},
"forearm_r":{"rot":[{"t":0,"v":0},{"t":0.5,"v":12},{"t":1.0,"v":0},{"t":1.5,"v":-6},{"t":2.0,"v":0}]},
"hand_r":{"rot":[{"t":0,"v":0},{"t":0.5,"v":6},{"t":1.0,"v":0},{"t":1.5,"v":-3},{"t":2.0,"v":0}]},
"upperarm_l":{"rot":[{"t":0,"v":0},{"t":0.5,"v":-4},{"t":1.0,"v":0},{"t":1.5,"v":8},{"t":2.0,"v":0}]},
"forearm_l":{"rot":[{"t":0,"v":0},{"t":0.5,"v":-6},{"t":1.0,"v":0},{"t":1.5,"v":12},{"t":2.0,"v":0}]},
"hand_l":{"rot":[{"t":0,"v":0},{"t":0.5,"v":-3},{"t":1.0,"v":0},{"t":1.5,"v":6},{"t":2.0,"v":0}]},
"thigh_r":{"rot":[{"t":0,"v":0},{"t":0.5,"v":3},{"t":1.0,"v":0},{"t":1.5,"v":-2},{"t":2.0,"v":0}]},
"shin_r":{"rot":[{"t":0,"v":0},{"t":0.5,"v":7},{"t":1.0,"v":0},{"t":1.5,"v":0},{"t":2.0,"v":0}]},
"thigh_l":{"rot":[{"t":0,"v":0},{"t":0.5,"v":-2},{"t":1.0,"v":0},{"t":1.5,"v":3},{"t":2.0,"v":0}]},
"shin_l":{"rot":[{"t":0,"v":0},{"t":0.5,"v":0},{"t":1.0,"v":0},{"t":1.5,"v":7},{"t":2.0,"v":0}]}
}
};
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
let rig, anim, bones, boneMap, playing=true, showBones=false, speed=1, t=0, last=performance.now();
const mul=(m,n)=>({a:m.a*n.a+m.c*n.b,b:m.b*n.a+m.d*n.b,c:m.a*n.c+m.c*n.d,d:m.b*n.c+m.d*n.d,e:m.a*n.e+m.c*n.f+m.e,f:m.b*n.e+m.d*n.f+m.f});
const T=(x,y)=>({a:1,b:0,c:0,d:1,e:x,f:y});
const R=deg=>{const r=deg*Math.PI/180,c=Math.cos(r),s=Math.sin(r);return{a:c,b:s,c:-s,d:c,e:0,f:0};};
const tp=(m,x,y)=>({x:m.a*x+m.c*y+m.e,y:m.b*x+m.d*y+m.f});
function ease(ty,x){return ty==='linear'?x:-(Math.cos(Math.PI*x)-1)/2;}
function sample(keys,t){ if(!keys||!keys.length)return 0; if(t<=keys[0].t)return keys[0].v; const n=keys.length; if(t>=keys[n-1].t)return keys[n-1].v;
for(let i=0;i<n-1;i++){ if(t>=keys[i].t&&t<=keys[i+1].t){ const k0=keys[i],k1=keys[i+1],sp=(k1.t-k0.t)||1e-6; return k0.v+(k1.v-k0.v)*ease(k0.e||anim.defaultEase||'sine',(t-k0.t)/sp);} } return keys[n-1].v; }
function setRig(r){ rig=r; cv.width=rig.canvas.width; cv.height=rig.canvas.height;
bones=rig.bones.map(b=>Object.assign({},b)); boneMap={}; bones.forEach(b=>{boneMap[b.name]=b; b._img=null;});
loadArt(); refreshInfo(); }
function setAnim(a){ anim=a; document.getElementById('clipName').textContent=a.name||'clip'; t=0; }
function loadArt(){ const base=rig.imageBase||'../03_Assets/Parts/Images/';
bones.forEach(b=>{ if(!b.image)return; const im=new Image(); im.onload=()=>{b._img=im;refreshInfo();}; im.onerror=()=>{b._img=null;refreshInfo();}; im.src=base+b.image; }); }
function refreshInfo(){ if(!bones)return; const ok=bones.filter(b=>b._img).length; document.getElementById('loadInfo').textContent=`파츠 이미지 로드: ${ok} / ${bones.length}`+(ok===0?' (없음 → 스켈레톤만)':''); }
function updateWorld(t){ bones.forEach(b=>{ const tr=anim.tracks[b.name]||{}; const rot=sample(tr.rot,t),tx=sample(tr.tx,t),ty=sample(tr.ty,t);
const jx=b.pivot[0],jy=b.pivot[1]; const local=mul(T(tx,ty),mul(T(jx,jy),mul(R(rot),T(-jx,-jy))));
b._world=b.parent?mul(boneMap[b.parent]._world,local):local; }); }
function render(){ ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,cv.width,cv.height);
bones.slice().sort((a,b)=>(a.z||0)-(b.z||0)).forEach(b=>{ if(!b._img)return; const w=b._world;
ctx.setTransform(w.a,w.b,w.c,w.d,w.e,w.f); ctx.drawImage(b._img,0,0); });
ctx.setTransform(1,0,0,1,0,0);
if(showBones){ bones.forEach(b=>{ const p=tp(b._world,b.pivot[0],b.pivot[1]);
if(b.parent){ const pp=boneMap[b.parent]; const q=tp(pp._world,pp.pivot[0],pp.pivot[1]); ctx.strokeStyle='rgba(255,90,130,.7)'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(q.x,q.y); ctx.lineTo(p.x,p.y); ctx.stroke(); } });
bones.forEach(b=>{ const p=tp(b._world,b.pivot[0],b.pivot[1]); ctx.fillStyle='#ff5a82'; ctx.beginPath(); ctx.arc(p.x,p.y,4,0,Math.PI*2); ctx.fill(); }); }
}
function frame(now){ const dt=(now-last)/1000; last=now; if(playing){ t+=dt*speed; if(t>=anim.duration)t%=anim.duration; } updateWorld(t); render(); requestAnimationFrame(frame); }
const playBtn=document.getElementById('playBtn'), boneBtn=document.getElementById('boneBtn');
playBtn.onclick=()=>{ playing=!playing; playBtn.textContent=playing?'⏸ 일시정지':'▶ 재생'; playBtn.classList.toggle('on',playing); };
boneBtn.onclick=()=>{ showBones=!showBones; boneBtn.classList.toggle('on',showBones); };
document.getElementById('speed').oninput=e=>{ speed=parseFloat(e.target.value); document.getElementById('speedV').textContent=speed.toFixed(2)+'×'; };
function readJson(f,cb){ const r=new FileReader(); r.onload=()=>{try{cb(JSON.parse(r.result));}catch(err){alert('JSON 파싱 실패: '+err.message);}}; r.readAsText(f); }
document.getElementById('rigFile').onchange=e=>{ if(e.target.files[0])readJson(e.target.files[0],setRig); };
document.getElementById('animFile').onchange=e=>{ if(e.target.files[0])readJson(e.target.files[0],setAnim); };
document.getElementById('imgFiles').onchange=e=>{ [...e.target.files].forEach(f=>{ const b=bones.find(x=>x.image===f.name); if(!b)return; const im=new Image(); im.onload=()=>{b._img=im;refreshInfo();}; im.src=URL.createObjectURL(f); }); };
setRig(DEFAULT_RIG); setAnim(DEFAULT_ANIM); requestAnimationFrame(frame);
</script>
</body>
</html>