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 * @constructor
10 */
11ImageEditor.Mode.Crop = function() {
12  ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
13};
14
15ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
16
17/**
18 * TODO(JSDOC).
19 */
20ImageEditor.Mode.Crop.prototype.setUp = function() {
21  ImageEditor.Mode.prototype.setUp.apply(this, arguments);
22
23  var container = this.getImageView().container_;
24  var doc = container.ownerDocument;
25
26  this.domOverlay_ = doc.createElement('div');
27  this.domOverlay_.className = 'crop-overlay';
28  container.appendChild(this.domOverlay_);
29
30  this.shadowTop_ = doc.createElement('div');
31  this.shadowTop_.className = 'shadow';
32  this.domOverlay_.appendChild(this.shadowTop_);
33
34  this.middleBox_ = doc.createElement('div');
35  this.middleBox_.className = 'middle-box';
36  this.domOverlay_.appendChild(this.middleBox_);
37
38  this.shadowLeft_ = doc.createElement('div');
39  this.shadowLeft_.className = 'shadow';
40  this.middleBox_.appendChild(this.shadowLeft_);
41
42  this.cropFrame_ = doc.createElement('div');
43  this.cropFrame_.className = 'crop-frame';
44  this.middleBox_.appendChild(this.cropFrame_);
45
46  this.shadowRight_ = doc.createElement('div');
47  this.shadowRight_.className = 'shadow';
48  this.middleBox_.appendChild(this.shadowRight_);
49
50  this.shadowBottom_ = doc.createElement('div');
51  this.shadowBottom_.className = 'shadow';
52  this.domOverlay_.appendChild(this.shadowBottom_);
53
54  var cropFrame = this.cropFrame_;
55  function addCropFrame(className) {
56    var div = doc.createElement('div');
57    div.className = className;
58    cropFrame.appendChild(div);
59  }
60
61  addCropFrame('left top corner');
62  addCropFrame('top horizontal');
63  addCropFrame('right top corner');
64  addCropFrame('left vertical');
65  addCropFrame('right vertical');
66  addCropFrame('left bottom corner');
67  addCropFrame('bottom horizontal');
68  addCropFrame('right bottom corner');
69
70  this.onResizedBound_ = this.onResized_.bind(this);
71  window.addEventListener('resize', this.onResizedBound_);
72
73  this.createDefaultCrop();
74};
75
76/**
77 * Handles resizing of the window and updates the crop rectangle.
78 * @private
79 */
80ImageEditor.Mode.Crop.prototype.onResized_ = function() {
81  this.positionDOM();
82};
83
84/**
85 * TODO(JSDOC).
86 */
87ImageEditor.Mode.Crop.prototype.reset = function() {
88  ImageEditor.Mode.prototype.reset.call(this);
89  this.createDefaultCrop();
90};
91
92/**
93 * TODO(JSDOC).
94 */
95ImageEditor.Mode.Crop.prototype.positionDOM = function() {
96  var screenClipped = this.viewport_.getScreenClipped();
97
98  var screenCrop = this.viewport_.imageToScreenRect(this.cropRect_.getRect());
99  var delta = ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS;
100  this.editor_.hideOverlappingTools(
101      screenCrop.inflate(delta, delta),
102      screenCrop.inflate(-delta, -delta));
103
104  this.domOverlay_.style.left = screenClipped.left + 'px';
105  this.domOverlay_.style.top = screenClipped.top + 'px';
106  this.domOverlay_.style.width = screenClipped.width + 'px';
107  this.domOverlay_.style.height = screenClipped.height + 'px';
108
109  this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px';
110
111  this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px';
112
113  this.shadowRight_.style.width = screenClipped.left + screenClipped.width -
114      (screenCrop.left + screenCrop.width) + 'px';
115
116  this.shadowBottom_.style.height = screenClipped.top + screenClipped.height -
117      (screenCrop.top + screenCrop.height) + 'px';
118};
119
120/**
121 * TODO(JSDOC).
122 */
123ImageEditor.Mode.Crop.prototype.cleanUpUI = function() {
124  ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments);
125  this.domOverlay_.parentNode.removeChild(this.domOverlay_);
126  this.domOverlay_ = null;
127  this.editor_.hideOverlappingTools();
128  window.removeEventListener(this.onResizedBound_);
129  this.onResizedBound_ = null;
130};
131
132/**
133 * @const
134 * @type {number}
135 */
136ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
137/**
138 * @const
139 * @type {number}
140 */
141ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
142
143/**
144 * TODO(JSDOC).
145 * @return {Command.Crop}  // TODO(JSDOC).
146 */
147ImageEditor.Mode.Crop.prototype.getCommand = function() {
148  var cropImageRect = this.cropRect_.getRect();
149  return new Command.Crop(cropImageRect);
150};
151
152/**
153 * TODO(JSDOC).
154 */
155ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
156  var rect = new Rect(this.getViewport().getImageClipped());
157  rect = rect.inflate(
158      -Math.round(rect.width / 6), -Math.round(rect.height / 6));
159  this.cropRect_ = new DraggableRect(rect, this.getViewport());
160  this.positionDOM();
161};
162
163/**
164 * TODO(JSDOC).
165 * @param {number} x X coordinate for cursor.
166 * @param {number} y Y coordinate for cursor.
167 * @param {boolean} mouseDown If mouse button is down.
168 * @return {string} A value for style.cursor CSS property.
169 */
170ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
171  return this.cropRect_.getCursorStyle(x, y, mouseDown);
172};
173
174/**
175 * TODO(JSDOC).
176 * @param {number} x Event X coordinate.
177 * @param {number} y Event Y coordinate.
178 * @param {boolean} touch True if it's a touch event, false if mouse.
179 * @return {function(number,number)} A function to be called on mouse drag.
180 */
181ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
182  var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
183  if (!cropDragHandler) return null;
184
185  var self = this;
186  return function(x, y) {
187    cropDragHandler(x, y);
188    self.markUpdated();
189    self.positionDOM();
190  };
191};
192
193/**
194 * TODO(JSDOC).
195 * @param {number} x X coordinate of the event.
196 * @param {number} y Y coordinate of the event.
197 * @return {ImageBuffer.DoubleTapAction} Action to perform as result.
198 */
199ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
200  return this.cropRect_.getDoubleTapAction(x, y);
201};
202
203/*
204 * A draggable rectangle over the image.
205 * @param {Rect} rect  // TODO(JSDOC).
206 * @param {Viewport} viewport  // TODO(JSDOC).
207 * @constructor
208 */
209function DraggableRect(rect, viewport) {
210  // The bounds are not held in a regular rectangle (with width/height).
211  // left/top/right/bottom held instead for convenience.
212  this.bounds_ = {};
213  this.bounds_[DraggableRect.LEFT] = rect.left;
214  this.bounds_[DraggableRect.RIGHT] = rect.left + rect.width;
215  this.bounds_[DraggableRect.TOP] = rect.top;
216  this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height;
217
218  this.viewport_ = viewport;
219
220  this.oppositeSide_ = {};
221  this.oppositeSide_[DraggableRect.LEFT] = DraggableRect.RIGHT;
222  this.oppositeSide_[DraggableRect.RIGHT] = DraggableRect.LEFT;
223  this.oppositeSide_[DraggableRect.TOP] = DraggableRect.BOTTOM;
224  this.oppositeSide_[DraggableRect.BOTTOM] = DraggableRect.TOP;
225
226  // Translation table to form CSS-compatible cursor style.
227  this.cssSide_ = {};
228  this.cssSide_[DraggableRect.LEFT] = 'w';
229  this.cssSide_[DraggableRect.TOP] = 'n';
230  this.cssSide_[DraggableRect.RIGHT] = 'e';
231  this.cssSide_[DraggableRect.BOTTOM] = 's';
232  this.cssSide_[DraggableRect.NONE] = '';
233}
234
235// Static members to simplify reflective access to the bounds.
236/**
237 * @const
238 * @type {string}
239 */
240DraggableRect.LEFT = 'left';
241/**
242 * @const
243 * @type {string}
244 */
245DraggableRect.RIGHT = 'right';
246/**
247 * @const
248 * @type {string}
249 */
250DraggableRect.TOP = 'top';
251/**
252 * @const
253 * @type {string}
254 */
255DraggableRect.BOTTOM = 'bottom';
256/**
257 * @const
258 * @type {string}
259 */
260DraggableRect.NONE = 'none';
261
262/**
263 * TODO(JSDOC)
264 * @return {number}  // TODO(JSDOC).
265 */
266DraggableRect.prototype.getLeft = function() {
267  return this.bounds_[DraggableRect.LEFT];
268};
269
270/**
271 * TODO(JSDOC)
272 * @return {number}  // TODO(JSDOC).
273 */
274DraggableRect.prototype.getRight = function() {
275  return this.bounds_[DraggableRect.RIGHT];
276};
277
278/**
279 * TODO(JSDOC)
280 * @return {number}  // TODO(JSDOC).
281 */
282DraggableRect.prototype.getTop = function() {
283  return this.bounds_[DraggableRect.TOP];
284};
285
286/**
287 * TODO(JSDOC)
288 * @return {number}  // TODO(JSDOC).
289 */
290DraggableRect.prototype.getBottom = function() {
291  return this.bounds_[DraggableRect.BOTTOM];
292};
293
294/**
295 * TODO(JSDOC)
296 * @return {Rect}  // TODO(JSDOC).
297 */
298DraggableRect.prototype.getRect = function() {
299  return new Rect(this.bounds_);
300};
301
302/**
303 * TODO(JSDOC)
304 * @param {number} x X coordinate for cursor.
305 * @param {number} y Y coordinate for cursor.
306 * @param {boolean} touch  // TODO(JSDOC).
307 * @return {Object}  // TODO(JSDOC).
308 */
309DraggableRect.prototype.getDragMode = function(x, y, touch) {
310  var result = {
311    xSide: DraggableRect.NONE,
312    ySide: DraggableRect.NONE
313  };
314
315  var bounds = this.bounds_;
316  var R = this.viewport_.screenToImageSize(
317      touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS :
318              ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
319
320  var circle = new Circle(x, y, R);
321
322  var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
323  var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
324
325  if (circle.inside(bounds.left, bounds.top)) {
326    result.xSide = DraggableRect.LEFT;
327    result.ySide = DraggableRect.TOP;
328  } else if (circle.inside(bounds.left, bounds.bottom)) {
329    result.xSide = DraggableRect.LEFT;
330    result.ySide = DraggableRect.BOTTOM;
331  } else if (circle.inside(bounds.right, bounds.top)) {
332    result.xSide = DraggableRect.RIGHT;
333    result.ySide = DraggableRect.TOP;
334  } else if (circle.inside(bounds.right, bounds.bottom)) {
335    result.xSide = DraggableRect.RIGHT;
336    result.ySide = DraggableRect.BOTTOM;
337  } else if (yBetween && Math.abs(x - bounds.left) <= R) {
338    result.xSide = DraggableRect.LEFT;
339  } else if (yBetween && Math.abs(x - bounds.right) <= R) {
340    result.xSide = DraggableRect.RIGHT;
341  } else if (xBetween && Math.abs(y - bounds.top) <= R) {
342    result.ySide = DraggableRect.TOP;
343  } else if (xBetween && Math.abs(y - bounds.bottom) <= R) {
344    result.ySide = DraggableRect.BOTTOM;
345  } else if (xBetween && yBetween) {
346    result.whole = true;
347  } else {
348    result.newcrop = true;
349    result.xSide = DraggableRect.RIGHT;
350    result.ySide = DraggableRect.BOTTOM;
351  }
352
353  return result;
354};
355
356/**
357 * TODO(JSDOC)
358 * @param {number} x X coordinate for cursor.
359 * @param {number} y Y coordinate for cursor.
360 * @param {boolean} mouseDown  If mouse button is down.
361 * @return {string}  // TODO(JSDOC).
362 */
363DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
364  var mode;
365  if (mouseDown) {
366    mode = this.dragMode_;
367  } else {
368    mode = this.getDragMode(
369        this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
370  }
371  if (mode.whole) return 'move';
372  if (mode.newcrop) return 'crop';
373  return this.cssSide_[mode.ySide] + this.cssSide_[mode.xSide] + '-resize';
374};
375
376/**
377 * TODO(JSDOC)
378 * @param {number} x X coordinate for cursor.
379 * @param {number} y Y coordinate for cursor.
380 * @param {boolean} touch  // TODO(JSDOC).
381 * @return {function(number,number)}  // TODO(JSDOC).
382 */
383DraggableRect.prototype.getDragHandler = function(x, y, touch) {
384  x = this.viewport_.screenToImageX(x);
385  y = this.viewport_.screenToImageY(y);
386
387  var clipRect = this.viewport_.getImageClipped();
388  if (!clipRect.inside(x, y)) return null;
389
390  this.dragMode_ = this.getDragMode(x, y, touch);
391
392  var self = this;
393
394  var mouseBiasX;
395  var mouseBiasY;
396
397  var fixedWidth = 0;
398  var fixedHeight = 0;
399
400  var resizeFuncX;
401  var resizeFuncY;
402
403  if (this.dragMode_.whole) {
404    mouseBiasX = this.bounds_.left - x;
405    fixedWidth = this.bounds_.right - this.bounds_.left;
406    resizeFuncX = function(x) {
407      self.bounds_.left = x;
408      self.bounds_.right = self.bounds_.left + fixedWidth;
409    };
410    mouseBiasY = this.bounds_.top - y;
411    fixedHeight = this.bounds_.bottom - this.bounds_.top;
412    resizeFuncY = function(y) {
413      self.bounds_.top = y;
414      self.bounds_.bottom = self.bounds_.top + fixedHeight;
415    };
416  } else {
417    var checkNewCrop = function() {
418      if (self.dragMode_.newcrop) {
419        self.dragMode_.newcrop = false;
420        self.bounds_.left = self.bounds_.right = x;
421        self.bounds_.top = self.bounds_.bottom = y;
422        mouseBiasX = 0;
423        mouseBiasY = 0;
424      }
425    };
426
427    var flipSide = function(side) {
428      var opposite = self.oppositeSide_[side];
429      var temp = self.bounds_[side];
430      self.bounds_[side] = self.bounds_[opposite];
431      self.bounds_[opposite] = temp;
432      return opposite;
433    };
434
435    if (this.dragMode_.xSide != DraggableRect.NONE) {
436      mouseBiasX = self.bounds_[this.dragMode_.xSide] - x;
437      resizeFuncX = function(x) {
438        checkNewCrop();
439        self.bounds_[self.dragMode_.xSide] = x;
440        if (self.bounds_.left > self.bounds_.right) {
441          self.dragMode_.xSide = flipSide(self.dragMode_.xSide);
442        }
443      };
444    }
445    if (this.dragMode_.ySide != DraggableRect.NONE) {
446      mouseBiasY = self.bounds_[this.dragMode_.ySide] - y;
447      resizeFuncY = function(y) {
448        checkNewCrop();
449        self.bounds_[self.dragMode_.ySide] = y;
450        if (self.bounds_.top > self.bounds_.bottom) {
451          self.dragMode_.ySide = flipSide(self.dragMode_.ySide);
452        }
453      };
454    }
455  }
456
457  function convertX(x) {
458    return ImageUtil.clamp(
459        clipRect.left,
460        self.viewport_.screenToImageX(x) + mouseBiasX,
461        clipRect.left + clipRect.width - fixedWidth);
462  }
463
464  function convertY(y) {
465    return ImageUtil.clamp(
466        clipRect.top,
467        self.viewport_.screenToImageY(y) + mouseBiasY,
468        clipRect.top + clipRect.height - fixedHeight);
469  }
470
471  return function(x, y) {
472    if (resizeFuncX) resizeFuncX(convertX(x));
473    if (resizeFuncY) resizeFuncY(convertY(y));
474  };
475};
476
477/**
478 * TODO(JSDOC)
479 * @param {number} x X coordinate for cursor.
480 * @param {number} y Y coordinate for cursor.
481 * @param {boolean} touch  // TODO(JSDOC).
482 * @return {ImageBuffer.DoubleTapAction}  // TODO(JSDOC).
483 */
484DraggableRect.prototype.getDoubleTapAction = function(x, y, touch) {
485  x = this.viewport_.screenToImageX(x);
486  y = this.viewport_.screenToImageY(y);
487
488  var clipRect = this.viewport_.getImageClipped();
489  if (clipRect.inside(x, y))
490    return ImageBuffer.DoubleTapAction.COMMIT;
491  else
492    return ImageBuffer.DoubleTapAction.NOTHING;
493};
494