136 lines
No EOL
4.1 KiB
JavaScript
136 lines
No EOL
4.1 KiB
JavaScript
function zoomToExtents(svg) {
|
|
try {
|
|
const bbox = svg.getBBox();
|
|
const padding = 10;
|
|
|
|
const svgClientWidth = svg.clientWidth || 300;
|
|
const svgClientHeight = svg.clientHeight || 150;
|
|
|
|
const viewWidthRaw = bbox.width + 2 * padding;
|
|
const viewHeightRaw = bbox.height + 2 * padding;
|
|
|
|
const aspectBBox = viewWidthRaw / viewHeightRaw;
|
|
const aspectViewport = svgClientWidth / svgClientHeight;
|
|
|
|
let viewWidth = viewWidthRaw;
|
|
let viewHeight = viewHeightRaw;
|
|
|
|
if (aspectBBox > aspectViewport) {
|
|
viewHeight = viewWidth / aspectViewport;
|
|
} else {
|
|
viewWidth = viewHeight * aspectViewport;
|
|
}
|
|
|
|
const cx = bbox.x + bbox.width / 2;
|
|
const cy = bbox.y + bbox.height / 2;
|
|
|
|
const vx = cx - viewWidth / 2;
|
|
const vy = cy - viewHeight / 2;
|
|
|
|
svg.setAttribute("viewBox", `${vx} ${vy} ${viewWidth} ${viewHeight}`);
|
|
} catch (e) {
|
|
console.warn("zoomToExtents failed", e);
|
|
}
|
|
}
|
|
|
|
function screenToSvgCoords(svg, x, y) {
|
|
const pt = svg.createSVGPoint();
|
|
pt.x = x;
|
|
pt.y = y;
|
|
return pt.matrixTransform(svg.getScreenCTM().inverse());
|
|
}
|
|
|
|
document.querySelectorAll('svg').forEach(svg => {
|
|
svg.style.cursor = "zoom-in";
|
|
svg.style.outline = "1px dashed #ccc";
|
|
|
|
// Initial zoom to extents
|
|
requestAnimationFrame(() => zoomToExtents(svg));
|
|
|
|
// Smooth animated zoom on wheel
|
|
svg.addEventListener("wheel", function (e) {
|
|
e.preventDefault();
|
|
|
|
const viewBox = svg.getAttribute("viewBox").split(" ").map(Number);
|
|
let [vx, vy, vw, vh] = viewBox;
|
|
|
|
const zoomFactor = 1.3; // smaller step for smoother feel
|
|
const zoomIn = e.deltaY < 0;
|
|
const scaleTarget = zoomIn ? 1 / zoomFactor : zoomFactor;
|
|
|
|
const svgPoint = screenToSvgCoords(svg, e.clientX, e.clientY);
|
|
|
|
const newVW = vw * scaleTarget;
|
|
const newVH = vh * scaleTarget;
|
|
|
|
const newVX = svgPoint.x - ((svgPoint.x - vx) * scaleTarget);
|
|
const newVY = svgPoint.y - ((svgPoint.y - vy) * scaleTarget);
|
|
|
|
let start = null;
|
|
const startViewBox = { vx, vy, vw, vh };
|
|
const endViewBox = { vx: newVX, vy: newVY, vw: newVW, vh: newVH };
|
|
const duration = 10; // ms animation duration
|
|
|
|
function animate(timestamp) {
|
|
if (!start) start = timestamp;
|
|
const progress = Math.min((timestamp - start) / duration, 1);
|
|
|
|
const interp = (startVal, endVal) =>
|
|
startVal + (endVal - startVal) * progress;
|
|
|
|
const currVX = interp(startViewBox.vx, endViewBox.vx);
|
|
const currVY = interp(startViewBox.vy, endViewBox.vy);
|
|
const currVW = interp(startViewBox.vw, endViewBox.vw);
|
|
const currVH = interp(startViewBox.vh, endViewBox.vh);
|
|
|
|
svg.setAttribute("viewBox", `${currVX} ${currVY} ${currVW} ${currVH}`);
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(animate);
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
}, { passive: false });
|
|
|
|
// Smooth middle mouse button panning
|
|
let isPanning = false;
|
|
let startClient = null;
|
|
let startViewBox = null;
|
|
|
|
svg.addEventListener("mousedown", function (e) {
|
|
if (e.button !== 1) return; // middle mouse only
|
|
e.preventDefault();
|
|
|
|
isPanning = true;
|
|
startClient = { x: e.clientX, y: e.clientY };
|
|
startViewBox = svg.getAttribute("viewBox").split(" ").map(Number);
|
|
svg.style.cursor = "grabbing";
|
|
});
|
|
|
|
svg.addEventListener("mousemove", function (e) {
|
|
if (!isPanning) return;
|
|
|
|
const dxPixels = e.clientX - startClient.x;
|
|
const dyPixels = e.clientY - startClient.y;
|
|
|
|
const [vx, vy, vw, vh] = startViewBox;
|
|
|
|
const scaleX = vw / svg.clientWidth;
|
|
const scaleY = vh / svg.clientHeight;
|
|
|
|
const dx = dxPixels * scaleX;
|
|
const dy = dyPixels * scaleY;
|
|
|
|
svg.setAttribute("viewBox", `${vx - dx} ${vy - dy} ${vw} ${vh}`);
|
|
});
|
|
|
|
const stopPan = () => {
|
|
isPanning = false;
|
|
svg.style.cursor = "zoom-in";
|
|
};
|
|
|
|
svg.addEventListener("mouseup", stopPan);
|
|
svg.addEventListener("mouseleave", stopPan);
|
|
});
|
|
|