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.
8 * @extends {ImageEditor.Mode}
12 ImageEditor.Mode.Crop = function() {
13 ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
16 * @type {HTMLDivElement}
19 this.domOverlay_ = null;
22 * @type {HTMLDivElement}
25 this.shadowTop_ = null;
28 * @type {HTMLDivElement}
31 this.middleBox_ = null;
34 * @type {HTMLDivElement}
37 this.shadowLeft_ = null;
40 * @type {HTMLDivElement}
43 this.cropFrame_ = null;
46 * @type {HTMLDivElement}
49 this.shadowRight_ = null;
52 * @type {HTMLDivElement}
55 this.shadowBottom_ = null;
58 * @type {ImageEditor.Toolbar}
67 this.onResizedBound_ = null;
70 * @type {DraggableRect}
73 this.cropRect_ = null;
76 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
82 ImageEditor.Mode.Crop.prototype.setUp = function() {
83 ImageEditor.Mode.prototype.setUp.apply(this, arguments);
85 var container = this.getImageView().container_;
86 var doc = container.ownerDocument;
88 this.domOverlay_ = doc.createElement('div');
89 this.domOverlay_.className = 'crop-overlay';
90 container.appendChild(this.domOverlay_);
92 this.shadowTop_ = doc.createElement('div');
93 this.shadowTop_.className = 'shadow';
94 this.domOverlay_.appendChild(this.shadowTop_);
96 this.middleBox_ = doc.createElement('div');
97 this.middleBox_.className = 'middle-box';
98 this.domOverlay_.appendChild(this.middleBox_);
100 this.shadowLeft_ = doc.createElement('div');
101 this.shadowLeft_.className = 'shadow';
102 this.middleBox_.appendChild(this.shadowLeft_);
104 this.cropFrame_ = doc.createElement('div');
105 this.cropFrame_.className = 'crop-frame';
106 this.middleBox_.appendChild(this.cropFrame_);
108 this.shadowRight_ = doc.createElement('div');
109 this.shadowRight_.className = 'shadow';
110 this.middleBox_.appendChild(this.shadowRight_);
112 this.shadowBottom_ = doc.createElement('div');
113 this.shadowBottom_.className = 'shadow';
114 this.domOverlay_.appendChild(this.shadowBottom_);
116 var cropFrame = this.cropFrame_;
117 function addCropFrame(className) {
118 var div = doc.createElement('div');
119 div.className = className;
120 cropFrame.appendChild(div);
123 addCropFrame('left top corner');
124 addCropFrame('top horizontal');
125 addCropFrame('right top corner');
126 addCropFrame('left vertical');
127 addCropFrame('right vertical');
128 addCropFrame('left bottom corner');
129 addCropFrame('bottom horizontal');
130 addCropFrame('right bottom corner');
132 this.onResizedBound_ = this.onResized_.bind(this);
133 window.addEventListener('resize', this.onResizedBound_);
135 this.createDefaultCrop();
141 ImageEditor.Mode.Crop.prototype.createTools = function(toolbar) {
143 GALLERY_ASPECT_RATIO_1_1: 1 / 1,
144 GALLERY_ASPECT_RATIO_6_4: 6 / 4,
145 GALLERY_ASPECT_RATIO_7_5: 7 / 5,
146 GALLERY_ASPECT_RATIO_16_9: 16 / 9
148 for (var name in aspects) {
149 var button = toolbar.addButton(
152 function(aspect, event) {
153 var button = event.target;
154 if (button.classList.contains('selected')) {
155 button.classList.remove('selected');
156 this.cropRect_.fixedAspectRatio = null;
158 var selectedButtons =
159 toolbar.getElement().querySelectorAll('button.selected');
160 for (var i = 0; i < selectedButtons.length; i++) {
161 selectedButtons[i].classList.remove('selected');
163 button.classList.add('selected');
164 var clipRect = this.viewport_.screenToImageRect(
165 this.viewport_.getImageBoundsOnScreenClipped());
166 this.cropRect_.fixedAspectRatio = aspect;
167 this.cropRect_.forceAspectRatio(aspect, clipRect);
170 this.toolbar_.getElement().classList.remove('dimmable');
171 this.toolbar_.getElement().removeAttribute('dimmed');
173 }.bind(this, aspects[name]));
174 // Prevent from cropping by Enter key if the button is focused.
175 button.addEventListener('keydown', function(event) {
176 var key = util.getKeyModifiers(event) + event.keyIdentifier;
178 event.stopPropagation();
181 this.toolbar_ = toolbar;
185 * Handles resizing of the window and updates the crop rectangle.
188 ImageEditor.Mode.Crop.prototype.onResized_ = function() {
195 ImageEditor.Mode.Crop.prototype.reset = function() {
196 ImageEditor.Mode.prototype.reset.call(this);
197 this.createDefaultCrop();
199 this.toolbar_.getElement().classList.add('dimmable');
200 this.toolbar_ = null;
205 * Updates the position of DOM elements.
207 ImageEditor.Mode.Crop.prototype.positionDOM = function() {
208 var screenClipped = this.viewport_.getImageBoundsOnScreenClipped();
210 var screenCrop = this.viewport_.imageToScreenRect(this.cropRect_.getRect());
211 var delta = ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS;
212 this.editor_.hideOverlappingTools(
213 screenCrop.inflate(delta, delta),
214 screenCrop.inflate(-delta, -delta));
216 this.domOverlay_.style.left = screenClipped.left + 'px';
217 this.domOverlay_.style.top = screenClipped.top + 'px';
218 this.domOverlay_.style.width = screenClipped.width + 'px';
219 this.domOverlay_.style.height = screenClipped.height + 'px';
221 this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px';
223 this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px';
225 this.shadowRight_.style.width = screenClipped.left + screenClipped.width -
226 (screenCrop.left + screenCrop.width) + 'px';
228 this.shadowBottom_.style.height = screenClipped.top + screenClipped.height -
229 (screenCrop.top + screenCrop.height) + 'px';
233 * Removes the overlay elements from the document.
235 ImageEditor.Mode.Crop.prototype.cleanUpUI = function() {
236 ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments);
237 this.domOverlay_.parentNode.removeChild(this.domOverlay_);
238 this.domOverlay_ = null;
239 this.editor_.hideOverlappingTools();
240 window.removeEventListener('resize', this.onResizedBound_);
241 this.onResizedBound_ = null;
248 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
254 ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
257 * Gets command to do the crop depending on the current state.
259 * @return {!Command.Crop} Crop command.
261 ImageEditor.Mode.Crop.prototype.getCommand = function() {
262 var cropImageRect = this.cropRect_.getRect();
263 return new Command.Crop(cropImageRect);
267 * Creates default (initial) crop.
269 ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
270 var viewport = this.getViewport();
273 var rect = viewport.screenToImageRect(
274 viewport.getImageBoundsOnScreenClipped());
276 -Math.round(rect.width / 6), -Math.round(rect.height / 6));
278 this.cropRect_ = new DraggableRect(rect, viewport);
284 * Obtains the cursor style depending on the mouse state.
286 * @param {number} x X coordinate for cursor.
287 * @param {number} y Y coordinate for cursor.
288 * @param {boolean} mouseDown If mouse button is down.
289 * @return {string} A value for style.cursor CSS property.
291 ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
292 return this.cropRect_.getCursorStyle(x, y, mouseDown);
296 * Obtains handler function depending on the mouse state.
298 * @param {number} x Event X coordinate.
299 * @param {number} y Event Y coordinate.
300 * @param {boolean} touch True if it's a touch event, false if mouse.
301 * @return {?function(number,number,boolean)} A function to be called on mouse
302 * drag. It takes x coordinate value, y coordinate value, and shift key
305 ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
306 var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
307 if (!cropDragHandler)
310 return function(x, y, shiftKey) {
312 this.toolbar_.getElement().classList.add('dimmable');
313 cropDragHandler(x, y, shiftKey);
320 * Obtains the double tap action depending on the coordinate.
322 * @param {number} x X coordinate of the event.
323 * @param {number} y Y coordinate of the event.
324 * @return {!ImageBuffer.DoubleTapAction} Action to perform as result.
326 ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
327 return this.cropRect_.getDoubleTapAction(x, y);
331 * A draggable rectangle over the image.
333 * @param {!ImageRect} rect Initial size of the image.
334 * @param {!Viewport} viewport Viewport.
338 function DraggableRect(rect, viewport) {
340 * The bounds are not held in a regular rectangle (with width/height).
341 * left/top/right/bottom held instead for convenience.
343 * @type {{left: number, right: number, top: number, bottom: number}}
348 right: rect.left + rect.width,
350 bottom: rect.top + rect.height
360 this.viewport_ = viewport;
368 this.dragMode_ = null;
371 * Fixed aspect ratio.
372 * The aspect ratio is not fixed when null.
375 this.fixedAspectRatio = null;
378 // Static members to simplify reflective access to the bounds.
383 DraggableRect.LEFT = 'left';
389 DraggableRect.RIGHT = 'right';
395 DraggableRect.TOP = 'top';
401 DraggableRect.BOTTOM = 'bottom';
407 DraggableRect.NONE = 'none';
410 * Obtains the left position.
411 * @return {number} Position.
413 DraggableRect.prototype.getLeft = function() {
414 return this.bounds_[DraggableRect.LEFT];
418 * Obtains the right position.
419 * @return {number} Position.
421 DraggableRect.prototype.getRight = function() {
422 return this.bounds_[DraggableRect.RIGHT];
426 * Obtains the top position.
427 * @return {number} Position.
429 DraggableRect.prototype.getTop = function() {
430 return this.bounds_[DraggableRect.TOP];
434 * Obtains the bottom position.
435 * @return {number} Position.
437 DraggableRect.prototype.getBottom = function() {
438 return this.bounds_[DraggableRect.BOTTOM];
442 * Obtains the geometry of the rectangle.
443 * @return {!ImageRect} Geometry of the rectangle.
445 DraggableRect.prototype.getRect = function() {
446 return ImageRect.createFromBounds(this.bounds_);
450 * Obtains the drag mode depending on the coordinate.
452 * @param {number} x X coordinate for cursor.
453 * @param {number} y Y coordinate for cursor.
454 * @param {boolean=} opt_touch Whether the operation is done by touch or not.
455 * @return {{xSide: string, ySide:string, whole:boolean, newCrop:boolean}}
458 DraggableRect.prototype.getDragMode = function(x, y, opt_touch) {
459 var touch = opt_touch || false;
462 xSide: DraggableRect.NONE,
463 ySide: DraggableRect.NONE,
468 var bounds = this.bounds_;
469 var R = this.viewport_.screenToImageSize(
470 touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS :
471 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
473 var circle = new Circle(x, y, R);
475 var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
476 var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
478 if (circle.inside(bounds.left, bounds.top)) {
479 result.xSide = DraggableRect.LEFT;
480 result.ySide = DraggableRect.TOP;
481 } else if (circle.inside(bounds.left, bounds.bottom)) {
482 result.xSide = DraggableRect.LEFT;
483 result.ySide = DraggableRect.BOTTOM;
484 } else if (circle.inside(bounds.right, bounds.top)) {
485 result.xSide = DraggableRect.RIGHT;
486 result.ySide = DraggableRect.TOP;
487 } else if (circle.inside(bounds.right, bounds.bottom)) {
488 result.xSide = DraggableRect.RIGHT;
489 result.ySide = DraggableRect.BOTTOM;
490 } else if (yBetween && Math.abs(x - bounds.left) <= R) {
491 result.xSide = DraggableRect.LEFT;
492 } else if (yBetween && Math.abs(x - bounds.right) <= R) {
493 result.xSide = DraggableRect.RIGHT;
494 } else if (xBetween && Math.abs(y - bounds.top) <= R) {
495 result.ySide = DraggableRect.TOP;
496 } else if (xBetween && Math.abs(y - bounds.bottom) <= R) {
497 result.ySide = DraggableRect.BOTTOM;
498 } else if (xBetween && yBetween) {
501 result.newcrop = true;
502 result.xSide = DraggableRect.RIGHT;
503 result.ySide = DraggableRect.BOTTOM;
510 * Obtains the cursor style depending on the coordinate.
512 * @param {number} x X coordinate for cursor.
513 * @param {number} y Y coordinate for cursor.
514 * @param {boolean} mouseDown If mouse button is down.
515 * @return {string} Cursor style.
517 DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
520 mode = this.dragMode_;
522 mode = this.getDragMode(
523 this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
531 switch (mode.xSide) {
532 case 'left': xSymbol = 'w'; break;
533 case 'right': xSymbol = 'e'; break;
536 switch (mode.ySide) {
537 case 'top': ySymbol = 'n'; break;
538 case 'bottom': ySymbol = 's'; break;
540 return ySymbol + xSymbol + '-resize';
544 * Obtains the drag handler depending on the coordinate.
546 * @param {number} initialScreenX X coordinate for cursor in the screen.
547 * @param {number} initialScreenY Y coordinate for cursor in the screen.
548 * @param {boolean} touch Whether the operation is done by touch or not.
549 * @return {?function(number,number,boolean)} Drag handler that takes x
550 * coordinate value, y coordinate value, and shift key flag.
552 DraggableRect.prototype.getDragHandler = function(
553 initialScreenX, initialScreenY, touch) {
554 // Check if the initial coordinate in the clip rect.
555 var initialX = this.viewport_.screenToImageX(initialScreenX);
556 var initialY = this.viewport_.screenToImageY(initialScreenY);
557 var initialWidth = this.bounds_.right - this.bounds_.left;
558 var initialHeight = this.bounds_.bottom - this.bounds_.top;
559 var clipRect = this.viewport_.screenToImageRect(
560 this.viewport_.getImageBoundsOnScreenClipped());
561 if (!clipRect.inside(initialX, initialY))
564 // Obtain the drag mode.
565 this.dragMode_ = this.getDragMode(initialX, initialY, touch);
567 if (this.dragMode_.whole) {
568 // Calc constant values during the operation.
569 var mouseBiasX = this.bounds_.left - initialX;
570 var mouseBiasY = this.bounds_.top - initialY;
571 var maxX = clipRect.left + clipRect.width - initialWidth;
572 var maxY = clipRect.top + clipRect.height - initialHeight;
574 // Returns a handler.
575 return function(newScreenX, newScreenY) {
576 var newX = this.viewport_.screenToImageX(newScreenX);
577 var newY = this.viewport_.screenToImageY(newScreenY);
578 var clamppedX = ImageUtil.clamp(clipRect.left, newX + mouseBiasX, maxX);
579 var clamppedY = ImageUtil.clamp(clipRect.top, newY + mouseBiasY, maxY);
580 this.bounds_.left = clamppedX;
581 this.bounds_.right = clamppedX + initialWidth;
582 this.bounds_.top = clamppedY;
583 this.bounds_.bottom = clamppedY + initialHeight;
586 // Calc constant values during the operation.
587 var mouseBiasX = this.bounds_[this.dragMode_.xSide] - initialX;
588 var mouseBiasY = this.bounds_[this.dragMode_.ySide] - initialY;
589 var maxX = clipRect.left + clipRect.width;
590 var maxY = clipRect.top + clipRect.height;
592 // Returns a handler.
593 return function(newScreenX, newScreenY, shiftKey) {
594 var newX = this.viewport_.screenToImageX(newScreenX);
595 var newY = this.viewport_.screenToImageY(newScreenY);
598 if (this.dragMode_.newcrop) {
599 this.dragMode_.newcrop = false;
600 this.bounds_.left = this.bounds_.right = initialX;
601 this.bounds_.top = this.bounds_.bottom = initialY;
606 // Update X coordinate.
607 if (this.dragMode_.xSide !== DraggableRect.NONE) {
608 this.bounds_[this.dragMode_.xSide] =
609 ImageUtil.clamp(clipRect.left, newX + mouseBiasX, maxX);
610 if (this.bounds_.left > this.bounds_.right) {
611 var left = this.bounds_.left;
612 var right = this.bounds_.right;
613 this.bounds_.left = right - 1;
614 this.bounds_.right = left + 1;
615 this.dragMode_.xSide =
616 this.dragMode_.xSide == 'left' ? 'right' : 'left';
620 // Update Y coordinate.
621 if (this.dragMode_.ySide !== DraggableRect.NONE) {
622 this.bounds_[this.dragMode_.ySide] =
623 ImageUtil.clamp(clipRect.top, newY + mouseBiasY, maxY);
624 if (this.bounds_.top > this.bounds_.bottom) {
625 var top = this.bounds_.top;
626 var bottom = this.bounds_.bottom;
627 this.bounds_.top = bottom - 1;
628 this.bounds_.bottom = top + 1;
629 this.dragMode_.ySide =
630 this.dragMode_.ySide === 'top' ? 'bottom' : 'top';
634 // Update aspect ratio.
635 if (this.fixedAspectRatio)
636 this.forceAspectRatio(this.fixedAspectRatio, clipRect);
638 this.forceAspectRatio(initialWidth / initialHeight, clipRect);
644 * Obtains double tap action depending on the coordinate.
646 * @param {number} x X coordinate for cursor.
647 * @param {number} y Y coordinate for cursor.
648 * @return {!ImageBuffer.DoubleTapAction} Double tap action.
650 DraggableRect.prototype.getDoubleTapAction = function(x, y) {
651 var clipRect = this.viewport_.getImageBoundsOnScreenClipped();
652 if (clipRect.inside(x, y))
653 return ImageBuffer.DoubleTapAction.COMMIT;
655 return ImageBuffer.DoubleTapAction.NOTHING;
659 * Forces the aspect ratio.
661 * @param {number} aspectRatio Aspect ratio.
662 * @param {!Object} clipRect Clip rect.
664 DraggableRect.prototype.forceAspectRatio = function(aspectRatio, clipRect) {
665 // Get current rectangle scale.
666 var width = this.bounds_.right - this.bounds_.left;
667 var height = this.bounds_.bottom - this.bounds_.top;
670 currentScale = ((width / aspectRatio) + height) / 2;
671 else if (this.dragMode_.xSide === 'none')
672 currentScale = height;
673 else if (this.dragMode_.ySide === 'none')
674 currentScale = width / aspectRatio;
676 currentScale = Math.max(width / aspectRatio, height);
678 // Get maximum width/height scale.
681 var center = (this.bounds_.left + this.bounds_.right) / 2;
682 var middle = (this.bounds_.top + this.bounds_.bottom) / 2;
683 var xSide = this.dragMode_ ? this.dragMode_.xSide : 'none';
684 var ySide = this.dragMode_ ? this.dragMode_.ySide : 'none';
687 maxWidth = this.bounds_.right - clipRect.left;
690 maxWidth = clipRect.left + clipRect.width - this.bounds_.left;
694 clipRect.left + clipRect.width - center,
695 center - clipRect.left) * 2;
700 maxHeight = this.bounds_.bottom - clipRect.top;
703 maxHeight = clipRect.top + clipRect.height - this.bounds_.top;
706 maxHeight = Math.min(
707 clipRect.top + clipRect.height - middle,
708 middle - clipRect.top) * 2;
712 // Obtains target scale.
713 var targetScale = Math.min(
715 maxWidth / aspectRatio,
719 var newWidth = targetScale * aspectRatio;
720 var newHeight = targetScale;
723 this.bounds_.left = this.bounds_.right - newWidth;
726 this.bounds_.right = this.bounds_.left + newWidth;
729 this.bounds_.left = center - newWidth / 2;
730 this.bounds_.right = center + newWidth / 2;
735 this.bounds_.top = this.bounds_.bottom - newHeight;
738 this.bounds_.bottom = this.bounds_.top + newHeight;
741 this.bounds_.top = middle - newHeight / 2;
742 this.bounds_.bottom = middle + newHeight / 2;