When zoomed in, scroll up/down such that a percentage of the current page is still...
[bookreader.git] / BookReader / BookReader.js
index 707f007..bd12273 100644 (file)
@@ -84,14 +84,24 @@ function BookReader() {
     
     // Zoom levels
     // $$$ provide finer grained zooming
-    this.reductionFactors = [0.5, 1, 2, 4, 8, 16];
-
+    this.reductionFactors = [ {reduce: 0.5, autofit: null},
+                              {reduce: 1, autofit: null},
+                              {reduce: 2, autofit: null},
+                              {reduce: 4, autofit: null},
+                              {reduce: 8, autofit: null},
+                              {reduce: 16, autofit: null} ];
+
+    // Object to hold parameters related to 1up mode
+    this.onePage = {
+        autofit: 'height'                                     // valid values are height, width, none
+    },
+    
     // Object to hold parameters related to 2up mode
     this.twoPage = {
         coverInternalPadding: 10, // Width of cover
         coverExternalPadding: 10, // Padding outside of cover
         bookSpineDivWidth: 30,    // Width of book spine  $$$ consider sizing based on book length
-        autofit: true
+        autofit: 'auto',
     };
     
     // Background color for pages (e.g. when loading page image)
@@ -149,7 +159,7 @@ BookReader.prototype.init = function() {
     $("#BRcontainer").bind('scroll', this, function(e) {
         e.data.loadLeafs();
     });
-    
+        
     this.setupKeyListeners();
     this.startLocationPolling();
 
@@ -157,6 +167,9 @@ BookReader.prototype.init = function() {
         //console.log('resize!');
         if (1 == e.data.mode) {
             //console.log('centering 1page view');
+            if (e.data.autofit) {
+                e.data.resizePageView();
+            }
             e.data.centerPageView();
             $('#BRpageview').empty()
             e.data.displayedIndices = [];
@@ -834,33 +847,47 @@ BookReader.prototype.loadLeafs = function() {
 BookReader.prototype.zoom = function(direction) {
     switch (this.mode) {
         case this.constMode1up:
-            return this.zoom1up(direction);
+            if (direction == 1) {
+                // XXX other cases
+                return this.zoom1up('in');
+            } else {
+                return this.zoom1up('out');
+            }
+            
         case this.constMode2up:
-            return this.zoom2up(direction);
+            if (direction == 1) {
+                // XXX other cases
+                return this.zoom2up('in');
+            } else { 
+                return this.zoom2up('out');
+            }
+            
         case this.constModeThumb:
+            // XXX update zoomThumb for named directions
             return this.zoomThumb(direction);
+            
     }
 }
 
 // zoom1up(dir)
 //______________________________________________________________________________
-BookReader.prototype.zoom1up = function(dir) {
+BookReader.prototype.zoom1up = function(direction) {
 
     if (2 == this.mode) {     //can only zoom in 1-page mode
         this.switchMode(1);
         return;
     }
     
-    // $$$ with flexible zoom we could "snap" to /2 page reductions
-    //     for better scaling
-    if (1 == dir) {
-        if (this.reduce <= 0.5) return;
-        this.reduce*=0.5;           //zoom in
-    } else {
-        if (this.reduce >= 8) return;
-        this.reduce*=2;             //zoom out
+    var reduceFactor = this.nextReduce(this.reduce, direction, this.onePage.reductionFactors);
+    
+    if (this.reduce == reduceFactor.reduce) {
+        // Already at this level
+        return;
     }
-
+    
+    this.reduce = reduceFactor.reduce; // $$$ incorporate into function
+    this.onePage.autofit = reduceFactor.autofit;
+        
     this.pageScale = this.reduce; // preserve current reduce
     this.resizePageView();
 
@@ -917,6 +944,15 @@ BookReader.prototype.resizePageView1up = function() {
         var scrollRatio = 0;
     }
     
+    // Recalculate 1up reduction factors
+    this.onePageCalculateReductionFactors( $('#BRcontainer').attr('clientWidth'),
+                                           $('#BRcontainer').attr('clientHeight') );                                        
+    // Update current reduce (if in autofit)
+    if (this.onePage.autofit) {
+        var reductionFactor = this.nextReduce(this.reduce, this.onePage.autofit, this.onePage.reductionFactors);
+        this.reduce = reductionFactor.reduce;
+    }
+    
     for (i=0; i<this.numLeafs; i++) {
         viewHeight += parseInt(this._getPageHeight(i)/this.reduce) + this.padding; 
         var width = parseInt(this._getPageWidth(i)/this.reduce);
@@ -986,13 +1022,17 @@ BookReader.prototype.zoom2up = function(direction) {
     // Hard stop autoplay
     this.stopFlipAnimations();
     
-    // Get new zoom state    
-    var newZoom = this.twoPageNextReduce(this.reduce, direction);
-    if ((this.reduce == newZoom.reduce) && (this.twoPage.autofit == newZoom.autofit)) {
+    // Recalculate autofit factors
+    this.twoPageCalculateReductionFactors();
+    
+    // Get new zoom state
+    var reductionFactor = this.nextReduce(this.reduce, direction, this.twoPage.reductionFactors);
+    if ((this.reduce == reductionFactor.reduce) && (this.twoPage.autofit == reductionFactor.autofit)) {
+        // Same zoom
         return;
     }
-    this.twoPage.autofit = newZoom.autofit;
-    this.reduce = newZoom.reduce;
+    this.twoPage.autofit = reductionFactor.autofit;
+    this.reduce = reductionFactor.reduce;
     this.pageScale = this.reduce; // preserve current reduce
 
     // Preserve view center position
@@ -1045,70 +1085,62 @@ BookReader.prototype.getThumbnailWidth = function(thumbnailColumns) {
 // quantizeReduce(reduce)
 //______________________________________________________________________________
 // Quantizes the given reduction factor to closest power of two from set from 12.5% to 200%
-BookReader.prototype.quantizeReduce = function(reduce) {
-    var quantized = this.reductionFactors[0];
+BookReader.prototype.quantizeReduce = function(reduce, reductionFactors) {
+    var quantized = reductionFactors[0].reduce;
     var distance = Math.abs(reduce - quantized);
-    for (var i = 1; i < this.reductionFactors.length; i++) {
-        newDistance = Math.abs(reduce - this.reductionFactors[i]);
+    for (var i = 1; i < reductionFactors.length; i++) {
+        newDistance = Math.abs(reduce - reductionFactors[i].reduce);
         if (newDistance < distance) {
             distance = newDistance;
-            quantized = this.reductionFactors[i];
+            quantized = reductionFactors[i].reduce;
         }
     }
     
     return quantized;
 }
 
-// twoPageNextReduce()
-//______________________________________________________________________________
-// Returns the next reduction level
-BookReader.prototype.twoPageNextReduce = function(reduce, direction) {
-    var result = {};
-    var autofitReduce = this.twoPageGetAutofitReduce();
+// reductionFactors should be array of sorted reduction factors
+// e.g. [ {reduce: 0.25, autofit: null}, {reduce: 0.3, autofit: 'width'}, {reduce: 1, autofit: null} ]
+BookReader.prototype.nextReduce = function( currentReduce, direction, reductionFactors ) {
 
-    if (0 == direction) { // autofit
-        result.autofit = true;
-        result.reduce = autofitReduce;
-        
-    } else if (1 == direction) { // zoom in
-        var newReduce = this.reductionFactors[0];
+    // XXX add 'closest', to replace quantize function
     
-        for (var i = 1; i < this.reductionFactors.length; i++) {
-            if (this.reductionFactors[i] < reduce) {
-                newReduce = this.reductionFactors[i];
+    if (direction == 'in') {
+        var newReduceIndex = 0;
+    
+        for (var i = 1; i < reductionFactors.length; i++) {
+            if (reductionFactors[i].reduce < currentReduce) {
+                newReduceIndex = i;
             }
         }
+        return reductionFactors[newReduceIndex];
         
-        if (!this.twoPage.autofit && (autofitReduce < reduce && autofitReduce > newReduce)) {
-            // use autofit
-            result.autofit = true;
-            result.reduce = autofitReduce;
-        } else {        
-            result.autofit = false;
-            result.reduce = newReduce;
-        }
-        
-    } else { // zoom out
-        var lastIndex = this.reductionFactors.length - 1;
-        var newReduce = this.reductionFactors[lastIndex];
+    } else if (direction == 'out') { // zoom out
+        var lastIndex = reductionFactors.length - 1;
+        var newReduceIndex = lastIndex;
         
         for (var i = lastIndex; i >= 0; i--) {
-            if (this.reductionFactors[i] > reduce) {
-                newReduce = this.reductionFactors[i];
+            if (reductionFactors[i].reduce > currentReduce) {
+                newReduceIndex = i;
             }
         }
-         
-        if (!this.twoPage.autofit && (autofitReduce > reduce && autofitReduce < newReduce)) {
-            // use autofit
-            result.autofit = true;
-            result.reduce = autofitReduce;
-        } else {
-            result.autofit = false;
-            result.reduce = newReduce;
+        return reductionFactors[newReduceIndex];
+    }
+    
+    // Asked for specific autofit mode
+    for (var i = 0; i < reductionFactors.length; i++) {
+        if (reductionFactors[i].autofit == direction) {
+            return reductionFactors[i];
         }
     }
     
-    return result;
+    alert('Could not find reduction factor for direction ' + direction);
+    return reductionFactors[0];
+
+}
+
+BookReader.prototype._reduceSort = function(a, b) {
+    return a.reduce - b.reduce;
 }
 
 // jumpToPage()
@@ -1134,7 +1166,7 @@ BookReader.prototype.jumpToPage = function(pageNum) {
 //______________________________________________________________________________
 BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
 
-    if (2 == this.mode) {
+    if (this.constMode2up == this.mode) {
         this.autoStop();
         
         // By checking against min/max we do nothing if requested index
@@ -1145,7 +1177,7 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
             this.flipFwdToIndex(index);
         }
 
-    } else if (3 == this.mode) {
+    } else if (this.constModeThumb == this.mode) {
         var viewWidth = $('#BRcontainer').attr('scrollWidth') - 20; // width minus buffer
         var i;
         var leafWidth = 0;
@@ -1178,6 +1210,7 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
             $('#BRcontainer').animate({scrollTop: leafTop },'fast');
         }
     } else {
+        // 1up
         var i;
         var leafTop = 0;
         var leafLeft = 0;
@@ -1193,6 +1226,9 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
             offset -= $('#BRcontainer').attr('clientHeight') >> 1;
             //console.log( 'jumping to ' + leafTop + ' ' + offset);
             leafTop += offset;
+        } else {
+            // Show page just a little below the top
+            leafTop -= this.padding / 2;
         }
 
         if (pageX) {
@@ -1235,16 +1271,19 @@ BookReader.prototype.switchMode = function(mode) {
     // $$$ TODO preserve center of view when switching between mode
     //     See https://bugs.edge.launchpad.net/gnubook/+bug/416682
 
+    // XXX maybe better to preserve zoom in each mode
     if (1 == mode) {
-        this.reduce = this.quantizeReduce(this.reduce);
+        this.onePageCalculateReductionFactors( $('#BRcontainer').attr('clientWidth'), $('#BRcontainer').attr('clientHeight'));
+        this.reduce = this.quantizeReduce(this.reduce, this.onePage.reductionFactors);
         this.prepareOnePageView();
     } else if (3 == mode) {
-        this.reduce = this.quantizeReduce(this.reduce);
+        this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors);
         this.prepareThumbnailView();
     } else {
         // $$$ why don't we save autofit?
-        this.twoPage.autofit = false; // Take zoom level from other mode
-        this.reduce = this.quantizeReduce(this.reduce);
+        this.twoPage.autofit = null; // Take zoom level from other mode
+        this.twoPageCalculateReductionFactors();
+        this.reduce = this.quantizeReduce(this.reduce, this.twoPage.reductionFactors);
         this.prepareTwoPageView();
         this.twoPageCenterView(0.5, 0.5); // $$$ TODO preserve center
     }
@@ -1395,33 +1434,27 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc
     }).appendTo('#BRtwopageview');
     
     this.leafEdgeR = document.createElement('div');
-    this.leafEdgeR.className = 'leafEdgeR'; // $$$ the static CSS should be moved into the .css file
+    this.leafEdgeR.className = 'BRleafEdgeR';
     $(this.leafEdgeR).css({
-        background: 'transparent url(' + this.imagesBaseURL + 'right_edges.png) repeat scroll 0% 0%',
         width: this.twoPage.leafEdgeWidthR + 'px',
         height: this.twoPage.height-1 + 'px',
-        /*right: '10px',*/
         left: this.twoPage.gutter+this.twoPage.scaledWR+'px',
         top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',
-        position: 'absolute'
     }).appendTo('#BRtwopageview');
     
     this.leafEdgeL = document.createElement('div');
-    this.leafEdgeL.className = 'leafEdgeL';
+    this.leafEdgeL.className = 'BRleafEdgeL';
     $(this.leafEdgeL).css({
-        background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: this.twoPage.leafEdgeWidthL + 'px',
         height: this.twoPage.height-1 + 'px',
         left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px',
         top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px',    
-        position: 'absolute'
     }).appendTo('#BRtwopageview');
 
     div = document.createElement('div');
     $(div).attr('id', 'BRbookspine').css({
         width:           this.twoPage.bookSpineDivWidth+'px',
         height:          this.twoPage.bookSpineDivHeight+'px',
-        position:        'absolute',
         left:            this.twoPage.bookSpineDivLeft+'px',
         top:             this.twoPage.bookSpineDivTop+'px'
     }).appendTo('#BRtwopageview');
@@ -1490,15 +1523,9 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc
 BookReader.prototype.prepareTwoPagePopUp = function() {
 
     this.twoPagePopUp = document.createElement('div');
+    this.twoPagePopUp.className = 'BRtwoPagePopUp';
     $(this.twoPagePopUp).css({
-        border: '1px solid black',
-        padding: '2px 6px',
-        position: 'absolute',
-        fontFamily: 'sans-serif',
-        fontSize: '14px',
         zIndex: '1000',
-        backgroundColor: 'rgb(255, 255, 238)',
-        opacity: 0.85
     }).appendTo('#BRcontainer');
     $(this.twoPagePopUp).hide();
     
@@ -1710,6 +1737,55 @@ BookReader.prototype.twoPageGetAutofitReduce = function() {
     return spreadSize.reduce;
 }
 
+BookReader.prototype.onePageGetAutofitWidth = function() {
+    var widthPadding = 20;
+    return (this.getMedianPageSize().width + 0.0) / ($('#BRcontainer').attr('clientWidth') - widthPadding * 2);
+}
+
+BookReader.prototype.onePageGetAutofitHeight = function() {
+    return (this.getMedianPageSize().height + 0.0) / ($('#BRcontainer').attr('clientHeight') - this.padding * 2); // make sure a little of adjacent pages show
+}
+
+BookReader.prototype.getMedianPageSize = function() {
+    if (this._medianPageSize) {
+        return this._medianPageSize;
+    }
+    
+    // A little expensive but we just do it once
+    var widths = [];
+    var heights = [];
+    for (var i = 0; i < this.numLeafs; i++) {
+        widths.push(this.getPageWidth(i));
+        heights.push(this.getPageHeight(i));
+    }
+    
+    widths.sort();
+    heights.sort();
+    
+    this._medianPageSize = { width: widths[parseInt(widths.length / 2)], height: heights[parseInt(heights.length / 2)] };
+    return this._medianPageSize; 
+}
+
+// Update the reduction factors for 1up mode given the available width and height.  Recalculates
+// the autofit reduction factors.
+BookReader.prototype.onePageCalculateReductionFactors = function( width, height ) {
+    this.onePage.reductionFactors = this.reductionFactors.concat(
+        [ 
+            { reduce: this.onePageGetAutofitWidth(), autofit: 'width' },
+            { reduce: this.onePageGetAutofitHeight(), autofit: 'height'}
+        ]);
+    this.onePage.reductionFactors.sort(this._reduceSort);
+}
+
+BookReader.prototype.twoPageCalculateReductionFactors = function() {    
+    this.twoPage.reductionFactors = this.reductionFactors.concat(
+        [
+            { reduce: this.getIdealSpreadSize( this.twoPage.currentIndexL, this.twoPage.currentIndexR ).reduce,
+              autofit: 'auto' }
+        ]);
+    this.twoPage.reductionFactors.sort(this._reduceSort);
+}
+
 // twoPageSetCursor()
 //______________________________________________________________________________
 // Set the cursor for two page view
@@ -1835,10 +1911,15 @@ BookReader.prototype.last = function() {
 //______________________________________________________________________________
 // Scrolls down one screen view
 BookReader.prototype.scrollDown = function() {
-    if ($.inArray(this.mode, [this.constMode2up, this.constModeThumb]) >= 0) {
+    if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) {
+        if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) {
+            // Whole pages are visible, scroll whole page only
+            return this.next();
+        }
+    
         $('#BRcontainer').animate(
-            { scrollTop: '+=' + $('#BRcontainer').height() * 0.95 + 'px'},
-            450, 'easeInOutQuint'
+            { scrollTop: '+=' + this._scrollAmount() + 'px'},
+            400, 'easeInOutExpo'
         );
         return true;
     } else {
@@ -1850,10 +1931,15 @@ BookReader.prototype.scrollDown = function() {
 //______________________________________________________________________________
 // Scrolls up one screen view
 BookReader.prototype.scrollUp = function() {
-    if ($.inArray(this.mode, [this.constMode2up, this.constModeThumb]) >= 0) {
+    if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) {
+        if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) {
+            // Whole pages are visible, scroll whole page only
+            return this.prev();
+        }
+
         $('#BRcontainer').animate(
-            { scrollTop: '-=' + $('#BRcontainer').height() * 0.95 + 'px'},
-            450, 'easeInOutQuint'
+            { scrollTop: '-=' + this._scrollAmount() + 'px'},
+            400, 'easeInOutExpo'
         );
         return true;
     } else {
@@ -1861,6 +1947,18 @@ BookReader.prototype.scrollUp = function() {
     }
 }
 
+// _scrollAmount()
+//______________________________________________________________________________
+// The amount to scroll vertically in integer pixels
+BookReader.prototype._scrollAmount = function() {
+    if (this.constMode1up == this.mode) {
+        // Overlap by % of page size
+        return parseInt($('#BRcontainer').attr('clientHeight') - this.getPageHeight(this.currentIndex()) / this.reduce * 0.03);
+    }
+    
+    return parseInt(0.9 * $('#BRcontainer').attr('clientHeight'));
+}
+
 
 // flipBackToIndex()
 //______________________________________________________________________________
@@ -1953,16 +2051,12 @@ BookReader.prototype.flipLeftToRight = function(newIndexL, newIndexR) {
     var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
 
     this.leafEdgeTmp = document.createElement('div');
+    this.leafEdgeTmp.className = 'BRleafEdgeTmp';
     $(this.leafEdgeTmp).css({
-        borderStyle: 'solid none solid solid',
-        borderColor: 'rgb(51, 51, 34)',
-        borderWidth: '1px 0px 1px 1px',
-        background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: leafEdgeTmpW + 'px',
         height: this.twoPage.height-1 + 'px',
         left: leftEdgeTmpLeft + 'px',
-        top: top+'px',    
-        position: 'absolute',
+        top: top+'px',
         zIndex:1000
     }).appendTo('#BRtwopageview');
     
@@ -2106,16 +2200,12 @@ BookReader.prototype.flipRightToLeft = function(newIndexL, newIndexR) {
     var gutter = middle + this.gutterOffsetForIndex(newIndexL);
     
     this.leafEdgeTmp = document.createElement('div');
+    this.leafEdgeTmp.className = 'BRleafEdgeTmp';
     $(this.leafEdgeTmp).css({
-        borderStyle: 'solid none solid solid',
-        borderColor: 'rgb(51, 51, 34)',
-        borderWidth: '1px 0px 1px 1px',
-        background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: leafEdgeTmpW + 'px',
         height: this.twoPage.height-1 + 'px',
         left: gutter+scaledW+'px',
         top: top+'px',    
-        position: 'absolute',
         zIndex:1000
     }).appendTo('#BRtwopageview');
 
@@ -2930,7 +3020,7 @@ BookReader.prototype.autoToggle = function() {
     
     // Change to autofit if book is too large
     if (this.reduce < this.twoPageGetAutofitReduce()) {
-        this.zoom2up(0);
+        this.zoom2up('auto');
     }
 
     var self = this;
@@ -3205,7 +3295,7 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) {
     });
         
     jToolbar.find('.book_up').bind('click', function(e) {
-        if ($.inArray(self.mode, [self.constMode2up, self.constModeThumb]) >= 0) {
+        if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
             self.scrollUp();
         } else {
             self.prev();
@@ -3214,7 +3304,7 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) {
     });        
         
     jToolbar.find('.book_down').bind('click', function(e) {
-        if ($.inArray(self.mode, [self.constMode2up, self.constModeThumb]) >= 0) {
+        if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
             self.scrollDown();
         } else {
             self.next();
@@ -3268,8 +3358,17 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) {
 // Update the displayed zoom factor based on reduction factor
 BookReader.prototype.updateToolbarZoom = function(reduce) {
     var value;
-    if (this.constMode2up == this.mode && this.twoPage.autofit) {
-        value = 'Auto';
+    var autofit = null;
+
+    // $$$ TODO preserve zoom/fit for each mode
+    if (this.mode == this.constMode2up) {
+        autofit = this.twoPage.autofit;
+    } else {
+        autofit = this.onePage.autofit;
+    }
+    
+    if (autofit) {
+        value = autofit[0].toUpperCase() + autofit.slice(1);
     } else {
         value = (100 / reduce).toFixed(2);
         // Strip trailing zeroes and decimal if all zeroes