Files

57 خطوط
3.3 KiB
JavaScript

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;
}