Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / image_editor / image_transform.js
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.
4
5 /**
6  * Crop mode.
7  *
8  * @extends {ImageEditor.Mode}
9  * @constructor
10  */
11 ImageEditor.Mode.Crop = function() {
12   ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
13 };
14
15 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
16
17 /**
18  * Sets the mode up.
19  * @override
20  */
21 ImageEditor.Mode.Crop.prototype.setUp = function() {
22   ImageEditor.Mode.prototype.setUp.apply(this, arguments);
23
24   var container = this.getImageView().container_;
25   var doc = container.ownerDocument;
26
27   this.domOverlay_ = doc.createElement('div');
28   this.domOverlay_.className = 'crop-overlay';
29   container.appendChild(this.domOverlay_);
30
31   this.shadowTop_ = doc.createElement('div');
32   this.shadowTop_.className = 'shadow';
33   this.domOverlay_.appendChild(this.shadowTop_);
34
35   this.middleBox_ = doc.createElement('div');
36   this.middleBox_.className = 'middle-box';
37   this.domOverlay_.appendChild(this.middleBox_);
38
39   this.shadowLeft_ = doc.createElement('div');
40   this.shadowLeft_.className = 'shadow';
41   this.middleBox_.appendChild(this.shadowLeft_);
42
43   this.cropFrame_ = doc.createElement('div');
44   this.cropFrame_.className = 'crop-frame';
45   this.middleBox_.appendChild(this.cropFrame_);
46
47   this.shadowRight_ = doc.createElement('div');
48   this.shadowRight_.className = 'shadow';
49   this.middleBox_.appendChild(this.shadowRight_);
50
51   this.shadowBottom_ = doc.createElement('div');
52   this.shadowBottom_.className = 'shadow';
53   this.domOverlay_.appendChild(this.shadowBottom_);
54
55   this.toolBar_ = null;
56
57   var cropFrame = this.cropFrame_;
58   function addCropFrame(className) {
59     var div = doc.createElement('div');
60     div.className = className;
61     cropFrame.appendChild(div);
62   }
63
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');
72
73   this.onResizedBound_ = this.onResized_.bind(this);
74   window.addEventListener('resize', this.onResizedBound_);
75
76   this.createDefaultCrop();
77 };
78
79 /**
80  * @override
81  */
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
88   };
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');
103             }
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');
113           }
114         }.bind(this, aspects[name]));
115   }
116   this.toolbar_ = toolbar;
117 };
118
119 /**
120  * Handles resizing of the window and updates the crop rectangle.
121  * @private
122  */
123 ImageEditor.Mode.Crop.prototype.onResized_ = function() {
124   this.positionDOM();
125 };
126
127 /**
128  * Resets the mode.
129  */
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;
136   }
137 };
138
139 /**
140  * Updates the position of DOM elements.
141  */
142 ImageEditor.Mode.Crop.prototype.positionDOM = function() {
143   var screenClipped = this.viewport_.getImageBoundsOnScreenClipped();
144
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));
150
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';
155
156   this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px';
157
158   this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px';
159
160   this.shadowRight_.style.width = screenClipped.left + screenClipped.width -
161       (screenCrop.left + screenCrop.width) + 'px';
162
163   this.shadowBottom_.style.height = screenClipped.top + screenClipped.height -
164       (screenCrop.top + screenCrop.height) + 'px';
165 };
166
167 /**
168  * Removes the overlay elements from the document.
169  */
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;
177 };
178
179 /**
180  * @const
181  * @type {number}
182  */
183 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
184
185 /**
186  * @const
187  * @type {number}
188  */
189 ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
190
191 /**
192  * Gets command to do the crop depending on the current state.
193  *
194  * @return {Command.Crop} Crop command.
195  */
196 ImageEditor.Mode.Crop.prototype.getCommand = function() {
197   var cropImageRect = this.cropRect_.getRect();
198   return new Command.Crop(cropImageRect);
199 };
200
201 /**
202  * Creates default (initial) crop.
203  */
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();
211 };
212
213 /**
214  * Obtains the cursor style depending on the mouse state.
215  *
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.
220  */
221 ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
222   return this.cropRect_.getCursorStyle(x, y, mouseDown);
223 };
224
225 /**
226  * Obtains handler function depending on the mouse state.
227  *
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.
234  */
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;
239
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);
247 };
248
249 /**
250  * Obtains the double tap action depending on the coordinate.
251  *
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.
255  */
256 ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
257   return this.cropRect_.getDoubleTapAction(x, y);
258 };
259
260 /**
261  * A draggable rectangle over the image.
262  *
263  * @param {ImageRect} rect Initial size of the image.
264  * @param {Viewport} viewport Viewport.
265  * @constructor
266  */
267 function DraggableRect(rect, viewport) {
268   /**
269    * The bounds are not held in a regular rectangle (with width/height).
270    * left/top/right/bottom held instead for convenience.
271    *
272    * @type {{left: number, right: number, top: number, bottom: number}}
273    * @private
274    */
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;
280
281   /**
282    * Viewport.
283    *
284    * @type {Viewport}
285    * @private
286    */
287   this.viewport_ = viewport;
288
289   /**
290    * Drag mode.
291    *
292    * @type {Object}
293    * @private
294    */
295   this.dragMode_ = null;
296
297   /**
298    * Fixed aspect ratio.
299    * The aspect ratio is not fixed when null.
300    * @type {?number}
301    */
302   this.fixedAspectRatio = null;
303
304   Object.seal(this);
305 }
306
307 // Static members to simplify reflective access to the bounds.
308 /**
309  * @const
310  * @type {string}
311  */
312 DraggableRect.LEFT = 'left';
313
314 /**
315  * @const
316  * @type {string}
317  */
318 DraggableRect.RIGHT = 'right';
319
320 /**
321  * @const
322  * @type {string}
323  */
324 DraggableRect.TOP = 'top';
325
326 /**
327  * @const
328  * @type {string}
329  */
330 DraggableRect.BOTTOM = 'bottom';
331
332 /**
333  * @const
334  * @type {string}
335  */
336 DraggableRect.NONE = 'none';
337
338 /**
339  * Obtains the left position.
340  * @return {number} Position.
341  */
342 DraggableRect.prototype.getLeft = function() {
343   return this.bounds_[DraggableRect.LEFT];
344 };
345
346 /**
347  * Obtains the right position.
348  * @return {number} Position.
349  */
350 DraggableRect.prototype.getRight = function() {
351   return this.bounds_[DraggableRect.RIGHT];
352 };
353
354 /**
355  * Obtains the top position.
356  * @return {number} Position.
357  */
358 DraggableRect.prototype.getTop = function() {
359   return this.bounds_[DraggableRect.TOP];
360 };
361
362 /**
363  * Obtains the bottom position.
364  * @return {number} Position.
365  */
366 DraggableRect.prototype.getBottom = function() {
367   return this.bounds_[DraggableRect.BOTTOM];
368 };
369
370 /**
371  * Obtains the geometry of the rectangle.
372  * @return {ImageRect} Geometry of the rectangle.
373  */
374 DraggableRect.prototype.getRect = function() {
375   return new ImageRect(this.bounds_);
376 };
377
378 /**
379  * Obtains the drag mode depending on the coordinate.
380  *
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.
385  */
386 DraggableRect.prototype.getDragMode = function(x, y, touch) {
387   var result = {
388     xSide: DraggableRect.NONE,
389     ySide: DraggableRect.NONE
390   };
391
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);
396
397   var circle = new Circle(x, y, R);
398
399   var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
400   var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
401
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;
428   }
429
430   return result;
431 };
432
433 /**
434  * Obtains the cursor style depending on the coordinate.
435  *
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.
440  */
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));
448   }
449   if (mode.whole)
450     return 'move';
451   if (mode.newcrop)
452     return 'crop';
453
454   var xSymbol = '';
455   switch (mode.xSide) {
456     case 'left': xSymbol = 'w'; break;
457     case 'right': xSymbol = 'e'; break;
458   }
459   var ySymbol = '';
460   switch (mode.ySide) {
461     case 'top': ySymbol = 'n'; break;
462     case 'bottom': ySymbol = 's'; break;
463   }
464   return ySymbol + xSymbol + '-resize';
465 };
466
467 /**
468  * Obtains the drag handler depending on the coordinate.
469  *
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.
475  */
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;
487
488   // Obtain the drag mode.
489   this.dragMode_ = this.getDragMode(initialX, initialY, touch);
490
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;
497
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;
515
516     // Returns a handler.
517     return function(newScreenX, newScreenY, shiftKey) {
518       var newX = this.viewport_.screenToImageX(newScreenX);
519       var newY = this.viewport_.screenToImageY(newScreenY);
520
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;
528       }
529
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';
541         }
542       }
543
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';
555         }
556       }
557
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);
564   }
565 };
566
567 /**
568  * Obtains double tap action depending on the coordinate.
569  *
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.
574  */
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;
581 };
582
583 /**
584  * Forces the aspect ratio.
585  *
586  * @param {number} aspectRatio Aspect ratio.
587  * @param {Object} clipRect Clip rect.
588  */
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);
602
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;
622   }
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;
635   }
636
637   // Obtains target scale.
638   var targetScale = Math.min(
639       currentScale,
640       maxWidth / aspectRatio,
641       maxHeight);
642
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;
657   }
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;
669   }
670 };