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}
11 ImageEditor
.Mode
.Crop = function() {
12 ImageEditor
.Mode
.call(this, 'crop', 'GALLERY_CROP');
15 ImageEditor
.Mode
.Crop
.prototype = {__proto__
: ImageEditor
.Mode
.prototype};
21 ImageEditor
.Mode
.Crop
.prototype.setUp = function() {
22 ImageEditor
.Mode
.prototype.setUp
.apply(this, arguments
);
24 var container
= this.getImageView().container_
;
25 var doc
= container
.ownerDocument
;
27 this.domOverlay_
= doc
.createElement('div');
28 this.domOverlay_
.className
= 'crop-overlay';
29 container
.appendChild(this.domOverlay_
);
31 this.shadowTop_
= doc
.createElement('div');
32 this.shadowTop_
.className
= 'shadow';
33 this.domOverlay_
.appendChild(this.shadowTop_
);
35 this.middleBox_
= doc
.createElement('div');
36 this.middleBox_
.className
= 'middle-box';
37 this.domOverlay_
.appendChild(this.middleBox_
);
39 this.shadowLeft_
= doc
.createElement('div');
40 this.shadowLeft_
.className
= 'shadow';
41 this.middleBox_
.appendChild(this.shadowLeft_
);
43 this.cropFrame_
= doc
.createElement('div');
44 this.cropFrame_
.className
= 'crop-frame';
45 this.middleBox_
.appendChild(this.cropFrame_
);
47 this.shadowRight_
= doc
.createElement('div');
48 this.shadowRight_
.className
= 'shadow';
49 this.middleBox_
.appendChild(this.shadowRight_
);
51 this.shadowBottom_
= doc
.createElement('div');
52 this.shadowBottom_
.className
= 'shadow';
53 this.domOverlay_
.appendChild(this.shadowBottom_
);
57 var cropFrame
= this.cropFrame_
;
58 function addCropFrame(className
) {
59 var div
= doc
.createElement('div');
60 div
.className
= className
;
61 cropFrame
.appendChild(div
);
64 addCropFrame('left top corner');
65 addCropFrame('top horizontal');
66 addCropFrame('right top corner');
67 addCropFrame('left vertical');
68 addCropFrame('right vertical');
69 addCropFrame('left bottom corner');
70 addCropFrame('bottom horizontal');
71 addCropFrame('right bottom corner');
73 this.onResizedBound_
= this.onResized_
.bind(this);
74 window
.addEventListener('resize', this.onResizedBound_
);
76 this.createDefaultCrop();
82 ImageEditor
.Mode
.Crop
.prototype.createTools = function(toolbar
) {
84 GALLERY_ASPECT_RATIO_1_1
: 1 / 1,
85 GALLERY_ASPECT_RATIO_6_4
: 6 / 4,
86 GALLERY_ASPECT_RATIO_7_5
: 7 / 5,
87 GALLERY_ASPECT_RATIO_16_9
: 16 / 9
89 for (name
in aspects
) {
93 function(aspect
, event
) {
94 var button
= event
.target
;
95 if (button
.classList
.contains('selected')) {
96 button
.classList
.remove('selected');
97 this.cropRect_
.fixedAspectRatio
= null;
100 toolbar
.element
.querySelectorAll('button.selected');
101 for (var i
= 0; i
< selectedButtons
.length
; i
++) {
102 selectedButtons
[i
].classList
.remove('selected');
104 button
.classList
.add('selected');
105 var clipRect
= this.viewport_
.screenToImageRect(
106 this.viewport_
.getImageBoundsOnScreenClipped());
107 this.cropRect_
.fixedAspectRatio
= aspect
;
108 this.cropRect_
.forceAspectRatio(aspect
, clipRect
);
111 this.toolbar_
.element
.classList
.remove('dimmable');
112 this.toolbar_
.element
.removeAttribute('dimmed');
114 }.bind(this, aspects
[name
]));
116 this.toolbar_
= toolbar
;
120 * Handles resizing of the window and updates the crop rectangle.
123 ImageEditor
.Mode
.Crop
.prototype.onResized_ = function() {
130 ImageEditor
.Mode
.Crop
.prototype.reset = function() {
131 ImageEditor
.Mode
.prototype.reset
.call(this);
132 this.createDefaultCrop();
134 this.toolbar_
.element
.classList
.add('dimmable');
135 this.toolbar_
= null;
140 * Updates the position of DOM elements.
142 ImageEditor
.Mode
.Crop
.prototype.positionDOM = function() {
143 var screenClipped
= this.viewport_
.getImageBoundsOnScreenClipped();
145 var screenCrop
= this.viewport_
.imageToScreenRect(this.cropRect_
.getRect());
146 var delta
= ImageEditor
.Mode
.Crop
.MOUSE_GRAB_RADIUS
;
147 this.editor_
.hideOverlappingTools(
148 screenCrop
.inflate(delta
, delta
),
149 screenCrop
.inflate(-delta
, -delta
));
151 this.domOverlay_
.style
.left
= screenClipped
.left
+ 'px';
152 this.domOverlay_
.style
.top
= screenClipped
.top
+ 'px';
153 this.domOverlay_
.style
.width
= screenClipped
.width
+ 'px';
154 this.domOverlay_
.style
.height
= screenClipped
.height
+ 'px';
156 this.shadowLeft_
.style
.width
= screenCrop
.left
- screenClipped
.left
+ 'px';
158 this.shadowTop_
.style
.height
= screenCrop
.top
- screenClipped
.top
+ 'px';
160 this.shadowRight_
.style
.width
= screenClipped
.left
+ screenClipped
.width
-
161 (screenCrop
.left
+ screenCrop
.width
) + 'px';
163 this.shadowBottom_
.style
.height
= screenClipped
.top
+ screenClipped
.height
-
164 (screenCrop
.top
+ screenCrop
.height
) + 'px';
168 * Removes the overlay elements from the document.
170 ImageEditor
.Mode
.Crop
.prototype.cleanUpUI = function() {
171 ImageEditor
.Mode
.prototype.cleanUpUI
.apply(this, arguments
);
172 this.domOverlay_
.parentNode
.removeChild(this.domOverlay_
);
173 this.domOverlay_
= null;
174 this.editor_
.hideOverlappingTools();
175 window
.removeEventListener('resize', this.onResizedBound_
);
176 this.onResizedBound_
= null;
183 ImageEditor
.Mode
.Crop
.MOUSE_GRAB_RADIUS
= 6;
189 ImageEditor
.Mode
.Crop
.TOUCH_GRAB_RADIUS
= 20;
192 * Gets command to do the crop depending on the current state.
194 * @return {Command.Crop} Crop command.
196 ImageEditor
.Mode
.Crop
.prototype.getCommand = function() {
197 var cropImageRect
= this.cropRect_
.getRect();
198 return new Command
.Crop(cropImageRect
);
202 * Creates default (initial) crop.
204 ImageEditor
.Mode
.Crop
.prototype.createDefaultCrop = function() {
205 var rect
= this.getViewport().screenToImageRect(
206 new ImageRect(this.getViewport().getImageBoundsOnScreenClipped()));
208 -Math
.round(rect
.width
/ 6), -Math
.round(rect
.height
/ 6));
209 this.cropRect_
= new DraggableRect(rect
, this.getViewport());
214 * Obtains the cursor style depending on the mouse state.
216 * @param {number} x X coordinate for cursor.
217 * @param {number} y Y coordinate for cursor.
218 * @param {boolean} mouseDown If mouse button is down.
219 * @return {string} A value for style.cursor CSS property.
221 ImageEditor
.Mode
.Crop
.prototype.getCursorStyle = function(x
, y
, mouseDown
) {
222 return this.cropRect_
.getCursorStyle(x
, y
, mouseDown
);
226 * Obtains handler function depending on the mouse state.
228 * @param {number} x Event X coordinate.
229 * @param {number} y Event Y coordinate.
230 * @param {boolean} touch True if it's a touch event, false if mouse.
231 * @return {function(number,number,boolean)} A function to be called on mouse
232 * drag. It takes x coordinate value, y coordinate value, and shift key
235 ImageEditor
.Mode
.Crop
.prototype.getDragHandler = function(x
, y
, touch
) {
236 var cropDragHandler
= this.cropRect_
.getDragHandler(x
, y
, touch
);
237 if (!cropDragHandler
)
240 return function(x
, y
, shiftKey
) {
242 this.toolbar_
.element
.classList
.add('dimmable');
243 cropDragHandler(x
, y
, shiftKey
);
250 * Obtains the double tap action depending on the coordinate.
252 * @param {number} x X coordinate of the event.
253 * @param {number} y Y coordinate of the event.
254 * @return {ImageBuffer.DoubleTapAction} Action to perform as result.
256 ImageEditor
.Mode
.Crop
.prototype.getDoubleTapAction = function(x
, y
) {
257 return this.cropRect_
.getDoubleTapAction(x
, y
);
261 * A draggable rectangle over the image.
263 * @param {ImageRect} rect Initial size of the image.
264 * @param {Viewport} viewport Viewport.
267 function DraggableRect(rect
, viewport
) {
269 * The bounds are not held in a regular rectangle (with width/height).
270 * left/top/right/bottom held instead for convenience.
272 * @type {{left: number, right: number, top: number, bottom: number}}
276 this.bounds_
[DraggableRect
.LEFT
] = rect
.left
;
277 this.bounds_
[DraggableRect
.RIGHT
] = rect
.left
+ rect
.width
;
278 this.bounds_
[DraggableRect
.TOP
] = rect
.top
;
279 this.bounds_
[DraggableRect
.BOTTOM
] = rect
.top
+ rect
.height
;
287 this.viewport_
= viewport
;
295 this.dragMode_
= null;
298 * Fixed aspect ratio.
299 * The aspect ratio is not fixed when null.
302 this.fixedAspectRatio
= null;
307 // Static members to simplify reflective access to the bounds.
312 DraggableRect
.LEFT
= 'left';
318 DraggableRect
.RIGHT
= 'right';
324 DraggableRect
.TOP
= 'top';
330 DraggableRect
.BOTTOM
= 'bottom';
336 DraggableRect
.NONE
= 'none';
339 * Obtains the left position.
340 * @return {number} Position.
342 DraggableRect
.prototype.getLeft = function() {
343 return this.bounds_
[DraggableRect
.LEFT
];
347 * Obtains the right position.
348 * @return {number} Position.
350 DraggableRect
.prototype.getRight = function() {
351 return this.bounds_
[DraggableRect
.RIGHT
];
355 * Obtains the top position.
356 * @return {number} Position.
358 DraggableRect
.prototype.getTop = function() {
359 return this.bounds_
[DraggableRect
.TOP
];
363 * Obtains the bottom position.
364 * @return {number} Position.
366 DraggableRect
.prototype.getBottom = function() {
367 return this.bounds_
[DraggableRect
.BOTTOM
];
371 * Obtains the geometry of the rectangle.
372 * @return {ImageRect} Geometry of the rectangle.
374 DraggableRect
.prototype.getRect = function() {
375 return new ImageRect(this.bounds_
);
379 * Obtains the drag mode depending on the coordinate.
381 * @param {number} x X coordinate for cursor.
382 * @param {number} y Y coordinate for cursor.
383 * @param {boolean} touch Whether the operation is done by touch or not.
384 * @return {Object} Drag mode.
386 DraggableRect
.prototype.getDragMode = function(x
, y
, touch
) {
388 xSide
: DraggableRect
.NONE
,
389 ySide
: DraggableRect
.NONE
392 var bounds
= this.bounds_
;
393 var R
= this.viewport_
.screenToImageSize(
394 touch
? ImageEditor
.Mode
.Crop
.TOUCH_GRAB_RADIUS
:
395 ImageEditor
.Mode
.Crop
.MOUSE_GRAB_RADIUS
);
397 var circle
= new Circle(x
, y
, R
);
399 var xBetween
= ImageUtil
.between(bounds
.left
, x
, bounds
.right
);
400 var yBetween
= ImageUtil
.between(bounds
.top
, y
, bounds
.bottom
);
402 if (circle
.inside(bounds
.left
, bounds
.top
)) {
403 result
.xSide
= DraggableRect
.LEFT
;
404 result
.ySide
= DraggableRect
.TOP
;
405 } else if (circle
.inside(bounds
.left
, bounds
.bottom
)) {
406 result
.xSide
= DraggableRect
.LEFT
;
407 result
.ySide
= DraggableRect
.BOTTOM
;
408 } else if (circle
.inside(bounds
.right
, bounds
.top
)) {
409 result
.xSide
= DraggableRect
.RIGHT
;
410 result
.ySide
= DraggableRect
.TOP
;
411 } else if (circle
.inside(bounds
.right
, bounds
.bottom
)) {
412 result
.xSide
= DraggableRect
.RIGHT
;
413 result
.ySide
= DraggableRect
.BOTTOM
;
414 } else if (yBetween
&& Math
.abs(x
- bounds
.left
) <= R
) {
415 result
.xSide
= DraggableRect
.LEFT
;
416 } else if (yBetween
&& Math
.abs(x
- bounds
.right
) <= R
) {
417 result
.xSide
= DraggableRect
.RIGHT
;
418 } else if (xBetween
&& Math
.abs(y
- bounds
.top
) <= R
) {
419 result
.ySide
= DraggableRect
.TOP
;
420 } else if (xBetween
&& Math
.abs(y
- bounds
.bottom
) <= R
) {
421 result
.ySide
= DraggableRect
.BOTTOM
;
422 } else if (xBetween
&& yBetween
) {
425 result
.newcrop
= true;
426 result
.xSide
= DraggableRect
.RIGHT
;
427 result
.ySide
= DraggableRect
.BOTTOM
;
434 * Obtains the cursor style depending on the coordinate.
436 * @param {number} x X coordinate for cursor.
437 * @param {number} y Y coordinate for cursor.
438 * @param {boolean} mouseDown If mouse button is down.
439 * @return {string} Cursor style.
441 DraggableRect
.prototype.getCursorStyle = function(x
, y
, mouseDown
) {
444 mode
= this.dragMode_
;
446 mode
= this.getDragMode(
447 this.viewport_
.screenToImageX(x
), this.viewport_
.screenToImageY(y
));
455 switch (mode
.xSide
) {
456 case 'left': xSymbol
= 'w'; break;
457 case 'right': xSymbol
= 'e'; break;
460 switch (mode
.ySide
) {
461 case 'top': ySymbol
= 'n'; break;
462 case 'bottom': ySymbol
= 's'; break;
464 return ySymbol
+ xSymbol
+ '-resize';
468 * Obtains the drag handler depending on the coordinate.
470 * @param {number} initialScreenX X coordinate for cursor in the screen.
471 * @param {number} initialScreenY Y coordinate for cursor in the screen.
472 * @param {boolean} touch Whether the operation is done by touch or not.
473 * @return {function(number,number,boolean)} Drag handler that takes x
474 * coordinate value, y coordinate value, and shift key flag.
476 DraggableRect
.prototype.getDragHandler = function(
477 initialScreenX
, initialScreenY
, touch
) {
478 // Check if the initial coordinate in the clip rect.
479 var initialX
= this.viewport_
.screenToImageX(initialScreenX
);
480 var initialY
= this.viewport_
.screenToImageY(initialScreenY
);
481 var initialWidth
= this.bounds_
.right
- this.bounds_
.left
;
482 var initialHeight
= this.bounds_
.bottom
- this.bounds_
.top
;
483 var clipRect
= this.viewport_
.screenToImageRect(
484 this.viewport_
.getImageBoundsOnScreenClipped());
485 if (!clipRect
.inside(initialX
, initialY
))
488 // Obtain the drag mode.
489 this.dragMode_
= this.getDragMode(initialX
, initialY
, touch
);
491 if (this.dragMode_
.whole
) {
492 // Calc constant values during the operation.
493 var mouseBiasX
= this.bounds_
.left
- initialX
;
494 var mouseBiasY
= this.bounds_
.top
- initialY
;
495 var maxX
= clipRect
.left
+ clipRect
.width
- initialWidth
;
496 var maxY
= clipRect
.top
+ clipRect
.height
- initialHeight
;
498 // Returns a handler.
499 return function(newScreenX
, newScreenY
) {
500 var newX
= this.viewport_
.screenToImageX(newScreenX
);
501 var newY
= this.viewport_
.screenToImageY(newScreenY
);
502 var clamppedX
= ImageUtil
.clamp(clipRect
.left
, newX
+ mouseBiasX
, maxX
);
503 var clamppedY
= ImageUtil
.clamp(clipRect
.top
, newY
+ mouseBiasY
, maxY
);
504 this.bounds_
.left
= clamppedX
;
505 this.bounds_
.right
= clamppedX
+ initialWidth
;
506 this.bounds_
.top
= clamppedY
;
507 this.bounds_
.bottom
= clamppedY
+ initialHeight
;
510 // Calc constant values during the operation.
511 var mouseBiasX
= this.bounds_
[this.dragMode_
.xSide
] - initialX
;
512 var mouseBiasY
= this.bounds_
[this.dragMode_
.ySide
] - initialY
;
513 var maxX
= clipRect
.left
+ clipRect
.width
;
514 var maxY
= clipRect
.top
+ clipRect
.height
;
516 // Returns a handler.
517 return function(newScreenX
, newScreenY
, shiftKey
) {
518 var newX
= this.viewport_
.screenToImageX(newScreenX
);
519 var newY
= this.viewport_
.screenToImageY(newScreenY
);
522 if (this.dragMode_
.newcrop
) {
523 this.dragMode_
.newcrop
= false;
524 this.bounds_
.left
= this.bounds_
.right
= initialX
;
525 this.bounds_
.top
= this.bounds_
.bottom
= initialY
;
530 // Update X coordinate.
531 if (this.dragMode_
.xSide
!== DraggableRect
.NONE
) {
532 this.bounds_
[this.dragMode_
.xSide
] =
533 ImageUtil
.clamp(clipRect
.left
, newX
+ mouseBiasX
, maxX
);
534 if (this.bounds_
.left
> this.bounds_
.right
) {
535 var left
= this.bounds_
.left
;
536 var right
= this.bounds_
.right
;
537 this.bounds_
.left
= right
- 1;
538 this.bounds_
.right
= left
+ 1;
539 this.dragMode_
.xSide
=
540 this.dragMode_
.xSide
== 'left' ? 'right' : 'left';
544 // Update Y coordinate.
545 if (this.dragMode_
.ySide
!== DraggableRect
.NONE
) {
546 this.bounds_
[this.dragMode_
.ySide
] =
547 ImageUtil
.clamp(clipRect
.top
, newY
+ mouseBiasY
, maxY
);
548 if (this.bounds_
.top
> this.bounds_
.bottom
) {
549 var top
= this.bounds_
.top
;
550 var bottom
= this.bounds_
.bottom
;
551 this.bounds_
.top
= bottom
- 1;
552 this.bounds_
.bottom
= top
+ 1;
553 this.dragMode_
.ySide
=
554 this.dragMode_
.ySide
=== 'top' ? 'bottom' : 'top';
558 // Update aspect ratio.
559 if (this.fixedAspectRatio
)
560 this.forceAspectRatio(this.fixedAspectRatio
, clipRect
);
562 this.forceAspectRatio(initialWidth
/ initialHeight
, clipRect
);
568 * Obtains double tap action depending on the coordinate.
570 * @param {number} x X coordinate for cursor.
571 * @param {number} y Y coordinate for cursor.
572 * @param {boolean} touch Whether the operation is done by touch or not.
573 * @return {ImageBuffer.DoubleTapAction} Double tap action.
575 DraggableRect
.prototype.getDoubleTapAction = function(x
, y
, touch
) {
576 var clipRect
= this.viewport_
.getImageBoundsOnScreenClipped();
577 if (clipRect
.inside(x
, y
))
578 return ImageBuffer
.DoubleTapAction
.COMMIT
;
580 return ImageBuffer
.DoubleTapAction
.NOTHING
;
584 * Forces the aspect ratio.
586 * @param {number} aspectRatio Aspect ratio.
587 * @param {Object} clipRect Clip rect.
589 DraggableRect
.prototype.forceAspectRatio = function(aspectRatio
, clipRect
) {
590 // Get current rectangle scale.
591 var width
= this.bounds_
.right
- this.bounds_
.left
;
592 var height
= this.bounds_
.bottom
- this.bounds_
.top
;
595 currentScale
= ((width
/ aspectRatio
) + height
) / 2;
596 else if (this.dragMode_
.xSide
=== 'none')
597 currentScale
= height
;
598 else if (this.dragMode_
.ySide
=== 'none')
599 currentScale
= width
/ aspectRatio
;
601 currentScale
= Math
.max(width
/ aspectRatio
, height
);
603 // Get maximum width/height scale.
606 var center
= (this.bounds_
.left
+ this.bounds_
.right
) / 2;
607 var middle
= (this.bounds_
.top
+ this.bounds_
.bottom
) / 2;
608 var xSide
= this.dragMode_
? this.dragMode_
.xSide
: 'none';
609 var ySide
= this.dragMode_
? this.dragMode_
.ySide
: 'none';
612 maxWidth
= this.bounds_
.right
- clipRect
.left
;
615 maxWidth
= clipRect
.left
+ clipRect
.width
- this.bounds_
.left
;
619 clipRect
.left
+ clipRect
.width
- center
,
620 center
- clipRect
.left
) * 2;
625 maxHeight
= this.bounds_
.bottom
- clipRect
.top
;
628 maxHeight
= clipRect
.top
+ clipRect
.height
- this.bounds_
.top
;
631 maxHeight
= Math
.min(
632 clipRect
.top
+ clipRect
.height
- middle
,
633 middle
- clipRect
.top
) * 2;
637 // Obtains target scale.
638 var targetScale
= Math
.min(
640 maxWidth
/ aspectRatio
,
644 var newWidth
= targetScale
* aspectRatio
;
645 var newHeight
= targetScale
;
648 this.bounds_
.left
= this.bounds_
.right
- newWidth
;
651 this.bounds_
.right
= this.bounds_
.left
+ newWidth
;
654 this.bounds_
.left
= center
- newWidth
/ 2;
655 this.bounds_
.right
= center
+ newWidth
/ 2;
660 this.bounds_
.top
= this.bounds_
.bottom
- newHeight
;
663 this.bounds_
.bottom
= this.bounds_
.top
+ newHeight
;
666 this.bounds_
.top
= middle
- newHeight
/ 2;
667 this.bounds_
.bottom
= middle
+ newHeight
/ 2;