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