Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_transform.js
blobfa146d9c19bea3804ddba5a6692eaedd48238b93
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 /**
6  * Crop mode.
7  *
8  * @extends {ImageEditor.Mode}
9  * @constructor
10  * @struct
11  */
12 ImageEditor.Mode.Crop = function() {
13   ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
15   /**
16    * @type {HTMLDivElement}
17    * @private
18    */
19   this.domOverlay_ = null;
21   /**
22    * @type {HTMLDivElement}
23    * @private
24    */
25   this.shadowTop_ = null;
27   /**
28    * @type {HTMLDivElement}
29    * @private
30    */
31   this.middleBox_ = null;
33   /**
34    * @type {HTMLDivElement}
35    * @private
36    */
37   this.shadowLeft_ = null;
39   /**
40    * @type {HTMLDivElement}
41    * @private
42    */
43   this.cropFrame_ = null;
45   /**
46    * @type {HTMLDivElement}
47    * @private
48    */
49   this.shadowRight_ = null;
51   /**
52    * @type {HTMLDivElement}
53    * @private
54    */
55   this.shadowBottom_ = null;
57   /**
58    * @type {?function()}
59    * @private
60    */
61   this.onViewportResizedBound_ = null;
63   /**
64    * @type {DraggableRect}
65    * @private
66    */
67   this.cropRect_ = null;
70 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
72 /**
73  * Sets the mode up.
74  * @override
75  */
76 ImageEditor.Mode.Crop.prototype.setUp = function() {
77   ImageEditor.Mode.prototype.setUp.apply(this, arguments);
79   var container = this.getImageView().container_;
80   var doc = container.ownerDocument;
82   this.domOverlay_ = doc.createElement('div');
83   this.domOverlay_.className = 'crop-overlay';
84   container.appendChild(this.domOverlay_);
86   this.shadowTop_ = doc.createElement('div');
87   this.shadowTop_.className = 'shadow';
88   this.domOverlay_.appendChild(this.shadowTop_);
90   this.middleBox_ = doc.createElement('div');
91   this.middleBox_.className = 'middle-box';
92   this.domOverlay_.appendChild(this.middleBox_);
94   this.shadowLeft_ = doc.createElement('div');
95   this.shadowLeft_.className = 'shadow';
96   this.middleBox_.appendChild(this.shadowLeft_);
98   this.cropFrame_ = doc.createElement('div');
99   this.cropFrame_.className = 'crop-frame';
100   this.middleBox_.appendChild(this.cropFrame_);
102   this.shadowRight_ = doc.createElement('div');
103   this.shadowRight_.className = 'shadow';
104   this.middleBox_.appendChild(this.shadowRight_);
106   this.shadowBottom_ = doc.createElement('div');
107   this.shadowBottom_.className = 'shadow';
108   this.domOverlay_.appendChild(this.shadowBottom_);
110   var cropFrame = this.cropFrame_;
111   function addCropFrame(className) {
112     var div = doc.createElement('div');
113     div.className = className;
114     cropFrame.appendChild(div);
115   }
117   addCropFrame('left top corner');
118   addCropFrame('top horizontal');
119   addCropFrame('right top corner');
120   addCropFrame('left vertical');
121   addCropFrame('right vertical');
122   addCropFrame('left bottom corner');
123   addCropFrame('bottom horizontal');
124   addCropFrame('right bottom corner');
126   // Scale the screen so that it doesn't overlap the toolbars.
127   this.getViewport().setScreenTop(
128       ImageEditor.Toolbar.HEIGHT + ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
129   this.getViewport().setScreenBottom(
130       ImageEditor.Toolbar.HEIGHT * 2 + ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
131   this.getImageView().applyViewportChange();
133   this.onViewportResizedBound_ = this.onViewportResized_.bind(this);
134   this.getViewport().addEventListener('resize', this.onViewportResizedBound_);
136   this.createDefaultCrop();
140  * @override
141  */
142 ImageEditor.Mode.Crop.prototype.createTools = function(toolbar) {
143   var aspects = {
144     GALLERY_ASPECT_RATIO_1_1: 1 / 1,
145     GALLERY_ASPECT_RATIO_6_4: 6 / 4,
146     GALLERY_ASPECT_RATIO_7_5: 7 / 5,
147     GALLERY_ASPECT_RATIO_16_9: 16 / 9
148   };
150   for (var name in aspects) {
151     var button = toolbar.addButton(
152         name,
153         ImageEditor.Toolbar.ButtonType.LABEL,
154         this.onCropAspectRatioClicked_.bind(this, toolbar, aspects[name]),
155         'crop-aspect-ratio');
157     // Prevent from cropping by Enter key if the button is focused.
158     button.addEventListener('keydown', function(event) {
159       var key = util.getKeyModifiers(event) + event.keyIdentifier;
160       if (key === 'Enter')
161         event.stopPropagation();
162     });
163   }
167  * Handles click events of crop aspect ratio buttons.
168  * @param {!ImageEditor.Toolbar} toolbar Toolbar.
169  * @param {number} aspect Aspect ratio.
170  * @param {Event} event An event.
171  * @private
172  */
173 ImageEditor.Mode.Crop.prototype.onCropAspectRatioClicked_ = function(
174     toolbar, aspect, event) {
175   var button = event.target;
177   if (button.classList.contains('selected')) {
178     button.classList.remove('selected');
179     this.cropRect_.fixedAspectRatio = null;
180   } else {
181     var selectedButtons =
182         toolbar.getElement().querySelectorAll('button.selected');
183     for (var i = 0; i < selectedButtons.length; i++) {
184       selectedButtons[i].classList.remove('selected');
185     }
186     button.classList.add('selected');
187     var clipRect = this.viewport_.screenToImageRect(
188         this.viewport_.getImageBoundsOnScreenClipped());
189     this.cropRect_.fixedAspectRatio = aspect;
190     this.cropRect_.forceAspectRatio(aspect, clipRect);
191     this.markUpdated();
192     this.positionDOM();
193   }
197  * Handles resizing of the viewport and updates the crop rectangle.
198  * @private
199  */
200 ImageEditor.Mode.Crop.prototype.onViewportResized_ = function() {
201   this.positionDOM();
205  * Resets the mode.
206  */
207 ImageEditor.Mode.Crop.prototype.reset = function() {
208   ImageEditor.Mode.prototype.reset.call(this);
209   this.createDefaultCrop();
213  * Updates the position of DOM elements.
214  */
215 ImageEditor.Mode.Crop.prototype.positionDOM = function() {
216   var screenCrop = this.viewport_.imageToScreenRect(this.cropRect_.getRect());
218   this.shadowLeft_.style.width = screenCrop.left + 'px';
219   this.shadowTop_.style.height = screenCrop.top + 'px';
220   this.shadowRight_.style.width = window.innerWidth - screenCrop.right + 'px';
221   this.shadowBottom_.style.height =
222       window.innerHeight - screenCrop.bottom + 'px';
226  * Removes the overlay elements from the document.
227  */
228 ImageEditor.Mode.Crop.prototype.cleanUpUI = function() {
229   ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments);
230   this.domOverlay_.parentNode.removeChild(this.domOverlay_);
231   this.domOverlay_ = null;
232   this.getViewport().removeEventListener(
233       'resize', this.onViewportResizedBound_);
234   this.onViewportResizedBound_ = null;
236   // Restore the screen to the full size of window.
237   this.getViewport().setScreenTop(ImageEditor.Toolbar.HEIGHT);
238   this.getViewport().setScreenBottom(ImageEditor.Toolbar.HEIGHT);
239   this.getImageView().applyViewportChange();
243  * @const
244  * @type {number}
245  */
246 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
249  * @const
250  * @type {number}
251  */
252 ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
255  * Gets command to do the crop depending on the current state.
257  * @return {!Command.Crop} Crop command.
258  */
259 ImageEditor.Mode.Crop.prototype.getCommand = function() {
260   var cropImageRect = this.cropRect_.getRect();
261   return new Command.Crop(cropImageRect);
265  * Creates default (initial) crop.
266  */
267 ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
268   var viewport = this.getViewport();
269   assert(viewport);
271   var rect = viewport.screenToImageRect(
272       viewport.getImageBoundsOnScreenClipped());
273   rect = rect.inflate(
274       -Math.round(rect.width / 6), -Math.round(rect.height / 6));
276   this.cropRect_ = new DraggableRect(rect, viewport);
278   this.positionDOM();
282  * Obtains the cursor style depending on the mouse state.
284  * @param {number} x X coordinate for cursor.
285  * @param {number} y Y coordinate for cursor.
286  * @param {boolean} mouseDown If mouse button is down.
287  * @return {string} A value for style.cursor CSS property.
288  */
289 ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
290   return this.cropRect_.getCursorStyle(x, y, mouseDown);
294  * Obtains handler function depending on the mouse state.
296  * @param {number} x Event X coordinate.
297  * @param {number} y Event Y coordinate.
298  * @param {boolean} touch True if it's a touch event, false if mouse.
299  * @return {?function(number,number,boolean)} A function to be called on mouse
300  *     drag. It takes x coordinate value, y coordinate value, and shift key
301  *     flag.
302  */
303 ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
304   var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
305   if (!cropDragHandler)
306     return null;
308   return function(x, y, shiftKey) {
309     cropDragHandler(x, y, shiftKey);
310     this.markUpdated();
311     this.positionDOM();
312   }.bind(this);
316  * Obtains the double tap action depending on the coordinate.
318  * @param {number} x X coordinate of the event.
319  * @param {number} y Y coordinate of the event.
320  * @return {!ImageBuffer.DoubleTapAction} Action to perform as result.
321  */
322 ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
323   return this.cropRect_.getDoubleTapAction(x, y);
327  * A draggable rectangle over the image.
329  * @param {!ImageRect} rect Initial size of the image.
330  * @param {!Viewport} viewport Viewport.
331  * @constructor
332  * @struct
333  */
334 function DraggableRect(rect, viewport) {
335   /**
336    * The bounds are not held in a regular rectangle (with width/height).
337    * left/top/right/bottom held instead for convenience.
338    *
339    * @type {{left: number, right: number, top: number, bottom: number}}
340    * @private
341    */
342   this.bounds_ = {
343     left: rect.left,
344     right: rect.left + rect.width,
345     top: rect.top,
346     bottom: rect.top + rect.height
347   };
349   /**
350    * Viewport.
351    *
352    * @type {!Viewport}
353    * @private
354    * @const
355    */
356   this.viewport_ = viewport;
358   /**
359    * Drag mode.
360    *
361    * @type {Object}
362    * @private
363    */
364   this.dragMode_ = null;
366   /**
367    * Fixed aspect ratio.
368    * The aspect ratio is not fixed when null.
369    * @type {?number}
370    */
371   this.fixedAspectRatio = null;
374 // Static members to simplify reflective access to the bounds.
376  * @const
377  * @type {string}
378  */
379 DraggableRect.LEFT = 'left';
382  * @const
383  * @type {string}
384  */
385 DraggableRect.RIGHT = 'right';
388  * @const
389  * @type {string}
390  */
391 DraggableRect.TOP = 'top';
394  * @const
395  * @type {string}
396  */
397 DraggableRect.BOTTOM = 'bottom';
400  * @const
401  * @type {string}
402  */
403 DraggableRect.NONE = 'none';
406  * Obtains the left position.
407  * @return {number} Position.
408  */
409 DraggableRect.prototype.getLeft = function() {
410   return this.bounds_[DraggableRect.LEFT];
414  * Obtains the right position.
415  * @return {number} Position.
416  */
417 DraggableRect.prototype.getRight = function() {
418   return this.bounds_[DraggableRect.RIGHT];
422  * Obtains the top position.
423  * @return {number} Position.
424  */
425 DraggableRect.prototype.getTop = function() {
426   return this.bounds_[DraggableRect.TOP];
430  * Obtains the bottom position.
431  * @return {number} Position.
432  */
433 DraggableRect.prototype.getBottom = function() {
434   return this.bounds_[DraggableRect.BOTTOM];
438  * Obtains the geometry of the rectangle.
439  * @return {!ImageRect} Geometry of the rectangle.
440  */
441 DraggableRect.prototype.getRect = function() {
442   return ImageRect.createFromBounds(this.bounds_);
446  * Obtains the drag mode depending on the coordinate.
448  * @param {number} x X coordinate for cursor.
449  * @param {number} y Y coordinate for cursor.
450  * @param {boolean=} opt_touch  Whether the operation is done by touch or not.
451  * @return {{xSide: string, ySide:string, whole:boolean, newCrop:boolean}}
452  *     Drag mode.
453  */
454 DraggableRect.prototype.getDragMode = function(x, y, opt_touch) {
455   var touch = opt_touch || false;
457   var result = {
458     xSide: DraggableRect.NONE,
459     ySide: DraggableRect.NONE,
460     whole: false,
461     newCrop: false
462   };
464   var bounds = this.bounds_;
465   var R = this.viewport_.screenToImageSize(
466       touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS :
467               ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
469   var circle = new Circle(x, y, R);
471   var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
472   var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
474   if (circle.inside(bounds.left, bounds.top)) {
475     result.xSide = DraggableRect.LEFT;
476     result.ySide = DraggableRect.TOP;
477   } else if (circle.inside(bounds.left, bounds.bottom)) {
478     result.xSide = DraggableRect.LEFT;
479     result.ySide = DraggableRect.BOTTOM;
480   } else if (circle.inside(bounds.right, bounds.top)) {
481     result.xSide = DraggableRect.RIGHT;
482     result.ySide = DraggableRect.TOP;
483   } else if (circle.inside(bounds.right, bounds.bottom)) {
484     result.xSide = DraggableRect.RIGHT;
485     result.ySide = DraggableRect.BOTTOM;
486   } else if (yBetween && Math.abs(x - bounds.left) <= R) {
487     result.xSide = DraggableRect.LEFT;
488   } else if (yBetween && Math.abs(x - bounds.right) <= R) {
489     result.xSide = DraggableRect.RIGHT;
490   } else if (xBetween && Math.abs(y - bounds.top) <= R) {
491     result.ySide = DraggableRect.TOP;
492   } else if (xBetween && Math.abs(y - bounds.bottom) <= R) {
493     result.ySide = DraggableRect.BOTTOM;
494   } else if (xBetween && yBetween) {
495     result.whole = true;
496   } else {
497     result.newcrop = true;
498     result.xSide = DraggableRect.RIGHT;
499     result.ySide = DraggableRect.BOTTOM;
500   }
502   return result;
506  * Obtains the cursor style depending on the coordinate.
508  * @param {number} x X coordinate for cursor.
509  * @param {number} y Y coordinate for cursor.
510  * @param {boolean} mouseDown  If mouse button is down.
511  * @return {string} Cursor style.
512  */
513 DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
514   var mode;
515   if (mouseDown) {
516     mode = this.dragMode_;
517   } else {
518     mode = this.getDragMode(
519         this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
520   }
521   if (mode.whole)
522     return 'move';
523   if (mode.newcrop)
524     return 'crop';
526   var xSymbol = '';
527   switch (mode.xSide) {
528     case 'left': xSymbol = 'w'; break;
529     case 'right': xSymbol = 'e'; break;
530   }
531   var ySymbol = '';
532   switch (mode.ySide) {
533     case 'top': ySymbol = 'n'; break;
534     case 'bottom': ySymbol = 's'; break;
535   }
536   return ySymbol + xSymbol + '-resize';
540  * Obtains the drag handler depending on the coordinate.
542  * @param {number} initialScreenX X coordinate for cursor in the screen.
543  * @param {number} initialScreenY Y coordinate for cursor in the screen.
544  * @param {boolean} touch Whether the operation is done by touch or not.
545  * @return {?function(number,number,boolean)} Drag handler that takes x
546  *     coordinate value, y coordinate value, and shift key flag.
547  */
548 DraggableRect.prototype.getDragHandler = function(
549     initialScreenX, initialScreenY, touch) {
550   // Check if the initial coordinate is in the image rect.
551   var boundsOnScreen = this.viewport_.getImageBoundsOnScreenClipped();
552   var handlerRadius = touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS :
553       ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS;
554   var draggableAreas = [
555       boundsOnScreen,
556       new Circle(boundsOnScreen.left, boundsOnScreen.top, handlerRadius),
557       new Circle(boundsOnScreen.right, boundsOnScreen.top, handlerRadius),
558       new Circle(boundsOnScreen.left, boundsOnScreen.bottom, handlerRadius),
559       new Circle(boundsOnScreen.right, boundsOnScreen.bottom, handlerRadius)
560   ];
562   if (!draggableAreas.some(
563       (area) => area.inside(initialScreenX, initialScreenY))) {
564     return null;
565   }
567   // Convert coordinates.
568   var initialX = this.viewport_.screenToImageX(initialScreenX);
569   var initialY = this.viewport_.screenToImageY(initialScreenY);
570   var initialWidth = this.bounds_.right - this.bounds_.left;
571   var initialHeight = this.bounds_.bottom - this.bounds_.top;
572   var clipRect = this.viewport_.screenToImageRect(boundsOnScreen);
574   // Obtain the drag mode.
575   this.dragMode_ = this.getDragMode(initialX, initialY, touch);
577   if (this.dragMode_.whole) {
578     // Calc constant values during the operation.
579     var mouseBiasX = this.bounds_.left - initialX;
580     var mouseBiasY = this.bounds_.top - initialY;
581     var maxX = clipRect.left + clipRect.width - initialWidth;
582     var maxY = clipRect.top + clipRect.height - initialHeight;
584     // Returns a handler.
585     return function(newScreenX, newScreenY) {
586       var newX = this.viewport_.screenToImageX(newScreenX);
587       var newY = this.viewport_.screenToImageY(newScreenY);
588       var clamppedX = ImageUtil.clamp(clipRect.left, newX + mouseBiasX, maxX);
589       var clamppedY = ImageUtil.clamp(clipRect.top, newY + mouseBiasY, maxY);
590       this.bounds_.left = clamppedX;
591       this.bounds_.right = clamppedX + initialWidth;
592       this.bounds_.top = clamppedY;
593       this.bounds_.bottom = clamppedY + initialHeight;
594     }.bind(this);
595   } else {
596     // Calc constant values during the operation.
597     var mouseBiasX = this.bounds_[this.dragMode_.xSide] - initialX;
598     var mouseBiasY = this.bounds_[this.dragMode_.ySide] - initialY;
599     var maxX = clipRect.left + clipRect.width;
600     var maxY = clipRect.top + clipRect.height;
602     // Returns a handler.
603     return function(newScreenX, newScreenY, shiftKey) {
604       var newX = this.viewport_.screenToImageX(newScreenX);
605       var newY = this.viewport_.screenToImageY(newScreenY);
607       // Check new crop.
608       if (this.dragMode_.newcrop) {
609         this.dragMode_.newcrop = false;
610         this.bounds_.left = this.bounds_.right = initialX;
611         this.bounds_.top = this.bounds_.bottom = initialY;
612         mouseBiasX = 0;
613         mouseBiasY = 0;
614       }
616       // Update X coordinate.
617       if (this.dragMode_.xSide !== DraggableRect.NONE) {
618         this.bounds_[this.dragMode_.xSide] =
619             ImageUtil.clamp(clipRect.left, newX + mouseBiasX, maxX);
620         if (this.bounds_.left > this.bounds_.right) {
621           var left = this.bounds_.left;
622           var right = this.bounds_.right;
623           this.bounds_.left = right - 1;
624           this.bounds_.right = left + 1;
625           this.dragMode_.xSide =
626               this.dragMode_.xSide == 'left' ? 'right' : 'left';
627         }
628       }
630       // Update Y coordinate.
631       if (this.dragMode_.ySide !== DraggableRect.NONE) {
632         this.bounds_[this.dragMode_.ySide] =
633             ImageUtil.clamp(clipRect.top, newY + mouseBiasY, maxY);
634         if (this.bounds_.top > this.bounds_.bottom) {
635           var top = this.bounds_.top;
636           var bottom = this.bounds_.bottom;
637           this.bounds_.top = bottom - 1;
638           this.bounds_.bottom = top + 1;
639           this.dragMode_.ySide =
640               this.dragMode_.ySide === 'top' ? 'bottom' : 'top';
641         }
642       }
644       // Update aspect ratio.
645       if (this.fixedAspectRatio)
646         this.forceAspectRatio(this.fixedAspectRatio, clipRect);
647       else if (shiftKey)
648         this.forceAspectRatio(initialWidth / initialHeight, clipRect);
649     }.bind(this);
650   }
654  * Obtains double tap action depending on the coordinate.
656  * @param {number} x X coordinate for cursor.
657  * @param {number} y Y coordinate for cursor.
658  * @return {!ImageBuffer.DoubleTapAction} Double tap action.
659  */
660 DraggableRect.prototype.getDoubleTapAction = function(x, y) {
661   var clipRect = this.viewport_.getImageBoundsOnScreenClipped();
662   if (clipRect.inside(x, y))
663     return ImageBuffer.DoubleTapAction.COMMIT;
664   else
665     return ImageBuffer.DoubleTapAction.NOTHING;
669  * Forces the aspect ratio.
671  * @param {number} aspectRatio Aspect ratio.
672  * @param {!Object} clipRect Clip rect.
673  */
674 DraggableRect.prototype.forceAspectRatio = function(aspectRatio, clipRect) {
675   // Get current rectangle scale.
676   var width = this.bounds_.right - this.bounds_.left;
677   var height = this.bounds_.bottom - this.bounds_.top;
678   var currentScale;
679   if (!this.dragMode_)
680     currentScale = ((width / aspectRatio) + height) / 2;
681   else if (this.dragMode_.xSide === 'none')
682     currentScale = height;
683   else if (this.dragMode_.ySide === 'none')
684     currentScale = width / aspectRatio;
685   else
686     currentScale = Math.max(width / aspectRatio, height);
688   // Get maximum width/height scale.
689   var maxWidth;
690   var maxHeight;
691   var center = (this.bounds_.left + this.bounds_.right) / 2;
692   var middle = (this.bounds_.top + this.bounds_.bottom) / 2;
693   var xSide = this.dragMode_ ? this.dragMode_.xSide : 'none';
694   var ySide = this.dragMode_ ? this.dragMode_.ySide : 'none';
695   switch (xSide) {
696     case 'left':
697       maxWidth = this.bounds_.right - clipRect.left;
698       break;
699     case 'right':
700       maxWidth = clipRect.left + clipRect.width - this.bounds_.left;
701       break;
702     case 'none':
703       maxWidth = Math.min(
704           clipRect.left + clipRect.width - center,
705           center - clipRect.left) * 2;
706       break;
707   }
708   switch (ySide) {
709     case 'top':
710       maxHeight = this.bounds_.bottom - clipRect.top;
711       break;
712     case 'bottom':
713       maxHeight = clipRect.top + clipRect.height - this.bounds_.top;
714       break;
715     case 'none':
716       maxHeight = Math.min(
717           clipRect.top + clipRect.height - middle,
718           middle - clipRect.top) * 2;
719       break;
720   }
722   // Obtains target scale.
723   var targetScale = Math.min(
724       currentScale,
725       maxWidth / aspectRatio,
726       maxHeight);
728   // Update bounds.
729   var newWidth = targetScale * aspectRatio;
730   var newHeight = targetScale;
731   switch (xSide) {
732     case 'left':
733       this.bounds_.left = this.bounds_.right - newWidth;
734       break;
735     case 'right':
736       this.bounds_.right = this.bounds_.left + newWidth;
737       break;
738     case 'none':
739       this.bounds_.left = center - newWidth / 2;
740       this.bounds_.right = center + newWidth / 2;
741       break;
742   }
743   switch (ySide) {
744     case 'top':
745       this.bounds_.top = this.bounds_.bottom - newHeight;
746       break;
747     case 'bottom':
748       this.bounds_.bottom = this.bounds_.top + newHeight;
749       break;
750     case 'none':
751       this.bounds_.top = middle - newHeight / 2;
752       this.bounds_.bottom = middle + newHeight / 2;
753       break;
754   }