Emberveil sourcemobile/public/touch.js
export function bindTouch(surface,actions,{setTimer=setTimeout,clearTimer=clearTimeout}={}) {
const fingers=new Map();let pointer=null,pinch=0;
const position=e=>{const r=surface.getBoundingClientRect();return {x:Math.floor((e.clientX-r.left)*765/r.width),y:Math.floor((e.clientY-r.top)*503/r.height)};};
function cancel(){if(pointer)clearTimer(pointer.timer);pointer=null;}
function distance(){const [a,b]=[...fingers.values()];return Math.hypot(a.x-b.x,a.y-b.y);}
surface.addEventListener('pointerdown',e=>{
if(!actions.ready())return;e.preventDefault();surface.setPointerCapture(e.pointerId);fingers.set(e.pointerId,{x:e.clientX,y:e.clientY});
if(fingers.size!==1){cancel();if(fingers.size===2)pinch=distance();return;}
const p=position(e);pointer={id:e.pointerId,...p,startX:e.clientX,startY:e.clientY,last:p,moved:false,long:false,right:e.button===2};
pointer.timer=setTimer(()=>{if(pointer&&!pointer.moved&&!pointer.right){pointer.long=true;actions.point(p.x,p.y,2);}},450);
});
surface.addEventListener('pointermove',e=>{
if(!fingers.has(e.pointerId))return;e.preventDefault();fingers.set(e.pointerId,{x:e.clientX,y:e.clientY});
if(fingers.size===2){const d=distance();if(Math.abs(d-pinch)>20){actions.zoom(d>pinch?-1:1);pinch=d;}return;}
if(!pointer||pointer.id!==e.pointerId)return;
if(Math.hypot(e.clientX-pointer.startX,e.clientY-pointer.startY)>5){
clearTimer(pointer.timer);pointer.moved=true;const p=position(e);actions.gesture(pointer.x,pointer.y,p.x-pointer.last.x,p.y-pointer.last.y);pointer.last=p;
}
});
surface.addEventListener('pointerup',e=>{
fingers.delete(e.pointerId);if(!pointer||pointer.id!==e.pointerId)return;clearTimer(pointer.timer);
if(!pointer.moved&&!pointer.long){const p=position(e);actions.point(p.x,p.y,pointer.right||actions.options()?2:1);actions.clearOptions();}pointer=null;
});
for(const name of ['pointercancel','lostpointercapture'])surface.addEventListener(name,e=>{fingers.delete(e.pointerId);if(pointer?.id===e.pointerId)cancel();});
surface.addEventListener('wheel',e=>{if(!actions.ready())return;e.preventDefault();const p=position(e);actions.wheel(p.x,p.y,Math.sign(e.deltaY));},{passive:false});
surface.addEventListener('contextmenu',e=>e.preventDefault());
return ()=>{fingers.clear();cancel();};
}
export function viewportSize(width,height,compact,typingHeight=0){
const availableWidth=Math.max(120,Math.min(980,width-18)-(compact?100:0));
const availableHeight=Math.max(100,height-(compact?42:180)-typingHeight);
const scale=Math.min(availableWidth/765,availableHeight/503,1.5);
return {width:765*scale,height:503*scale,scale};
}