371 lines
10 KiB
JavaScript
371 lines
10 KiB
JavaScript
/*global define*/
|
||
// most of this was shamelessly taken from @jakearchibald's svgomg ui:
|
||
// https://github.com/jakearchibald/svgomg/blob/master/src/js/page/ui/pan-zoom.js
|
||
define(
|
||
[ 'util/css', 'util/dom', 'util/addpublishers' ],
|
||
function ( cssHelper, domHelper, addPublishers ) {
|
||
function getXY ( obj ) {
|
||
return {
|
||
x: obj.pageX,
|
||
y: obj.pageY
|
||
};
|
||
}
|
||
|
||
function touchDistance ( touch1, touch2 ) {
|
||
var x = Math.abs( touch2.x - touch1.x );
|
||
var y = Math.abs( touch2.y - touch1.y );
|
||
return Math.sqrt( x * x + y * y );
|
||
}
|
||
|
||
function getMidpoint ( point1, point2 ) {
|
||
return {
|
||
x: ( point1.x + point2.x ) / 2,
|
||
y: ( point1.y + point2.y ) / 2
|
||
};
|
||
}
|
||
|
||
function getPoints ( event ) {
|
||
if ( event.touches ) {
|
||
return Array.prototype.map.call( event.touches, getXY );
|
||
} else {
|
||
return [ getXY( event ) ];
|
||
}
|
||
}
|
||
|
||
function PanZoom ( containerEl, targetEl, opts ) {
|
||
if ( ! ( this instanceof PanZoom ) ) {
|
||
return new PanZoom ( targetEl, containerEl, opts );
|
||
}
|
||
|
||
var self = this;
|
||
var canZoomWithPonter = true;
|
||
var publishers = addPublishers( self, 'scale' );
|
||
opts = opts || { }
|
||
|
||
var shouldCaptureFn = opts.shouldCaptureFn || function ( el ) { return true; };
|
||
var limitToContainer = typeof opts.limitToContainer === 'boolean' ? opts.limitToContainer : true;
|
||
var limitPadding = opts.limitPadding || 50;
|
||
|
||
var matrix = cssHelper.getCSSMatrix( targetEl );
|
||
var transform = cssHelper.cssMatrixToTransformObj( matrix );
|
||
|
||
var x = transform.translateX || 0;
|
||
var y = transform.translateY || 0;
|
||
var scale = 1;
|
||
var active = 0;
|
||
var lastPoints = [ ];
|
||
var containerBounds;
|
||
var updateAnimationFrameId = NaN;
|
||
var scaleAnimationFrameId = NaN;
|
||
var centerAnimationFrameId = NaN;
|
||
var resizeTimeoutId = NaN;
|
||
var targetElSize = { width: 100, height: 100 };
|
||
var containerElSize = { width: 100, height: 100 };
|
||
var centerScale = 1;
|
||
|
||
containerEl.addEventListener( 'mousedown', pointerPressed );
|
||
containerEl.addEventListener( 'touchstart', pointerPressed );
|
||
containerEl.addEventListener( 'wheel', wheelTurned );
|
||
|
||
window.addEventListener( 'resize', resized );
|
||
|
||
targetEl.style.WebkitTransformOrigin = targetEl.style.transformOrigin = '0 0';
|
||
|
||
updateContainerBounds();
|
||
|
||
function reset () {
|
||
// x = transform.translateX || 0;
|
||
// y = transform.translateY || 0;
|
||
|
||
updateValues ( transform.translateX || 0, transform.translateY || 0, 1 );
|
||
}
|
||
|
||
function updateValues ( newX, newY, newScale, targetBounds ) {
|
||
cancelAnimationFrame( updateAnimationFrameId );
|
||
|
||
if ( limitToContainer && targetEl !== containerEl && containerBounds ) {
|
||
targetBounds = targetBounds || targetEl.getBoundingClientRect();
|
||
|
||
if ( newX !== x || newY !== y || newScale !== scale ) {
|
||
var scaleDelta = 1 / ( scale / newScale );
|
||
|
||
if ( newX + targetBounds.width * scaleDelta < limitPadding ) {
|
||
newX = limitPadding - targetBounds.width * scaleDelta;
|
||
}
|
||
|
||
if ( newX > containerBounds.width - limitPadding ) {
|
||
newX = containerBounds.width - limitPadding;
|
||
}
|
||
|
||
if ( newY + targetBounds.height * scaleDelta < limitPadding ) {
|
||
newY = limitPadding - targetBounds.height * scaleDelta;
|
||
}
|
||
|
||
if ( newY > containerBounds.height - limitPadding ) {
|
||
newY = containerBounds.height - limitPadding;
|
||
}
|
||
}
|
||
}
|
||
|
||
newScale = Math.min( 5, Math.max( 0.01, newScale ) );
|
||
|
||
if ( scale === newScale && newScale === 5 ) {
|
||
return;
|
||
}
|
||
|
||
x = newX;
|
||
y = newY;
|
||
scale = newScale
|
||
|
||
publishers.scale.dispatch( scale );
|
||
|
||
updateAnimationFrameId = requestAnimationFrame( update );
|
||
}
|
||
|
||
function update () {
|
||
targetEl.style.WebkitTransform = targetEl.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
|
||
}
|
||
|
||
function wheelTurned ( event ) {
|
||
if ( ! shouldCaptureFn( event.target ) ) { return; }
|
||
if ( ! canZoomWithPonter ) { return; }
|
||
event.preventDefault();
|
||
|
||
var targetBounds = targetEl.getBoundingClientRect();
|
||
var delta = event.deltaY;
|
||
|
||
if ( event.deltaMode === 1 ) { // 1 is "lines", 0 is "pixels"
|
||
// Firefox uses "lines" when mouse is connected
|
||
delta *= 15;
|
||
}
|
||
|
||
// stop mouse wheel producing huge values
|
||
delta = Math.max( Math.min( delta, 60 ), -60 );
|
||
|
||
var scaleDiff = ( delta / 300 ) + 1;
|
||
|
||
// avoid to-small values
|
||
if ( scale * scaleDiff < 0.05 ) { return; }
|
||
|
||
updateValues(
|
||
x - ( event.pageX - targetBounds.left ) * ( scaleDiff - 1 ),
|
||
y - ( event.pageY - targetBounds.top ) * ( scaleDiff - 1 ),
|
||
scale * scaleDiff,
|
||
targetBounds
|
||
);
|
||
}
|
||
|
||
function firstPointerPressed ( event ) {
|
||
document.addEventListener( 'mousemove', pointerMoved );
|
||
document.addEventListener( 'mouseup', pointerReleased );
|
||
document.addEventListener( 'touchmove', pointerMoved );
|
||
document.addEventListener( 'touchend', pointerReleased );
|
||
}
|
||
|
||
function pointerPressed ( event ) {
|
||
if ( event.type == 'mousedown' && event.which != 1 ) {
|
||
return;
|
||
}
|
||
|
||
if ( ! shouldCaptureFn( event.target ) ) {
|
||
return;
|
||
}
|
||
|
||
if ( domHelper.isDescendant( containerEl, event.target ) ) {
|
||
event.preventDefault();
|
||
|
||
lastPoints = getPoints( event );
|
||
active++;
|
||
|
||
if ( active === 1 ) {
|
||
firstPointerPressed( event );
|
||
}
|
||
}
|
||
}
|
||
|
||
function pointerMoved ( event ) {
|
||
if ( event && event.target && domHelper.isDescendant( containerEl, event.target ) ) {
|
||
event.preventDefault();
|
||
|
||
var points = getPoints( event );
|
||
var averagePoint = points.reduce( getMidpoint );
|
||
var averageLastPoint = lastPoints.reduce( getMidpoint );
|
||
var targetBounds = targetEl.getBoundingClientRect();
|
||
|
||
var tmpX = x + averagePoint.x - averageLastPoint.x;
|
||
var tmpY = y + averagePoint.y - averageLastPoint.y;
|
||
|
||
if ( points[1] ) {
|
||
if ( ! canZoomWithPonter ) { return; }
|
||
var scaleDiff = touchDistance( points[0], points[1] ) / touchDistance( lastPoints[0], lastPoints[1] );
|
||
|
||
updateValues(
|
||
tmpX - ( averagePoint.x - targetBounds.left ) * ( scaleDiff - 1 ),
|
||
tmpY - ( averagePoint.y - targetBounds.top ) * ( scaleDiff - 1 ),
|
||
scale * scaleDiff,
|
||
targetBounds
|
||
);
|
||
} else {
|
||
updateValues( tmpX, tmpY, scale );
|
||
}
|
||
|
||
lastPoints = points;
|
||
}
|
||
}
|
||
|
||
function pointerReleased ( event ) {
|
||
event.preventDefault();
|
||
active--;
|
||
lastPoints.pop();
|
||
|
||
if ( active ) {
|
||
lastPoints = getPoints( event );
|
||
return;
|
||
}
|
||
|
||
document.removeEventListener( 'mousemove', pointerMoved );
|
||
document.removeEventListener( 'mouseup', pointerReleased );
|
||
document.removeEventListener( 'touchmove', pointerMoved );
|
||
document.removeEventListener( 'touchend', pointerReleased );
|
||
}
|
||
|
||
function updateContainerBounds () {
|
||
containerElSize = {
|
||
width: containerEl.clientWidth,
|
||
height: containerEl.clientHeight
|
||
}
|
||
|
||
targetElSize = {
|
||
width: targetEl.clientWidth,
|
||
height: targetEl.clientHeight
|
||
};
|
||
|
||
containerBounds = containerEl.getBoundingClientRect();
|
||
updateValues( x, y, scale );
|
||
}
|
||
|
||
function resized () {
|
||
clearTimeout( resizeTimeoutId );
|
||
|
||
resizeTimeoutId = setTimeout( function () {
|
||
updateContainerBounds();
|
||
|
||
if ( Math.abs( scale - centerScale ) <= 0.01 ) {
|
||
animateToCenter();
|
||
}
|
||
}, 100 );
|
||
}
|
||
|
||
function setToCenter () {
|
||
var centerValues = getCenterValues();
|
||
updateValues ( centerValues.x, centerValues.y, centerValues.scale );
|
||
|
||
return self;
|
||
}
|
||
|
||
function animateToCenter () {
|
||
var deltaX = 0;
|
||
var deltaY = 0;
|
||
var deltaScale = 0;
|
||
var easing = 0.1;
|
||
var centerValues = getCenterValues();
|
||
|
||
centerScale = centerValues.scale;
|
||
|
||
cancelAnimationFrame( scaleAnimationFrameId );
|
||
cancelAnimationFrame( centerAnimationFrameId );
|
||
|
||
function centerAnimationStep () {
|
||
deltaX = centerValues.x - x;
|
||
deltaY = centerValues.y - y;
|
||
deltaScale = centerValues.scale - scale;
|
||
|
||
if (
|
||
Math.abs( deltaX ) > 1 ||
|
||
Math.abs( deltaY ) > 1 ||
|
||
Math.abs( deltaScale ) > 0.01
|
||
) {
|
||
updateValues( x + deltaX * easing, y + deltaY * easing, scale + deltaScale * easing );
|
||
|
||
centerAnimationFrameId = requestAnimationFrame( centerAnimationStep );
|
||
} else {
|
||
updateValues( centerValues.x, centerValues.y, centerValues.scale );
|
||
}
|
||
}
|
||
|
||
centerAnimationFrameId = requestAnimationFrame( centerAnimationStep );
|
||
|
||
return self;
|
||
}
|
||
|
||
function settingUpdated ( key, value ) {
|
||
if ( key === 'canZoomWithPointer' && typeof value === 'boolean' ){
|
||
canZoomWithPonter = value;
|
||
}
|
||
}
|
||
|
||
function setScale ( newScale ) {
|
||
var targetX = ( containerElSize.width - targetElSize.width * newScale ) / 2;
|
||
var targetY = ( containerElSize.height - targetElSize.height * newScale ) / 2;
|
||
var deltaX = 0;
|
||
var deltaY = 0;
|
||
var deltaScale = 0;
|
||
var easing = 0.2;
|
||
|
||
cancelAnimationFrame( scaleAnimationFrameId );
|
||
cancelAnimationFrame( centerAnimationFrameId );
|
||
|
||
function scaleAnimationStep () {
|
||
deltaX = targetX - x;
|
||
deltaY = targetY - y;
|
||
deltaScale = newScale - scale;
|
||
|
||
if (
|
||
Math.abs( deltaX ) > 1 ||
|
||
Math.abs( deltaY ) > 1 ||
|
||
Math.abs( deltaScale ) > 0.01
|
||
) {
|
||
updateValues( x + deltaX * easing, y + deltaY * easing, scale + deltaScale * easing );
|
||
|
||
scaleAnimationFrameId = requestAnimationFrame( scaleAnimationStep );
|
||
} else {
|
||
updateValues( targetX, targetY, newScale );
|
||
}
|
||
}
|
||
|
||
scaleAnimationStep();
|
||
|
||
return self;
|
||
}
|
||
|
||
function getCenterValues () {
|
||
updateContainerBounds();
|
||
|
||
var targetScale = 1;
|
||
|
||
if ( targetElSize.width > containerElSize.width || targetElSize.height > containerElSize.height ) {
|
||
targetScale = Math.min(
|
||
containerElSize.width / targetElSize.width,
|
||
( containerElSize.height - 80 ) / targetElSize.height
|
||
);
|
||
}
|
||
|
||
return {
|
||
x: ( containerElSize.width - targetElSize.width * targetScale ) / 2,
|
||
y: ( containerElSize.height - targetElSize.height * targetScale ) / 2,
|
||
scale: targetScale
|
||
};
|
||
}
|
||
|
||
this.updateContainerBounds = updateContainerBounds;
|
||
this.reset = reset;
|
||
this.setToCenter = setToCenter;
|
||
this.animateToCenter = animateToCenter;
|
||
this.setScale = setScale;
|
||
this.settingUpdated = settingUpdated;
|
||
this.resized = resized;
|
||
}
|
||
|
||
return PanZoom;
|
||
}
|
||
); |