Gallery: Random fixes for the Viewport class.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / viewport.js
blob5f63d1b8c5bfa999300c67fc4b93fd883331622f
1 // 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.
5 'use strict';
7 /**
8  * Viewport class controls the way the image is displayed (scale, offset etc).
9  * @constructor
10  */
11 function Viewport() {
12   /**
13    * Size of the full resolution image.
14    * @type {Rect}
15    * @private
16    */
17   this.imageBounds_ = new Rect();
19   /**
20    * Size of the application window.
21    * @type {Rect}
22    * @private
23    */
24   this.screenBounds_ = new Rect();
26   /**
27    * Bounds of the image element on screen without zoom and offset.
28    * @type {Rect}
29    * @private
30    */
31   this.imageElementBoundsOnScreen_ = null;
33   /**
34    * Bounds of the image with zoom and offset.
35    * @type {Rect}
36    * @private
37    */
38   this.imageBoundsOnScreen_ = null;
40   /**
41    * Image bounds that is clipped with the screen bounds.
42    * @type {Rect}
43    * @private
44    */
45   this.imageBoundsOnScreenClipped_ = null;
47   /**
48    * Scale from the full resolution image to the screen displayed image. This is
49    * not zoom operated by users.
50    * @type {number}
51    * @private
52    */
53   this.scale_ = 1;
55   /**
56    * Index of zoom ratio. 0 is "zoom ratio = 1".
57    * @type {number}
58    * @private
59    */
60   this.zoomIndex_ = 0;
62   /**
63    * Zoom ratio specified by user operations.
64    * @type {number}
65    * @private
66    */
67   this.zoom_ = 1;
69   /**
70    * Offset specified by user operations.
71    * @type {number}
72    */
73   this.offsetX_ = 0;
75   /**
76    * Offset specified by user operations.
77    * @type {number}
78    */
79   this.offsetY_ = 0;
81   /**
82    * Generation of the screen size image cache.
83    * This is incremented every time when the size of image cache is changed.
84    * @type {number}
85    * @private
86    */
87   this.generation_ = 0;
89   this.update();
90   Object.seal(this);
93 /**
94  * Zoom ratios.
95  *
96  * @type {Object.<string, number>}
97  * @const
98  */
99 Viewport.ZOOM_RATIOS = Object.freeze({
100   '3': 3,
101   '2': 2,
102   '1': 1.5,
103   '0': 1,
104   '-1': 0.75,
105   '-2': 0.5,
106   '-3': 0.25
110  * @param {number} width Image width.
111  * @param {number} height Image height.
112  */
113 Viewport.prototype.setImageSize = function(width, height) {
114   this.imageBounds_ = new Rect(width, height);
115   this.update();
116   this.invalidateCaches();
120  * @param {number} width Screen width.
121  * @param {number} height Screen height.
122  */
123 Viewport.prototype.setScreenSize = function(width, height) {
124   this.screenBounds_ = new Rect(width, height);
125   this.update();
126   this.invalidateCaches();
130  * Sets the new zoom ratio.
131  * @param {number} zoomIndex New zoom index.
132  */
133 Viewport.prototype.setZoomIndex = function(zoomIndex) {
134   // Ignore the invalid zoomIndex.
135   if (!Viewport.ZOOM_RATIOS[zoomIndex.toString()])
136     return;
137   this.zoomIndex_ = zoomIndex;
138   this.zoom_ = Viewport.ZOOM_RATIOS[zoomIndex];
139   this.update();
143  * Returns the current zoom index.
144  * @return {number} Zoon index.
145  */
146 Viewport.prototype.getZoomIndex = function() {
147   return this.zoomIndex_;
151  * @return {number} Scale.
152  */
153 Viewport.prototype.getScale = function() { return this.scale_; };
156  * @param {number} scale The new scale.
157  */
158 Viewport.prototype.setScale = function(scale) {
159   if (this.scale_ == scale)
160     return;
161   this.scale_ = scale;
162   this.update();
163   this.invalidateCaches();
167  * @return {number} Best scale to fit the current image into the current screen.
168  */
169 Viewport.prototype.getFittingScale = function() {
170   return this.getFittingScaleForImageSize_(
171       this.imageBounds_.width, this.imageBounds_.height);
175  * Obtains the scale for the specified image size.
177  * @param {number} width Width of the full resolution image.
178  * @param {number} height Height of the full resolution image.
179  * @return {number} The ratio of the fullresotion image size and the calculated
180  * displayed image size.
181  */
182 Viewport.prototype.getFittingScaleForImageSize_ = function(width, height) {
183   var scaleX = this.screenBounds_.width / width;
184   var scaleY = this.screenBounds_.height / height;
185   // Scales > (1 / devicePixelRatio) do not look good. Also they are
186   // not really useful as we do not have any pixel-level operations.
187   return Math.min(1 / window.devicePixelRatio, scaleX, scaleY);
191  * Set the scale to fit the image into the screen.
192  */
193 Viewport.prototype.fitImage = function() {
194   this.setScale(this.getFittingScale());
198  * @return {number} X-offset of the viewport.
199  */
200 Viewport.prototype.getOffsetX = function() { return this.offsetX_; };
203  * @return {number} Y-offset of the viewport.
204  */
205 Viewport.prototype.getOffsetY = function() { return this.offsetY_; };
208  * Set the image offset in the viewport.
209  * @param {number} x X-offset.
210  * @param {number} y Y-offset.
211  * @param {boolean} ignoreClipping True if no clipping should be applied.
212  */
213 Viewport.prototype.setOffset = function(x, y, ignoreClipping) {
214   if (!ignoreClipping) {
215     x = this.clampOffsetX_(x);
216     y = this.clampOffsetY_(y);
217   }
218   if (this.offsetX_ == x && this.offsetY_ == y) return;
219   this.offsetX_ = x;
220   this.offsetY_ = y;
221   this.invalidateCaches();
225  * @return {Rect} The image bounds in image coordinates.
226  */
227 Viewport.prototype.getImageBounds = function() { return this.imageBounds_; };
230 * @return {Rect} The screen bounds in screen coordinates.
232 Viewport.prototype.getScreenBounds = function() { return this.screenBounds_; };
235  * @return {Rect} The size of screen cache canvas.
236  */
237 Viewport.prototype.getDeviceBounds = function() {
238   var size = this.getImageElementBoundsOnScreen();
239   return new Rect(
240       size.width * window.devicePixelRatio,
241       size.height * window.devicePixelRatio);
245  * A counter that is incremented with each viewport state change.
246  * Clients that cache anything that depends on the viewport state should keep
247  * track of this counter.
248  * @return {number} counter.
249  */
250 Viewport.prototype.getCacheGeneration = function() { return this.generation_; };
253  * Called on event view port state change.
254  */
255 Viewport.prototype.invalidateCaches = function() { this.generation_++; };
258  * @return {Rect} The image bounds in screen coordinates.
259  */
260 Viewport.prototype.getImageBoundsOnScreen = function() {
261   return this.imageBoundsOnScreen_;
265  * The image bounds in screen coordinates.
266  * This returns the bounds of element before applying zoom and offset.
267  * @return {Rect}
268  */
269 Viewport.prototype.getImageElementBoundsOnScreen = function() {
270   return this.imageElementBoundsOnScreen_;
274  * The image bounds on screen, which is clipped with the screen size.
275  * @return {Rect}
276  */
277 Viewport.prototype.getImageBoundsOnScreenClipped = function() {
278   return this.imageBoundsOnScreenClipped_;
282  * @param {number} size Size in screen coordinates.
283  * @return {number} Size in image coordinates.
284  */
285 Viewport.prototype.screenToImageSize = function(size) {
286   return size / this.getScale();
290  * @param {number} x X in screen coordinates.
291  * @return {number} X in image coordinates.
292  */
293 Viewport.prototype.screenToImageX = function(x) {
294   return Math.round((x - this.imageBoundsOnScreen_.left) / this.getScale());
298  * @param {number} y Y in screen coordinates.
299  * @return {number} Y in image coordinates.
300  */
301 Viewport.prototype.screenToImageY = function(y) {
302   return Math.round((y - this.imageBoundsOnScreen_.top) / this.getScale());
306  * @param {Rect} rect Rectangle in screen coordinates.
307  * @return {Rect} Rectangle in image coordinates.
308  */
309 Viewport.prototype.screenToImageRect = function(rect) {
310   return new Rect(
311       this.screenToImageX(rect.left),
312       this.screenToImageY(rect.top),
313       this.screenToImageSize(rect.width),
314       this.screenToImageSize(rect.height));
318  * @param {number} size Size in image coordinates.
319  * @return {number} Size in screen coordinates.
320  */
321 Viewport.prototype.imageToScreenSize = function(size) {
322   return size * this.getScale();
326  * @param {number} x X in image coordinates.
327  * @return {number} X in screen coordinates.
328  */
329 Viewport.prototype.imageToScreenX = function(x) {
330   return Math.round(this.imageBoundsOnScreen_.left + x * this.getScale());
334  * @param {number} y Y in image coordinates.
335  * @return {number} Y in screen coordinates.
336  */
337 Viewport.prototype.imageToScreenY = function(y) {
338   return Math.round(this.imageBoundsOnScreen_.top + y * this.getScale());
342  * @param {Rect} rect Rectangle in image coordinates.
343  * @return {Rect} Rectangle in screen coordinates.
344  */
345 Viewport.prototype.imageToScreenRect = function(rect) {
346   return new Rect(
347       this.imageToScreenX(rect.left),
348       this.imageToScreenY(rect.top),
349       Math.round(this.imageToScreenSize(rect.width)),
350       Math.round(this.imageToScreenSize(rect.height)));
354  * @return {boolean} True if some part of the image is clipped by the screen.
355  */
356 Viewport.prototype.isClipped = function() {
357   return this.getMarginX_() < 0 || this.getMarginY_() < 0;
361  * @return {number} Horizontal margin.
362  *   Negative if the image is clipped horizontally.
363  * @private
364  */
365 Viewport.prototype.getMarginX_ = function() {
366   return Math.round(
367     (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2);
371  * @return {number} Vertical margin.
372  *   Negative if the image is clipped vertically.
373  * @private
374  */
375 Viewport.prototype.getMarginY_ = function() {
376   return Math.round(
377     (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2);
381  * @param {number} x X-offset.
382  * @return {number} X-offset clamped to the valid range.
383  * @private
384  */
385 Viewport.prototype.clampOffsetX_ = function(x) {
386   var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale()));
387   return ImageUtil.clamp(-limit, x, limit);
391  * @param {number} y Y-offset.
392  * @return {number} Y-offset clamped to the valid range.
393  * @private
394  */
395 Viewport.prototype.clampOffsetY_ = function(y) {
396   var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale()));
397   return ImageUtil.clamp(-limit, y, limit);
401  * @private
402  */
403 Viewport.prototype.getCenteredRect_ = function(
404     width, height, offsetX, offsetY) {
405   return new Rect(
406       ~~((this.screenBounds_.width - width) / 2) + offsetX,
407       ~~((this.screenBounds_.height - height) / 2) + offsetY,
408       width,
409       height);
413  * Recalculate the viewport parameters.
414  */
415 Viewport.prototype.update = function() {
416   var scale = this.getScale();
418   // Image bounds on screen.
419   this.imageBoundsOnScreen_ = this.getCenteredRect_(
420       ~~(this.imageBounds_.width * scale * this.zoom_),
421       ~~(this.imageBounds_.height * scale * this.zoom_),
422       this.offsetX_,
423       this.offsetY_);
425   // Image bounds of element (that is not applied zoom and offset) on screen.
426   this.imageElementBoundsOnScreen_ = this.getCenteredRect_(
427       ~~(this.imageBounds_.width * scale),
428       ~~(this.imageBounds_.height * scale),
429       0,
430       0);
432   // Image bounds on screen cliped with the screen bounds.
433   var left = Math.max(this.imageBoundsOnScreen_.left, 0);
434   var top = Math.max(this.imageBoundsOnScreen_.top, 0);
435   var right = Math.min(
436       this.imageBoundsOnScreen_.right, this.screenBounds_.width);
437   var bottom = Math.min(
438       this.imageBoundsOnScreen_.bottom, this.screenBounds_.height);
439   this.imageBoundsOnScreenClipped_ = new Rect(
440       left, top, right - left, bottom - top);
444  * Obtains CSS transformation for the screen image.
445  * @return {string} Transformation description.
446  */
447 Viewport.prototype.getTransformation = function() {
448   return 'scale(' + (1 / window.devicePixelRatio * this.zoom_) + ')';
452  * Obtains shift CSS transformation for the screen image.
453  * @param {number} dx Amount of shift.
454  * @return {string} Transformation description.
455  */
456 Viewport.prototype.getShiftTransformation = function(dx) {
457   return 'translateX(' + dx + 'px) ' + this.getTransformation();
461  * Obtains CSS transformation that makes the rotated image fit the original
462  * image. The new rotated image that the transformation is applied to looks the
463  * same with original image.
465  * @param {boolean} orientation Orientation of the rotation from the original
466  *     image to the rotated image. True is for clockwise and false is for
467  *     counterclockwise.
468  * @return {string} Transformation description.
469  */
470 Viewport.prototype.getInverseTransformForRotatedImage = function(orientation) {
471   var previousImageWidth = this.imageBounds_.height;
472   var previousImageHeight = this.imageBounds_.width;
473   var oldScale = this.getFittingScaleForImageSize_(
474       previousImageWidth, previousImageHeight);
475   var scaleRatio = oldScale / this.getScale();
476   var degree = orientation ? '-90deg' : '90deg';
477   return [
478     'scale(' + scaleRatio + ')',
479     'rotate(' + degree + ')',
480     this.getTransformation()
481   ].join(' ');
485  * Obtains CSS transformation that makes the cropped image fit the original
486  * image. The new cropped image that the transformaton is applied to fits to the
487  * the cropped rectangle in the original image.
489  * @param {number} imageWidth Width of the original image.
490  * @param {number} imageHeight Height of the origianl image.
491  * @param {Rect} imageCropRect Crop rectangle in the image's coordinate system.
492  * @return {string} Transformation description.
493  */
494 Viewport.prototype.getInverseTransformForCroppedImage =
495     function(imageWidth, imageHeight, imageCropRect) {
496   var wholeScale = this.getFittingScaleForImageSize_(
497       imageWidth, imageHeight);
498   var croppedScale = this.getFittingScaleForImageSize_(
499       imageCropRect.width, imageCropRect.height);
500   var dx =
501       (imageCropRect.left + imageCropRect.width / 2 - imageWidth / 2) *
502       wholeScale;
503   var dy =
504       (imageCropRect.top + imageCropRect.height / 2 - imageHeight / 2) *
505       wholeScale;
506   return [
507     'translate(' + dx + 'px,' + dy + 'px)',
508     'scale(' + wholeScale / croppedScale + ')',
509     this.getTransformation()
510   ].join(' ');
514  * Obtains CSS transformaton that makes the image fit to the screen rectangle.
516  * @param {Rect} screenRect Screen rectangle.
517  * @return {string} Transformation description.
518  */
519 Viewport.prototype.getScreenRectTransformForImage = function(screenRect) {
520   var imageBounds = this.getImageElementBoundsOnScreen();
521   var scaleX = screenRect.width / imageBounds.width;
522   var scaleY = screenRect.height / imageBounds.height;
523   var screenWidth = this.screenBounds_.width;
524   var screenHeight = this.screenBounds_.height;
525   var dx = screenRect.left + screenRect.width / 2 - screenWidth / 2;
526   var dy = screenRect.top + screenRect.height / 2 - screenHeight / 2;
527   return [
528     'translate(' + dx + 'px,' + dy + 'px)',
529     'scale(' + scaleX + ',' + scaleY + ')',
530     this.getTransformation()
531   ].join(' ');