105 lines
19 KiB
HTML
105 lines
19 KiB
HTML
<!doctype html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="theme-color" content="#101b2a">
|
||
<title>R2C 일반도 · 3D 뷰어</title>
|
||
<style>
|
||
:root{color-scheme:dark;--bg:#101a28;--panel:#172435;--panel2:#1b2b3e;--line:#2d4055;--muted:#95a7b9;--text:#edf3f8;--cyan:#65d3d0;--amber:#f1bd70;--red:#ff8278}
|
||
*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);font:14px/1.5 Inter,"Noto Sans KR",system-ui,-apple-system,"Segoe UI",sans-serif}button,a,input{font:inherit}button{color:inherit}
|
||
.app{min-height:100vh;display:grid;grid-template-rows:68px minmax(0,1fr);padding:0 26px 24px;max-width:1680px;margin:auto}.topbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);gap:16px}.brand{display:flex;align-items:center;gap:12px}.mark{height:34px;width:34px;border:1px solid #47707a;border-radius:10px;display:grid;place-items:center;color:var(--cyan);font-weight:800;letter-spacing:-1px;background:linear-gradient(140deg,#173d4b,#172537)}.brand-title{font-weight:700;letter-spacing:.01em}.brand-sub{color:var(--muted);font-size:11px;margin-top:1px}.top-actions{display:flex;align-items:center;gap:9px}.pill{border:1px solid var(--line);border-radius:99px;padding:6px 11px;color:#c8d5df;font-size:11px;white-space:nowrap}.pill .dot{display:inline-block;width:6px;height:6px;background:#65d3a2;border-radius:50%;margin:0 7px 1px 1px}.pdf-link{display:flex;align-items:center;gap:7px;color:var(--text);text-decoration:none;padding:8px 12px;border-radius:8px;border:1px solid var(--line);background:var(--panel);font-size:12px}.pdf-link:hover{border-color:#5b758d;background:var(--panel2)}
|
||
main{display:grid;grid-template-columns:minmax(0,1fr) 314px;gap:18px;padding-top:18px;min-height:0}.stage-wrap{min-width:0;display:grid;grid-template-rows:auto minmax(400px,1fr) auto;gap:12px}.stage-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:20px}.eyebrow{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--cyan);font-weight:700}.stage-heading h1{font-size:20px;line-height:1.25;margin:3px 0 0;letter-spacing:-.025em}.stage-note{font-size:11px;color:var(--muted);text-align:right}.canvas-shell{position:relative;border:1px solid #2b3e51;border-radius:12px;overflow:hidden;min-height:400px;background:#e7edf0;box-shadow:0 20px 60px #0002}.canvas-shell:after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#fff2,transparent 18%,transparent 78%,#23394d12)}canvas{display:block;width:100%;height:100%;touch-action:none;cursor:grab}canvas:active{cursor:grabbing}.canvas-hud{position:absolute;z-index:1;left:16px;top:14px;display:flex;gap:7px;pointer-events:none}.hud-tag{background:#f7fafbe8;border:1px solid #cfdae0;color:#415365;padding:5px 8px;border-radius:6px;font-size:10px;font-weight:650}.canvas-hint{position:absolute;z-index:1;right:16px;bottom:14px;color:#506376;font-size:10px;background:#f7fafbe8;border:1px solid #cfdae0;border-radius:6px;padding:5px 8px;pointer-events:none}.viewbar{display:flex;justify-content:space-between;align-items:center;gap:12px}.views{display:flex;gap:6px;flex-wrap:wrap}.view-btn,.tool-btn{border:1px solid var(--line);background:var(--panel);border-radius:7px;padding:7px 11px;font-size:11px;color:#bfccd7;cursor:pointer}.view-btn:hover,.tool-btn:hover{background:var(--panel2)}.view-btn.active{background:#17464b;border-color:#337b7b;color:#baf0e9}.tools{display:flex;gap:6px;align-items:center}.tool-btn{display:flex;align-items:center;gap:6px}.tool-btn.active{border-color:#397d79;color:#9ce4dc;background:#163c43}.tool-btn svg,.pdf-link svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
||
aside{display:flex;flex-direction:column;gap:12px;min-width:0}.card{border:1px solid var(--line);background:var(--panel);border-radius:10px;padding:15px}.card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.card-title{font-weight:680;font-size:12px}.card-kicker{color:var(--muted);font-size:10px}.status{color:var(--cyan);font-size:10px;background:#15383c;border:1px solid #255456;padding:3px 7px;border-radius:99px}.dims{display:grid;gap:7px}.dim-row{display:flex;justify-content:space-between;align-items:center;color:#c3d0da;font-size:11px}.dim-row strong{font-size:12px;color:#f2f6f8;font-variant-numeric:tabular-nums}.dim-row span{color:var(--muted)}.dim-row .guess{color:var(--amber)}.divider{height:1px;background:var(--line);margin:5px 0}.range-wrap{margin-top:13px}.range-top{display:flex;justify-content:space-between;color:#c3d0da;font-size:10px;margin-bottom:6px}.range-top output{color:var(--cyan);font-variant-numeric:tabular-nums}input[type=range]{appearance:none;width:100%;height:3px;background:#3a4b5f;border-radius:5px;outline:none}input[type=range]::-webkit-slider-thumb{appearance:none;width:13px;height:13px;border-radius:50%;background:var(--cyan);border:2px solid #163541;cursor:pointer}input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;background:var(--cyan);border:2px solid #163541;cursor:pointer}.materials{display:grid;gap:9px}.mat-row{display:flex;align-items:center;gap:9px;font-size:11px;color:#c6d3dd}.swatch{width:9px;height:9px;border-radius:3px;background:#bbc4c8;border:1px solid #d5dcde}.swatch.rebar{background:#e47a69;border-color:#f59c89}.swatch.dimline{background:#d69b4d;border-color:#f5cb7b}.mat-row small{margin-left:auto;color:var(--muted);font-size:10px}.info{background:#202d3a;border:1px solid #3e4b53}.info .card-title{display:flex;align-items:center;gap:7px}.info .icon{width:17px;height:17px;display:grid;place-items:center;border-radius:50%;font-size:11px;color:#2f2412;background:#eab969;font-weight:800}.info p{margin:0;color:#bdc9d2;font-size:10px;line-height:1.65}.info strong{color:#f1d29c;font-weight:650}.file-name{font-size:10px;color:#aab9c6;overflow-wrap:anywhere;margin-top:9px;padding-top:9px;border-top:1px solid #354454}.footer-note{color:#71869a;font-size:10px;margin:0 2px}.toast{position:absolute;left:50%;top:20px;transform:translate(-50%,-8px);opacity:0;transition:.2s;background:#142735;color:#bceee8;border:1px solid #38716e;border-radius:7px;padding:7px 11px;font-size:11px;pointer-events:none;z-index:3}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||
@media(max-width:950px){.app{padding:0 16px 18px}main{grid-template-columns:minmax(0,1fr) 270px;gap:12px}.stage-wrap{grid-template-rows:auto minmax(360px,1fr) auto}.canvas-shell{min-height:360px}}
|
||
@media(max-width:720px){.app{grid-template-rows:60px 1fr;padding:0 12px 14px}.brand-sub,.top-actions .pill{display:none}main{grid-template-columns:1fr;padding-top:13px}.stage-wrap{grid-template-rows:auto minmax(330px,55vh) auto}.canvas-shell{min-height:330px}.stage-heading h1{font-size:17px}.stage-note{font-size:10px;max-width:130px}.viewbar{align-items:flex-start;flex-direction:column}.tools{width:100%}aside{display:grid;grid-template-columns:1fr 1fr}.card.info{grid-column:1/-1}.card{padding:12px}.topbar{height:60px}}
|
||
@media(max-width:430px){aside{grid-template-columns:1fr}.card.info{grid-column:auto}.canvas-hint{right:9px;bottom:9px}.canvas-hud{left:9px;top:9px}.app{padding-inline:9px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<header class="topbar">
|
||
<div class="brand"><div class="mark">R2</div><div><div class="brand-title">도면 3D 뷰어</div><div class="brand-sub">R2C · 일반도 기반 형상 검토</div></div></div>
|
||
<div class="top-actions"><span class="pill"><i class="dot"></i>로컬 인터랙티브 모델</span><a class="pdf-link" href="./drawing/R2C_일반도_20260601.pdf" target="_blank" rel="noopener"><svg viewBox="0 0 24 24"><path d="M14 3h7v7M10 14 21 3M19 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h6"/></svg>원본 도면 열기</a></div>
|
||
</header>
|
||
<main>
|
||
<section class="stage-wrap">
|
||
<div class="stage-heading"><div><div class="eyebrow">STRUCTURAL MODEL / R2C</div><h1>R2C 일반도 3D 형상</h1></div><div class="stage-note">마우스로 회전 · 휠로 확대/축소</div></div>
|
||
<div class="canvas-shell"><canvas id="scene" aria-label="R2C 도면 기반 3D 모델"></canvas><div class="canvas-hud"><span class="hud-tag">RC BOX · SCHEMATIC</span><span class="hud-tag">단위 mm</span></div><div class="canvas-hint">드래그하여 모델을 회전하세요</div><div class="toast" id="toast"></div></div>
|
||
<div class="viewbar"><div class="views" role="group" aria-label="보기 방향"><button class="view-btn active" data-view="iso">등각</button><button class="view-btn" data-view="front">정면</button><button class="view-btn" data-view="side">측면</button><button class="view-btn" data-view="top">평면</button></div><div class="tools"><button class="tool-btn active" id="rebarBtn" aria-pressed="true"><svg viewBox="0 0 24 24"><path d="M5 19 19 5M4 12l8-8M12 20l8-8M4 17l3 3M17 4l3 3"/></svg>철근 표시</button><button class="tool-btn" id="resetBtn"><svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 2.6-6.4L3 8M3 3v5h5"/></svg>초기화</button></div></div>
|
||
</section>
|
||
<aside>
|
||
<section class="card"><div class="card-head"><span class="card-title">주요 치수</span><span class="status">도면 확인</span></div><div class="dims"><div class="dim-row"><span>외형 폭 <span>W</span></span><strong>2,500 mm</strong></div><div class="dim-row"><span>외형 높이 <span>H</span></span><strong>1,620 mm</strong></div><div class="dim-row"><span>벽체 두께 <span>T</span></span><strong>120 mm</strong></div><div class="divider"></div><div class="dim-row"><span>모델 깊이 <span class="guess">L · 시각화값</span></span><strong id="depthReadout">1,600 mm</strong></div></div><div class="range-wrap"><div class="range-top"><label for="depth">깊이 조절</label><output id="depthOutput">1,600 mm</output></div><input id="depth" type="range" min="800" max="3200" step="100" value="1600"></div></section>
|
||
<section class="card"><div class="card-head"><span class="card-title">표현 요소</span><span class="card-kicker">LEGEND</span></div><div class="materials"><div class="mat-row"><i class="swatch"></i>철근 콘크리트<small>회색</small></div><div class="mat-row"><i class="swatch rebar"></i>철근 배근 개략<small>표시 전환 가능</small></div><div class="mat-row"><i class="swatch dimline"></i>치수 기준 형상<small>mm</small></div></div></section>
|
||
<section class="card info"><div class="card-head"><span class="card-title"><i class="icon">i</i>모델 참고</span></div><p>도면의 <strong>2,500 × 1,620 mm</strong> 외형 치수와 <strong>120 mm</strong> 치수 표기를 반영한 개략 모델입니다. 도면에서 깊이를 특정하기 어려워 조절값으로 제공하며, 철근 표시는 배근 형상을 이해하기 위한 시각화입니다.</p><div class="file-name">R2C_일반도_20260601.pdf</div></section>
|
||
<p class="footer-note">형상 검토용 시각화 · 제작·시공 전 원본 도면과 구조 검토 자료를 확인하세요.</p>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
<script>
|
||
(()=>{
|
||
const canvas=document.getElementById('scene'),ctx=canvas.getContext('2d');
|
||
const W=2500,H=1620,T=120;
|
||
let L=1600, yaw=-.72,pitch=.48,zoom=1,showRebar=true,drag=null;
|
||
const fmt=n=>new Intl.NumberFormat('ko-KR').format(n);
|
||
function resize(){const r=canvas.getBoundingClientRect(),d=Math.min(devicePixelRatio||1,2);canvas.width=Math.round(r.width*d);canvas.height=Math.round(r.height*d);ctx.setTransform(d,0,0,d,0,0);draw()}
|
||
const V=(x,y,z)=>[x,y,z];
|
||
function quad(a,b,c,d,color,edge='#67737b',normal){return {pts:[a,b,c,d],color,edge,normal}}
|
||
function faces(){
|
||
const x=W/2,y=L/2,z=H,ix=x-T,iy=y,iz0=T,iz1=H-T, f=[];
|
||
const cTop='#c4cdd0',cFront='#d2d9da',cSide='#aeb9be',cInner='#99a7ad',cBottom='#aab5b9';
|
||
// outside shell surfaces
|
||
f.push(quad(V(-x,-y,z),V(x,-y,z),V(x,y,z),V(-x,y,z),cTop,'#76838a',[0,0,1]));
|
||
f.push(quad(V(-x,-y,0),V(-x,y,0),V(x,y,0),V(x,-y,0),cBottom,'#68767d',[0,0,-1]));
|
||
f.push(quad(V(-x,-y,0),V(-x,-y,z),V(-x,y,z),V(-x,y,0),cSide,'#66747c',[-1,0,0]));
|
||
f.push(quad(V(x,-y,0),V(x,y,0),V(x,y,z),V(x,-y,z),'#b8c2c6','#6d7980',[1,0,0]));
|
||
// tunnel interior
|
||
f.push(quad(V(-ix,-y,iz1),V(ix,-y,iz1),V(ix,y,iz1),V(-ix,y,iz1),'#b8c2c5','#6c7a81',[0,0,-1]));
|
||
f.push(quad(V(-ix,-y,iz0),V(-ix,y,iz0),V(ix,y,iz0),V(ix,-y,iz0),'#a9b5b9','#68777d',[0,0,1]));
|
||
f.push(quad(V(-ix,-y,iz0),V(-ix,-y,iz1),V(-ix,y,iz1),V(-ix,y,iz0),cInner,'#64727a',[1,0,0]));
|
||
f.push(quad(V(ix,-y,iz0),V(ix,y,iz0),V(ix,y,iz1),V(ix,-y,iz1),'#9eacb1','#64727a',[-1,0,0]));
|
||
// visible entrance ring at front
|
||
f.push(quad(V(-x,-y,z),V(x,-y,z),V(ix,-y,iz1),V(-ix,-y,iz1),cFront,'#68767d',[0,-1,0]));
|
||
f.push(quad(V(-x,-y,0),V(-ix,-y,iz0),V(ix,-y,iz0),V(x,-y,0),'#bdc7ca','#68767d',[0,-1,0]));
|
||
f.push(quad(V(-x,-y,0),V(-x,-y,z),V(-ix,-y,iz1),V(-ix,-y,iz0),'#c8d1d3','#68767d',[0,-1,0]));
|
||
f.push(quad(V(x,-y,0),V(ix,-y,iz0),V(ix,-y,iz1),V(x,-y,z),'#c2cccf','#68767d',[0,-1,0]));
|
||
// rear ring, visible as a shaded face through tunnel
|
||
f.push(quad(V(-x,y,z),V(-ix,y,iz1),V(ix,y,iz1),V(x,y,z),'#b6c0c3','#77838a',[0,1,0]));
|
||
f.push(quad(V(-x,y,0),V(x,y,0),V(ix,y,iz0),V(-ix,y,iz0),'#9eaaaf','#77838a',[0,1,0]));
|
||
f.push(quad(V(-x,y,0),V(-ix,y,iz0),V(-ix,y,iz1),V(-x,y,z),'#a9b5b9','#77838a',[0,1,0]));
|
||
f.push(quad(V(x,y,0),V(x,y,z),V(ix,y,iz1),V(ix,y,iz0),'#a5b1b6','#77838a',[0,1,0]));
|
||
return f;
|
||
}
|
||
function rot(p){let [x,y,z]=p;const cy=Math.cos(yaw),sy=Math.sin(yaw),cp=Math.cos(pitch),sp=Math.sin(pitch);const x1=x*cy-y*sy,y1=x*sy+y*cy;return [x1,y1*cp-z*sp,y1*sp+z*cp]}
|
||
function projection(){const r=canvas.getBoundingClientRect(),scale=Math.min(r.width/(W+L*.68),r.height/(H+L*.5))*1.14*zoom;return {cx:r.width*.5,cy:r.height*.53,scale}}
|
||
function project(p,P){const [x,y,z]=rot(p);return [P.cx+x*P.scale,P.cy-(z-y*.45)*P.scale]}
|
||
function shade(hex,n){const l=Math.max(.72,Math.min(1.12,.88+(-n[0]*.13-n[1]*.17+n[2]*.22)));const h=hex.replace('#',''),v=[0,2,4].map(i=>Math.min(255,Math.round(parseInt(h.slice(i,i+2),16)*l)));return `rgb(${v.join(',')})`}
|
||
function drawGrid(P){const r=canvas.getBoundingClientRect();ctx.fillStyle='#e7edf0';ctx.fillRect(0,0,r.width,r.height);ctx.save();ctx.strokeStyle='#d5dfe3';ctx.lineWidth=1;const extent=4200,step=400;for(let x=-extent;x<=extent;x+=step){const a=project([x,-extent,-4],P),b=project([x,extent,-4],P);ctx.beginPath();ctx.moveTo(...a);ctx.lineTo(...b);ctx.stroke()}for(let y=-extent;y<=extent;y+=step){const a=project([-extent,y,-4],P),b=project([extent,y,-4],P);ctx.beginPath();ctx.moveTo(...a);ctx.lineTo(...b);ctx.stroke()}ctx.restore()}
|
||
function line3(a,b,P,color,width=1){const A=project(a,P),B=project(b,P);ctx.beginPath();ctx.moveTo(...A);ctx.lineTo(...B);ctx.strokeStyle=color;ctx.lineWidth=width;ctx.stroke()}
|
||
function drawRebar(P){const rebar='#d97567',thin=2;
|
||
// perimeter bars following the two opening rings
|
||
for(const y of [-L/2+T*.46,L/2-T*.46]){
|
||
const x=W/2-T*.34,z0=T*.48,z1=H-T*.48;
|
||
const corners=[V(-x,y,z0),V(x,y,z0),V(x,y,z1),V(-x,y,z1)];
|
||
for(let i=0;i<4;i++)line3(corners[i],corners[(i+1)%4],P,rebar,thin);
|
||
}
|
||
// longitudinal bars at slab edges and sidewalls
|
||
for(const xx of [-W/2+T*.46,W/2-T*.46])for(const zz of [T*.47,H-T*.47])line3(V(xx,-L/2+T*.46,zz),V(xx,L/2-T*.46,zz),P,rebar,thin);
|
||
for(let i=1;i<=5;i++){const x=-W/2+(W*i/6);line3(V(x,-L/2+T*.46,T*.48),V(x,-L/2+T*.46,H-T*.48),P,'#cb7165',1.25)}
|
||
// simple ties along the visible near side of the two walls
|
||
for(let i=1;i<8;i++){const yy=-L/2+T+(L-2*T)*i/8;line3(V(-W/2+T*.42,yy,T*.42),V(-W/2+T*.42,yy,H-T*.42),'#d8897d',1);line3(V(W/2-T*.42,yy,T*.42),V(W/2-T*.42,yy,H-T*.42),'#d8897d',1)}
|
||
}
|
||
function dimension(a,b,label,offset,P){const A=project(a,P),B=project(b,P),ox=offset[0],oy=offset[1],C=[A[0]+ox,A[1]+oy],D=[B[0]+ox,B[1]+oy];ctx.save();ctx.strokeStyle='#b17935';ctx.fillStyle='#795421';ctx.lineWidth=1;ctx.setLineDash([4,3]);ctx.beginPath();ctx.moveTo(...A);ctx.lineTo(...C);ctx.moveTo(...B);ctx.lineTo(...D);ctx.setLineDash([]);ctx.moveTo(...C);ctx.lineTo(...D);ctx.stroke();const ang=Math.atan2(D[1]-C[1],D[0]-C[0]);for(const [p,sgn] of [[C,1],[D,-1]]){ctx.beginPath();ctx.moveTo(...p);ctx.lineTo(p[0]+Math.cos(ang+sgn*.45)*8,p[1]+Math.sin(ang+sgn*.45)*8);ctx.moveTo(...p);ctx.lineTo(p[0]+Math.cos(ang-sgn*.45)*8,p[1]+Math.sin(ang-sgn*.45)*8);ctx.stroke()}ctx.font='600 11px system-ui';ctx.textAlign='center';ctx.textBaseline='middle';const mx=(C[0]+D[0])/2,my=(C[1]+D[1])/2;ctx.fillStyle='#e7edf0';ctx.fillRect(mx-35,my-9,70,18);ctx.fillStyle='#795421';ctx.fillText(label,mx,my);ctx.restore()}
|
||
function draw(){const r=canvas.getBoundingClientRect();if(!r.width||!r.height)return;const P=projection();drawGrid(P);const fs=faces().map(face=>({face,depth:face.pts.reduce((s,p)=>s+rot(p)[1],0)/4})).sort((a,b)=>a.depth-b.depth);ctx.lineJoin='round';for(const {face}=fs){const pts=face.pts.map(p=>project(p,P));ctx.beginPath();ctx.moveTo(...pts[0]);for(let i=1;i<pts.length;i++)ctx.lineTo(...pts[i]);ctx.closePath();ctx.fillStyle=shade(face.color,face.normal);ctx.fill();ctx.strokeStyle=face.edge;ctx.lineWidth=1.1;ctx.stroke()}if(showRebar)drawRebar(P);dimension(V(-W/2,-L/2,0),V(W/2,-L/2,0),'2,500 mm',[0,36],P);dimension(V(-W/2,-L/2,0),V(-W/2,-L/2,H),'1,620 mm',[-48,-2],P)}
|
||
const views={iso:[-.72,.48],front:[0,.02],side:[-Math.PI/2,.02],top:[0,Math.PI/2-.02]};
|
||
document.querySelectorAll('.view-btn').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.view-btn').forEach(x=>x.classList.toggle('active',x===b));[yaw,pitch]=views[b.dataset.view];draw()}));
|
||
document.getElementById('depth').addEventListener('input',e=>{L=Number(e.target.value);const val=fmt(L)+' mm';document.getElementById('depthOutput').textContent=val;document.getElementById('depthReadout').textContent=val;draw()});
|
||
document.getElementById('rebarBtn').addEventListener('click',e=>{showRebar=!showRebar;e.currentTarget.classList.toggle('active',showRebar);e.currentTarget.setAttribute('aria-pressed',String(showRebar));draw()});
|
||
document.getElementById('resetBtn').addEventListener('click',()=>{[yaw,pitch]=views.iso;zoom=1;L=1600;document.getElementById('depth').value=L;document.getElementById('depthOutput').textContent='1,600 mm';document.getElementById('depthReadout').textContent='1,600 mm';draw();const t=document.getElementById('toast');t.textContent='기본 시점으로 돌아왔습니다';t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1300)});
|
||
canvas.addEventListener('pointerdown',e=>{drag={x:e.clientX,y:e.clientY,yaw,pitch};canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!drag)return;yaw=drag.yaw+(e.clientX-drag.x)*.008;pitch=Math.max(-.08,Math.min(1.48,drag.pitch+(e.clientY-drag.y)*.006));document.querySelectorAll('.view-btn').forEach(x=>x.classList.remove('active'));draw()});canvas.addEventListener('pointerup',()=>drag=null);canvas.addEventListener('pointercancel',()=>drag=null);canvas.addEventListener('wheel',e=>{e.preventDefault();zoom=Math.max(.55,Math.min(1.8,zoom*Math.exp(-e.deltaY*.001)));draw()},{passive:false});
|
||
new ResizeObserver(resize).observe(canvas);resize();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|