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;
61 this.onViewportResizedBound_ = null;
64 * @type {DraggableRect}
67 this.cropRect_ = null;
70 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
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);
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();
142 ImageEditor.Mode.Crop.prototype.createTools = function(toolbar) {
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
150 for (var name in aspects) {
151 var button = toolbar.addButton(
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;
161 event.stopPropagation();
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.
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;
181 var selectedButtons =
182 toolbar.getElement().querySelectorAll('button.selected');
183 for (var i = 0; i < selectedButtons.length; i++) {
184 selectedButtons[i].classList.remove('selected');
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);
197 * Handles resizing of the viewport and updates the crop rectangle.
200 ImageEditor.Mode.Crop.prototype.onViewportResized_ = function() {
207 ImageEditor.Mode.Crop.prototype.reset = function() {
208 ImageEditor.Mode.prototype.reset.call(this);
209 this.createDefaultCrop();
213 * Updates the position of DOM elements.
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.
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();
246 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
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.
259 ImageEditor.Mode.Crop.prototype.getCommand = function() {
260 var cropImageRect = this.cropRect_.getRect();
261 return new Command.Crop(cropImageRect);
265 * Creates default (initial) crop.
267 ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
268 var viewport = this.getViewport();
271 var rect = viewport.screenToImageRect(
272 viewport.getImageBoundsOnScreenClipped());
274 -Math.round(rect.width / 6), -Math.round(rect.height / 6));
276 this.cropRect_ = new DraggableRect(rect, viewport);
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.
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
303 ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
304 var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
305 if (!cropDragHandler)
308 return function(x, y, shiftKey) {
309 cropDragHandler(x, y, shiftKey);
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.
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.
334 function DraggableRect(rect, viewport) {
336 * The bounds are not held in a regular rectangle (with width/height).
337 * left/top/right/bottom held instead for convenience.
339 * @type {{left: number, right: number, top: number, bottom: number}}
344 right: rect.left + rect.width,
346 bottom: rect.top + rect.height
356 this.viewport_ = viewport;
364 this.dragMode_ = null;
367 * Fixed aspect ratio.
368 * The aspect ratio is not fixed when null.
371 this.fixedAspectRatio = null;
374 // Static members to simplify reflective access to the bounds.
379 DraggableRect.LEFT = 'left';
385 DraggableRect.RIGHT = 'right';
391 DraggableRect.TOP = 'top';
397 DraggableRect.BOTTOM = 'bottom';
403 DraggableRect.NONE = 'none';
406 * Obtains the left position.
407 * @return {number} Position.
409 DraggableRect.prototype.getLeft = function() {
410 return this.bounds_[DraggableRect.LEFT];
414 * Obtains the right position.
415 * @return {number} Position.
417 DraggableRect.prototype.getRight = function() {
418 return this.bounds_[DraggableRect.RIGHT];
422 * Obtains the top position.
423 * @return {number} Position.
425 DraggableRect.prototype.getTop = function() {
426 return this.bounds_[DraggableRect.TOP];
430 * Obtains the bottom position.
431 * @return {number} Position.
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.
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}}
454 DraggableRect.prototype.getDragMode = function(x, y, opt_touch) {
455 var touch = opt_touch || false;
458 xSide: DraggableRect.NONE,
459 ySide: DraggableRect.NONE,
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) {
497 result.newcrop = true;
498 result.xSide = DraggableRect.RIGHT;
499 result.ySide = DraggableRect.BOTTOM;
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.
513 DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
516 mode = this.dragMode_;
518 mode = this.getDragMode(
519 this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
527 switch (mode.xSide) {
528 case 'left': xSymbol = 'w'; break;
529 case 'right': xSymbol = 'e'; break;
532 switch (mode.ySide) {
533 case 'top': ySymbol = 'n'; break;
534 case 'bottom': ySymbol = 's'; break;
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.
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 = [
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)
562 if (!draggableAreas.some(
563 (area) => area.inside(initialScreenX, initialScreenY))) {
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;
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);
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;
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';
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';
644 // Update aspect ratio.
645 if (this.fixedAspectRatio)
646 this.forceAspectRatio(this.fixedAspectRatio, clipRect);
648 this.forceAspectRatio(initialWidth / initialHeight, clipRect);
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.
660 DraggableRect.prototype.getDoubleTapAction = function(x, y) {
661 var clipRect = this.viewport_.getImageBoundsOnScreenClipped();
662 if (clipRect.inside(x, y))
663 return ImageBuffer.DoubleTapAction.COMMIT;
665 return ImageBuffer.DoubleTapAction.NOTHING;
669 * Forces the aspect ratio.
671 * @param {number} aspectRatio Aspect ratio.
672 * @param {!Object} clipRect Clip rect.
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;
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;
686 currentScale = Math.max(width / aspectRatio, height);
688 // Get maximum width/height scale.
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';
697 maxWidth = this.bounds_.right - clipRect.left;
700 maxWidth = clipRect.left + clipRect.width - this.bounds_.left;
704 clipRect.left + clipRect.width - center,
705 center - clipRect.left) * 2;
710 maxHeight = this.bounds_.bottom - clipRect.top;
713 maxHeight = clipRect.top + clipRect.height - this.bounds_.top;
716 maxHeight = Math.min(
717 clipRect.top + clipRect.height - middle,
718 middle - clipRect.top) * 2;
722 // Obtains target scale.
723 var targetScale = Math.min(
725 maxWidth / aspectRatio,
729 var newWidth = targetScale * aspectRatio;
730 var newHeight = targetScale;
733 this.bounds_.left = this.bounds_.right - newWidth;
736 this.bounds_.right = this.bounds_.left + newWidth;
739 this.bounds_.left = center - newWidth / 2;
740 this.bounds_.right = center + newWidth / 2;
745 this.bounds_.top = this.bounds_.bottom - newHeight;
748 this.bounds_.bottom = this.bounds_.top + newHeight;
751 this.bounds_.top = middle - newHeight / 2;
752 this.bounds_.bottom = middle + newHeight / 2;