UNPKG

svg-pan-zoom-m

Version:

JavaScript library for panning and zooming an SVG image from the mouse, touches and programmatically.

367 lines (312 loc) 8.98 kB
var SvgUtils = require("./svg-utilities"), Utils = require("./utilities"); var ShadowViewport = function(viewport, options) { this.init(viewport, options); }; /** * Initialization * * @param {SVGElement} viewport * @param {Object} options */ ShadowViewport.prototype.init = function(viewport, options) { // DOM Elements this.viewport = viewport; this.options = options; // State cache this.originalState = { zoom: 1, x: 0, y: 0 }; this.activeState = { zoom: 1, x: 0, y: 0 }; this.updateCTMCached = Utils.proxy(this.updateCTM, this); // Create a custom requestAnimationFrame taking in account refreshRate this.requestAnimationFrame = Utils.createRequestAnimationFrame( this.options.refreshRate ); // ViewBox this.viewBox = { x: 0, y: 0, width: 0, height: 0 }; this.cacheViewBox(); // Process CTM var newCTM = this.processCTM(); // Update viewport CTM and cache zoom and pan this.setCTM(newCTM); // Update CTM in this frame this.updateCTM(); }; /** * Cache initial viewBox value * If no viewBox is defined, then use viewport size/position instead for viewBox values */ ShadowViewport.prototype.cacheViewBox = function() { var svgViewBox = this.options.svg.getAttribute("viewBox"); if (svgViewBox) { var viewBoxValues = svgViewBox .split(/[\s\,]/) .filter(function(v) { return v; }) .map(parseFloat); // Cache viewbox x and y offset this.viewBox.x = viewBoxValues[0]; this.viewBox.y = viewBoxValues[1]; this.viewBox.width = viewBoxValues[2]; this.viewBox.height = viewBoxValues[3]; var zoom = Math.min( this.options.width / this.viewBox.width, this.options.height / this.viewBox.height ); // Update active state this.activeState.zoom = zoom; this.activeState.x = (this.options.width - this.viewBox.width * zoom) / 2; this.activeState.y = (this.options.height - this.viewBox.height * zoom) / 2; // Force updating CTM this.updateCTMOnNextFrame(); this.options.svg.removeAttribute("viewBox"); } else { this.simpleViewBoxCache(); } }; /** * Recalculate viewport sizes and update viewBox cache */ ShadowViewport.prototype.simpleViewBoxCache = function() { var bBox = this.viewport.getBBox(); this.viewBox.x = bBox.x; this.viewBox.y = bBox.y; this.viewBox.width = bBox.width; this.viewBox.height = bBox.height; }; /** * Returns a viewbox object. Safe to alter * * @return {Object} viewbox object */ ShadowViewport.prototype.getViewBox = function() { return Utils.extend({}, this.viewBox); }; /** * Get initial zoom and pan values. Save them into originalState * Parses viewBox attribute to alter initial sizes * * @return {CTM} CTM object based on options */ ShadowViewport.prototype.processCTM = function() { var newCTM = this.getCTM(); if (this.options.fit || this.options.contain) { var newScale; if (this.options.fit) { newScale = Math.min( this.options.width / this.viewBox.width, this.options.height / this.viewBox.height ); } else { newScale = Math.max( this.options.width / this.viewBox.width, this.options.height / this.viewBox.height ); } newCTM.a = newScale; //x-scale newCTM.d = newScale; //y-scale newCTM.e = -this.viewBox.x * newScale; //x-transform newCTM.f = -this.viewBox.y * newScale; //y-transform } if (this.options.center) { var offsetX = (this.options.width - (this.viewBox.width + this.viewBox.x * 2) * newCTM.a) * 0.5, offsetY = (this.options.height - (this.viewBox.height + this.viewBox.y * 2) * newCTM.a) * 0.5; newCTM.e = offsetX; newCTM.f = offsetY; } // Cache initial values. Based on activeState and fix+center opitons this.originalState.zoom = newCTM.a; this.originalState.x = newCTM.e; this.originalState.y = newCTM.f; return newCTM; }; /** * Return originalState object. Safe to alter * * @return {Object} */ ShadowViewport.prototype.getOriginalState = function() { return Utils.extend({}, this.originalState); }; /** * Return actualState object. Safe to alter * * @return {Object} */ ShadowViewport.prototype.getState = function() { return Utils.extend({}, this.activeState); }; /** * Get zoom scale * * @return {Float} zoom scale */ ShadowViewport.prototype.getZoom = function() { return this.activeState.zoom; }; /** * Get zoom scale for pubilc usage * * @return {Float} zoom scale */ ShadowViewport.prototype.getRelativeZoom = function() { return this.activeState.zoom / this.originalState.zoom; }; /** * Compute zoom scale for pubilc usage * * @return {Float} zoom scale */ ShadowViewport.prototype.computeRelativeZoom = function(scale) { return scale / this.originalState.zoom; }; /** * Get pan * * @return {Object} */ ShadowViewport.prototype.getPan = function() { return { x: this.activeState.x, y: this.activeState.y }; }; /** * Return cached viewport CTM value that can be safely modified * * @return {SVGMatrix} */ ShadowViewport.prototype.getCTM = function() { var safeCTM = this.options.svg.createSVGMatrix(); // Copy values manually as in FF they are not itterable safeCTM.a = this.activeState.zoom; safeCTM.b = 0; safeCTM.c = 0; safeCTM.d = this.activeState.zoom; safeCTM.e = this.activeState.x; safeCTM.f = this.activeState.y; return safeCTM; }; /** * Set a new CTM * * @param {SVGMatrix} newCTM */ ShadowViewport.prototype.setCTM = function(newCTM) { var willZoom = this.isZoomDifferent(newCTM), willPan = this.isPanDifferent(newCTM); if (willZoom || willPan) { // Before zoom if (willZoom) { // If returns false then cancel zooming if ( this.options.beforeZoom( this.getRelativeZoom(), this.computeRelativeZoom(newCTM.a) ) === false ) { newCTM.a = newCTM.d = this.activeState.zoom; willZoom = false; } else { this.updateCache(newCTM); this.options.onZoom(this.getRelativeZoom()); } } // Before pan if (willPan) { var preventPan = this.options.beforePan(this.getPan(), { x: newCTM.e, y: newCTM.f }), // If prevent pan is an object preventPanX = false, preventPanY = false; // If prevent pan is Boolean false if (preventPan === false) { // Set x and y same as before newCTM.e = this.getPan().x; newCTM.f = this.getPan().y; preventPanX = preventPanY = true; } else if (Utils.isObject(preventPan)) { // Check for X axes attribute if (preventPan.x === false) { // Prevent panning on x axes newCTM.e = this.getPan().x; preventPanX = true; } else if (Utils.isNumber(preventPan.x)) { // Set a custom pan value newCTM.e = preventPan.x; } // Check for Y axes attribute if (preventPan.y === false) { // Prevent panning on x axes newCTM.f = this.getPan().y; preventPanY = true; } else if (Utils.isNumber(preventPan.y)) { // Set a custom pan value newCTM.f = preventPan.y; } } // Update willPan flag // Check if newCTM is still different if ((preventPanX && preventPanY) || !this.isPanDifferent(newCTM)) { willPan = false; } else { this.updateCache(newCTM); this.options.onPan(this.getPan()); } } // Check again if should zoom or pan if (willZoom || willPan) { this.updateCTMOnNextFrame(); } } }; ShadowViewport.prototype.isZoomDifferent = function(newCTM) { return this.activeState.zoom !== newCTM.a; }; ShadowViewport.prototype.isPanDifferent = function(newCTM) { return this.activeState.x !== newCTM.e || this.activeState.y !== newCTM.f; }; /** * Update cached CTM and active state * * @param {SVGMatrix} newCTM */ ShadowViewport.prototype.updateCache = function(newCTM) { this.activeState.zoom = newCTM.a; this.activeState.x = newCTM.e; this.activeState.y = newCTM.f; }; ShadowViewport.prototype.pendingUpdate = false; /** * Place a request to update CTM on next Frame */ ShadowViewport.prototype.updateCTMOnNextFrame = function() { if (!this.pendingUpdate) { // Lock this.pendingUpdate = true; // Throttle next update this.requestAnimationFrame.call(window, this.updateCTMCached); } }; /** * Update viewport CTM with cached CTM */ ShadowViewport.prototype.updateCTM = function() { var ctm = this.getCTM(); // Updates SVG element SvgUtils.setCTM(this.viewport, ctm, this.defs); // Free the lock this.pendingUpdate = false; // Notify about the update if (this.options.onUpdatedCTM) { this.options.onUpdatedCTM(ctm); } }; module.exports = function(viewport, options) { return new ShadowViewport(viewport, options); };