mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
8.8 KiB
366 lines
8.8 KiB
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);
|
|
};
|
|
|