39 lines
3.6 KiB
JavaScript
39 lines
3.6 KiB
JavaScript
export class BoardViewport {
|
||
constructor(){this.reset();}
|
||
reset(){this.scale=1;this.x=0;this.y=0;}
|
||
fit(view,width,height){
|
||
this.bounds=view.bounds;
|
||
this.maxScale=this.bounds?Math.max(1,Math.min(4,height/(this.bounds.bottom-this.bounds.top))):4;
|
||
this.zoom(1,width/2,height/2,width,height);
|
||
return this.apply(view);
|
||
}
|
||
apply(view){return {...view,size:view.size*this.scale,ox:view.ox*this.scale+this.x,oy:view.oy*this.scale+this.y};}
|
||
constrain(width,height){this.x=Math.max(width*(1-this.scale),Math.min(0,this.x));this.y=this.bounds?Math.max(-this.bounds.top*this.scale,Math.min(height-this.bounds.bottom*this.scale,this.y)):Math.max(height*(1-this.scale),Math.min(0,this.y));}
|
||
zoom(factor,x,y,width,height){const next=Math.max(1,Math.min(this.maxScale??4,this.scale*factor)),ratio=next/this.scale;this.x=x-(x-this.x)*ratio;this.y=y-(y-this.y)*ratio;this.scale=next;this.constrain(width,height);}
|
||
pan(x,y,width,height){this.x+=x;this.y+=y;this.constrain(width,height);}
|
||
}
|
||
export function attachBoardViewport(canvas,{changed,tap,drag}){
|
||
const camera=new BoardViewport(),pointers=new Map();let gesture=false,start=null,last=null;
|
||
const point=e=>{const r=canvas.getBoundingClientRect();return {x:e.clientX-r.left,y:e.clientY-r.top};};
|
||
const size=()=>canvas.getBoundingClientRect();
|
||
const pair=()=>{const [a,b]=[...pointers.values()];return {x:(a.x+b.x)/2,y:(a.y+b.y)/2,d:Math.max(1,Math.hypot(a.x-b.x,a.y-b.y))};};
|
||
canvas.addEventListener('pointerdown',e=>{const p=point(e);if(!pointers.size){gesture=false;start=p;}pointers.set(e.pointerId,p);canvas.setPointerCapture(e.pointerId);last=pointers.size===2?pair():p;if(pointers.size>1)gesture=true;});
|
||
canvas.addEventListener('pointermove',e=>{
|
||
if(!pointers.has(e.pointerId))return;const p=point(e),old=pointers.get(e.pointerId),r=size();pointers.set(e.pointerId,p);
|
||
if(pointers.size===2){const next=pair();camera.zoom(next.d/last.d,last.x,last.y,r.width,r.height);camera.pan(next.x-last.x,next.y-last.y,r.width,r.height);last=next;gesture=true;changed();}
|
||
else if(pointers.size===1&&camera.scale>1){if(gesture||Math.hypot(p.x-start.x,p.y-start.y)>8){gesture=true;camera.pan(p.x-old.x,p.y-old.y,r.width,r.height);changed();}}
|
||
});
|
||
function end(e,cancel=false){if(!pointers.has(e.pointerId))return;const p=point(e);pointers.delete(e.pointerId);
|
||
if(!pointers.size){if(!cancel&&!gesture&&start){if(Math.hypot(p.x-start.x,p.y-start.y)>14)drag?.(start,p);else tap(p);}start=null;last=null;gesture=false;}
|
||
else{gesture=true;last=pointers.size===2?pair():[...pointers.values()][0];}
|
||
}
|
||
canvas.addEventListener('pointerup',e=>end(e));canvas.addEventListener('pointercancel',e=>end(e,true));
|
||
canvas.addEventListener('lostpointercapture',e=>{if(pointers.has(e.pointerId))end(e,true);});
|
||
canvas.addEventListener('wheel',e=>{if(!e.ctrlKey&&!e.metaKey)return;e.preventDefault();const p=point(e),r=size();camera.zoom(Math.exp(-e.deltaY*.01),p.x,p.y,r.width,r.height);changed();},{passive:false});
|
||
const toolbar=document.createElement('div');toolbar.className='board-zoom';toolbar.innerHTML='<button type="button" aria-label="缩小棋盘">−</button><button type="button" aria-label="重置棋盘视图">100%</button><button type="button" aria-label="放大棋盘">+</button><span>双指缩放 · 放大后拖动</span>';canvas.parentElement.insertBefore(toolbar,canvas.nextSibling);
|
||
const buttons=toolbar.querySelectorAll('button');
|
||
const update=()=>{buttons[1].textContent=`${Math.round(camera.scale*100)}%`;};
|
||
buttons.forEach((b,i)=>b.onclick=()=>{const r=size();if(i===1)camera.reset();else camera.zoom(i===0?1/1.35:1.35,r.width/2,r.height/2,r.width,r.height);changed();update();});
|
||
return {camera,update};
|
||
}
|