vnodes
Version:
Vue components to create svg interactive graphs, diagrams or node visual tools.
339 lines (283 loc) • 8.98 kB
JavaScript
/* 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)
}