UNPKG

vnodes

Version:

Vue components to create svg interactive graphs, diagrams or node visual tools.

339 lines (283 loc) 8.98 kB
/* eslint-disable */ import SvgUtils from './svg-utilities' import Utils from './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) } } export default function(viewport, options){ return new ShadowViewport(viewport, options) }