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