Files
HexTrails/src/board-viewport.js
T

39 lines
3.6 KiB
JavaScript
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.
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};
}