import { Camera } from './camera.js'; /** Pointer/touch camera adapter shared by both comparison panes. */ export function bindCamera(surface, svg, bounds, onChange = () => {}) { const camera = new Camera(bounds, surface.clientWidth, surface.clientHeight); let frame = 0; const pointers = new Map(); function paint(notify = true) { if (!frame) frame = requestAnimationFrame(() => { frame = 0; svg.setAttribute('viewBox', camera.viewBox); }); if (notify) onChange(view); } const view = { camera, paint, pose() { return { center: camera.world(camera.width/2,camera.height/2), scale: camera.scale }; }, apply({center,scale}) { camera.scale=scale; camera.x=camera.width/2-center.x*scale; camera.y=camera.height/2-center.y*scale; paint(false); }, fit() { camera.fit(); paint(); }, focus(box) { const scale=Math.min(camera.width/Math.max(20,box.width+30),camera.height/Math.max(20,box.height+30),camera.fitScale*100); view.apply({center:{x:box.x+box.width/2,y:box.y+box.height/2},scale}); onChange(view); }, destroy() { cancelAnimationFrame(frame); observer.disconnect(); events.abort(); } }; const events = new AbortController(); const options={ signal:events.signal }; surface.addEventListener('wheel',e => { e.preventDefault();const rect=surface.getBoundingClientRect(); const delta=e.deltaY*(e.deltaMode===1?16:e.deltaMode===2?camera.height:1); camera.zoom(Math.exp(-Math.max(-400,Math.min(400,delta))*.002),e.clientX-rect.left,e.clientY-rect.top);paint(); },{...options,passive:false}); surface.addEventListener('pointerdown',e => { if(e.button!==0)return;surface.focus();surface.setPointerCapture(e.pointerId); pointers.set(e.pointerId,{x:e.clientX,y:e.clientY}); },options); function metrics() { const p=[...pointers.values()], rect=surface.getBoundingClientRect(); return { x:p.reduce((s,p)=>s+p.x,0)/p.length-rect.left,y:p.reduce((s,p)=>s+p.y,0)/p.length-rect.top, distance:p.length>1?Math.hypot(p[0].x-p[1].x,p[0].y-p[1].y):0 }; } surface.addEventListener('pointermove',e => { if(!pointers.has(e.pointerId))return; const before=metrics();pointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const after=metrics(); if(before.distance&&after.distance)camera.zoom(after.distance/before.distance,before.x,before.y); camera.pan(after.x-before.x,after.y-before.y);paint(); },options); for(const type of ['pointerup','pointercancel','lostpointercapture'])surface.addEventListener(type,e=>pointers.delete(e.pointerId),options); surface.addEventListener('dblclick',()=>view.fit(),options); surface.addEventListener('keydown',e=>{ if(e.key==='Home'||e.key==='0')camera.fit(); else if(e.key==='+'||e.key==='=')camera.zoom(1.25,camera.width/2,camera.height/2); else if(e.key==='-')camera.zoom(.8,camera.width/2,camera.height/2); else if(e.key.startsWith('Arrow'))camera.pan(e.key==='ArrowLeft'?40:e.key==='ArrowRight'?-40:0,e.key==='ArrowUp'?40:e.key==='ArrowDown'?-40:0); else return;e.preventDefault();paint(); },options); const observer=new ResizeObserver(()=>{ const pose=view.pose();camera.resize(surface.clientWidth,surface.clientHeight); camera.fitScale=Math.min(camera.width/bounds.width,camera.height/bounds.height);view.apply(pose); });observer.observe(surface);paint(false);return view; }