OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * Viewport class controls the way the image is displayed (scale, offset etc). | 6 * Viewport class controls the way the image is displayed (scale, offset etc). |
| 7 * @constructor |
7 */ | 8 */ |
8 function Viewport() { | 9 function Viewport() { |
9 this.imageBounds_ = new Rect(); | 10 this.imageBounds_ = new Rect(); |
10 this.screenBounds_ = new Rect(); | 11 this.screenBounds_ = new Rect(); |
11 | 12 |
12 this.scale_ = 1; | 13 this.scale_ = 1; |
13 this.offsetX_ = 0; | 14 this.offsetX_ = 0; |
14 this.offsetY_ = 0; | 15 this.offsetY_ = 0; |
15 | 16 |
16 this.generation_ = 0; | 17 this.generation_ = 0; |
17 | 18 |
18 this.scaleControl_ = null; | 19 this.scaleControl_ = null; |
19 this.repaintCallbacks_ = []; | 20 this.repaintCallbacks_ = []; |
20 this.update(); | 21 this.update(); |
21 } | 22 } |
22 | 23 |
23 /* | 24 /* |
24 * Viewport modification. | 25 * Viewport modification. |
25 */ | 26 */ |
26 | 27 |
| 28 /** |
| 29 * @param {object} scaleControl The UI object responsible for scaling. |
| 30 */ |
27 Viewport.prototype.setScaleControl = function(scaleControl) { | 31 Viewport.prototype.setScaleControl = function(scaleControl) { |
28 this.scaleControl_ = scaleControl; | 32 this.scaleControl_ = scaleControl; |
29 }; | 33 }; |
30 | 34 |
| 35 /** |
| 36 * @param {number} width Image width. |
| 37 * @param {number} height Image height. |
| 38 */ |
31 Viewport.prototype.setImageSize = function(width, height) { | 39 Viewport.prototype.setImageSize = function(width, height) { |
32 this.imageBounds_ = new Rect(width, height); | 40 this.imageBounds_ = new Rect(width, height); |
33 if (this.scaleControl_) this.scaleControl_.displayImageSize(width, height); | 41 if (this.scaleControl_) this.scaleControl_.displayImageSize(width, height); |
34 this.invalidateCaches(); | 42 this.invalidateCaches(); |
35 }; | 43 }; |
36 | 44 |
| 45 /** |
| 46 * @param {number} width Screen width. |
| 47 * @param {number} height Screen height. |
| 48 */ |
37 Viewport.prototype.setScreenSize = function(width, height) { | 49 Viewport.prototype.setScreenSize = function(width, height) { |
38 this.screenBounds_ = new Rect(width, height); | 50 this.screenBounds_ = new Rect(width, height); |
39 if (this.scaleControl_) | 51 if (this.scaleControl_) |
40 this.scaleControl_.setMinScale(this.getFittingScale()); | 52 this.scaleControl_.setMinScale(this.getFittingScale()); |
41 this.invalidateCaches(); | 53 this.invalidateCaches(); |
42 }; | 54 }; |
43 | 55 |
| 56 /** |
| 57 * Set the size by an HTML element. |
| 58 * |
| 59 * @param {HTMLElement} frame The element acting as the "screen". |
| 60 */ |
44 Viewport.prototype.sizeByFrame = function(frame) { | 61 Viewport.prototype.sizeByFrame = function(frame) { |
45 this.setScreenSize(frame.clientWidth, frame.clientHeight); | 62 this.setScreenSize(frame.clientWidth, frame.clientHeight); |
46 }; | 63 }; |
47 | 64 |
| 65 /** |
| 66 * Set the size and scale to fit an HTML element. |
| 67 * |
| 68 * @param {HTMLElement} frame The element acting as the "screen". |
| 69 */ |
48 Viewport.prototype.sizeByFrameAndFit = function(frame) { | 70 Viewport.prototype.sizeByFrameAndFit = function(frame) { |
49 var wasFitting = this.getScale() == this.getFittingScale(); | 71 var wasFitting = this.getScale() == this.getFittingScale(); |
50 this.sizeByFrame(frame); | 72 this.sizeByFrame(frame); |
51 var minScale = this.getFittingScale(); | 73 var minScale = this.getFittingScale(); |
52 if (wasFitting || (this.getScale() < minScale)) { | 74 if (wasFitting || (this.getScale() < minScale)) { |
53 this.setScale(minScale, true); | 75 this.setScale(minScale, true); |
54 } | 76 } |
55 }; | 77 }; |
56 | 78 |
| 79 /** |
| 80 * @return {number} Scale |
| 81 */ |
57 Viewport.prototype.getScale = function() { return this.scale_ }; | 82 Viewport.prototype.getScale = function() { return this.scale_ }; |
58 | 83 |
| 84 /** |
| 85 * @param {number} scale The new scale. |
| 86 * @param {boolean} notify True if the change should be reflected in the UI. |
| 87 */ |
59 Viewport.prototype.setScale = function(scale, notify) { | 88 Viewport.prototype.setScale = function(scale, notify) { |
60 if (this.scale_ == scale) return; | 89 if (this.scale_ == scale) return; |
61 this.scale_ = scale; | 90 this.scale_ = scale; |
62 if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale); | 91 if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale); |
63 this.invalidateCaches(); | 92 this.invalidateCaches(); |
64 }; | 93 }; |
65 | 94 |
| 95 /** |
| 96 * @return {number} Best scale to fit the current image into the current screen. |
| 97 */ |
66 Viewport.prototype.getFittingScale = function() { | 98 Viewport.prototype.getFittingScale = function() { |
67 var scaleX = this.screenBounds_.width / this.imageBounds_.width; | 99 var scaleX = this.screenBounds_.width / this.imageBounds_.width; |
68 var scaleY = this.screenBounds_.height / this.imageBounds_.height; | 100 var scaleY = this.screenBounds_.height / this.imageBounds_.height; |
69 // Scales > (1 / this.getDevicePixelRatio()) do not look good. Also they are | 101 // Scales > (1 / this.getDevicePixelRatio()) do not look good. Also they are |
70 // not really useful as we do not have any pixel-level operations. | 102 // not really useful as we do not have any pixel-level operations. |
71 return Math.min(1 / this.getDevicePixelRatio(), scaleX, scaleY); | 103 return Math.min(1 / this.getDevicePixelRatio(), scaleX, scaleY); |
72 }; | 104 }; |
73 | 105 |
| 106 /** |
| 107 * Set the scale to fit the image into the screen. |
| 108 */ |
74 Viewport.prototype.fitImage = function() { | 109 Viewport.prototype.fitImage = function() { |
75 var scale = this.getFittingScale(); | 110 var scale = this.getFittingScale(); |
76 if (this.scaleControl_) this.scaleControl_.setMinScale(scale); | 111 if (this.scaleControl_) this.scaleControl_.setMinScale(scale); |
77 this.setScale(scale, true); | 112 this.setScale(scale, true); |
78 }; | 113 }; |
79 | 114 |
80 Viewport.prototype.getOffsetX = function () { return this.offsetX_ }; | 115 /** |
| 116 * @return {number} X-offset of the viewport. |
| 117 */ |
| 118 Viewport.prototype.getOffsetX = function() { return this.offsetX_ }; |
81 | 119 |
82 Viewport.prototype.getOffsetY = function () { return this.offsetY_ }; | 120 /** |
| 121 * @return {number} Y-offset of the viewport. |
| 122 */ |
| 123 Viewport.prototype.getOffsetY = function() { return this.offsetY_ }; |
83 | 124 |
| 125 /** |
| 126 * Set the image offset in the viewport. |
| 127 * @param {number} x X-offset. |
| 128 * @param {number} y Y-offset. |
| 129 * @param {boolean} ignoreClipping True if no clipping should be applied. |
| 130 */ |
84 Viewport.prototype.setOffset = function(x, y, ignoreClipping) { | 131 Viewport.prototype.setOffset = function(x, y, ignoreClipping) { |
85 if (!ignoreClipping) { | 132 if (!ignoreClipping) { |
86 x = this.clampOffsetX_(x); | 133 x = this.clampOffsetX_(x); |
87 y = this.clampOffsetY_(y); | 134 y = this.clampOffsetY_(y); |
88 } | 135 } |
89 if (this.offsetX_ == x && this.offsetY_ == y) return; | 136 if (this.offsetX_ == x && this.offsetY_ == y) return; |
90 this.offsetX_ = x; | 137 this.offsetX_ = x; |
91 this.offsetY_ = y; | 138 this.offsetY_ = y; |
92 this.invalidateCaches(); | 139 this.invalidateCaches(); |
93 }; | 140 }; |
94 | 141 |
95 Viewport.prototype.setCenter = function(x, y, ignoreClipping) { | |
96 this.setOffset( | |
97 this.imageBounds_.width / 2 - x, | |
98 this.imageBounds_.height / 2 - y, | |
99 ignoreClipping); | |
100 }; | |
101 | |
102 /** | 142 /** |
103 * Return a closure that can be called to pan the image. | 143 * Return a closure that can be called to pan the image. |
104 * Useful for implementing non-trivial variants of panning (overview etc). | 144 * Useful for implementing non-trivial variants of panning (overview etc). |
105 * @param {number} originalX The x coordinate on the screen canvas that | 145 * @param {number} originalX The x coordinate on the screen canvas that |
106 * corresponds to zero change to offsetX. | 146 * corresponds to zero change to offsetX. |
107 * @param {number} originalY The y coordinate on the screen canvas that | 147 * @param {number} originalY The y coordinate on the screen canvas that |
108 * corresponds to zero change to offsetY. | 148 * corresponds to zero change to offsetY. |
109 * @param {function():number} scaleFunc returns the image to screen scale. | 149 * @param {function():number} scaleFunc returns the image to screen scale. |
110 * @param {function(number,number):boolean} hitFunc returns true if (x,y) is | 150 * @param {function(number,number):boolean} hitFunc returns true if (x,y) is |
111 * in the valid region. | 151 * in the valid region. |
| 152 * @return {function} The closure to pan the image. |
112 */ | 153 */ |
113 Viewport.prototype.createOffsetSetter = function ( | 154 Viewport.prototype.createOffsetSetter = function( |
114 originalX, originalY, scaleFunc, hitFunc) { | 155 originalX, originalY, scaleFunc, hitFunc) { |
115 var originalOffsetX = this.offsetX_; | 156 var originalOffsetX = this.offsetX_; |
116 var originalOffsetY = this.offsetY_; | 157 var originalOffsetY = this.offsetY_; |
117 if (!hitFunc) hitFunc = function() { return true }; | 158 if (!hitFunc) hitFunc = function() { return true }; |
118 if (!scaleFunc) scaleFunc = this.getScale.bind(this); | 159 if (!scaleFunc) scaleFunc = this.getScale.bind(this); |
119 | 160 |
120 var self = this; | 161 var self = this; |
121 return function(x, y) { | 162 return function(x, y) { |
122 if (hitFunc(x, y)) { | 163 if (hitFunc(x, y)) { |
123 var scale = scaleFunc(); | 164 var scale = scaleFunc(); |
(...skipping 26 matching lines...) Expand all Loading... |
150 | 191 |
151 /** | 192 /** |
152 * @return {Rect} The visible part of the image, in screen coordinates. | 193 * @return {Rect} The visible part of the image, in screen coordinates. |
153 */ | 194 */ |
154 Viewport.prototype.getScreenClipped = function() { return this.screenClipped_ }; | 195 Viewport.prototype.getScreenClipped = function() { return this.screenClipped_ }; |
155 | 196 |
156 /** | 197 /** |
157 * A counter that is incremented with each viewport state change. | 198 * A counter that is incremented with each viewport state change. |
158 * Clients that cache anything that depends on the viewport state should keep | 199 * Clients that cache anything that depends on the viewport state should keep |
159 * track of this counter. | 200 * track of this counter. |
| 201 * @return {number} counter |
160 */ | 202 */ |
161 Viewport.prototype.getCacheGeneration = function() { return this.generation_ }; | 203 Viewport.prototype.getCacheGeneration = function() { return this.generation_ }; |
162 | 204 |
163 /** | 205 /** |
164 * Called on evert view port state change (even if repaint has not been called). | 206 * Called on evert view port state change (even if repaint has not been called). |
165 */ | 207 */ |
166 Viewport.prototype.invalidateCaches = function() { this.generation_++ }; | 208 Viewport.prototype.invalidateCaches = function() { this.generation_++ }; |
167 | 209 |
168 /** | 210 /** |
169 * @return {Rect} The image bounds in screen coordinates. | 211 * @return {Rect} The image bounds in screen coordinates. |
170 */ | 212 */ |
171 Viewport.prototype.getImageBoundsOnScreen = function() { | 213 Viewport.prototype.getImageBoundsOnScreen = function() { |
172 return this.imageOnScreen_; | 214 return this.imageOnScreen_; |
173 }; | 215 }; |
174 | 216 |
175 /* | 217 /* |
176 * Conversion between the screen and image coordinate spaces. | 218 * Conversion between the screen and image coordinate spaces. |
177 */ | 219 */ |
178 | 220 |
| 221 /** |
| 222 * @param {number} size Size in screen coordinates. |
| 223 * @return {number} Size in image coordinates. |
| 224 */ |
179 Viewport.prototype.screenToImageSize = function(size) { | 225 Viewport.prototype.screenToImageSize = function(size) { |
180 return size / this.getScale(); | 226 return size / this.getScale(); |
181 }; | 227 }; |
182 | 228 |
| 229 /** |
| 230 * @param {number} x X in screen coordinates. |
| 231 * @return {number} X in image coordinates. |
| 232 */ |
183 Viewport.prototype.screenToImageX = function(x) { | 233 Viewport.prototype.screenToImageX = function(x) { |
184 return Math.round((x - this.imageOnScreen_.left) / this.getScale()); | 234 return Math.round((x - this.imageOnScreen_.left) / this.getScale()); |
185 }; | 235 }; |
186 | 236 |
| 237 /** |
| 238 * @param {number} y Y in screen coordinates. |
| 239 * @return {number} Y in image coordinates. |
| 240 */ |
187 Viewport.prototype.screenToImageY = function(y) { | 241 Viewport.prototype.screenToImageY = function(y) { |
188 return Math.round((y - this.imageOnScreen_.top) / this.getScale()); | 242 return Math.round((y - this.imageOnScreen_.top) / this.getScale()); |
189 }; | 243 }; |
190 | 244 |
| 245 /** |
| 246 * @param {Rect} rect Rectange in screen coordinates. |
| 247 * @return {Rect} Rectange in image coordinates. |
| 248 */ |
191 Viewport.prototype.screenToImageRect = function(rect) { | 249 Viewport.prototype.screenToImageRect = function(rect) { |
192 return new Rect( | 250 return new Rect( |
193 this.screenToImageX(rect.left), | 251 this.screenToImageX(rect.left), |
194 this.screenToImageY(rect.top), | 252 this.screenToImageY(rect.top), |
195 this.screenToImageSize(rect.width), | 253 this.screenToImageSize(rect.width), |
196 this.screenToImageSize(rect.height)); | 254 this.screenToImageSize(rect.height)); |
197 }; | 255 }; |
198 | 256 |
| 257 /** |
| 258 * @param {number} size Size in image coordinates. |
| 259 * @return {number} Size in screen coordinates. |
| 260 */ |
199 Viewport.prototype.imageToScreenSize = function(size) { | 261 Viewport.prototype.imageToScreenSize = function(size) { |
200 return size * this.getScale(); | 262 return size * this.getScale(); |
201 }; | 263 }; |
202 | 264 |
| 265 /** |
| 266 * @param {number} x X in image coordinates. |
| 267 * @return {number} X in screen coordinates. |
| 268 */ |
203 Viewport.prototype.imageToScreenX = function(x) { | 269 Viewport.prototype.imageToScreenX = function(x) { |
204 return Math.round(this.imageOnScreen_.left + x * this.getScale()); | 270 return Math.round(this.imageOnScreen_.left + x * this.getScale()); |
205 }; | 271 }; |
206 | 272 |
| 273 /** |
| 274 * @param {number} y Y in image coordinates. |
| 275 * @return {number} Y in screen coordinates. |
| 276 */ |
207 Viewport.prototype.imageToScreenY = function(y) { | 277 Viewport.prototype.imageToScreenY = function(y) { |
208 return Math.round(this.imageOnScreen_.top + y * this.getScale()); | 278 return Math.round(this.imageOnScreen_.top + y * this.getScale()); |
209 }; | 279 }; |
210 | 280 |
| 281 /** |
| 282 * @param {Rect} rect Rectange in image coordinates. |
| 283 * @return {Rect} Rectange in screen coordinates. |
| 284 */ |
211 Viewport.prototype.imageToScreenRect = function(rect) { | 285 Viewport.prototype.imageToScreenRect = function(rect) { |
212 return new Rect( | 286 return new Rect( |
213 this.imageToScreenX(rect.left), | 287 this.imageToScreenX(rect.left), |
214 this.imageToScreenY(rect.top), | 288 this.imageToScreenY(rect.top), |
215 Math.round(this.imageToScreenSize(rect.width)), | 289 Math.round(this.imageToScreenSize(rect.width)), |
216 Math.round(this.imageToScreenSize(rect.height))); | 290 Math.round(this.imageToScreenSize(rect.height))); |
217 }; | 291 }; |
218 | 292 |
219 /** | 293 /** |
220 * @return {number} The number of physical pixels in one CSS pixel. | 294 * @return {number} The number of physical pixels in one CSS pixel. |
(...skipping 24 matching lines...) Expand all Loading... |
245 }; | 319 }; |
246 | 320 |
247 /** | 321 /** |
248 * @return {Rect} The visible part of the image, in device coordinates. | 322 * @return {Rect} The visible part of the image, in device coordinates. |
249 */ | 323 */ |
250 Viewport.prototype.getDeviceClipped = function() { | 324 Viewport.prototype.getDeviceClipped = function() { |
251 return this.screenToDeviceRect(this.getScreenClipped()); | 325 return this.screenToDeviceRect(this.getScreenClipped()); |
252 }; | 326 }; |
253 | 327 |
254 /** | 328 /** |
255 * @return {Boolean} True if some part of the image is clipped by the screen. | 329 * @return {boolean} True if some part of the image is clipped by the screen. |
256 */ | 330 */ |
257 Viewport.prototype.isClipped = function () { | 331 Viewport.prototype.isClipped = function() { |
258 return this.getMarginX_() < 0 || this.getMarginY_() < 0; | 332 return this.getMarginX_() < 0 || this.getMarginY_() < 0; |
259 }; | 333 }; |
260 | 334 |
261 /** | 335 /** |
262 * Horizontal margin. Negative if the image is clipped horizontally. | 336 * @return {number} Horizontal margin. |
| 337 * Negative if the image is clipped horizontally. |
| 338 * @private |
263 */ | 339 */ |
264 Viewport.prototype.getMarginX_ = function() { | 340 Viewport.prototype.getMarginX_ = function() { |
265 return Math.round( | 341 return Math.round( |
266 (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2); | 342 (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2); |
267 }; | 343 }; |
268 | 344 |
269 /** | 345 /** |
270 * Vertical margin. Negative if the image is clipped vertically. | 346 * @return {number} Vertical margin. |
| 347 * Negative if the image is clipped vertically. |
| 348 * @private |
271 */ | 349 */ |
272 Viewport.prototype.getMarginY_ = function() { | 350 Viewport.prototype.getMarginY_ = function() { |
273 return Math.round( | 351 return Math.round( |
274 (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2); | 352 (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2); |
275 }; | 353 }; |
276 | 354 |
| 355 /** |
| 356 * @param {number} x X-offset. |
| 357 * @return {number} X-offset clamped to the valid range. |
| 358 * @private |
| 359 */ |
277 Viewport.prototype.clampOffsetX_ = function(x) { | 360 Viewport.prototype.clampOffsetX_ = function(x) { |
278 var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale())); | 361 var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale())); |
279 return ImageUtil.clamp(-limit, x, limit); | 362 return ImageUtil.clamp(-limit, x, limit); |
280 }; | 363 }; |
281 | 364 |
| 365 /** |
| 366 * @param {number} y Y-offset. |
| 367 * @return {number} Y-offset clamped to the valid range. |
| 368 * @private |
| 369 */ |
282 Viewport.prototype.clampOffsetY_ = function(y) { | 370 Viewport.prototype.clampOffsetY_ = function(y) { |
283 var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale())); | 371 var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale())); |
284 return ImageUtil.clamp(-limit, y, limit); | 372 return ImageUtil.clamp(-limit, y, limit); |
285 }; | 373 }; |
286 | 374 |
287 /** | 375 /** |
288 * Recalculate the viewport parameters. | 376 * Recalculate the viewport parameters. |
289 */ | 377 */ |
290 Viewport.prototype.update = function() { | 378 Viewport.prototype.update = function() { |
291 var scale = this.getScale(); | 379 var scale = this.getScale(); |
(...skipping 26 matching lines...) Expand all Loading... |
318 this.imageOnScreen_.top += | 406 this.imageOnScreen_.top += |
319 Math.round(this.clampOffsetY_(this.offsetY_) * scale); | 407 Math.round(this.clampOffsetY_(this.offsetY_) * scale); |
320 this.imageClipped_.top = Math.round(-this.imageOnScreen_.top / scale); | 408 this.imageClipped_.top = Math.round(-this.imageOnScreen_.top / scale); |
321 this.imageClipped_.height = Math.round(this.screenBounds_.height / scale); | 409 this.imageClipped_.height = Math.round(this.screenBounds_.height / scale); |
322 } else { | 410 } else { |
323 this.screenClipped_.top = this.imageOnScreen_.top; | 411 this.screenClipped_.top = this.imageOnScreen_.top; |
324 this.screenClipped_.height = this.imageOnScreen_.height; | 412 this.screenClipped_.height = this.imageOnScreen_.height; |
325 } | 413 } |
326 }; | 414 }; |
327 | 415 |
328 Viewport.prototype.addRepaintCallback = function (callback) { | 416 /** |
| 417 * @param {function} callback Repaint callback. |
| 418 */ |
| 419 Viewport.prototype.addRepaintCallback = function(callback) { |
329 this.repaintCallbacks_.push(callback); | 420 this.repaintCallbacks_.push(callback); |
330 }; | 421 }; |
331 | 422 |
332 Viewport.prototype.repaint = function () { | 423 /** |
| 424 * Repaint all clients. |
| 425 */ |
| 426 Viewport.prototype.repaint = function() { |
333 this.update(); | 427 this.update(); |
334 for (var i = 0; i != this.repaintCallbacks_.length; i++) | 428 for (var i = 0; i != this.repaintCallbacks_.length; i++) |
335 this.repaintCallbacks_[i](); | 429 this.repaintCallbacks_[i](); |
336 }; | 430 }; |
OLD | NEW |