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.
10 * @extends {ImageEditor.Mode}
13 ImageEditor.Mode.Crop = function() {
14 ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
17 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
23 ImageEditor.Mode.Crop.prototype.setUp = function() {
24 ImageEditor.Mode.prototype.setUp.apply(this, arguments);
26 var container = this.getImageView().container_;
27 var doc = container.ownerDocument;
29 this.domOverlay_ = doc.createElement('div');
30 this.domOverlay_.className = 'crop-overlay';
31 container.appendChild(this.domOverlay_);
33 this.shadowTop_ = doc.createElement('div');
34 this.shadowTop_.className = 'shadow';
35 this.domOverlay_.appendChild(this.shadowTop_);
37 this.middleBox_ = doc.createElement('div');
38 this.middleBox_.className = 'middle-box';
39 this.domOverlay_.appendChild(this.middleBox_);
41 this.shadowLeft_ = doc.createElement('div');
42 this.shadowLeft_.className = 'shadow';
43 this.middleBox_.appendChild(this.shadowLeft_);
45 this.cropFrame_ = doc.createElement('div');
46 this.cropFrame_.className = 'crop-frame';
47 this.middleBox_.appendChild(this.cropFrame_);
49 this.shadowRight_ = doc.createElement('div');
50 this.shadowRight_.className = 'shadow';
51 this.middleBox_.appendChild(this.shadowRight_);
53 this.shadowBottom_ = doc.createElement('div');
54 this.shadowBottom_.className = 'shadow';
55 this.domOverlay_.appendChild(this.shadowBottom_);
59 var cropFrame = this.cropFrame_;
60 function addCropFrame(className) {
61 var div = doc.createElement('div');
62 div.className = className;
63 cropFrame.appendChild(div);
66 addCropFrame('left top corner');
67 addCropFrame('top horizontal');
68 addCropFrame('right top corner');
69 addCropFrame('left vertical');
70 addCropFrame('right vertical');
71 addCropFrame('left bottom corner');
72 addCropFrame('bottom horizontal');
73 addCropFrame('right bottom corner');
75 this.onResizedBound_ = this.onResized_.bind(this);
76 window.addEventListener('resize', this.onResizedBound_);
78 this.createDefaultCrop();
84 ImageEditor.Mode.Crop.prototype.createTools = function(toolbar) {
86 GALLERY_ASPECT_RATIO_1_1: 1 / 1,
87 GALLERY_ASPECT_RATIO_6_4: 6 / 4,
88 GALLERY_ASPECT_RATIO_7_5: 7 / 5,
89 GALLERY_ASPECT_RATIO_16_9: 16 / 9
91 for (name in aspects) {
95 function(aspect, event) {
96 var button = event.target;
97 if (button.classList.contains('selected')) {
98 button.classList.remove('selected');
99 this.cropRect_.fixedAspectRatio = null;
101 var selectedButtons =
102 toolbar.element.querySelectorAll('button.selected');
103 for (var i = 0; i < selectedButtons.length; i++) {
104 selectedButtons[i].classList.remove('selected');
106 button.classList.add('selected');
107 var clipRect = this.viewport_.screenToImageRect(
108 this.viewport_.getImageBoundsOnScreenClipped());
109 this.cropRect_.fixedAspectRatio = aspect;
110 this.cropRect_.forceAspectRatio(aspect, clipRect);
113 this.toolbar_.element.classList.remove('dimmable');
114 this.toolbar_.element.removeAttribute('dimmed');
116 }.bind(this, aspects[name]));
118 this.toolbar_ = toolbar;
122 * Handles resizing of the window and updates the crop rectangle.
125 ImageEditor.Mode.Crop.prototype.onResized_ = function() {
132 ImageEditor.Mode.Crop.prototype.reset = function() {
133 ImageEditor.Mode.prototype.reset.call(this);
134 this.createDefaultCrop();
136 this.toolbar_.element.classList.add('dimmable');
137 this.toolbar_ = null;
142 * Updates the position of DOM elements.
144 ImageEditor.Mode.Crop.prototype.positionDOM = function() {
145 var screenClipped = this.viewport_.getImageBoundsOnScreenClipped();
147 var screenCrop = this.viewport_.imageToScreenRect(this.cropRect_.getRect());
148 var delta = ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS;
149 this.editor_.hideOverlappingTools(
150 screenCrop.inflate(delta, delta),
151 screenCrop.inflate(-delta, -delta));
153 this.domOverlay_.style.left = screenClipped.left + 'px';
154 this.domOverlay_.style.top = screenClipped.top + 'px';
155 this.domOverlay_.style.width = screenClipped.width + 'px';
156 this.domOverlay_.style.height = screenClipped.height + 'px';
158 this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px';
160 this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px';
162 this.shadowRight_.style.width = screenClipped.left + screenClipped.width -
163 (screenCrop.left + screenCrop.width) + 'px';
165 this.shadowBottom_.style.height = screenClipped.top + screenClipped.height -
166 (screenCrop.top + screenCrop.height) + 'px';
170 * Removes the overlay elements from the document.
172 ImageEditor.Mode.Crop.prototype.cleanUpUI = function() {
173 ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments);
174 this.domOverlay_.parentNode.removeChild(this.domOverlay_);
175 this.domOverlay_ = null;
176 this.editor_.hideOverlappingTools();
177 window.removeEventListener('resize', this.onResizedBound_);
178 this.onResizedBound_ = null;
185 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
191 ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
194 * Gets command to do the crop depending on the current state.
196 * @return {Command.Crop} Crop command.
198 ImageEditor.Mode.Crop.prototype.getCommand = function() {
199 var cropImageRect = this.cropRect_.getRect();
200 return new Command.Crop(cropImageRect);
204 * Creates default (initial) crop.
206 ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
207 var rect = this.getViewport().screenToImageRect(
208 new Rect(this.getViewport().getImageBoundsOnScreenClipped()));
210 -Math.round(rect.width / 6), -Math.round(rect.height / 6));
211 this.cropRect_ = new DraggableRect(rect, this.getViewport());
216 * Obtains the cursor style depending on the mouse state.
218 * @param {number} x X coordinate for cursor.
219 * @param {number} y Y coordinate for cursor.
220 * @param {boolean} mouseDown If mouse button is down.
221 * @return {string} A value for style.cursor CSS property.
223 ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
224 return this.cropRect_.getCursorStyle(x, y, mouseDown);
228 * Obtains handler function depending on the mouse state.
230 * @param {number} x Event X coordinate.
231 * @param {number} y Event Y coordinate.
232 * @param {boolean} touch True if it's a touch event, false if mouse.
233 * @return {function(number,number,boolean)} A function to be called on mouse
234 * drag. It takes x coordinate value, y coordinate value, and shift key
237 ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
238 var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
239 if (!cropDragHandler)
242 return function(x, y, shiftKey) {
244 this.toolbar_.element.classList.add('dimmable');
245 cropDragHandler(x, y, shiftKey);
252 * Obtains the double tap action depending on the coordinate.
254 * @param {number} x X coordinate of the event.
255 * @param {number} y Y coordinate of the event.
256 * @return {ImageBuffer.DoubleTapAction} Action to perform as result.
258 ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
259 return this.cropRect_.getDoubleTapAction(x, y);
263 * A draggable rectangle over the image.
265 * @param {Rect} rect Initial size of the image.
266 * @param {Viewport} viewport Viewport.
269 function DraggableRect(rect, viewport) {
271 * The bounds are not held in a regular rectangle (with width/height).
272 * left/top/right/bottom held instead for convenience.
274 * @type {{left: number, right: number, top: number, bottom: number}}
278 this.bounds_[DraggableRect.LEFT] = rect.left;
279 this.bounds_[DraggableRect.RIGHT] = rect.left + rect.width;
280 this.bounds_[DraggableRect.TOP] = rect.top;
281 this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height;
289 this.viewport_ = viewport;
297 this.dragMode_ = null;
300 * Fixed aspect ratio.
301 * The aspect ratio is not fixed when null.
304 this.fixedAspectRatio = null;
309 // Static members to simplify reflective access to the bounds.
314 DraggableRect.LEFT = 'left';
320 DraggableRect.RIGHT = 'right';
326 DraggableRect.TOP = 'top';
332 DraggableRect.BOTTOM = 'bottom';
338 DraggableRect.NONE = 'none';
341 * Obtains the left position.
342 * @return {number} Position.
344 DraggableRect.prototype.getLeft = function() {
345 return this.bounds_[DraggableRect.LEFT];
349 * Obtains the right position.
350 * @return {number} Position.
352 DraggableRect.prototype.getRight = function() {
353 return this.bounds_[DraggableRect.RIGHT];
357 * Obtains the top position.
358 * @return {number} Position.
360 DraggableRect.prototype.getTop = function() {
361 return this.bounds_[DraggableRect.TOP];
365 * Obtains the bottom position.
366 * @return {number} Position.
368 DraggableRect.prototype.getBottom = function() {
369 return this.bounds_[DraggableRect.BOTTOM];
373 * Obtains the geometry of the rectangle.
374 * @return {Rect} Geometry of the rectangle.
376 DraggableRect.prototype.getRect = function() {
377 return new Rect(this.bounds_);
381 * Obtains the drag mode depending on the coordinate.
383 * @param {number} x X coordinate for cursor.
384 * @param {number} y Y coordinate for cursor.
385 * @param {boolean} touch Whether the operation is done by touch or not.
386 * @return {Object} Drag mode.
388 DraggableRect.prototype.getDragMode = function(x, y, touch) {
390 xSide: DraggableRect.NONE,
391 ySide: DraggableRect.NONE
394 var bounds = this.bounds_;
395 var R = this.viewport_.screenToImageSize(
396 touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS :
397 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
399 var circle = new Circle(x, y, R);
401 var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
402 var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
404 if (circle.inside(bounds.left, bounds.top)) {
405 result.xSide = DraggableRect.LEFT;
406 result.ySide = DraggableRect.TOP;
407 } else if (circle.inside(bounds.left, bounds.bottom)) {
408 result.xSide = DraggableRect.LEFT;
409 result.ySide = DraggableRect.BOTTOM;
410 } else if (circle.inside(bounds.right, bounds.top)) {
411 result.xSide = DraggableRect.RIGHT;
412 result.ySide = DraggableRect.TOP;
413 } else if (circle.inside(bounds.right, bounds.bottom)) {
414 result.xSide = DraggableRect.RIGHT;
415 result.ySide = DraggableRect.BOTTOM;
416 } else if (yBetween && Math.abs(x - bounds.left) <= R) {
417 result.xSide = DraggableRect.LEFT;
418 } else if (yBetween && Math.abs(x - bounds.right) <= R) {
419 result.xSide = DraggableRect.RIGHT;
420 } else if (xBetween && Math.abs(y - bounds.top) <= R) {
421 result.ySide = DraggableRect.TOP;
422 } else if (xBetween && Math.abs(y - bounds.bottom) <= R) {
423 result.ySide = DraggableRect.BOTTOM;
424 } else if (xBetween && yBetween) {
427 result.newcrop = true;
428 result.xSide = DraggableRect.RIGHT;
429 result.ySide = DraggableRect.BOTTOM;
436 * Obtains the cursor style depending on the coordinate.
438 * @param {number} x X coordinate for cursor.
439 * @param {number} y Y coordinate for cursor.
440 * @param {boolean} mouseDown If mouse button is down.
441 * @return {string} Cursor style.
443 DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
446 mode = this.dragMode_;
448 mode = this.getDragMode(
449 this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
457 switch (mode.xSide) {
458 case 'left': xSymbol = 'w'; break;
459 case 'right': xSymbol = 'e'; break;
462 switch (mode.ySide) {
463 case 'top': ySymbol = 'n'; break;
464 case 'bottom': ySymbol = 's'; break;
466 return ySymbol + xSymbol + '-resize';
470 * Obtains the drag handler depending on the coordinate.
472 * @param {number} initialScreenX X coordinate for cursor in the screen.
473 * @param {number} initialScreenY Y coordinate for cursor in the screen.
474 * @param {boolean} touch Whether the operation is done by touch or not.
475 * @return {function(number,number,boolean)} Drag handler that takes x
476 * coordinate value, y coordinate value, and shift key flag.
478 DraggableRect.prototype.getDragHandler = function(
479 initialScreenX, initialScreenY, touch) {
480 // Check if the initial coordinate in the clip rect.
481 var initialX = this.viewport_.screenToImageX(initialScreenX);
482 var initialY = this.viewport_.screenToImageY(initialScreenY);
483 var initialWidth = this.bounds_.right - this.bounds_.left;
484 var initialHeight = this.bounds_.bottom - this.bounds_.top;
485 var clipRect = this.viewport_.screenToImageRect(
486 this.viewport_.getImageBoundsOnScreenClipped());
487 if (!clipRect.inside(initialX, initialY))
490 // Obtain the drag mode.
491 this.dragMode_ = this.getDragMode(initialX, initialY, touch);
493 if (this.dragMode_.whole) {
494 // Calc constant values during the operation.
495 var mouseBiasX = this.bounds_.left - initialX;
496 var mouseBiasY = this.bounds_.top - initialY;
497 var maxX = clipRect.left + clipRect.width - initialWidth;
498 var maxY = clipRect.top + clipRect.height - initialHeight;
500 // Returns a handler.
501 return function(newScreenX, newScreenY) {
502 var newX = this.viewport_.screenToImageX(newScreenX);
503 var newY = this.viewport_.screenToImageY(newScreenY);
504 var clamppedX = ImageUtil.clamp(clipRect.left, newX + mouseBiasX, maxX);
505 var clamppedY = ImageUtil.clamp(clipRect.top, newY + mouseBiasY, maxY);
506 this.bounds_.left = clamppedX;
507 this.bounds_.right = clamppedX + initialWidth;
508 this.bounds_.top = clamppedY;
509 this.bounds_.bottom = clamppedY + initialHeight;
512 // Calc constant values during the operation.
513 var mouseBiasX = this.bounds_[this.dragMode_.xSide] - initialX;
514 var mouseBiasY = this.bounds_[this.dragMode_.ySide] - initialY;
515 var maxX = clipRect.left + clipRect.width;
516 var maxY = clipRect.top + clipRect.height;
518 // Returns a handler.
519 return function(newScreenX, newScreenY, shiftKey) {
520 var newX = this.viewport_.screenToImageX(newScreenX);
521 var newY = this.viewport_.screenToImageY(newScreenY);
524 if (this.dragMode_.newcrop) {
525 this.dragMode_.newcrop = false;
526 this.bounds_.left = this.bounds_.right = newX;
527 this.bounds_.top = this.bounds_.bottom = newY;
532 // Update X coordinate.
533 if (this.dragMode_.xSide !== DraggableRect.NONE) {
534 this.bounds_[this.dragMode_.xSide] =
535 ImageUtil.clamp(clipRect.left, newX + mouseBiasX, maxX);
536 if (this.bounds_.left > this.bounds_.right) {
537 var left = this.bounds_.left;
538 var right = this.bounds_.right;
539 this.bounds_.left = right - 1;
540 this.bounds_.right = left + 1;
541 this.dragMode_.xSide =
542 this.dragMode_.xSide == 'left' ? 'right' : 'left';
546 // Update Y coordinate.
547 if (this.dragMode_.ySide !== DraggableRect.NONE) {
548 this.bounds_[this.dragMode_.ySide] =
549 ImageUtil.clamp(clipRect.top, newY + mouseBiasY, maxY);
550 if (this.bounds_.top > this.bounds_.bottom) {
551 var top = this.bounds_.top;
552 var bottom = this.bounds_.bottom;
553 this.bounds_.top = bottom - 1;
554 this.bounds_.bottom = top + 1;
555 this.dragMode_.ySide =
556 this.dragMode_.ySide === 'top' ? 'bottom' : 'top';
560 // Update aspect ratio.
561 if (this.fixedAspectRatio)
562 this.forceAspectRatio(this.fixedAspectRatio, clipRect);
564 this.forceAspectRatio(initialWidth / initialHeight, clipRect);
570 * Obtains double tap action depending on the coordinate.
572 * @param {number} x X coordinate for cursor.
573 * @param {number} y Y coordinate for cursor.
574 * @param {boolean} touch Whether the operation is done by touch or not.
575 * @return {ImageBuffer.DoubleTapAction} Double tap action.
577 DraggableRect.prototype.getDoubleTapAction = function(x, y, touch) {
578 var clipRect = this.viewport_.getImageBoundsOnScreenClipped();
579 if (clipRect.inside(x, y))
580 return ImageBuffer.DoubleTapAction.COMMIT;
582 return ImageBuffer.DoubleTapAction.NOTHING;
586 * Forces the aspect ratio.
588 * @param {number} aspectRatio Aspect ratio.
589 * @param {Object} clipRect Clip rect.
591 DraggableRect.prototype.forceAspectRatio = function(aspectRatio, clipRect) {
592 // Get current rectangle scale.
593 var width = this.bounds_.right - this.bounds_.left;
594 var height = this.bounds_.bottom - this.bounds_.top;
597 currentScale = ((width / aspectRatio) + height) / 2;
598 else if (this.dragMode_.xSide === 'none')
599 currentScale = height;
600 else if (this.dragMode_.ySide === 'none')
601 currentScale = width / aspectRatio;
603 currentScale = Math.max(width / aspectRatio, height);
605 // Get maximum width/height scale.
608 var center = (this.bounds_.left + this.bounds_.right) / 2;
609 var middle = (this.bounds_.top + this.bounds_.bottom) / 2;
610 var xSide = this.dragMode_ ? this.dragMode_.xSide : 'none';
611 var ySide = this.dragMode_ ? this.dragMode_.ySide : 'none';
614 maxWidth = this.bounds_.right - clipRect.left;
617 maxWidth = clipRect.left + clipRect.width - this.bounds_.left;
621 clipRect.left + clipRect.width - center,
622 center - clipRect.left) * 2;
627 maxHeight = this.bounds_.bottom - clipRect.top;
630 maxHeight = clipRect.top + clipRect.height - this.bounds_.top;
633 maxHeight = Math.min(
634 clipRect.top + clipRect.height - middle,
635 middle - clipRect.top) * 2;
639 // Obtains target scale.
640 var targetScale = Math.min(
642 maxWidth / aspectRatio,
646 var newWidth = targetScale * aspectRatio;
647 var newHeight = targetScale;
650 this.bounds_.left = this.bounds_.right - newWidth;
653 this.bounds_.right = this.bounds_.left + newWidth;
656 this.bounds_.left = center - newWidth / 2;
657 this.bounds_.right = center + newWidth / 2;
662 this.bounds_.top = this.bounds_.bottom - newHeight;
665 this.bounds_.bottom = this.bounds_.top + newHeight;
668 this.bounds_.top = middle - newHeight / 2;
669 this.bounds_.bottom = middle + newHeight / 2;