57 خطوط
3.3 KiB
JavaScript
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;
|
|
}
|