339 lines
8.4 KiB
JavaScript
339 lines
8.4 KiB
JavaScript
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)
|
|
}
|