diff --git a/images/flip_grouphover.png b/images/flip_grouphover.png new file mode 100644 index 00000000..014ead6a Binary files /dev/null and b/images/flip_grouphover.png differ diff --git a/images/flip_hover.png b/images/flip_hover.png new file mode 100644 index 00000000..526dc991 Binary files /dev/null and b/images/flip_hover.png differ diff --git a/images/flip_pressed.png b/images/flip_pressed.png new file mode 100644 index 00000000..28b074c2 Binary files /dev/null and b/images/flip_pressed.png differ diff --git a/images/flip_rest.png b/images/flip_rest.png new file mode 100644 index 00000000..6f15e5da Binary files /dev/null and b/images/flip_rest.png differ diff --git a/src/drawer.js b/src/drawer.js index c904abbb..9aa7ef44 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -500,6 +500,10 @@ $.Drawer.prototype = { if ( this.viewport.degrees !== 0 ) { this._offsetForRotation({degrees: this.viewport.degrees}); + } else{ + if(this.viewer.flipped) { + this._flip({}); + } } if (tiledImage.getRotation(true) % 360 !== 0) { this._offsetForRotation({ @@ -620,10 +624,29 @@ $.Drawer.prototype = { context.save(); context.translate(point.x, point.y); - context.rotate(Math.PI / 180 * options.degrees); + if(this.viewer.flipped){ + context.rotate(Math.PI / 180 * -options.degrees); + context.scale(-1, 1); + } + else{ + context.rotate(Math.PI / 180 * options.degrees); + } context.translate(-point.x, -point.y); }, + // private + _flip: function(options) { + var point = options.point ? + options.point.times($.pixelDensityRatio) : + this.getCanvasCenter(); + var context = this._getContext(options.useSketch); + context.save(); + + context.translate(point.x, 0); + context.scale(-1, 1); + context.translate(-point.x, 0); + }, + // private _restoreRotationChanges: function(useSketch) { var context = this._getContext(useSketch); diff --git a/src/navigator.js b/src/navigator.js index 4c9848cf..7ee60297 100644 --- a/src/navigator.js +++ b/src/navigator.js @@ -208,12 +208,14 @@ $.Navigator = function( options ){ var degrees = options.viewer.viewport ? options.viewer.viewport.getRotation() : options.viewer.degrees || 0; + rotate(degrees); options.viewer.addHandler("rotate", function (args) { rotate(args.degrees); }); } + // Remove the base class' (Viewer's) innerTracker and replace it with our own this.innerTracker.destroy(); this.innerTracker = new $.MouseTracker({ @@ -271,6 +273,50 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* } } }, + /** + /* Flip navigator element + */ + toogleFlip: function() { + this.flipped = !this.flipped; + if (this.viewer.flipped){ + this.displayRegion.style.webkitTransform = "scale(-1,1)"; + this.displayRegion.style.mozTransform = "scale(-1,1)"; + this.displayRegion.style.msTransform = "scale(-1,1)"; + this.displayRegion.style.oTransform = "scale(-1,1)"; + this.displayRegion.style.transform = "scale(-1,1)"; + + this.canvas.style.webkitTransform = "scale(-1,1)"; + this.canvas.style.mozTransform = "scale(-1,1)"; + this.canvas.style.msTransform = "scale(-1,1)"; + this.canvas.style.oTransform = "scale(-1,1)"; + this.canvas.style.transform = "scale(-1,1)"; + + this.element.style.webkitTransform = "scale(-1,1)"; + this.element.style.mozTransform = "scale(-1,1)"; + this.element.style.msTransform = "scale(-1,1)"; + this.element.style.oTransform = "scale(-1,1)"; + this.element.style.transform = "scale(-1,1)"; + } else{ + this.displayRegion.style.webkitTransform = "scale(1,1)"; + this.displayRegion.style.mozTransform = "scale(1,1)"; + this.displayRegion.style.msTransform = "scale(1,1)"; + this.displayRegion.style.oTransform = "scale(1,1)"; + this.displayRegion.style.transform = "scale(1,1)"; + + this.canvas.style.webkitTransform = "scale(1,1)"; + this.canvas.style.mozTransform = "scale(1,1)"; + this.canvas.style.msTransform = "scale(1,1)"; + this.canvas.style.oTransform = "scale(1,1)"; + this.canvas.style.transform = "scale(1,1)"; + + this.element.style.webkitTransform = "scale(1,1)"; + this.element.style.mozTransform = "scale(1,1)"; + this.element.style.msTransform = "scale(1,1)"; + this.element.style.oTransform = "scale(1,1)"; + this.element.style.transform = "scale(1,1)"; + } + this.viewport.viewer.forceRedraw(); + }, /** * Used to update the navigator minimap's viewport rectangle when a change in the viewer's viewport occurs. @@ -406,6 +452,9 @@ $.extend( $.Navigator.prototype, $.EventSource.prototype, $.Viewer.prototype, /* */ function onCanvasClick( event ) { if ( event.quick && this.viewer.viewport ) { + if(this.viewer.flipped){ + event.position.x = this.viewport.getContainerSize().x - event.position.x; + } this.viewer.viewport.panTo(this.viewport.pointFromPixel(event.position)); this.viewer.viewport.applyConstraints(); } @@ -424,6 +473,11 @@ function onCanvasDrag( event ) { if( !this.panVertical ){ event.delta.y = 0; } + + if(this.viewer.flipped){ + event.delta.x = -event.delta.x; + } + this.viewer.viewport.panBy( this.viewport.deltaPointsFromPixels( event.delta @@ -488,11 +542,11 @@ function onCanvasScroll( event ) { * @param {Number} degrees */ function _setTransformRotate (element, degrees) { - element.style.webkitTransform = "rotate(" + degrees + "deg)"; - element.style.mozTransform = "rotate(" + degrees + "deg)"; - element.style.msTransform = "rotate(" + degrees + "deg)"; - element.style.oTransform = "rotate(" + degrees + "deg)"; - element.style.transform = "rotate(" + degrees + "deg)"; + element.style.webkitTransform = "rotate(" + degrees + "deg)"; + element.style.mozTransform = "rotate(" + degrees + "deg)"; + element.style.msTransform = "rotate(" + degrees + "deg)"; + element.style.oTransform = "rotate(" + degrees + "deg)"; + element.style.transform = "rotate(" + degrees + "deg)"; } }( OpenSeadragon )); diff --git a/src/openseadragon.js b/src/openseadragon.js index 4cb36dd3..8963e77f 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -478,6 +478,10 @@ * Note: {@link OpenSeadragon.Options.showNavigationControl} is overriding * this setting when set to false. * + * @property {Boolean} [showFlipControl=false] + * If true then the flip controls will be displayed as part of the + * standard controls. + * * @property {Boolean} [showSequenceControl=true] * If sequenceMode is true, then provide buttons for navigating forward and * backward through the images. @@ -688,6 +692,12 @@ * @property {String} rotateright.HOVER * @property {String} rotateright.DOWN * + * @property {Object} flip - Images for the flip button. + * @property {String} flip.REST + * @property {String} flip.GROUP + * @property {String} flip.HOVER + * @property {String} flip.DOWN + * * @property {Object} previous - Images for the previous button. * @property {String} previous.REST * @property {String} previous.GROUP @@ -1120,6 +1130,7 @@ function OpenSeadragon( options ){ showHomeControl: true, //HOME showFullPageControl: true, //FULL showRotationControl: false, //ROTATION + showFlipControl: false, //FLIP controlsFadeDelay: 2000, //ZOOM/HOME/FULL/SEQUENCE controlsFadeLength: 1500, //ZOOM/HOME/FULL/SEQUENCE mouseNavEnabled: true, //GENERAL MOUSE INTERACTIVITY @@ -1141,6 +1152,9 @@ function OpenSeadragon( options ){ // INITIAL ROTATION degrees: 0, + // INITIAL FLIP STATE + flipped: false, + // APPEARANCE opacity: 1, preload: false, @@ -1209,6 +1223,12 @@ function OpenSeadragon( options ){ HOVER: 'rotateright_hover.png', DOWN: 'rotateright_pressed.png' }, + flip: { // Flip icon by Yaroslav Samoylov from the Noun Project + REST: 'flip_rest.png', + GROUP: 'flip_grouphover.png', + HOVER: 'flip_hover.png', + DOWN: 'flip_pressed.png' + }, previous: { REST: 'previous_rest.png', GROUP: 'previous_grouphover.png', diff --git a/src/strings.js b/src/strings.js index cd3c7907..88a6a7e5 100644 --- a/src/strings.js +++ b/src/strings.js @@ -57,7 +57,8 @@ var I18N = { NextPage: "Next page", PreviousPage: "Previous page", RotateLeft: "Rotate left", - RotateRight: "Rotate right" + RotateRight: "Rotate right", + Flip: "Flip Horizontally" } }; diff --git a/src/tiledimage.js b/src/tiledimage.js index 49ebcb22..1523e5a7 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -844,7 +844,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag return; } if (immediately) { - this._degreesSpring.resetTo(degrees); + this._degreesSpring.resetTo(degrees); } else { this._degreesSpring.springTo(degrees); } @@ -1886,6 +1886,10 @@ function drawTiles( tiledImage, lastDrawn ) { degrees: tiledImage.viewport.degrees, useSketch: useSketch }); + } else { + if(tiledImage._drawer.viewer.flipped) { + tiledImage._drawer._flip({}); + } } if (tiledImage.getRotation(true) % 360 !== 0) { tiledImage._drawer._offsetForRotation({ @@ -1969,6 +1973,10 @@ function drawTiles( tiledImage, lastDrawn ) { } if (tiledImage.viewport.degrees !== 0) { tiledImage._drawer._restoreRotationChanges(useSketch); + } else{ + if(tiledImage._drawer.viewer.flipped) { + tiledImage._drawer._flip({}); + } } } diff --git a/src/viewer.js b/src/viewer.js index 341e3d49..792ebb73 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1252,6 +1252,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * @param {Boolean} [options.preload=false] Default switch for loading hidden images (true loads, false blocks) * @param {Number} [options.degrees=0] Initial rotation of the tiled image around * its top left corner in degrees. + * @param {Boolean} [options.flipped=false] Initial flip/mirror state * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {String} [options.crossOriginPolicy] The crossOriginPolicy for this specific image, * overriding viewer.crossOriginPolicy. @@ -1414,6 +1415,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, opacity: queueItem.options.opacity, preload: queueItem.options.preload, degrees: queueItem.options.degrees, + flipped: queueItem.options.flipped, compositeOperation: queueItem.options.compositeOperation, springStiffness: _this.springStiffness, animationTime: _this.animationTime, @@ -1674,6 +1676,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, onFullScreenHandler = $.delegate( this, onFullScreen ), onRotateLeftHandler = $.delegate( this, onRotateLeft ), onRotateRightHandler = $.delegate( this, onRotateRight ), + onFlipHandler = $.delegate( this, onFlip), onFocusHandler = $.delegate( this, onFocus ), onBlurHandler = $.delegate( this, onBlur ), navImages = this.navImages, @@ -1685,7 +1688,8 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if( this.zoomInButton || this.zoomOutButton || this.homeButton || this.fullPageButton || - this.rotateLeftButton || this.rotateRightButton ) { + this.rotateLeftButton || this.rotateRightButton || + this.flipButton ) { //if we are binding to custom buttons then layout and //grouping is the responsibility of the page author useGroup = false; @@ -1789,7 +1793,22 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, onFocus: onFocusHandler, onBlur: onBlurHandler })); + } + if ( this.showFlipControl ) { + buttons.push( this.flipButton = new $.Button({ + element: this.flipButton ? $.getElement( this.flipButton ) : null, + clickTimeThreshold: this.clickTimeThreshold, + clickDistThreshold: this.clickDistThreshold, + tooltip: $.getString( "Tooltips.Flip" ), + srcRest: resolveUrl( this.prefixUrl, navImages.flip.REST ), + srcGroup: resolveUrl( this.prefixUrl, navImages.flip.GROUP ), + srcHover: resolveUrl( this.prefixUrl, navImages.flip.HOVER ), + srcDown: resolveUrl( this.prefixUrl, navImages.flip.DOWN ), + onRelease: onFlipHandler, + onFocus: onFocusHandler, + onBlur: onBlurHandler + })); } if ( useGroup ) { @@ -2602,8 +2621,26 @@ function onCanvasKeyPress( event ) { this.viewport.applyConstraints(); } return false; + case 114: //r + case 82: //R + if(this.flipped){ + this.viewport.setRotation(this.viewport.degrees - 90); + } else{ + this.viewport.setRotation(this.viewport.degrees + 90); + } + this.viewport.applyConstraints(); + return false; + case 70: //F + case 102: //f + this.flipped = !this.flipped; + if(this.navigator){ + this.navigator.toogleFlip(); + } + this._forceRedraw = !this._forceRedraw; + this.forceRedraw(); + return false; default: - //console.log( 'navigator keycode %s', event.keyCode ); + // console.log( 'navigator keycode %s', event.keyCode ); return true; } } else { @@ -2620,6 +2657,9 @@ function onCanvasClick( event ) { if ( !haveKeyboardFocus ) { this.canvas.focus(); } + if(this.flipped){ + event.position.x = this.viewport.getContainerSize().x - event.position.x; + } var canvasClickEventArgs = { tracker: event.eventSource, @@ -2739,6 +2779,9 @@ function onCanvasDrag( event ) { if( !this.panVertical ){ event.delta.y = 0; } + if(this.flipped){ + event.delta.x = -event.delta.x; + } if( this.constrainDuringPan ){ var delta = this.viewport.deltaPointsFromPixels( event.delta.negate() ); @@ -3064,6 +3107,10 @@ function onCanvasScroll( event ) { if (deltaScrollTime > this.minScrollDeltaTime) { this._lastScrollTime = thisScrollTime; + if(this.flipped){ + event.position.x = this.viewport.getContainerSize().x - event.position.x; + } + if ( !event.preventDefaultAction && this.viewport ) { gestureSettings = this.gestureSettingsByDeviceType( event.pointerType ); if ( gestureSettings.scrollToZoom ) { @@ -3259,7 +3306,7 @@ function updateOnce( viewer ) { drawWorld( viewer ); viewer._drawOverlays(); if( viewer.navigator ){ - viewer.navigator.update( viewer.viewport ); + viewer.navigator.update( viewer.viewport ); } THIS[ viewer.hash ].forceRedraw = false; @@ -3429,11 +3476,20 @@ function onFullScreen() { function onRotateLeft() { if ( this.viewport ) { var currRotation = this.viewport.getRotation(); - if (currRotation === 0) { - currRotation = 270; - } - else { - currRotation -= 90; + if ( this.flipped ){ + if (currRotation === 270) { + currRotation = 0; + } + else { + currRotation += 90; + } + } else { + if (currRotation === 0) { + currRotation = 270; + } + else { + currRotation -= 90; + } } this.viewport.setRotation(currRotation); } @@ -3445,16 +3501,38 @@ function onRotateLeft() { function onRotateRight() { if ( this.viewport ) { var currRotation = this.viewport.getRotation(); - if (currRotation === 270) { - currRotation = 0; - } - else { - currRotation += 90; + if ( this.flipped ){ + if (currRotation === 0) { + currRotation = 270; + } + else { + currRotation -= 90; + } + } else { + if (currRotation === 270) { + currRotation = 0; + } + else { + currRotation += 90; + } } this.viewport.setRotation(currRotation); } } +/** + * Note: The current rotation feature is limited to 90 degree turns. + */ +function onFlip() { + this.flipped = !this.flipped; + if(this.navigator){ + this.navigator.toogleFlip(); + } + this._forceRedraw = !this._forceRedraw; + this.forceRedraw(); +} + + function onPrevious(){ var previous = this._sequenceIndex - 1; diff --git a/src/viewport.js b/src/viewport.js index 9d357910..e44f46bb 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -886,7 +886,6 @@ $.Viewport.prototype = { this.viewer.world.getHomeBounds(), this.viewer.world.getContentFactor()); this.viewer.forceRedraw(); - /** * Raised when rotation has been changed. *