Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / image_transform.js
blobda2ec750f423d6119f6e4cf3d4d1eb1112fea0b9
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.
8 * @extends {ImageEditor.Mode}
9 * @constructor
11 ImageEditor.Mode.Crop = function() {
12 ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
15 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
17 /**
18 * Sets the mode up.
19 * @override
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_);
55 this.toolBar_ = null;
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();
79 /**
80 * @override
82 ImageEditor.Mode.Crop.prototype.createTools = function(toolbar) {
83 var aspects = {
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) {
90 toolbar.addButton(
91 name,
92 name,
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;
98 } else {
99 var selectedButtons =
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);
109 this.markUpdated();
110 this.positionDOM();
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.
121 * @private
123 ImageEditor.Mode.Crop.prototype.onResized_ = function() {
124 this.positionDOM();
128 * Resets the mode.
130 ImageEditor.Mode.Crop.prototype.reset = function() {
131 ImageEditor.Mode.prototype.reset.call(this);
132 this.createDefaultCrop();
133 if (this.toolbar_) {
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;
180 * @const
181 * @type {number}
183 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
186 * @const
187 * @type {number}
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()));
207 rect = rect.inflate(
208 -Math.round(rect.width / 6), -Math.round(rect.height / 6));
209 this.cropRect_ = new DraggableRect(rect, this.getViewport());
210 this.positionDOM();
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
233 * flag.
235 ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
236 var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
237 if (!cropDragHandler)
238 return null;
240 return function(x, y, shiftKey) {
241 if (this.toolbar_)
242 this.toolbar_.element.classList.add('dimmable');
243 cropDragHandler(x, y, shiftKey);
244 this.markUpdated();
245 this.positionDOM();
246 }.bind(this);
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.
265 * @constructor
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}}
273 * @private
275 this.bounds_ = {};
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;
282 * Viewport.
284 * @type {Viewport}
285 * @private
287 this.viewport_ = viewport;
290 * Drag mode.
292 * @type {Object}
293 * @private
295 this.dragMode_ = null;
298 * Fixed aspect ratio.
299 * The aspect ratio is not fixed when null.
300 * @type {?number}
302 this.fixedAspectRatio = null;
304 Object.seal(this);
307 // Static members to simplify reflective access to the bounds.
309 * @const
310 * @type {string}
312 DraggableRect.LEFT = 'left';
315 * @const
316 * @type {string}
318 DraggableRect.RIGHT = 'right';
321 * @const
322 * @type {string}
324 DraggableRect.TOP = 'top';
327 * @const
328 * @type {string}
330 DraggableRect.BOTTOM = 'bottom';
333 * @const
334 * @type {string}
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) {
387 var result = {
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) {
423 result.whole = true;
424 } else {
425 result.newcrop = true;
426 result.xSide = DraggableRect.RIGHT;
427 result.ySide = DraggableRect.BOTTOM;
430 return result;
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) {
442 var mode;
443 if (mouseDown) {
444 mode = this.dragMode_;
445 } else {
446 mode = this.getDragMode(
447 this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
449 if (mode.whole)
450 return 'move';
451 if (mode.newcrop)
452 return 'crop';
454 var xSymbol = '';
455 switch (mode.xSide) {
456 case 'left': xSymbol = 'w'; break;
457 case 'right': xSymbol = 'e'; break;
459 var ySymbol = '';
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))
486 return null;
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;
508 }.bind(this);
509 } else {
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);
521 // Check new crop.
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;
526 mouseBiasX = 0;
527 mouseBiasY = 0;
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);
561 else if (shiftKey)
562 this.forceAspectRatio(initialWidth / initialHeight, clipRect);
563 }.bind(this);
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;
579 else
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;
593 var currentScale;
594 if (!this.dragMode_)
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;
600 else
601 currentScale = Math.max(width / aspectRatio, height);
603 // Get maximum width/height scale.
604 var maxWidth;
605 var maxHeight;
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';
610 switch (xSide) {
611 case 'left':
612 maxWidth = this.bounds_.right - clipRect.left;
613 break;
614 case 'right':
615 maxWidth = clipRect.left + clipRect.width - this.bounds_.left;
616 break;
617 case 'none':
618 maxWidth = Math.min(
619 clipRect.left + clipRect.width - center,
620 center - clipRect.left) * 2;
621 break;
623 switch (ySide) {
624 case 'top':
625 maxHeight = this.bounds_.bottom - clipRect.top;
626 break;
627 case 'bottom':
628 maxHeight = clipRect.top + clipRect.height - this.bounds_.top;
629 break;
630 case 'none':
631 maxHeight = Math.min(
632 clipRect.top + clipRect.height - middle,
633 middle - clipRect.top) * 2;
634 break;
637 // Obtains target scale.
638 var targetScale = Math.min(
639 currentScale,
640 maxWidth / aspectRatio,
641 maxHeight);
643 // Update bounds.
644 var newWidth = targetScale * aspectRatio;
645 var newHeight = targetScale;
646 switch (xSide) {
647 case 'left':
648 this.bounds_.left = this.bounds_.right - newWidth;
649 break;
650 case 'right':
651 this.bounds_.right = this.bounds_.left + newWidth;
652 break;
653 case 'none':
654 this.bounds_.left = center - newWidth / 2;
655 this.bounds_.right = center + newWidth / 2;
656 break;
658 switch (ySide) {
659 case 'top':
660 this.bounds_.top = this.bounds_.bottom - newHeight;
661 break;
662 case 'bottom':
663 this.bounds_.bottom = this.bounds_.top + newHeight;
664 break;
665 case 'none':
666 this.bounds_.top = middle - newHeight / 2;
667 this.bounds_.bottom = middle + newHeight / 2;
668 break;