Correct indentation, JSDoc, etc... to comply with closure linter.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / viewport.js
blob0a9406fd13b43f02798270231d3349c1307abbf5
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    * Zoom ratio specified by user operations.
57    * @type {number}
58    * @private
59    */
60   this.zoom_ = 1;
62   /**
63    * Offset specified by user operations.
64    * @type {number}
65    * @private
66    */
67   this.offsetX_ = 0;
69   /**
70    * Offset specified by user operations.
71    * @type {number}
72    * @private
73    */
74   this.offsetY_ = 0;
76   /**
77    * Integer Rotation value.
78    * The rotation angle is this.rotation_ * 90.
79    * @type {number}
80    * @private
81    */
82   this.rotation_ = 0;
84   /**
85    * Generation of the screen size image cache.
86    * This is incremented every time when the size of image cache is changed.
87    * @type {number}
88    * @private
89    */
90   this.generation_ = 0;
92   this.update_();
93   Object.seal(this);
96 /**
97  * Zoom ratios.
98  *
99  * @type {Array.<number>}
100  * @const
101  */
102 Viewport.ZOOM_RATIOS = Object.freeze([1, 1.5, 2, 3]);
105  * @param {number} width Image width.
106  * @param {number} height Image height.
107  */
108 Viewport.prototype.setImageSize = function(width, height) {
109   this.imageBounds_ = new Rect(width, height);
110   this.update_();
114  * @param {number} width Screen width.
115  * @param {number} height Screen height.
116  */
117 Viewport.prototype.setScreenSize = function(width, height) {
118   this.screenBounds_ = new Rect(width, height);
119   this.update_();
123  * Sets zoom value directly.
124  * @param {number} zoom New zoom value.
125  */
126 Viewport.prototype.setZoom = function(zoom) {
127   var zoomMin = Viewport.ZOOM_RATIOS[0];
128   var zoomMax = Viewport.ZOOM_RATIOS[Viewport.ZOOM_RATIOS.length - 1];
129   var adjustedZoom = Math.max(zoomMin, Math.min(zoom, zoomMax));
130   this.zoom_ = adjustedZoom;
131   this.update_();
135  * Returns the value of zoom.
136  * @return {number} Zoom value.
137  */
138 Viewport.prototype.getZoom = function() {
139   return this.zoom_;
143  * Sets the nearest larger value of ZOOM_RATIOS.
144  */
145 Viewport.prototype.zoomIn = function() {
146   var zoom = Viewport.ZOOM_RATIOS[0];
147   for (var i = 0; i < Viewport.ZOOM_RATIOS.length; i++) {
148     zoom = Viewport.ZOOM_RATIOS[i];
149     if (zoom > this.zoom_)
150       break;
151   }
152   this.setZoom(zoom);
156  * Sets the nearest smaller value of ZOOM_RATIOS.
157  */
158 Viewport.prototype.zoomOut = function() {
159   var zoom = Viewport.ZOOM_RATIOS[Viewport.ZOOM_RATIOS.length - 1];
160   for (var i = Viewport.ZOOM_RATIOS.length - 1; i >= 0; i--) {
161     zoom = Viewport.ZOOM_RATIOS[i];
162     if (zoom < this.zoom_)
163       break;
164   }
165   this.setZoom(zoom);
169  * Obtains whether the picture is zoomed or not.
170  * @return {boolean}
171  */
172 Viewport.prototype.isZoomed = function() {
173   return this.zoom_ !== 1;
177  * Sets the rotation value.
178  * @param {number} rotation New rotation value.
179  */
180 Viewport.prototype.setRotation = function(rotation) {
181   this.rotation_ = rotation;
182   this.update_();
187  * Obtains the rotation value.
188  * @return {number} Current rotation value.
189  */
190 Viewport.prototype.getRotation = function() {
191   return this.rotation_;
195  * Obtains the scale for the specified image size.
197  * @param {number} width Width of the full resolution image.
198  * @param {number} height Height of the full resolution image.
199  * @return {number} The ratio of the full resotion image size and the calculated
200  * displayed image size.
201  * @private
202  */
203 Viewport.prototype.getFittingScaleForImageSize_ = function(width, height) {
204   var scaleX = this.screenBounds_.width / width;
205   var scaleY = this.screenBounds_.height / height;
206   // Scales > (1 / devicePixelRatio) do not look good. Also they are
207   // not really useful as we do not have any pixel-level operations.
208   return Math.min(1 / window.devicePixelRatio, scaleX, scaleY);
212  * @return {number} X-offset of the viewport.
213  */
214 Viewport.prototype.getOffsetX = function() { return this.offsetX_; };
217  * @return {number} Y-offset of the viewport.
218  */
219 Viewport.prototype.getOffsetY = function() { return this.offsetY_; };
222  * Set the image offset in the viewport.
223  * @param {number} x X-offset.
224  * @param {number} y Y-offset.
225  */
226 Viewport.prototype.setOffset = function(x, y) {
227   if (this.offsetX_ == x && this.offsetY_ == y)
228     return;
229   this.offsetX_ = x;
230   this.offsetY_ = y;
231   this.update_();
235  * @return {Rect} The image bounds in image coordinates.
236  */
237 Viewport.prototype.getImageBounds = function() { return this.imageBounds_; };
240 * @return {Rect} The screen bounds in screen coordinates.
242 Viewport.prototype.getScreenBounds = function() { return this.screenBounds_; };
245  * @return {Rect} The size of screen cache canvas.
246  */
247 Viewport.prototype.getDeviceBounds = function() {
248   var size = this.getImageElementBoundsOnScreen();
249   return new Rect(
250       size.width * window.devicePixelRatio,
251       size.height * window.devicePixelRatio);
255  * A counter that is incremented with each viewport state change.
256  * Clients that cache anything that depends on the viewport state should keep
257  * track of this counter.
258  * @return {number} counter.
259  */
260 Viewport.prototype.getCacheGeneration = function() { return this.generation_; };
263  * @return {Rect} The image bounds in screen coordinates.
264  */
265 Viewport.prototype.getImageBoundsOnScreen = function() {
266   return this.imageBoundsOnScreen_;
270  * The image bounds in screen coordinates.
271  * This returns the bounds of element before applying zoom and offset.
272  * @return {Rect}
273  */
274 Viewport.prototype.getImageElementBoundsOnScreen = function() {
275   return this.imageElementBoundsOnScreen_;
279  * The image bounds on screen, which is clipped with the screen size.
280  * @return {Rect}
281  */
282 Viewport.prototype.getImageBoundsOnScreenClipped = function() {
283   return this.imageBoundsOnScreenClipped_;
287  * @param {number} size Size in screen coordinates.
288  * @return {number} Size in image coordinates.
289  */
290 Viewport.prototype.screenToImageSize = function(size) {
291   return size / this.scale_;
295  * @param {number} x X in screen coordinates.
296  * @return {number} X in image coordinates.
297  */
298 Viewport.prototype.screenToImageX = function(x) {
299   return Math.round((x - this.imageBoundsOnScreen_.left) / this.scale_);
303  * @param {number} y Y in screen coordinates.
304  * @return {number} Y in image coordinates.
305  */
306 Viewport.prototype.screenToImageY = function(y) {
307   return Math.round((y - this.imageBoundsOnScreen_.top) / this.scale_);
311  * @param {Rect} rect Rectangle in screen coordinates.
312  * @return {Rect} Rectangle in image coordinates.
313  */
314 Viewport.prototype.screenToImageRect = function(rect) {
315   return new Rect(
316       this.screenToImageX(rect.left),
317       this.screenToImageY(rect.top),
318       this.screenToImageSize(rect.width),
319       this.screenToImageSize(rect.height));
323  * @param {number} size Size in image coordinates.
324  * @return {number} Size in screen coordinates.
325  */
326 Viewport.prototype.imageToScreenSize = function(size) {
327   return size * this.scale_;
331  * @param {number} x X in image coordinates.
332  * @return {number} X in screen coordinates.
333  */
334 Viewport.prototype.imageToScreenX = function(x) {
335   return Math.round(this.imageBoundsOnScreen_.left + x * this.scale_);
339  * @param {number} y Y in image coordinates.
340  * @return {number} Y in screen coordinates.
341  */
342 Viewport.prototype.imageToScreenY = function(y) {
343   return Math.round(this.imageBoundsOnScreen_.top + y * this.scale_);
347  * @param {Rect} rect Rectangle in image coordinates.
348  * @return {Rect} Rectangle in screen coordinates.
349  */
350 Viewport.prototype.imageToScreenRect = function(rect) {
351   return new Rect(
352       this.imageToScreenX(rect.left),
353       this.imageToScreenY(rect.top),
354       Math.round(this.imageToScreenSize(rect.width)),
355       Math.round(this.imageToScreenSize(rect.height)));
359  * @param {number} width Width of the rectangle.
360  * @param {number} height Height of the rectangle.
361  * @param {number} offsetX X-offset of center position of the rectangle.
362  * @param {number} offsetY Y-offset of center position of the rectangle.
363  * @return {Rect} Rectangle with given geometry.
364  * @private
365  */
366 Viewport.prototype.getCenteredRect_ = function(
367     width, height, offsetX, offsetY) {
368   return new Rect(
369       ~~((this.screenBounds_.width - width) / 2) + offsetX,
370       ~~((this.screenBounds_.height - height) / 2) + offsetY,
371       width,
372       height);
376  * Resets zoom and offset.
377  */
378 Viewport.prototype.resetView = function() {
379   this.zoom_ = 1;
380   this.offsetX_ = 0;
381   this.offsetY_ = 0;
382   this.rotation_ = 0;
383   this.update_();
387  * Recalculate the viewport parameters.
388  * @private
389  */
390 Viewport.prototype.update_ = function() {
391   // Update scale.
392   this.scale_ = this.getFittingScaleForImageSize_(
393       this.imageBounds_.width, this.imageBounds_.height);
395   // Limit offset values.
396   var zoomedWidht;
397   var zoomedHeight;
398   if (this.rotation_ % 2 == 0) {
399     zoomedWidht = ~~(this.imageBounds_.width * this.scale_ * this.zoom_);
400     zoomedHeight = ~~(this.imageBounds_.height * this.scale_ * this.zoom_);
401   } else {
402     var scale = this.getFittingScaleForImageSize_(
403         this.imageBounds_.height, this.imageBounds_.width);
404     zoomedWidht = ~~(this.imageBounds_.height * scale * this.zoom_);
405     zoomedHeight = ~~(this.imageBounds_.width * scale * this.zoom_);
406   }
407   var dx = Math.max(zoomedWidht - this.screenBounds_.width, 0) / 2;
408   var dy = Math.max(zoomedHeight - this.screenBounds_.height, 0) /2;
409   this.offsetX_ = ImageUtil.clamp(-dx, this.offsetX_, dx);
410   this.offsetY_ = ImageUtil.clamp(-dy, this.offsetY_, dy);
412   // Image bounds on screen.
413   this.imageBoundsOnScreen_ = this.getCenteredRect_(
414       zoomedWidht, zoomedHeight, this.offsetX_, this.offsetY_);
416   // Image bounds of element (that is not applied zoom and offset) on screen.
417   var oldBounds = this.imageElementBoundsOnScreen_;
418   this.imageElementBoundsOnScreen_ = this.getCenteredRect_(
419       ~~(this.imageBounds_.width * this.scale_),
420       ~~(this.imageBounds_.height * this.scale_),
421       0,
422       0);
423   if (!oldBounds ||
424       this.imageElementBoundsOnScreen_.width != oldBounds.width ||
425       this.imageElementBoundsOnScreen_.height != oldBounds.height) {
426     this.generation_++;
427   }
429   // Image bounds on screen clipped with the screen bounds.
430   var left = Math.max(this.imageBoundsOnScreen_.left, 0);
431   var top = Math.max(this.imageBoundsOnScreen_.top, 0);
432   var right = Math.min(
433       this.imageBoundsOnScreen_.right, this.screenBounds_.width);
434   var bottom = Math.min(
435       this.imageBoundsOnScreen_.bottom, this.screenBounds_.height);
436   this.imageBoundsOnScreenClipped_ = new Rect(
437       left, top, right - left, bottom - top);
441  * Clones the viewport.
442  * @return {Viewport} New instance.
443  */
444 Viewport.prototype.clone = function() {
445   var viewport = new Viewport();
446   viewport.imageBounds_ = new Rect(this.imageBounds_);
447   viewport.screenBounds_ = new Rect(this.screenBounds_);
448   viewport.scale_ = this.scale_;
449   viewport.zoom_ = this.zoom_;
450   viewport.offsetX_ = this.offsetX_;
451   viewport.offsetY_ = this.offsetY_;
452   viewport.rotation_ = this.rotation_;
453   viewport.generation_ = this.generation_;
454   viewport.update_();
455   return viewport;
459  * Obtains CSS transformation for the screen image.
460  * @return {string} Transformation description.
461  */
462 Viewport.prototype.getTransformation = function() {
463   var rotationScaleAdjustment;
464   if (this.rotation_ % 2) {
465     rotationScaleAdjustment = this.getFittingScaleForImageSize_(
466         this.imageBounds_.height, this.imageBounds_.width) / this.scale_;
467   } else {
468     rotationScaleAdjustment = 1;
469   }
470   return [
471     'translate(' + this.offsetX_ + 'px, ' + this.offsetY_ + 'px) ',
472     'rotate(' + (this.rotation_ * 90) + 'deg)',
473     'scale(' + (this.zoom_ * rotationScaleAdjustment) + ')'
474   ].join(' ');
478  * Obtains shift CSS transformation for the screen image.
479  * @param {number} dx Amount of shift.
480  * @return {string} Transformation description.
481  */
482 Viewport.prototype.getShiftTransformation = function(dx) {
483   return 'translateX(' + dx + 'px) ' + this.getTransformation();
487  * Obtains CSS transformation that makes the rotated image fit the original
488  * image. The new rotated image that the transformation is applied to looks the
489  * same with original image.
491  * @param {boolean} orientation Orientation of the rotation from the original
492  *     image to the rotated image. True is for clockwise and false is for
493  *     counterclockwise.
494  * @return {string} Transformation description.
495  */
496 Viewport.prototype.getInverseTransformForRotatedImage = function(orientation) {
497   var previousImageWidth = this.imageBounds_.height;
498   var previousImageHeight = this.imageBounds_.width;
499   var oldScale = this.getFittingScaleForImageSize_(
500       previousImageWidth, previousImageHeight);
501   var scaleRatio = oldScale / this.scale_;
502   var degree = orientation ? '-90deg' : '90deg';
503   return [
504     'scale(' + scaleRatio + ')',
505     'rotate(' + degree + ')',
506     this.getTransformation()
507   ].join(' ');
511  * Obtains CSS transformation that makes the cropped image fit the original
512  * image. The new cropped image that the transformation is applied to fits to
513  * the cropped rectangle in the original image.
515  * @param {number} imageWidth Width of the original image.
516  * @param {number} imageHeight Height of the original image.
517  * @param {Rect} imageCropRect Crop rectangle in the image's coordinate system.
518  * @return {string} Transformation description.
519  */
520 Viewport.prototype.getInverseTransformForCroppedImage =
521     function(imageWidth, imageHeight, imageCropRect) {
522   var wholeScale = this.getFittingScaleForImageSize_(
523       imageWidth, imageHeight);
524   var croppedScale = this.getFittingScaleForImageSize_(
525       imageCropRect.width, imageCropRect.height);
526   var dx =
527       (imageCropRect.left + imageCropRect.width / 2 - imageWidth / 2) *
528       wholeScale;
529   var dy =
530       (imageCropRect.top + imageCropRect.height / 2 - imageHeight / 2) *
531       wholeScale;
532   return [
533     'translate(' + dx + 'px,' + dy + 'px)',
534     'scale(' + wholeScale / croppedScale + ')',
535     this.getTransformation()
536   ].join(' ');
540  * Obtains CSS transformation that makes the image fit to the screen rectangle.
542  * @param {Rect} screenRect Screen rectangle.
543  * @return {string} Transformation description.
544  */
545 Viewport.prototype.getScreenRectTransformForImage = function(screenRect) {
546   var imageBounds = this.getImageElementBoundsOnScreen();
547   var scaleX = screenRect.width / imageBounds.width;
548   var scaleY = screenRect.height / imageBounds.height;
549   var screenWidth = this.screenBounds_.width;
550   var screenHeight = this.screenBounds_.height;
551   var dx = screenRect.left + screenRect.width / 2 - screenWidth / 2;
552   var dy = screenRect.top + screenRect.height / 2 - screenHeight / 2;
553   return [
554     'translate(' + dx + 'px,' + dy + 'px)',
555     'scale(' + scaleX + ',' + scaleY + ')',
556     this.getTransformation()
557   ].join(' ');