blob: a9b27b635ab5358e06006a4795c5c2baf7cfc0c7 [file] [log] [blame]
[email protected]3528d6302014-02-19 08:13:071// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Predefined zoom factors to be used when zooming in/out. These are in
7 * ascending order.
8 */
9var ZOOM_FACTORS = [0.25, 0.333, 0.5, 0.666, 0.75, 0.9, 1,
10 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
11
12/**
13 * Returns the area of the intersection of two rectangles.
14 * @param {Object} rect1 the first rect
15 * @param {Object} rect2 the second rect
16 * @return {number} the area of the intersection of the rects
17 */
18function getIntersectionArea(rect1, rect2) {
19 var xOverlap = Math.max(0,
20 Math.min(rect1.x + rect1.width, rect2.x + rect2.width) -
21 Math.max(rect1.x, rect2.x));
22 var yOverlap = Math.max(0,
23 Math.min(rect1.y + rect1.height, rect2.y + rect2.height) -
24 Math.max(rect1.y, rect2.y));
25 return xOverlap * yOverlap;
26}
27
28/**
29 * @return {number} width of a scrollbar in pixels
30 */
31function getScrollbarWidth() {
32 var parentDiv = document.createElement('div');
33 parentDiv.style.visibility = 'hidden';
34 var parentDivWidth = 500;
35 parentDiv.style.width = parentDivWidth + 'px';
36 document.body.appendChild(parentDiv);
37 parentDiv.style.overflow = 'scroll';
38 var childDiv = document.createElement('div');
39 childDiv.style.width = '100%';
40 parentDiv.appendChild(childDiv);
41 var childDivWidth = childDiv.offsetWidth;
42 parentDiv.parentNode.removeChild(parentDiv);
43 return parentDivWidth - childDivWidth;
44}
45
46/**
47 * Create a new viewport.
48 * @param {Window} window the window
49 * @param {Object} sizer is the element which represents the size of the
50 * document in the viewport
51 * @param {Function} fitToPageEnabledFunction returns true if fit-to-page is
52 * enabled
53 * @param {Function} viewportChangedCallback is run when the viewport changes
54 */
55function Viewport(window,
56 sizer,
57 fitToPageEnabledFunction,
58 viewportChangedCallback) {
59 this.window_ = window;
60 this.sizer_ = sizer;
61 this.fitToPageEnabledFunction_ = fitToPageEnabledFunction;
62 this.viewportChangedCallback_ = viewportChangedCallback;
63 this.zoom_ = 1;
64 this.documentDimensions_ = {};
65 this.pageDimensions_ = [];
66 this.scrollbarWidth_ = getScrollbarWidth();
67
68 window.addEventListener('scroll', this.updateViewport_.bind(this));
69}
70
71Viewport.prototype = {
72 /**
73 * @private
74 * Returns true if the document needs scrollbars at the given zoom level.
75 * @param {number} zoom compute whether scrollbars are needed at this zoom
76 * @return {Object} with 'x' and 'y' keys which map to bool values
77 * indicating if the horizontal and vertical scrollbars are needed
78 * respectively.
79 */
80 documentNeedsScrollbars_: function(zoom) {
81 return {
82 x: this.documentDimensions_.width * zoom > this.window_.innerWidth,
83 y: this.documentDimensions_.height * zoom > this.window_.innerHeight
84 };
85 },
86
87 /**
88 * Returns true if the document needs scrollbars at the current zoom level.
89 * @return {Object} with 'x' and 'y' keys which map to bool values
90 * indicating if the horizontal and vertical scrollbars are needed
91 * respectively.
92 */
93 documentHasScrollbars: function() {
94 return this.documentNeedsScrollbars_(this.zoom_);
95 },
96
97 /**
98 * @private
99 * Helper function called when the zoomed document size changes.
100 */
101 contentSizeChanged_: function() {
102 this.sizer_.style.width =
103 this.documentDimensions_.width * this.zoom_ + 'px';
104 this.sizer_.style.height =
105 this.documentDimensions_.height * this.zoom_ + 'px';
106 },
107
108 /**
109 * Sets the zoom of the viewport.
110 * @param {number} newZoom the zoom level to zoom to
111 */
112 setZoom: function(newZoom) {
113 var oldZoom = this.zoom_;
114 this.zoom_ = newZoom;
115 // Record the scroll position (relative to the middle of the window).
116 var currentScrollPos = [
117 (this.window_.scrollX + this.window_.innerWidth / 2) / oldZoom,
118 (this.window_.scrollY + this.window_.innerHeight / 2) / oldZoom
119 ];
120 this.contentSizeChanged_();
121 // Scroll to the scaled scroll position.
122 this.window_.scrollTo(
123 currentScrollPos[0] * newZoom - this.window_.innerWidth / 2,
124 currentScrollPos[1] * newZoom - this.window_.innerHeight / 2);
125 },
126
127 /**
128 * @private
129 * Called when the viewport should be updated.
130 */
131 updateViewport_: function() {
[email protected]3528d6302014-02-19 08:13:07132 var needsScrollbars = this.documentHasScrollbars();
[email protected]8ea559b2014-02-20 04:21:35133 var page = this.getMostVisiblePage();
[email protected]3528d6302014-02-19 08:13:07134 this.viewportChangedCallback_(this.zoom_,
135 this.window_.pageXOffset,
136 this.window_.pageYOffset,
137 this.scrollbarWidth_,
[email protected]8ea559b2014-02-20 04:21:35138 needsScrollbars,
139 page);
[email protected]3528d6302014-02-19 08:13:07140 },
141
142 /**
143 * @private
144 * Returns a rect representing the current viewport.
145 * @return {Object} a rect representing the current viewport.
146 */
147 getCurrentViewportRect_: function() {
148 return {
149 x: this.window_.pageXOffset / this.zoom_,
150 y: this.window_.pageYOffset / this.zoom_,
151 width: this.window_.innerWidth / this.zoom_,
152 height: this.window_.innerHeight / this.zoom_,
153 };
154 },
155
156 /**
[email protected]56b7e3c2014-02-20 04:31:24157 * @private
158 * @param {integer} y the y-coordinate to get the page at.
159 * @return {integer} the index of a page overlapping the given y-coordinate.
160 */
161 getPageAtY_: function(y) {
162 var min = 0;
163 var max = this.pageDimensions_.length - 1;
164 while (max >= min) {
165 var page = Math.floor(min + ((max - min) / 2));
[email protected]13df2a42014-02-27 03:50:41166 // There might be a gap between the pages, in which case use the bottom
167 // of the previous page as the top for finding the page.
168 var top = 0;
169 if (page > 0) {
170 top = this.pageDimensions_[page - 1].y +
171 this.pageDimensions_[page - 1].height;
172 }
173 var bottom = this.pageDimensions_[page].y +
174 this.pageDimensions_[page].height;
175
[email protected]56b7e3c2014-02-20 04:31:24176 if (top <= y && bottom > y)
177 return page;
178 else if (top > y)
179 max = page - 1;
180 else
181 min = page + 1;
182 }
183 return 0;
184 },
185
186 /**
[email protected]3528d6302014-02-19 08:13:07187 * Returns the page with the most pixels in the current viewport.
188 * @return {int} the index of the most visible page.
189 */
190 getMostVisiblePage: function() {
[email protected]56b7e3c2014-02-20 04:31:24191 var firstVisiblePage = this.getPageAtY_(this.getCurrentViewportRect_().y);
[email protected]3528d6302014-02-19 08:13:07192 var mostVisiblePage = {'number': 0, 'area': 0};
[email protected]56b7e3c2014-02-20 04:31:24193 for (var i = firstVisiblePage; i < this.pageDimensions_.length; i++) {
[email protected]3528d6302014-02-19 08:13:07194 var area = getIntersectionArea(this.pageDimensions_[i],
195 this.getCurrentViewportRect_());
[email protected]56b7e3c2014-02-20 04:31:24196 // If we hit a page with 0 area overlap, we must have gone past the
197 // pages visible in the viewport so we can break.
198 if (area == 0)
199 break;
[email protected]3528d6302014-02-19 08:13:07200 if (area > mostVisiblePage.area) {
201 mostVisiblePage.area = area;
202 mostVisiblePage.number = i;
203 }
204 }
205 return mostVisiblePage.number;
206 },
207
208 /**
209 * @private
210 * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
211 * the dimensions for a given page and if |widthOnly| is true, it indicates
212 * that fit-to-page zoom should be computed rather than fit-to-page.
213 * @param {Object} pageDimensions the dimensions of a given page
214 * @param {boolean} widthOnly a bool indicating whether fit-to-page or
215 * fit-to-width should be computed.
216 * @return {number} the zoom to use
217 */
218 computeFittingZoom_: function(pageDimensions, widthOnly) {
219 // First compute the zoom without scrollbars.
220 var zoomWidth = this.window_.innerWidth / pageDimensions.width;
221 var zoom;
222 if (widthOnly) {
223 zoom = zoomWidth;
224 } else {
225 var zoomHeight = this.window_.innerHeight / pageDimensions.height;
226 zoom = Math.min(zoomWidth, zoomHeight);
227 }
228 // Check if there needs to be any scrollbars.
229 var needsScrollbars = this.documentNeedsScrollbars_(zoom);
230
231 // If the document fits, just return the zoom.
232 if (!needsScrollbars.x && !needsScrollbars.y)
233 return zoom;
234
235 var zoomedDimensions = {
236 width: this.documentDimensions_.width * zoom,
237 height: this.documentDimensions_.height * zoom
238 };
239
240 // Check if adding a scrollbar will result in needing the other scrollbar.
241 var scrollbarWidth = this.scrollbarWidth_;
242 if (needsScrollbars.x &&
243 zoomedDimensions.height > this.window_.innerHeight - scrollbarWidth) {
244 needsScrollbars.y = true;
245 }
246 if (needsScrollbars.y &&
247 zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) {
248 needsScrollbars.x = true;
249 }
250
251 // Compute available window space.
252 var windowWithScrollbars = {
253 width: this.window_.innerWidth,
254 height: this.window_.innerHeight
255 };
256 if (needsScrollbars.x)
257 windowWithScrollbars.height -= scrollbarWidth;
258 if (needsScrollbars.y)
259 windowWithScrollbars.width -= scrollbarWidth;
260
261 // Recompute the zoom.
262 zoomWidth = windowWithScrollbars.width / pageDimensions.width;
263 if (widthOnly) {
264 zoom = zoomWidth;
265 } else {
266 var zoomHeight = windowWithScrollbars.height / pageDimensions.height;
267 zoom = Math.min(zoomWidth, zoomHeight);
268 }
269 return zoom;
270 },
271
272 /**
273 * Zoom the viewport so that the page-width consumes the entire viewport.
274 */
275 fitToWidth: function() {
276 var page = this.getMostVisiblePage();
277 this.setZoom(this.computeFittingZoom_(this.pageDimensions_[page], true));
278 this.window_.scrollTo(this.pageDimensions_[page].x * this.zoom_,
279 this.window_.scrollY);
280 this.updateViewport_();
281 },
282
283 /**
284 * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
285 * to the top of the most visible page.
286 */
287 fitToPage: function() {
288 var page = this.getMostVisiblePage();
289 this.setZoom(this.computeFittingZoom_(this.pageDimensions_[page], false));
290 this.window_.scrollTo(this.pageDimensions_[page].x * this.zoom_,
291 this.pageDimensions_[page].y * this.zoom_);
292 this.updateViewport_();
293 },
294
295 /**
296 * Zoom out to the next predefined zoom level.
297 */
298 zoomOut: function() {
299 var nextZoom = ZOOM_FACTORS[0];
300 for (var i = 0; i < ZOOM_FACTORS.length; i++) {
301 if (ZOOM_FACTORS[i] < this.zoom_)
302 nextZoom = ZOOM_FACTORS[i];
303 }
304 this.setZoom(nextZoom);
305 this.updateViewport_();
306 },
307
308 /**
309 * Zoom in to the next predefined zoom level.
310 */
311 zoomIn: function() {
312 var nextZoom = ZOOM_FACTORS[ZOOM_FACTORS.length - 1];
313 for (var i = ZOOM_FACTORS.length - 1; i >= 0; i--) {
314 if (ZOOM_FACTORS[i] > this.zoom_)
315 nextZoom = ZOOM_FACTORS[i];
316 }
317 this.setZoom(nextZoom);
318 this.updateViewport_();
319 },
320
321 /**
322 * Go to the given page index.
323 * @param {number} page the index of the page to go to
324 */
325 goToPage: function(page) {
326 if (page < 0)
327 page = 0;
328 var dimensions = this.pageDimensions_[page];
329 this.window_.scrollTo(dimensions.x * this.zoom_, dimensions.y * this.zoom_);
330 },
331
332 /**
333 * Set the dimensions of the document.
334 * @param {Object} documentDimensions the dimensions of the document
335 */
336 setDocumentDimensions: function(documentDimensions) {
337 this.documentDimensions_ = documentDimensions;
338 this.pageDimensions_ = this.documentDimensions_.pageDimensions;
339 this.contentSizeChanged_();
340 this.updateViewport_();
341 }
342};