372 lines
15 KiB
JavaScript
372 lines
15 KiB
JavaScript
/*!
|
|
* @svgdotjs/svg.panzoom.js - A plugin for svg.js that enables panzoom for viewport elements
|
|
* @version 2.1.2
|
|
* https://github.com/svgdotjs/svg.panzoom.js#readme
|
|
*
|
|
* @copyright undefined
|
|
* @license MIT
|
|
*
|
|
* BUILT: Thu Jul 22 2021 14:51:35 GMT+0200 (Mitteleuropäische Sommerzeit)
|
|
*/;
|
|
(function (svg_js) {
|
|
'use strict';
|
|
|
|
var normalizeEvent = function normalizeEvent(ev) {
|
|
return ev.touches || [{
|
|
clientX: ev.clientX,
|
|
clientY: ev.clientY
|
|
}];
|
|
};
|
|
|
|
svg_js.extend(svg_js.Svg, {
|
|
panZoom: function panZoom(options) {
|
|
var _options,
|
|
_options$zoomFactor,
|
|
_options$zoomMin,
|
|
_options$zoomMax,
|
|
_options$wheelZoom,
|
|
_options$pinchZoom,
|
|
_options$panning,
|
|
_options$panButton,
|
|
_options$oneFingerPan,
|
|
_options$margins,
|
|
_options$wheelZoomDel,
|
|
_options$wheelZoomDel2,
|
|
_this = this;
|
|
|
|
this.off('.panZoom'); // when called with false, disable panZoom
|
|
|
|
if (options === false) return this;
|
|
options = (_options = options) != null ? _options : {};
|
|
var zoomFactor = (_options$zoomFactor = options.zoomFactor) != null ? _options$zoomFactor : 2;
|
|
var zoomMin = (_options$zoomMin = options.zoomMin) != null ? _options$zoomMin : Number.MIN_VALUE;
|
|
var zoomMax = (_options$zoomMax = options.zoomMax) != null ? _options$zoomMax : Number.MAX_VALUE;
|
|
var doWheelZoom = (_options$wheelZoom = options.wheelZoom) != null ? _options$wheelZoom : true;
|
|
var doPinchZoom = (_options$pinchZoom = options.pinchZoom) != null ? _options$pinchZoom : true;
|
|
var doPanning = (_options$panning = options.panning) != null ? _options$panning : true;
|
|
var panButton = (_options$panButton = options.panButton) != null ? _options$panButton : 0;
|
|
var oneFingerPan = (_options$oneFingerPan = options.oneFingerPan) != null ? _options$oneFingerPan : false;
|
|
var margins = (_options$margins = options.margins) != null ? _options$margins : false;
|
|
var wheelZoomDeltaModeLinePixels = (_options$wheelZoomDel = options.wheelZoomDeltaModeLinePixels) != null ? _options$wheelZoomDel : 17;
|
|
var wheelZoomDeltaModeScreenPixels = (_options$wheelZoomDel2 = options.wheelZoomDeltaModeScreenPixels) != null ? _options$wheelZoomDel2 : 53;
|
|
var lastP;
|
|
var lastTouches;
|
|
var zoomInProgress = false;
|
|
var viewbox = this.viewbox();
|
|
|
|
var restrictToMargins = function restrictToMargins(box) {
|
|
if (!margins) return box;
|
|
var top = margins.top,
|
|
left = margins.left,
|
|
bottom = margins.bottom,
|
|
right = margins.right;
|
|
|
|
var _this$attr = _this.attr(['width', 'height']),
|
|
width = _this$attr.width,
|
|
height = _this$attr.height;
|
|
|
|
var preserveAspectRatio = _this.node.preserveAspectRatio.baseVal; // The current viewport (exactly what is shown on the screen, what we ultimately want to restrict)
|
|
// is not always exactly the same as current viewbox. They are different when the viewbox aspectRatio and the svg aspectRatio
|
|
// are different and preserveAspectRatio is not "none". These offsets represent the difference in user coordinates
|
|
// between the side of the viewbox and the side of the viewport.
|
|
|
|
var viewportLeftOffset = 0;
|
|
var viewportRightOffset = 0;
|
|
var viewportTopOffset = 0;
|
|
var viewportBottomOffset = 0; // preserveAspectRatio none has no offsets
|
|
|
|
if (preserveAspectRatio.align !== preserveAspectRatio.SVG_PRESERVEASPECTRATIO_NONE) {
|
|
var svgAspectRatio = width / height;
|
|
var viewboxAspectRatio = viewbox.width / viewbox.height; // when aspectRatios are the same, there are no offsets
|
|
|
|
if (viewboxAspectRatio !== svgAspectRatio) {
|
|
// aspectRatio unknown is like meet because that's the default
|
|
var isMeet = preserveAspectRatio.meetOrSlice !== preserveAspectRatio.SVG_MEETORSLICE_SLICE;
|
|
var changedAxis = svgAspectRatio > viewboxAspectRatio ? 'width' : 'height';
|
|
var isWidth = changedAxis === 'width';
|
|
var changeHorizontal = isMeet && isWidth || !isMeet && !isWidth;
|
|
var ratio = changeHorizontal ? svgAspectRatio / viewboxAspectRatio : viewboxAspectRatio / svgAspectRatio;
|
|
var offset = box[changedAxis] - box[changedAxis] * ratio;
|
|
|
|
if (changeHorizontal) {
|
|
if (preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMIN || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMAX) {
|
|
viewportLeftOffset = offset / 2;
|
|
viewportRightOffset = -offset / 2;
|
|
} else if (preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMIN || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMID || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMAX) {
|
|
viewportRightOffset = -offset;
|
|
} else if (preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMID || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMAX) {
|
|
viewportLeftOffset = offset;
|
|
}
|
|
} else {
|
|
if (preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMID || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMID) {
|
|
viewportTopOffset = offset / 2;
|
|
viewportBottomOffset = -offset / 2;
|
|
} else if (preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMIN || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMIN || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN) {
|
|
viewportBottomOffset = -offset;
|
|
} else if (preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMAX || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMAX || preserveAspectRatio.align === preserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMAX) {
|
|
viewportTopOffset = offset;
|
|
}
|
|
}
|
|
}
|
|
} // when box.x == leftLimit, the image is panned to the left,
|
|
// i.e the current box is to the right of the initial viewbox,
|
|
// and only the right part of the initial image is visible, i.e.
|
|
// the right side of the initial viewbox minus left margin (viewbox.x+viewbox.width-left)
|
|
// is aligned with the left side of the viewport (box.x + viewportLeftOffset):
|
|
// viewbox.width + viewbox.x - left = box.x + viewportLeftOffset
|
|
// viewbox.width + viewbox.x - left - viewportLeftOffset = box.x (= leftLimit)
|
|
|
|
|
|
var leftLimit = viewbox.width + viewbox.x - left - viewportLeftOffset; // when box.x == rightLimit, the image is panned to the right,
|
|
// i.e the current box is to the left of the initial viewbox
|
|
// and only the left part of the initial image is visible, i.e
|
|
// the left side of the initial viewbox plus right margin (viewbox.x + right)
|
|
// is aligned with the right side of the viewport (box.x + box.width + viewportRightOffset)
|
|
// viewbox.x + right = box.x + box.width + viewportRightOffset
|
|
// viewbox.x + right - box.width - viewportRightOffset = box.x (= rightLimit)
|
|
|
|
var rightLimit = viewbox.x + right - box.width - viewportRightOffset; // same with top and bottom
|
|
|
|
var topLimit = viewbox.height + viewbox.y - top - viewportTopOffset;
|
|
var bottomLimit = viewbox.y + bottom - box.height - viewportBottomOffset;
|
|
box.x = Math.min(leftLimit, Math.max(rightLimit, box.x)); // enforce rightLimit <= box.x <= leftLimit
|
|
|
|
box.y = Math.min(topLimit, Math.max(bottomLimit, box.y)); // enforce bottomLimit <= box.y <= topLimit
|
|
|
|
return box;
|
|
};
|
|
|
|
var wheelZoom = function wheelZoom(ev) {
|
|
ev.preventDefault(); // When wheeling on a mouse,
|
|
// - chrome by default uses deltaY = 53, deltaMode = 0 (pixel)
|
|
// - firefox by default uses deltaY = 3, deltaMode = 1 (line)
|
|
// - chrome and firefox on windows after configuring "One screen at a time"
|
|
// use deltaY = 1, deltaMode = 2 (screen)
|
|
//
|
|
// Note that when when wheeling on a touchpad, deltaY depends on how fast
|
|
// you swipe, but the deltaMode is still different between the browsers.
|
|
//
|
|
// Normalize everything so that zooming speed is approximately the same in all cases
|
|
|
|
var normalizedPixelDeltaY;
|
|
|
|
switch (ev.deltaMode) {
|
|
case 1:
|
|
normalizedPixelDeltaY = ev.deltaY * wheelZoomDeltaModeLinePixels;
|
|
break;
|
|
|
|
case 2:
|
|
normalizedPixelDeltaY = ev.deltaY * wheelZoomDeltaModeScreenPixels;
|
|
break;
|
|
|
|
default:
|
|
// 0 (already pixels) or new mode (avoid crashing)
|
|
normalizedPixelDeltaY = ev.deltaY;
|
|
break;
|
|
}
|
|
|
|
var lvl = Math.pow(1 + zoomFactor, -1 * normalizedPixelDeltaY / 100) * this.zoom();
|
|
var p = this.point(ev.clientX, ev.clientY);
|
|
|
|
if (lvl > zoomMax) {
|
|
lvl = zoomMax;
|
|
}
|
|
|
|
if (lvl < zoomMin) {
|
|
lvl = zoomMin;
|
|
}
|
|
|
|
if (this.dispatch('zoom', {
|
|
level: lvl,
|
|
focus: p
|
|
}).defaultPrevented) {
|
|
return this;
|
|
}
|
|
|
|
this.zoom(lvl, p);
|
|
|
|
if (margins) {
|
|
var box = restrictToMargins(this.viewbox());
|
|
this.viewbox(box);
|
|
}
|
|
};
|
|
|
|
var pinchZoomStart = function pinchZoomStart(ev) {
|
|
lastTouches = normalizeEvent(ev); // Start panning in case only one touch is found
|
|
|
|
if (lastTouches.length < 2) {
|
|
if (doPanning && oneFingerPan) {
|
|
panStart.call(this, ev);
|
|
}
|
|
|
|
return;
|
|
} // Stop panning for more than one touch
|
|
|
|
|
|
if (doPanning && oneFingerPan) {
|
|
panStop.call(this, ev);
|
|
} // We call it so late, so the user is still able to scroll / reload the page via gesture
|
|
// In case oneFingerPan is not active
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
if (this.dispatch('pinchZoomStart', {
|
|
event: ev
|
|
}).defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
this.off('touchstart.panZoom', pinchZoomStart);
|
|
zoomInProgress = true;
|
|
svg_js.on(document, 'touchmove.panZoom', pinchZoom, this, {
|
|
passive: false
|
|
});
|
|
svg_js.on(document, 'touchend.panZoom', pinchZoomStop, this, {
|
|
passive: false
|
|
});
|
|
};
|
|
|
|
var pinchZoomStop = function pinchZoomStop(ev) {
|
|
ev.preventDefault();
|
|
var currentTouches = normalizeEvent(ev);
|
|
|
|
if (currentTouches.length > 1) {
|
|
return;
|
|
}
|
|
|
|
zoomInProgress = false;
|
|
this.dispatch('pinchZoomEnd', {
|
|
event: ev
|
|
});
|
|
svg_js.off(document, 'touchmove.panZoom', pinchZoom);
|
|
svg_js.off(document, 'touchend.panZoom', pinchZoomStop);
|
|
this.on('touchstart.panZoom', pinchZoomStart);
|
|
|
|
if (currentTouches.length && doPanning && oneFingerPan) {
|
|
panStart.call(this, ev);
|
|
}
|
|
};
|
|
|
|
var pinchZoom = function pinchZoom(ev) {
|
|
ev.preventDefault();
|
|
var currentTouches = normalizeEvent(ev);
|
|
var zoom = this.zoom(); // Distance Formula
|
|
|
|
var lastDelta = Math.sqrt(Math.pow(lastTouches[0].clientX - lastTouches[1].clientX, 2) + Math.pow(lastTouches[0].clientY - lastTouches[1].clientY, 2));
|
|
var currentDelta = Math.sqrt(Math.pow(currentTouches[0].clientX - currentTouches[1].clientX, 2) + Math.pow(currentTouches[0].clientY - currentTouches[1].clientY, 2));
|
|
var zoomAmount = lastDelta / currentDelta;
|
|
|
|
if (zoom < zoomMin && zoomAmount > 1 || zoom > zoomMax && zoomAmount < 1) {
|
|
zoomAmount = 1;
|
|
}
|
|
|
|
var currentFocus = {
|
|
x: currentTouches[0].clientX + 0.5 * (currentTouches[1].clientX - currentTouches[0].clientX),
|
|
y: currentTouches[0].clientY + 0.5 * (currentTouches[1].clientY - currentTouches[0].clientY)
|
|
};
|
|
var lastFocus = {
|
|
x: lastTouches[0].clientX + 0.5 * (lastTouches[1].clientX - lastTouches[0].clientX),
|
|
y: lastTouches[0].clientY + 0.5 * (lastTouches[1].clientY - lastTouches[0].clientY)
|
|
};
|
|
var p = this.point(currentFocus.x, currentFocus.y);
|
|
var focusP = this.point(2 * currentFocus.x - lastFocus.x, 2 * currentFocus.y - lastFocus.y);
|
|
var box = new svg_js.Box(this.viewbox()).transform(new svg_js.Matrix().translate(-focusP.x, -focusP.y).scale(zoomAmount, 0, 0).translate(p.x, p.y));
|
|
restrictToMargins(box);
|
|
this.viewbox(box);
|
|
lastTouches = currentTouches;
|
|
this.dispatch('zoom', {
|
|
box: box,
|
|
focus: focusP
|
|
});
|
|
};
|
|
|
|
var panStart = function panStart(ev) {
|
|
var isMouse = ev.type.indexOf('mouse') > -1; // In case panStart is called with touch, ev.button is undefined
|
|
|
|
if (isMouse && ev.button !== panButton && ev.which !== panButton + 1) {
|
|
return;
|
|
}
|
|
|
|
ev.preventDefault();
|
|
this.off('mousedown.panZoom', panStart);
|
|
lastTouches = normalizeEvent(ev);
|
|
if (zoomInProgress) return;
|
|
this.dispatch('panStart', {
|
|
event: ev
|
|
});
|
|
lastP = {
|
|
x: lastTouches[0].clientX,
|
|
y: lastTouches[0].clientY
|
|
};
|
|
svg_js.on(document, 'touchmove.panZoom mousemove.panZoom', panning, this, {
|
|
passive: false
|
|
});
|
|
svg_js.on(document, 'touchend.panZoom mouseup.panZoom', panStop, this, {
|
|
passive: false
|
|
});
|
|
};
|
|
|
|
var panStop = function panStop(ev) {
|
|
ev.preventDefault();
|
|
svg_js.off(document, 'touchmove.panZoom mousemove.panZoom', panning);
|
|
svg_js.off(document, 'touchend.panZoom mouseup.panZoom', panStop);
|
|
this.on('mousedown.panZoom', panStart);
|
|
this.dispatch('panEnd', {
|
|
event: ev
|
|
});
|
|
};
|
|
|
|
var panning = function panning(ev) {
|
|
ev.preventDefault();
|
|
var currentTouches = normalizeEvent(ev);
|
|
var currentP = {
|
|
x: currentTouches[0].clientX,
|
|
y: currentTouches[0].clientY
|
|
};
|
|
var p1 = this.point(currentP.x, currentP.y);
|
|
var p2 = this.point(lastP.x, lastP.y);
|
|
var deltaP = [p2.x - p1.x, p2.y - p1.y];
|
|
|
|
if (!deltaP[0] && !deltaP[1]) {
|
|
return;
|
|
}
|
|
|
|
var box = new svg_js.Box(this.viewbox()).transform(new svg_js.Matrix().translate(deltaP[0], deltaP[1]));
|
|
lastP = currentP;
|
|
restrictToMargins(box);
|
|
|
|
if (this.dispatch('panning', {
|
|
box: box,
|
|
event: ev
|
|
}).defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
this.viewbox(box);
|
|
};
|
|
|
|
if (doWheelZoom) {
|
|
this.on('wheel.panZoom', wheelZoom, this, {
|
|
passive: false
|
|
});
|
|
}
|
|
|
|
if (doPinchZoom) {
|
|
this.on('touchstart.panZoom', pinchZoomStart, this, {
|
|
passive: false
|
|
});
|
|
}
|
|
|
|
if (doPanning) {
|
|
this.on('mousedown.panZoom', panStart, this, {
|
|
passive: false
|
|
});
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
|
|
}(SVG));
|
|
//# sourceMappingURL=svg.panzoom.js.map
|