Upstream version 9.38.198.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 'use strict';
6
7 /**
8  * Crop mode.
9  *
10  * @extends {ImageEditor.Mode}
11  * @constructor
12  */
13 ImageEditor.Mode.Crop = function() {
14   ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
15 };
16
17 ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
18
19 /**
20  * Sets the mode up.
21  * @override
22  */
23 ImageEditor.Mode.Crop.prototype.setUp = function() {
24   ImageEditor.Mode.prototype.setUp.apply(this, arguments);
25
26   var container = this.getImageView().container_;
27   var doc = container.ownerDocument;
28
29   this.domOverlay_ = doc.createElement('div');
30   this.domOverlay_.className = 'crop-overlay';
31   container.appendChild(this.domOverlay_);
32
33   this.shadowTop_ = doc.createElement('div');
34   this.shadowTop_.className = 'shadow';
35   this.domOverlay_.appendChild(this.shadowTop_);
36
37   this.middleBox_ = doc.createElement('div');
38   this.middleBox_.className = 'middle-box';
39   this.domOverlay_.appendChild(this.middleBox_);
40
41   this.shadowLeft_ = doc.createElement('div');
42   this.shadowLeft_.className = 'shadow';
43   this.middleBox_.appendChild(this.shadowLeft_);
44
45   this.cropFrame_ = doc.createElement('div');
46   this.cropFrame_.className = 'crop-frame';
47   this.middleBox_.appendChild(this.cropFrame_);
48
49   this.shadowRight_ = doc.createElement('div');
50   this.shadowRight_.className = 'shadow';
51   this.middleBox_.appendChild(this.shadowRight_);
52
53   this.shadowBottom_ = doc.createElement('div');
54   this.shadowBottom_.className = 'shadow';
55   this.domOverlay_.appendChild(this.shadowBottom_);
56
57   this.toolBar_ = null;
58
59   var cropFrame = this.cropFrame_;
60   function addCropFrame(className) {
61     var div = doc.createElement('div');
62     div.className = className;
63     cropFrame.appendChild(div);
64   }
65
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');
74
75   this.onResizedBound_ = this.onResized_.bind(this);
76   window.addEventListener('resize', this.onResizedBound_);
77
78   this.createDefaultCrop();
79 };
80
81 /**
82  * @override
83  */
84 ImageEditor.Mode.Crop.prototype.createTools = function(toolbar) {
85   var aspects = {
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
90   };
91   for (name in aspects) {
92     toolbar.addButton(
93         name,
94         name,
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;
100           } else {
101             var selectedButtons =
102                 toolbar.element.querySelectorAll('button.selected');
103             for (var i = 0; i < selectedButtons.length; i++) {
104               selectedButtons[i].classList.remove('selected');
105             }
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);
111             this.markUpdated();
112             this.positionDOM();
113             this.toolbar_.element.classList.remove('dimmable');
114             this.toolbar_.element.removeAttribute('dimmed');
115           }
116         }.bind(this, aspects[name]));
117   }
118   this.toolbar_ = toolbar;
119 };
120
121 /**
122  * Handles resizing of the window and updates the crop rectangle.
123  * @private
124  */
125 ImageEditor.Mode.Crop.prototype.onResized_ = function() {
126   this.positionDOM();
127 };
128
129 /**
130  * Resets the mode.
131  */
132 ImageEditor.Mode.Crop.prototype.reset = function() {
133   ImageEditor.Mode.prototype.reset.call(this);
134   this.createDefaultCrop();
135   if (this.toolbar_) {
136     this.toolbar_.element.classList.add('dimmable');
137     this.toolbar_ = null;
138   }
139 };
140
141 /**
142  * Updates the position of DOM elements.
143  */
144 ImageEditor.Mode.Crop.prototype.positionDOM = function() {
145   var screenClipped = this.viewport_.getImageBoundsOnScreenClipped();
146
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));
152
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';
157
158   this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px';
159
160   this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px';
161
162   this.shadowRight_.style.width = screenClipped.left + screenClipped.width -
163       (screenCrop.left + screenCrop.width) + 'px';
164
165   this.shadowBottom_.style.height = screenClipped.top + screenClipped.height -
166       (screenCrop.top + screenCrop.height) + 'px';
167 };
168
169 /**
170  * Removes the overlay elements from the document.
171  */
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;
179 };
180
181 /**
182  * @const
183  * @type {number}
184  */
185 ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
186
187 /**
188  * @const
189  * @type {number}
190  */
191 ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
192
193 /**
194  * Gets command to do the crop depending on the current state.
195  *
196  * @return {Command.Crop} Crop command.
197  */
198 ImageEditor.Mode.Crop.prototype.getCommand = function() {
199   var cropImageRect = this.cropRect_.getRect();
200   return new Command.Crop(cropImageRect);
201 };
202
203 /**
204  * Creates default (initial) crop.
205  */
206 ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
207   var rect = this.getViewport().screenToImageRect(
208       new Rect(this.getViewport().getImageBoundsOnScreenClipped()));
209   rect = rect.inflate(
210       -Math.round(rect.width / 6), -Math.round(rect.height / 6));
211   this.cropRect_ = new DraggableRect(rect, this.getViewport());
212   this.positionDOM();
213 };
214
215 /**
216  * Obtains the cursor style depending on the mouse state.
217  *
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.
222  */
223 ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
224   return this.cropRect_.getCursorStyle(x, y, mouseDown);
225 };
226
227 /**
228  * Obtains handler function depending on the mouse state.
229  *
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
235  *     flag.
236  */
237 ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
238   var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
239   if (!cropDragHandler)
240     return null;
241
242   return function(x, y, shiftKey) {
243     if (this.toolbar_)
244       this.toolbar_.element.classList.add('dimmable');
245     cropDragHandler(x, y, shiftKey);
246     this.markUpdated();
247     this.positionDOM();
248   }.bind(this);
249 };
250
251 /**
252  * Obtains the double tap action depending on the coordinate.
253  *
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.
257  */
258 ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
259   return this.cropRect_.getDoubleTapAction(x, y);
260 };
261
262 /**
263  * A draggable rectangle over the image.
264  *
265  * @param {Rect} rect Initial size of the image.
266  * @param {Viewport} viewport Viewport.
267  * @constructor
268  */
269 function DraggableRect(rect, viewport) {
270   /**
271    * The bounds are not held in a regular rectangle (with width/height).
272    * left/top/right/bottom held instead for convenience.
273    *
274    * @type {{left: number, right: number, top: number, bottom: number}}
275    * @private
276    */
277   this.bounds_ = {};
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;
282
283   /**
284    * Viewport.
285    *
286    * @param {Viewport}
287    * @private
288    */
289   this.viewport_ = viewport;
290
291   /**
292    * Drag mode.
293    *
294    * @type {Object}
295    * @private
296    */
297   this.dragMode_ = null;
298
299   /**
300    * Fixed aspect ratio.
301    * The aspect ratio is not fixed when null.
302    * @type {?number}
303    */
304   this.fixedAspectRatio = null;
305
306   Object.seal(this);
307 }
308
309 // Static members to simplify reflective access to the bounds.
310 /**
311  * @const
312  * @type {string}
313  */
314 DraggableRect.LEFT = 'left';
315
316 /**
317  * @const
318  * @type {string}
319  */
320 DraggableRect.RIGHT = 'right';
321
322 /**
323  * @const
324  * @type {string}
325  */
326 DraggableRect.TOP = 'top';
327
328 /**
329  * @const
330  * @type {string}
331  */
332 DraggableRect.BOTTOM = 'bottom';
333
334 /**
335  * @const
336  * @type {string}
337  */
338 DraggableRect.NONE = 'none';
339
340 /**
341  * Obtains the left position.
342  * @return {number} Position.
343  */
344 DraggableRect.prototype.getLeft = function() {
345   return this.bounds_[DraggableRect.LEFT];
346 };
347
348 /**
349  * Obtains the right position.
350  * @return {number} Position.
351  */
352 DraggableRect.prototype.getRight = function() {
353   return this.bounds_[DraggableRect.RIGHT];
354 };
355
356 /**
357  * Obtains the top position.
358  * @return {number} Position.
359  */
360 DraggableRect.prototype.getTop = function() {
361   return this.bounds_[DraggableRect.TOP];
362 };
363
364 /**
365  * Obtains the bottom position.
366  * @return {number} Position.
367  */
368 DraggableRect.prototype.getBottom = function() {
369   return this.bounds_[DraggableRect.BOTTOM];
370 };
371
372 /**
373  * Obtains the geometry of the rectangle.
374  * @return {Rect} Geometry of the rectangle.
375  */
376 DraggableRect.prototype.getRect = function() {
377   return new Rect(this.bounds_);
378 };
379
380 /**
381  * Obtains the drag mode depending on the coordinate.
382  *
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.
387  */
388 DraggableRect.prototype.getDragMode = function(x, y, touch) {
389   var result = {
390     xSide: DraggableRect.NONE,
391     ySide: DraggableRect.NONE
392   };
393
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);
398
399   var circle = new Circle(x, y, R);
400
401   var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
402   var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
403
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) {
425     result.whole = true;
426   } else {
427     result.newcrop = true;
428     result.xSide = DraggableRect.RIGHT;
429     result.ySide = DraggableRect.BOTTOM;
430   }
431
432   return result;
433 };
434
435 /**
436  * Obtains the cursor style depending on the coordinate.
437  *
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.
442  */
443 DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
444   var mode;
445   if (mouseDown) {
446     mode = this.dragMode_;
447   } else {
448     mode = this.getDragMode(
449         this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
450   }
451   if (mode.whole)
452     return 'move';
453   if (mode.newcrop)
454     return 'crop';
455
456   var xSymbol = '';
457   switch (mode.xSide) {
458     case 'left': xSymbol = 'w'; break;
459     case 'right': xSymbol = 'e'; break;
460   }
461   var ySymbol = '';
462   switch (mode.ySide) {
463     case 'top': ySymbol = 'n'; break;
464     case 'bottom': ySymbol = 's'; break;
465   }
466   return ySymbol + xSymbol + '-resize';
467 };
468
469 /**
470  * Obtains the drag handler depending on the coordinate.
471  *
472  * @param {number} startScreenX X coordinate for cursor in the screen.
473  * @param {number} startScreenY 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.
477  */
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))
488     return null;
489
490   // Obtain the drag mode.
491   this.dragMode_ = this.getDragMode(initialX, initialY, touch);
492
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;
499
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;
510     }.bind(this);
511   } else {
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;
517
518     // Returns a handler.
519     return function(newScreenX, newScreenY, shiftKey) {
520       var newX = this.viewport_.screenToImageX(newScreenX);
521       var newY = this.viewport_.screenToImageY(newScreenY);
522
523       // Check new crop.
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;
528         mouseBiasX = 0;
529         mouseBiasY = 0;
530       }
531
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';
543         }
544       }
545
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';
557         }
558       }
559
560       // Update aspect ratio.
561       if (this.fixedAspectRatio)
562         this.forceAspectRatio(this.fixedAspectRatio, clipRect);
563       else if (shiftKey)
564         this.forceAspectRatio(initialWidth / initialHeight, clipRect);
565     }.bind(this);
566   }
567 };
568
569 /**
570  * Obtains double tap action depending on the coordinate.
571  *
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.
576  */
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;
581   else
582     return ImageBuffer.DoubleTapAction.NOTHING;
583 };
584
585 /**
586  * Forces the aspect ratio.
587  *
588  * @param {number} aspectRatio Aspect ratio.
589  * @param {Object} clipRect Clip rect.
590  */
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;
595   var currentScale;
596   if (!this.dragMode_)
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;
602   else
603     currentScale = Math.max(width / aspectRatio, height);
604
605   // Get maximum width/height scale.
606   var maxWidth;
607   var maxHeight;
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';
612   switch (xSide) {
613     case 'left':
614       maxWidth = this.bounds_.right - clipRect.left;
615       break;
616     case 'right':
617       maxWidth = clipRect.left + clipRect.width - this.bounds_.left;
618       break;
619     case 'none':
620       maxWidth = Math.min(
621           clipRect.left + clipRect.width - center,
622           center - clipRect.left) * 2;
623       break;
624   }
625   switch (ySide) {
626     case 'top':
627       maxHeight = this.bounds_.bottom - clipRect.top;
628       break;
629     case 'bottom':
630       maxHeight = clipRect.top + clipRect.height - this.bounds_.top;
631       break;
632     case 'none':
633       maxHeight = Math.min(
634           clipRect.top + clipRect.height - middle,
635           middle - clipRect.top) * 2;
636       break;
637   }
638
639   // Obtains target scale.
640   var targetScale = Math.min(
641       currentScale,
642       maxWidth / aspectRatio,
643       maxHeight);
644
645   // Update bounds.
646   var newWidth = targetScale * aspectRatio;
647   var newHeight = targetScale;
648   switch (xSide) {
649     case 'left':
650       this.bounds_.left = this.bounds_.right - newWidth;
651       break;
652     case 'right':
653       this.bounds_.right = this.bounds_.left + newWidth;
654       break;
655     case 'none':
656       this.bounds_.left = center - newWidth / 2;
657       this.bounds_.right = center + newWidth / 2;
658       break;
659   }
660   switch (ySide) {
661     case 'top':
662       this.bounds_.top = this.bounds_.bottom - newHeight;
663       break;
664     case 'bottom':
665       this.bounds_.bottom = this.bounds_.top + newHeight;
666       break;
667     case 'none':
668       this.bounds_.top = middle - newHeight / 2;
669       this.bounds_.bottom = middle + newHeight / 2;
670       break;
671   }
672 };