1/*! 2 * Cropper v2.3.4 3 * https://github.com/fengyuanchen/cropper 4 * 5 * Copyright (c) 2014-2016 Fengyuan Chen and contributors 6 * Released under the MIT license 7 * 8 * Date: 2016-09-03T05:50:45.412Z 9 */ 10 11(function (factory) { 12 if (typeof define === 'function' && define.amd) { 13 // AMD. Register as anonymous module. 14 define(['jquery'], factory); 15 } else if (typeof exports === 'object') { 16 // Node / CommonJS 17 factory(require('jquery')); 18 } else { 19 // Browser globals. 20 factory(jQuery); 21 } 22})(function ($) { 23 24 'use strict'; 25 26 // Globals 27 var $window = $(window); 28 var $document = $(document); 29 var location = window.location; 30 var navigator = window.navigator; 31 var ArrayBuffer = window.ArrayBuffer; 32 var Uint8Array = window.Uint8Array; 33 var DataView = window.DataView; 34 var btoa = window.btoa; 35 36 // Constants 37 var NAMESPACE = 'cropper'; 38 39 // Classes 40 var CLASS_MODAL = 'cropper-modal'; 41 var CLASS_HIDE = 'cropper-hide'; 42 var CLASS_HIDDEN = 'cropper-hidden'; 43 var CLASS_INVISIBLE = 'cropper-invisible'; 44 var CLASS_MOVE = 'cropper-move'; 45 var CLASS_CROP = 'cropper-crop'; 46 var CLASS_DISABLED = 'cropper-disabled'; 47 var CLASS_BG = 'cropper-bg'; 48 49 // Events 50 var EVENT_MOUSE_DOWN = 'mousedown touchstart pointerdown MSPointerDown'; 51 var EVENT_MOUSE_MOVE = 'mousemove touchmove pointermove MSPointerMove'; 52 var EVENT_MOUSE_UP = 'mouseup touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel'; 53 var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll'; 54 var EVENT_DBLCLICK = 'dblclick'; 55 var EVENT_LOAD = 'load.' + NAMESPACE; 56 var EVENT_ERROR = 'error.' + NAMESPACE; 57 var EVENT_RESIZE = 'resize.' + NAMESPACE; // Bind to window with namespace 58 var EVENT_BUILD = 'build.' + NAMESPACE; 59 var EVENT_BUILT = 'built.' + NAMESPACE; 60 var EVENT_CROP_START = 'cropstart.' + NAMESPACE; 61 var EVENT_CROP_MOVE = 'cropmove.' + NAMESPACE; 62 var EVENT_CROP_END = 'cropend.' + NAMESPACE; 63 var EVENT_CROP = 'crop.' + NAMESPACE; 64 var EVENT_ZOOM = 'zoom.' + NAMESPACE; 65 66 // RegExps 67 var REGEXP_ACTIONS = /^(e|w|s|n|se|sw|ne|nw|all|crop|move|zoom)$/; 68 var REGEXP_DATA_URL = /^data:/; 69 var REGEXP_DATA_URL_HEAD = /^data:([^;]+);base64,/; 70 var REGEXP_DATA_URL_JPEG = /^data:image\/jpeg.*;base64,/; 71 72 // Data keys 73 var DATA_PREVIEW = 'preview'; 74 var DATA_ACTION = 'action'; 75 76 // Actions 77 var ACTION_EAST = 'e'; 78 var ACTION_WEST = 'w'; 79 var ACTION_SOUTH = 's'; 80 var ACTION_NORTH = 'n'; 81 var ACTION_SOUTH_EAST = 'se'; 82 var ACTION_SOUTH_WEST = 'sw'; 83 var ACTION_NORTH_EAST = 'ne'; 84 var ACTION_NORTH_WEST = 'nw'; 85 var ACTION_ALL = 'all'; 86 var ACTION_CROP = 'crop'; 87 var ACTION_MOVE = 'move'; 88 var ACTION_ZOOM = 'zoom'; 89 var ACTION_NONE = 'none'; 90 91 // Supports 92 var SUPPORT_CANVAS = $.isFunction($('<canvas>')[0].getContext); 93 var IS_SAFARI_OR_UIWEBVIEW = navigator && /(Macintosh|iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent); 94 95 // Maths 96 var num = Number; 97 var min = Math.min; 98 var max = Math.max; 99 var abs = Math.abs; 100 var sin = Math.sin; 101 var cos = Math.cos; 102 var sqrt = Math.sqrt; 103 var round = Math.round; 104 var floor = Math.floor; 105 106 // Utilities 107 var fromCharCode = String.fromCharCode; 108 109 function isNumber(n) { 110 return typeof n === 'number' && !isNaN(n); 111 } 112 113 function isUndefined(n) { 114 return typeof n === 'undefined'; 115 } 116 117 function toArray(obj, offset) { 118 var args = []; 119 120 // This is necessary for IE8 121 if (isNumber(offset)) { 122 args.push(offset); 123 } 124 125 return args.slice.apply(obj, args); 126 } 127 128 // Custom proxy to avoid jQuery's guid 129 function proxy(fn, context) { 130 var args = toArray(arguments, 2); 131 132 return function () { 133 return fn.apply(context, args.concat(toArray(arguments))); 134 }; 135 } 136 137 function isCrossOriginURL(url) { 138 var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i); 139 140 return parts && ( 141 parts[1] !== location.protocol || 142 parts[2] !== location.hostname || 143 parts[3] !== location.port 144 ); 145 } 146 147 function addTimestamp(url) { 148 var timestamp = 'timestamp=' + (new Date()).getTime(); 149 150 return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp); 151 } 152 153 function getCrossOrigin(crossOrigin) { 154 return crossOrigin ? ' crossOrigin="' + crossOrigin + '"' : ''; 155 } 156 157 function getImageSize(image, callback) { 158 var newImage; 159 160 // Modern browsers (ignore Safari, #120 & #509)
161 if (image.naturalWidth && !IS_SAFARI_OR_UIWEBVIEW) { 162 return callback(image.naturalWidth, image.naturalHeight); 163 } 164 165 // IE8: Don't use `new Image()` here (#319) 166 newImage = document.createElement('img'); 167 168 newImage.onload = function () { 169 callback(this.width, this.height); 170 }; 171 172 newImage.src = image.src; 173 } 174 175 function getTransform(options) { 176 var transforms = []; 177 var rotate = options.rotate; 178 var scaleX = options.scaleX; 179 var scaleY = options.scaleY; 180 181 // Rotate should come first before scale to match orientation transform 182 if (isNumber(rotate) && rotate !== 0) { 183 transforms.push('rotate(' + rotate + 'deg)'); 184 } 185 186 if (isNumber(scaleX) && scaleX !== 1) { 187 transforms.push('scaleX(' + scaleX + ')'); 188 } 189 190 if (isNumber(scaleY) && scaleY !== 1) { 191 transforms.push('scaleY(' + scaleY + ')'); 192 } 193 194 return transforms.length ? transforms.join(' ') : 'none'; 195 } 196 197 function getRotatedSizes(data, isReversed) { 198 var deg = abs(data.degree) % 180; 199 var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180; 200 var sinArc = sin(arc); 201 var cosArc = cos(arc); 202 var width = data.width; 203 var height = data.height; 204 var aspectRatio = data.aspectRatio; 205 var newWidth; 206 var newHeight; 207 208 if (!isReversed) { 209 newWidth = width * cosArc + height * sinArc; 210 newHeight = width * sinArc + height * cosArc; 211 } else { 212 newWidth = width / (cosArc + sinArc / aspectRatio); 213 newHeight = newWidth / aspectRatio; 214 } 215 216 return { 217 width: newWidth, 218 height: newHeight 219 }; 220 } 221 222 function getSourceCanvas(image, data) { 223 var canvas = $('<canvas>')[0]; 224 var context = canvas.getContext('2d'); 225 var dstX = 0; 226 var dstY = 0; 227 var dstWidth = data.naturalWidth; 228 var dstHeight = data.naturalHeight; 229 var rotate = data.rotate; 230 var scaleX = data.scaleX; 231 var scaleY = data.scaleY; 232 var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1); 233 var rotatable = isNumber(rotate) && rotate !== 0; 234 var advanced = rotatable || scalable; 235 var canvasWidth = dstWidth * abs(scaleX || 1); 236 var canvasHeight = dstHeight * abs(scaleY || 1); 237 var translateX; 238 var translateY; 239 var rotated; 240 241 if (scalable) { 242 translateX = canvasWidth / 2; 243 translateY = canvasHeight / 2; 244 } 245 246 if (rotatable) { 247 rotated = getRotatedSizes({ 248 width: canvasWidth, 249 height: canvasHeight, 250 degree: rotate 251 }); 252 253 canvasWidth = rotated.width; 254 canvasHeight = rotated.height; 255 translateX = canvasWidth / 2; 256 translateY = canvasHeight / 2; 257 } 258 259 canvas.width = canvasWidth; 260 canvas.height = canvasHeight; 261 262 if (advanced) { 263 dstX = -dstWidth / 2; 264 dstY = -dstHeight / 2; 265 266 context.save(); 267 context.translate(translateX, translateY); 268 } 269 270 // Rotate should come first before scale as in the "getTransform" function 271 if (rotatable) { 272 context.rotate(rotate * Math.PI / 180); 273 } 274 275 if (scalable) { 276 context.scale(scaleX, scaleY); 277 } 278 279 context.drawImage(image, floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight)); 280 281 if (advanced) { 282 context.restore(); 283 } 284 285 return canvas; 286 } 287 288 function getTouchesCenter(touches) { 289 var length = touches.length; 290 var pageX = 0; 291 var pageY = 0; 292 293 if (length) { 294 $.each(touches, function (i, touch) { 295 pageX += touch.pageX; 296 pageY += touch.pageY; 297 }); 298 299 pageX /= length; 300 pageY /= length; 301 } 302 303 return { 304 pageX: pageX, 305 pageY: pageY 306 }; 307 } 308 309 function getStringFromCharCode(dataView, start, length) { 310 var str = ''; 311 var i; 312 313 for (i = start, length += start; i < length; i++) { 314 str += fromCharCode(dataView.getUint8(i)); 315 } 316 317 return str; 318 } 319 320 function getOrientation(arrayBuffer) { 321 var dataView = new DataView(arrayBuffer); 322 var length = dataView.byteLength; 323 var orientation; 324 var exifIDCode; 325 var tiffOffset; 326 var firstIFDOffset; 327 var littleEndian; 328 var endianness; 329 var app1Start; 330 var ifdStart; 331 var offset; 332 var i; 333 334 // Only handle JPEG image (start by 0xFFD8) 335 if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) { 336 offset = 2; 337 338 while (offset < length) { 339 if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) { 340 app1Start = offset; 341 break; 342 } 343 344 offset++; 345 } 346 } 347 348 if (app1Start) { 349 exifIDCode = app1Start + 4; 350 tiffOffset = app1Start + 10; 351 352 if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') { 353 endianness = dataView.getUint16(tiffOffset); 354 littleEndian = endianness === 0x4949; 355 356 if (littleEndian || endianness === 0x4D4D /* bigEndian */) { 357 if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) { 358 firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian); 359 360 if (firstIFDOffset >= 0x00000008) { 361 ifdStart = tiffOffset + firstIFDOffset; 362 } 363 } 364 } 365 } 366 } 367 368 if (ifdStart) { 369 length = dataView.getUint16(ifdStart, littleEndian); 370 371 for (i = 0; i < length; i++) { 372 offset = ifdStart + i * 12 + 2; 373 374 if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) { 375 376 // 8 is the offset of the current tag's value 377 offset += 8; 378 379 // Get the original orientation value 380 orientation = dataView.getUint16(offset, littleEndian); 381 382 // Override the orientation with its default value for Safari (#120) 383 if (IS_SAFARI_OR_UIWEBVIEW) { 384 dataView.setUint16(offset, 1, littleEndian); 385 } 386 387 break; 388 } 389 } 390 } 391 392 return orientation; 393 } 394 395 function dataURLToArrayBuffer(dataURL) { 396 var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, ''); 397 var binary = atob(base64); 398 var length = binary.length; 399 var arrayBuffer = new ArrayBuffer(length); 400 var dataView = new Uint8Array(arrayBuffer); 401 var i; 402 403 for (i = 0; i < length; i++) {
404 dataView[i] = binary.charCodeAt(i); 405 } 406 407 return arrayBuffer; 408 } 409 410 // Only available for JPEG image 411 function arrayBufferToDataURL(arrayBuffer) { 412 var dataView = new Uint8Array(arrayBuffer); 413 var length = dataView.length; 414 var base64 = ''; 415 var i; 416 417 for (i = 0; i < length; i++) { 418 base64 += fromCharCode(dataView[i]); 419 } 420 421 return 'data:image/jpeg;base64,' + btoa(base64); 422 } 423 424 function Cropper(element, options) { 425 this.$element = $(element); 426 this.options = $.extend({}, Cropper.DEFAULTS, $.isPlainObject(options) && options); 427 this.isLoaded = false; 428 this.isBuilt = false; 429 this.isCompleted = false; 430 this.isRotated = false; 431 this.isCropped = false; 432 this.isDisabled = false; 433 this.isReplaced = false; 434 this.isLimited = false; 435 this.wheeling = false; 436 this.isImg = false; 437 this.originalUrl = ''; 438 this.canvas = null; 439 this.cropBox = null; 440 this.init(); 441 } 442 443 Cropper.prototype = { 444 constructor: Cropper, 445 446 init: function () { 447 var $this = this.$element; 448 var url; 449 450 if ($this.is('img')) { 451 this.isImg = true; 452 453 // Should use `$.fn.attr` here. e.g.: "img/picture.jpg" 454 this.originalUrl = url = $this.attr('src'); 455 456 // Stop when it's a blank image 457 if (!url) { 458 return; 459 } 460 461 // Should use `$.fn.prop` here. e.g.: "http://example.com/img/picture.jpg" 462 url = $this.prop('src'); 463 } else if ($this.is('canvas') && SUPPORT_CANVAS) { 464 url = $this[0].toDataURL(); 465 } 466 467 this.load(url); 468 }, 469 470 // A shortcut for triggering custom events 471 trigger: function (type, data) { 472 var e = $.Event(type, data); 473 474 this.$element.trigger(e); 475 476 return e; 477 }, 478 479 load: function (url) { 480 var options = this.options; 481 var $this = this.$element; 482 var read; 483 var xhr; 484 485 if (!url) { 486 return; 487 } 488 489 // Trigger build event first 490 $this.one(EVENT_BUILD, options.build); 491 492 if (this.trigger(EVENT_BUILD).isDefaultPrevented()) { 493 return; 494 } 495 496 this.url = url; 497 this.image = {}; 498 499 if (!options.checkOrientation || !ArrayBuffer) { 500 return this.clone(); 501 } 502 503 read = $.proxy(this.read, this); 504 505 // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari 506 if (REGEXP_DATA_URL.test(url)) { 507 return REGEXP_DATA_URL_JPEG.test(url) ? 508 read(dataURLToArrayBuffer(url)) : 509 this.clone(); 510 } 511 512 xhr = new XMLHttpRequest(); 513 514 xhr.onerror = xhr.onabort = $.proxy(function () { 515 this.clone(); 516 }, this); 517 518 xhr.onload = function () { 519 read(this.response); 520 }; 521 522 if (options.checkCrossOrigin && isCrossOriginURL(url) && $this.prop('crossOrigin')) { 523 url = addTimestamp(url); 524 } 525 526 xhr.open('get', url); 527 xhr.responseType = 'arraybuffer'; 528 xhr.send(); 529 }, 530 531 read: function (arrayBuffer) { 532 var options = this.options; 533 var orientation = getOrientation(arrayBuffer); 534 var image = this.image; 535 var rotate = 0; 536 var scaleX = 1; 537 var scaleY = 1; 538 539 if (orientation > 1) { 540 this.url = arrayBufferToDataURL(arrayBuffer); 541 542 switch (orientation) { 543 544 // flip horizontal 545 case 2: 546 scaleX = -1; 547 break; 548 549 // rotate left 180° 550 case 3: 551 rotate = -180; 552 break; 553 554 // flip vertical 555 case 4: 556 scaleY = -1; 557 break; 558 559 // flip vertical + rotate right 90° 560 case 5: 561 rotate = 90; 562 scaleY = -1; 563 break; 564 565 // rotate right 90° 566 case 6: 567 rotate = 90; 568 break; 569 570 // flip horizontal + rotate right 90° 571 case 7: 572 rotate = 90; 573 scaleX = -1; 574 break; 575 576 // rotate left 90° 577 case 8: 578 rotate = -90; 579 break; 580 } 581 } 582 583 if (options.rotatable) { 584 image.rotate = rotate; 585 } 586 587 if (options.scalable) { 588 image.scaleX = scaleX; 589 image.scaleY = scaleY; 590 } 591 592 this.clone(); 593 }, 594 595 clone: function () { 596 var options = this.options; 597 var $this = this.$element; 598 var url = this.url; 599 var crossOrigin = ''; 600 var crossOriginUrl; 601 var $clone; 602 603 if (options.checkCrossOrigin && isCrossOriginURL(url)) { 604 crossOrigin = $this.prop('crossOrigin'); 605 606 if (crossOrigin) { 607 crossOriginUrl = url; 608 } else { 609 crossOrigin = 'anonymous'; 610 611 // Bust cache (#148) when there is not a "crossOrigin" property 612 crossOriginUrl = addTimestamp(url); 613 } 614 } 615 616 this.crossOrigin = crossOrigin; 617 this.crossOriginUrl = crossOriginUrl; 618 this.$clone = $clone = $('<img' + getCrossOrigin(crossOrigin) + ' src="' + (crossOriginUrl || url) + '">'); 619 620 if (this.isImg) { 621 if ($this[0].complete) { 622 this.start(); 623 } else { 624 $this.one(EVENT_LOAD, $.proxy(this.start, this)); 625 } 626 } else { 627 $clone. 628 one(EVENT_LOAD, $.proxy(this.start, this)). 629 one(EVENT_ERROR, $.proxy(this.stop, this)). 630 addClass(CLASS_HIDE). 631 insertAfter($this); 632 } 633 }, 634 635 start: function () { 636 var $image = this.$element; 637 var $clone = this.$clone; 638 639 if (!this.isImg) { 640 $clone.off(EVENT_ERROR, this.stop); 641 $image = $clone; 642 } 643
644 getImageSize($image[0], $.proxy(function (naturalWidth, naturalHeight) { 645 $.extend(this.image, { 646 naturalWidth: naturalWidth, 647 naturalHeight: naturalHeight, 648 aspectRatio: naturalWidth / naturalHeight 649 }); 650 651 this.isLoaded = true; 652 this.build(); 653 }, this)); 654 }, 655 656 stop: function () { 657 this.$clone.remove(); 658 this.$clone = null; 659 }, 660 661 build: function () { 662 var options = this.options; 663 var $this = this.$element; 664 var $clone = this.$clone; 665 var $cropper; 666 var $cropBox; 667 var $face; 668 669 if (!this.isLoaded) { 670 return; 671 } 672 673 // Unbuild first when replace 674 if (this.isBuilt) { 675 this.unbuild(); 676 } 677 678 // Create cropper elements 679 this.$container = $this.parent(); 680 this.$cropper = $cropper = $(Cropper.TEMPLATE); 681 this.$canvas = $cropper.find('.cropper-canvas').append($clone); 682 this.$dragBox = $cropper.find('.cropper-drag-box'); 683 this.$cropBox = $cropBox = $cropper.find('.cropper-crop-box'); 684 this.$viewBox = $cropper.find('.cropper-view-box'); 685 this.$face = $face = $cropBox.find('.cropper-face'); 686 687 // Hide the original image 688 $this.addClass(CLASS_HIDDEN).after($cropper); 689 690 // Show the clone image if is hidden 691 if (!this.isImg) { 692 $clone.removeClass(CLASS_HIDE); 693 } 694 695 this.initPreview(); 696 this.bind(); 697 698 options.aspectRatio = max(0, options.aspectRatio) || NaN; 699 options.viewMode = max(0, min(3, round(options.viewMode))) || 0; 700 701 if (options.autoCrop) { 702 this.isCropped = true; 703 704 if (options.modal) { 705 this.$dragBox.addClass(CLASS_MODAL); 706 } 707 } else { 708 $cropBox.addClass(CLASS_HIDDEN); 709 } 710 711 if (!options.guides) { 712 $cropBox.find('.cropper-dashed').addClass(CLASS_HIDDEN); 713 } 714 715 if (!options.center) { 716 $cropBox.find('.cropper-center').addClass(CLASS_HIDDEN); 717 } 718 719 if (options.cropBoxMovable) { 720 $face.addClass(CLASS_MOVE).data(DATA_ACTION, ACTION_ALL); 721 } 722 723 if (!options.highlight) { 724 $face.addClass(CLASS_INVISIBLE); 725 } 726 727 if (options.background) { 728 $cropper.addClass(CLASS_BG); 729 } 730 731 if (!options.cropBoxResizable) { 732 $cropBox.find('.cropper-line, .cropper-point').addClass(CLASS_HIDDEN); 733 } 734 735 this.setDragMode(options.dragMode); 736 this.render(); 737 this.isBuilt = true; 738 this.setData(options.data); 739 $this.one(EVENT_BUILT, options.built); 740 741 // Trigger the built event asynchronously to keep `data('cropper')` is defined 742 this.completing = setTimeout($.proxy(function () { 743 this.trigger(EVENT_BUILT); 744 this.trigger(EVENT_CROP, this.getData()); 745 this.isCompleted = true; 746 }, this), 0); 747 }, 748 749 unbuild: function () { 750 if (!this.isBuilt) { 751 return; 752 } 753 754 if (!this.isCompleted) { 755 clearTimeout(this.completing); 756 } 757 758 this.isBuilt = false; 759 this.isCompleted = false; 760 this.initialImage = null; 761 762 // Clear `initialCanvas` is necessary when replace 763 this.initialCanvas = null; 764 this.initialCropBox = null; 765 this.container = null; 766 this.canvas = null; 767 768 // Clear `cropBox` is necessary when replace 769 this.cropBox = null; 770 this.unbind(); 771 772 this.resetPreview(); 773 this.$preview = null; 774 775 this.$viewBox = null; 776 this.$cropBox = null; 777 this.$dragBox = null; 778 this.$canvas = null; 779 this.$container = null; 780 781 this.$cropper.remove(); 782 this.$cropper = null; 783 }, 784 785 render: function () { 786 this.initContainer(); 787 this.initCanvas(); 788 this.initCropBox(); 789 790 this.renderCanvas(); 791 792 if (this.isCropped) { 793 this.renderCropBox(); 794 } 795 }, 796 797 initContainer: function () { 798 var options = this.options; 799 var $this = this.$element; 800 var $container = this.$container; 801 var $cropper = this.$cropper; 802 803 $cropper.addClass(CLASS_HIDDEN); 804 $this.removeClass(CLASS_HIDDEN); 805 806 $cropper.css((this.container = { 807 width: max($container.width(), num(options.minContainerWidth) || 200), 808 height: max($container.height(), num(options.minContainerHeight) || 100) 809 })); 810 811 $this.addClass(CLASS_HIDDEN); 812 $cropper.removeClass(CLASS_HIDDEN); 813 }, 814 815 // Canvas (image wrapper)
816 initCanvas: function () { 817 var viewMode = this.options.viewMode; 818 var container = this.container; 819 var containerWidth = container.width; 820 var containerHeight = container.height; 821 var image = this.image; 822 var imageNaturalWidth = image.naturalWidth; 823 var imageNaturalHeight = image.naturalHeight; 824 var is90Degree = abs(image.rotate) === 90; 825 var naturalWidth = is90Degree ? imageNaturalHeight : imageNaturalWidth; 826 var naturalHeight = is90Degree ? imageNaturalWidth : imageNaturalHeight; 827 var aspectRatio = naturalWidth / naturalHeight; 828 var canvasWidth = containerWidth; 829 var canvasHeight = containerHeight; 830 var canvas; 831 832 if (containerHeight * aspectRatio > containerWidth) { 833 if (viewMode === 3) { 834 canvasWidth = containerHeight * aspectRatio; 835 } else { 836 canvasHeight = containerWidth / aspectRatio; 837 } 838 } else { 839 if (viewMode === 3) { 840 canvasHeight = containerWidth / aspectRatio; 841 } else { 842 canvasWidth = containerHeight * aspectRatio; 843 } 844 } 845 846 canvas = { 847 naturalWidth: naturalWidth, 848 naturalHeight: naturalHeight, 849 aspectRatio: aspectRatio, 850 width: canvasWidth, 851 height: canvasHeight 852 }; 853 854 canvas.oldLeft = canvas.left = (containerWidth - canvasWidth) / 2; 855 canvas.oldTop = canvas.top = (containerHeight - canvasHeight) / 2; 856 857 this.canvas = canvas; 858 this.isLimited = (viewMode === 1 || viewMode === 2); 859 this.limitCanvas(true, true); 860 this.initialImage = $.extend({}, image); 861 this.initialCanvas = $.extend({}, canvas); 862 }, 863 864 limitCanvas: function (isSizeLimited, isPositionLimited) { 865 var options = this.options; 866 var viewMode = options.viewMode; 867 var container = this.container; 868 var containerWidth = container.width; 869 var containerHeight = container.height; 870 var canvas = this.canvas; 871 var aspectRatio = canvas.aspectRatio; 872 var cropBox = this.cropBox; 873 var isCropped = this.isCropped && cropBox; 874 var minCanvasWidth; 875 var minCanvasHeight; 876 var newCanvasLeft; 877 var newCanvasTop; 878 879 if (isSizeLimited) { 880 minCanvasWidth = num(options.minCanvasWidth) || 0; 881 minCanvasHeight = num(options.minCanvasHeight) || 0; 882 883 if (viewMode) { 884 if (viewMode > 1) { 885 minCanvasWidth = max(minCanvasWidth, containerWidth); 886 minCanvasHeight = max(minCanvasHeight, containerHeight); 887 888 if (viewMode === 3) { 889 if (minCanvasHeight * aspectRatio > minCanvasWidth) { 890 minCanvasWidth = minCanvasHeight * aspectRatio; 891 } else { 892 minCanvasHeight = minCanvasWidth / aspectRatio; 893 } 894 } 895 } else { 896 if (minCanvasWidth) { 897 minCanvasWidth = max(minCanvasWidth, isCropped ? cropBox.width : 0); 898 } else if (minCanvasHeight) { 899 minCanvasHeight = max(minCanvasHeight, isCropped ? cropBox.height : 0); 900 } else if (isCropped) { 901 minCanvasWidth = cropBox.width; 902 minCanvasHeight = cropBox.height; 903 904 if (minCanvasHeight * aspectRatio > minCanvasWidth) { 905 minCanvasWidth = minCanvasHeight * aspectRatio; 906 } else { 907 minCanvasHeight = minCanvasWidth / aspectRatio; 908 } 909 } 910 } 911 } 912 913 if (minCanvasWidth && minCanvasHeight) { 914 if (minCanvasHeight * aspectRatio > minCanvasWidth) { 915 minCanvasHeight = minCanvasWidth / aspectRatio; 916 } else { 917 minCanvasWidth = minCanvasHeight * aspectRatio; 918 } 919 } else if (minCanvasWidth) { 920 minCanvasHeight = minCanvasWidth / aspectRatio; 921 } else if (minCanvasHeight) { 922 minCanvasWidth = minCanvasHeight * aspectRatio; 923 } 924 925 canvas.minWidth = minCanvasWidth; 926 canvas.minHeight = minCanvasHeight; 927 canvas.maxWidth = Infinity; 928 canvas.maxHeight = Infinity; 929 } 930 931 if (isPositionLimited) { 932 if (viewMode) { 933 newCanvasLeft = containerWidth - canvas.width; 934 newCanvasTop = containerHeight - canvas.height; 935 936 canvas.minLeft = min(0, newCanvasLeft); 937 canvas.minTop = min(0, newCanvasTop); 938 canvas.maxLeft = max(0, newCanvasLeft); 939 canvas.maxTop = max(0, newCanvasTop); 940 941 if (isCropped && this.isLimited) { 942 canvas.minLeft = min( 943 cropBox.left, 944 cropBox.left + cropBox.width - canvas.width 945 ); 946 canvas.minTop = min( 947 cropBox.top, 948 cropBox.top + cropBox.height - canvas.height 949 ); 950 canvas.maxLeft = cropBox.left; 951 canvas.maxTop = cropBox.top; 952 953 if (viewMode === 2) { 954 if (canvas.width >= containerWidth) { 955 canvas.minLeft = min(0, newCanvasLeft); 956 canvas.maxLeft = max(0, newCanvasLeft); 957 } 958 959 if (canvas.height >= containerHeight) { 960 canvas.minTop = min(0, newCanvasTop); 961 canvas.maxTop = max(0, newCanvasTop); 962 } 963 } 964 } 965 } else { 966 canvas.minLeft = -canvas.width; 967 canvas.minTop = -canvas.height; 968 canvas.maxLeft = containerWidth; 969 canvas.maxTop = containerHeight; 970 } 971 } 972 }, 973 974 renderCanvas: function (isChanged) { 975 var canvas = this.canvas; 976 var image = this.image; 977 var rotate = image.rotate;
978 var naturalWidth = image.naturalWidth; 979 var naturalHeight = image.naturalHeight; 980 var aspectRatio; 981 var rotated; 982 983 if (this.isRotated) { 984 this.isRotated = false; 985 986 // Computes rotated sizes with image sizes 987 rotated = getRotatedSizes({ 988 width: image.width, 989 height: image.height, 990 degree: rotate 991 }); 992 993 aspectRatio = rotated.width / rotated.height; 994 995 if (aspectRatio !== canvas.aspectRatio) { 996 canvas.left -= (rotated.width - canvas.width) / 2; 997 canvas.top -= (rotated.height - canvas.height) / 2; 998 canvas.width = rotated.width; 999 canvas.height = rotated.height; 1000 canvas.aspectRatio = aspectRatio; 1001 canvas.naturalWidth = naturalWidth; 1002 canvas.naturalHeight = naturalHeight; 1003 1004 // Computes rotated sizes with natural image sizes 1005 if (rotate % 180) { 1006 rotated = getRotatedSizes({ 1007 width: naturalWidth, 1008 height: naturalHeight, 1009 degree: rotate 1010 }); 1011 1012 canvas.naturalWidth = rotated.width; 1013 canvas.naturalHeight = rotated.height; 1014 } 1015 1016 this.limitCanvas(true, false); 1017 } 1018 } 1019 1020 if (canvas.width > canvas.maxWidth || canvas.width < canvas.minWidth) { 1021 canvas.left = canvas.oldLeft; 1022 } 1023 1024 if (canvas.height > canvas.maxHeight || canvas.height < canvas.minHeight) { 1025 canvas.top = canvas.oldTop; 1026 } 1027 1028 canvas.width = min(max(canvas.width, canvas.minWidth), canvas.maxWidth); 1029 canvas.height = min(max(canvas.height, canvas.minHeight), canvas.maxHeight); 1030 1031 this.limitCanvas(false, true); 1032 1033 canvas.oldLeft = canvas.left = min(max(canvas.left, canvas.minLeft), canvas.maxLeft); 1034 canvas.oldTop = canvas.top = min(max(canvas.top, canvas.minTop), canvas.maxTop); 1035 1036 this.$canvas.css({ 1037 width: canvas.width, 1038 height: canvas.height, 1039 left: canvas.left, 1040 top: canvas.top 1041 }); 1042 1043 this.renderImage(); 1044 1045 if (this.isCropped && this.isLimited) { 1046 this.limitCropBox(true, true); 1047 } 1048 1049 if (isChanged) { 1050 this.output(); 1051 } 1052 }, 1053 1054 renderImage: function (isChanged) { 1055 var canvas = this.canvas; 1056 var image = this.image; 1057 var reversed; 1058 1059 if (image.rotate) { 1060 reversed = getRotatedSizes({ 1061 width: canvas.width, 1062 height: canvas.height, 1063 degree: image.rotate, 1064 aspectRatio: image.aspectRatio 1065 }, true); 1066 } 1067 1068 $.extend(image, reversed ? { 1069 width: reversed.width, 1070 height: reversed.height, 1071 left: (canvas.width - reversed.width) / 2, 1072 top: (canvas.height - reversed.height) / 2 1073 } : { 1074 width: canvas.width, 1075 height: canvas.height, 1076 left: 0, 1077 top: 0 1078 }); 1079 1080 this.$clone.css({ 1081 width: image.width, 1082 height: image.height, 1083 marginLeft: image.left, 1084 marginTop: image.top, 1085 transform: getTransform(image) 1086 }); 1087 1088 if (isChanged) { 1089 this.output(); 1090 } 1091 }, 1092 1093 initCropBox: function () { 1094 var options = this.options; 1095 var canvas = this.canvas; 1096 var aspectRatio = options.aspectRatio; 1097 var autoCropArea = num(options.autoCropArea) || 0.8; 1098 var cropBox = { 1099 width: canvas.width, 1100 height: canvas.height 1101 }; 1102 1103 if (aspectRatio) { 1104 if (canvas.height * aspectRatio > canvas.width) { 1105 cropBox.height = cropBox.width / aspectRatio; 1106 } else { 1107 cropBox.width = cropBox.height * aspectRatio; 1108 } 1109 } 1110 1111 this.cropBox = cropBox; 1112 this.limitCropBox(true, true); 1113 1114 // Initialize auto crop area 1115 cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth); 1116 cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight); 1117 1118 // The width of auto crop area must large than "minWidth", and the height too. (#164) 1119 cropBox.width = max(cropBox.minWidth, cropBox.width * autoCropArea); 1120 cropBox.height = max(cropBox.minHeight, cropBox.height * autoCropArea); 1121 cropBox.oldLeft = cropBox.left = canvas.left + (canvas.width - cropBox.width) / 2; 1122 cropBox.oldTop = cropBox.top = canvas.top + (canvas.height - cropBox.height) / 2; 1123 1124 this.initialCropBox = $.extend({}, cropBox); 1125 }, 1126 1127 limitCropBox: function (isSizeLimited, isPositionLimited) { 1128 var options = this.options; 1129 var aspectRatio = options.aspectRatio; 1130 var container = this.container; 1131 var containerWidth = container.width; 1132 var containerHeight = container.height; 1133 var canvas = this.canvas; 1134 var cropBox = this.cropBox; 1135 var isLimited = this.isLimited; 1136 var minCropBoxWidth; 1137 var minCropBoxHeight; 1138 var maxCropBoxWidth; 1139 var maxCropBoxHeight; 1140 1141 if (isSizeLimited) { 1142 minCropBoxWidth = num(options.minCropBoxWidth) || 0; 1143 minCropBoxHeight = num(options.minCropBoxHeight) || 0; 1144 1145 // The min/maxCropBoxWidth/Height must be less than containerWidth/Height 1146 minCropBoxWidth = min(minCropBoxWidth, containerWidth); 1147 minCropBoxHeight = min(minCropBoxHeight, containerHeight); 1148 maxCropBoxWidth = min(containerWidth, isLimited ? canvas.width : containerWidth); 1149 maxCropBoxHeight = min(containerHeight, isLimited ? canvas.height : containerHeight); 1150 1151 if (aspectRatio) { 1152 if (minCropBoxWidth && minCropBoxHeight) { 1153 if (minCropBoxHeight * aspectRatio > minCropBoxWidth) { 1154 minCropBoxHeight = minCropBoxWidth / aspectRatio; 1155 } else { 1156 minCropBoxWidth = minCropBoxHeight * aspectRatio; 1157 } 1158 } else if (minCropBoxWidth) { 1159 minCropBoxHeight = minCropBoxWidth / aspectRatio; 1160 } else if (minCropBoxHeight) { 1161 minCropBoxWidth = minCropBoxHeight * aspectRatio; 1162 } 1163 1164 if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) { 1165 maxCropBoxHeight = maxCropBoxWidth / aspectRatio; 1166 } else { 1167 maxCropBoxWidth = maxCropBoxHeight * aspectRatio; 1168 } 1169 } 1170 1171 // The minWidth/Height must be less than maxWidth/Height 1172 cropBox.minWidth = min(minCropBoxWidth, maxCropBoxWidth); 1173 cropBox.minHeight = min(minCropBoxHeight, maxCropBoxHeight); 1174 cropBox.maxWidth = maxCropBoxWidth; 1175 cropBox.maxHeight = maxCropBoxHeight; 1176 } 1177 1178 if (isPositionLimited) { 1179 if (isLimited) { 1180 cropBox.minLeft = max(0, canvas.left); 1181 cropBox.minTop = max(0, canvas.top); 1182 cropBox.maxLeft = min(containerWidth, canvas.left + canvas.width) - cropBox.width; 1183 cropBox.maxTop = min(containerHeight, canvas.top + canvas.height) - cropBox.height; 1184 } else { 1185 cropBox.minLeft = 0; 1186 cropBox.minTop = 0; 1187 cropBox.maxLeft = containerWidth - cropBox.width; 1188 cropBox.maxTop = containerHeight - cropBox.height; 1189 } 1190 } 1191 }, 1192 1193 renderCropBox: function () { 1194 var options = this.options; 1195 var container = this.container; 1196 var containerWidth = container.width; 1197 var containerHeight = container.height; 1198 var cropBox = this.cropBox; 1199 1200 if (cropBox.width > cropBox.maxWidth || cropBox.width < cropBox.minWidth) { 1201 cropBox.left = cropBox.oldLeft; 1202 } 1203 1204 if (cropBox.height > cropBox.maxHeight || cropBox.height < cropBox.minHeight) { 1205 cropBox.top = cropBox.oldTop; 1206 } 1207 1208 cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth); 1209 cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight); 1210 1211 this.limitCropBox(false, true); 1212 1213 cropBox.oldLeft = cropBox.left = min(max(cropBox.left, cropBox.minLeft), cropBox.maxLeft); 1214 cropBox.oldTop = cropBox.top = min(max(cropBox.top, cropBox.minTop), cropBox.maxTop); 1215 1216 if (options.movable && options.cropBoxMovable) { 1217 1218 // Turn to move the canvas when the crop box is equal to the container 1219 this.$face.data(DATA_ACTION, (cropBox.width === containerWidth && cropBox.height === containerHeight) ? ACTION_MOVE : ACTION_ALL); 1220 } 1221 1222 this.$cropBox.css({ 1223 width: cropBox.width, 1224 height: cropBox.height, 1225 left: cropBox.left, 1226 top: cropBox.top 1227 }); 1228 1229 if (this.isCropped && this.isLimited) { 1230 this.limitCanvas(true, true); 1231 } 1232 1233 if (!this.isDisabled) { 1234 this.output(); 1235 } 1236 }, 1237 1238 output: function () { 1239 this.preview(); 1240 1241 if (this.isCompleted) { 1242 this.trigger(EVENT_CROP, this.getData()); 1243 } 1244 }, 1245 1246 initPreview: function () { 1247 var crossOrigin = getCrossOrigin(this.crossOrigin); 1248 var url = crossOrigin ? this.crossOriginUrl : this.url; 1249 var $clone2; 1250 1251 this.$preview = $(this.options.preview); 1252 this.$clone2 = $clone2 = $('<img' + crossOrigin + ' src="' + url + '">'); 1253 this.$viewBox.html($clone2); 1254 this.$preview.each(function () { 1255 var $this = $(this); 1256 1257 // Save the original size for recover 1258 $this.data(DATA_PREVIEW, { 1259 width: $this.width(), 1260 height: $this.height(), 1261 html: $this.html() 1262 }); 1263 1264 /** 1265 * Override img element styles 1266 * Add `display:block` to avoid margin top issue 1267 * (Occur only when margin-top <= -height) 1268 */ 1269 $this.html( 1270 '<img' + crossOrigin + ' src="' + url + '" style="' + 1271 'display:block;width:100%;height:auto;' + 1272 'min-width:0!important;min-height:0!important;' + 1273 'max-width:none!important;max-height:none!important;' + 1274 'image-orientation:0deg!important;">' 1275 ); 1276 }); 1277 }, 1278 1279 resetPreview: function () { 1280 this.$preview.each(function () { 1281 var $this = $(this); 1282 var data = $this.data(DATA_PREVIEW); 1283 1284 $this.css({ 1285 width: data.width, 1286 height: data.height 1287 }).html(data.html).removeData(DATA_PREVIEW); 1288 }); 1289 }, 1290 1291 preview: function () { 1292 var image = this.image; 1293 var canvas = this.canvas; 1294 var cropBox = this.cropBox; 1295 var cropBoxWidth = cropBox.width; 1296 var cropBoxHeight = cropBox.height; 1297 var width = image.width; 1298 var height = image.height; 1299 var left = cropBox.left - canvas.left - image.left; 1300 var top = cropBox.top - canvas.top - image.top; 1301 1302 if (!this.isCropped || this.isDisabled) { 1303 return; 1304 } 1305 1306 this.$clone2.css({ 1307 width: width, 1308 height: height, 1309 marginLeft: -left, 1310 marginTop: -top, 1311 transform: getTransform(image) 1312 }); 1313 1314 this.$preview.each(function () { 1315 var $this = $(this); 1316 var data = $this.data(DATA_PREVIEW); 1317 var originalWidth = data.width; 1318 var originalHeight = data.height; 1319 var newWidth = originalWidth; 1320 var newHeight = originalHeight; 1321 var ratio = 1; 1322 1323 if (cropBoxWidth) { 1324 ratio = originalWidth / cropBoxWidth; 1325 newHeight = cropBoxHeight * ratio; 1326 } 1327 1328 if (cropBoxHeight && newHeight > originalHeight) { 1329 ratio = originalHeight / cropBoxHeight; 1330 newWidth = cropBoxWidth * ratio; 1331 newHeight = originalHeight; 1332 } 1333 1334 $this.css({ 1335 width: newWidth, 1336 height: newHeight 1337 }).find('img').css({ 1338 width: width * ratio, 1339 height: height * ratio, 1340 marginLeft: -left * ratio, 1341 marginTop: -top * ratio, 1342 transform: getTransform(image) 1343 }); 1344 }); 1345 }, 1346 1347 bind: function () { 1348 var options = this.options; 1349 var $this = this.$element; 1350 var $cropper = this.$cropper; 1351 1352 if ($.isFunction(options.cropstart)) { 1353 $this.on(EVENT_CROP_START, options.cropstart); 1354 } 1355 1356 if ($.isFunction(options.cropmove)) { 1357 $this.on(EVENT_CROP_MOVE, options.cropmove); 1358 } 1359 1360 if ($.isFunction(options.cropend)) { 1361 $this.on(EVENT_CROP_END, options.cropend); 1362 } 1363 1364 if ($.isFunction(options.crop)) { 1365 $this.on(EVENT_CROP, options.crop); 1366 } 1367 1368 if ($.isFunction(options.zoom)) { 1369 $this.on(EVENT_ZOOM, options.zoom); 1370 } 1371 1372 $cropper.on(EVENT_MOUSE_DOWN, $.proxy(this.cropStart, this)); 1373 1374 if (options.zoomable && options.zoomOnWheel) { 1375 $cropper.on(EVENT_WHEEL, $.proxy(this.wheel, this)); 1376 } 1377 1378 if (options.toggleDragModeOnDblclick) { 1379 $cropper.on(EVENT_DBLCLICK, $.proxy(this.dblclick, this)); 1380 } 1381 1382 $document. 1383 on(EVENT_MOUSE_MOVE, (this._cropMove = proxy(this.cropMove, this))). 1384 on(EVENT_MOUSE_UP, (this._cropEnd = proxy(this.cropEnd, this))); 1385 1386 if (options.responsive) { 1387 $window.on(EVENT_RESIZE, (this._resize = proxy(this.resize, this))); 1388 } 1389 }, 1390 1391 unbind: function () { 1392 var options = this.options; 1393 var $this = this.$element; 1394 var $cropper = this.$cropper; 1395 1396 if ($.isFunction(options.cropstart)) { 1397 $this.off(EVENT_CROP_START, options.cropstart); 1398 } 1399 1400 if ($.isFunction(options.cropmove)) { 1401 $this.off(EVENT_CROP_MOVE, options.cropmove); 1402 } 1403 1404 if ($.isFunction(options.cropend)) { 1405 $this.off(EVENT_CROP_END, options.cropend); 1406 } 1407 1408 if ($.isFunction(options.crop)) { 1409 $this.off(EVENT_CROP, options.crop); 1410 } 1411 1412 if ($.isFunction(options.zoom)) { 1413 $this.off(EVENT_ZOOM, options.zoom); 1414 } 1415 1416 $cropper.off(EVENT_MOUSE_DOWN, this.cropStart); 1417 1418 if (options.zoomable && options.zoomOnWheel) { 1419 $cropper.off(EVENT_WHEEL, this.wheel); 1420 } 1421 1422 if (options.toggleDragModeOnDblclick) { 1423 $cropper.off(EVENT_DBLCLICK, this.dblclick); 1424 } 1425 1426 $document. 1427 off(EVENT_MOUSE_MOVE, this._cropMove). 1428 off(EVENT_MOUSE_UP, this._cropEnd); 1429 1430 if (options.responsive) { 1431 $window.off(EVENT_RESIZE, this._resize); 1432 } 1433 }, 1434 1435 resize: function () { 1436 var restore = this.options.restore; 1437 var $container = this.$container; 1438 var container = this.container; 1439 var canvasData; 1440 var cropBoxData; 1441 var ratio; 1442 1443 // Check `container` is necessary for IE8 1444 if (this.isDisabled || !container) { 1445 return; 1446 } 1447 1448 ratio = $container.width() / container.width; 1449 1450 // Resize when width changed or height changed 1451 if (ratio !== 1 || $container.height() !== container.height) { 1452 if (restore) { 1453 canvasData = this.getCanvasData(); 1454 cropBoxData = this.getCropBoxData(); 1455 } 1456 1457 this.render(); 1458 1459 if (restore) { 1460 this.setCanvasData($.each(canvasData, function (i, n) { 1461 canvasData[i] = n * ratio; 1462 })); 1463 this.setCropBoxData($.each(cropBoxData, function (i, n) { 1464 cropBoxData[i] = n * ratio; 1465 })); 1466 } 1467 } 1468 }, 1469 1470 dblclick: function () { 1471 if (this.isDisabled) { 1472 return; 1473 } 1474 1475 if (this.$dragBox.hasClass(CLASS_CROP)) { 1476 this.setDragMode(ACTION_MOVE); 1477 } else { 1478 this.setDragMode(ACTION_CROP); 1479 } 1480 }, 1481 1482 wheel: function (event) { 1483 var e = event.originalEvent || event; 1484 var ratio = num(this.options.wheelZoomRatio) || 0.1; 1485 var delta = 1; 1486 1487 if (this.isDisabled) { 1488 return; 1489 } 1490 1491 event.preventDefault(); 1492 1493 // Limit wheel speed to prevent zoom too fast 1494 if (this.wheeling) { 1495 return; 1496 } 1497 1498 this.wheeling = true; 1499 1500 setTimeout($.proxy(function () { 1501 this.wheeling = false;
1502 }, this), 50); 1503 1504 if (e.deltaY) { 1505 delta = e.deltaY > 0 ? 1 : -1; 1506 } else if (e.wheelDelta) { 1507 delta = -e.wheelDelta / 120; 1508 } else if (e.detail) { 1509 delta = e.detail > 0 ? 1 : -1; 1510 } 1511 1512 this.zoom(-delta * ratio, event); 1513 }, 1514 1515 cropStart: function (event) { 1516 var options = this.options; 1517 var originalEvent = event.originalEvent; 1518 var touches = originalEvent && originalEvent.touches; 1519 var e = event; 1520 var touchesLength; 1521 var action; 1522 1523 if (this.isDisabled) { 1524 return; 1525 } 1526 1527 if (touches) { 1528 touchesLength = touches.length; 1529 1530 if (touchesLength > 1) { 1531 if (options.zoomable && options.zoomOnTouch && touchesLength === 2) { 1532 e = touches[1]; 1533 this.startX2 = e.pageX; 1534 this.startY2 = e.pageY; 1535 action = ACTION_ZOOM; 1536 } else { 1537 return; 1538 } 1539 } 1540 1541 e = touches[0]; 1542 } 1543 1544 action = action || $(e.target).data(DATA_ACTION); 1545 1546 if (REGEXP_ACTIONS.test(action)) { 1547 if (this.trigger(EVENT_CROP_START, { 1548 originalEvent: originalEvent, 1549 action: action 1550 }).isDefaultPrevented()) { 1551 return; 1552 } 1553 1554 event.preventDefault(); 1555 1556 this.action = action; 1557 this.cropping = false; 1558 1559 // IE8 has `event.pageX/Y`, but not `event.originalEvent.pageX/Y` 1560 // IE10 has `event.originalEvent.pageX/Y`, but not `event.pageX/Y` 1561 this.startX = e.pageX || originalEvent && originalEvent.pageX; 1562 this.startY = e.pageY || originalEvent && originalEvent.pageY; 1563 1564 if (action === ACTION_CROP) { 1565 this.cropping = true; 1566 this.$dragBox.addClass(CLASS_MODAL); 1567 } 1568 } 1569 }, 1570 1571 cropMove: function (event) { 1572 var options = this.options; 1573 var originalEvent = event.originalEvent; 1574 var touches = originalEvent && originalEvent.touches; 1575 var e = event; 1576 var action = this.action; 1577 var touchesLength; 1578 1579 if (this.isDisabled) { 1580 return; 1581 } 1582 1583 if (touches) { 1584 touchesLength = touches.length; 1585 1586 if (touchesLength > 1) { 1587 if (options.zoomable && options.zoomOnTouch && touchesLength === 2) { 1588 e = touches[1]; 1589 this.endX2 = e.pageX; 1590 this.endY2 = e.pageY; 1591 } else { 1592 return; 1593 } 1594 } 1595 1596 e = touches[0]; 1597 } 1598 1599 if (action) { 1600 if (this.trigger(EVENT_CROP_MOVE, { 1601 originalEvent: originalEvent, 1602 action: action 1603 }).isDefaultPrevented()) { 1604 return; 1605 } 1606 1607 event.preventDefault(); 1608 1609 this.endX = e.pageX || originalEvent && originalEvent.pageX; 1610 this.endY = e.pageY || originalEvent && originalEvent.pageY; 1611 1612 this.change(e.shiftKey, action === ACTION_ZOOM ? event : null); 1613 } 1614 }, 1615 1616 cropEnd: function (event) { 1617 var originalEvent = event.originalEvent; 1618 var action = this.action; 1619 1620 if (this.isDisabled) { 1621 return; 1622 } 1623 1624 if (action) { 1625 event.preventDefault(); 1626 1627 if (this.cropping) { 1628 this.cropping = false; 1629 this.$dragBox.toggleClass(CLASS_MODAL, this.isCropped && this.options.modal); 1630 } 1631 1632 this.action = ''; 1633 1634 this.trigger(EVENT_CROP_END, { 1635 originalEvent: originalEvent, 1636 action: action 1637 }); 1638 } 1639 }, 1640 1641 change: function (shiftKey, event) { 1642 var options = this.options; 1643 var aspectRatio = options.aspectRatio; 1644 var action = this.action; 1645 var container = this.container; 1646 var canvas = this.canvas; 1647 var cropBox = this.cropBox; 1648 var width = cropBox.width; 1649 var height = cropBox.height; 1650 var left = cropBox.left; 1651 var top = cropBox.top; 1652 var right = left + width; 1653 var bottom = top + height; 1654 var minLeft = 0; 1655 var minTop = 0; 1656 var maxWidth = container.width; 1657 var maxHeight = container.height; 1658 var renderable = true; 1659 var offset; 1660 var range; 1661 1662 // Locking aspect ratio in "free mode" by holding shift key (#259)
1663 if (!aspectRatio && shiftKey) { 1664 aspectRatio = width && height ? width / height : 1; 1665 } 1666 1667 if (this.isLimited) { 1668 minLeft = cropBox.minLeft; 1669 minTop = cropBox.minTop; 1670 maxWidth = minLeft + min(container.width, canvas.width, canvas.left + canvas.width); 1671 maxHeight = minTop + min(container.height, canvas.height, canvas.top + canvas.height); 1672 } 1673 1674 range = { 1675 x: this.endX - this.startX, 1676 y: this.endY - this.startY 1677 }; 1678 1679 if (aspectRatio) { 1680 range.X = range.y * aspectRatio; 1681 range.Y = range.x / aspectRatio; 1682 } 1683 1684 switch (action) { 1685 // Move crop box 1686 case ACTION_ALL: 1687 left += range.x; 1688 top += range.y; 1689 break; 1690 1691 // Resize crop box 1692 case ACTION_EAST: 1693 if (range.x >= 0 && (right >= maxWidth || aspectRatio && 1694 (top <= minTop || bottom >= maxHeight))) { 1695 1696 renderable = false; 1697 break; 1698 } 1699 1700 width += range.x; 1701 1702 if (aspectRatio) { 1703 height = width / aspectRatio; 1704 top -= range.Y / 2; 1705 } 1706 1707 if (width < 0) { 1708 action = ACTION_WEST; 1709 width = 0; 1710 } 1711 1712 break; 1713 1714 case ACTION_NORTH: 1715 if (range.y <= 0 && (top <= minTop || aspectRatio && 1716 (left <= minLeft || right >= maxWidth))) { 1717 1718 renderable = false; 1719 break; 1720 } 1721 1722 height -= range.y; 1723 top += range.y; 1724 1725 if (aspectRatio) { 1726 width = height * aspectRatio; 1727 left += range.X / 2; 1728 } 1729 1730 if (height < 0) { 1731 action = ACTION_SOUTH; 1732 height = 0; 1733 } 1734 1735 break; 1736 1737 case ACTION_WEST: 1738 if (range.x <= 0 && (left <= minLeft || aspectRatio && 1739 (top <= minTop || bottom >= maxHeight))) { 1740 1741 renderable = false; 1742 break; 1743 } 1744 1745 width -= range.x; 1746 left += range.x; 1747 1748 if (aspectRatio) { 1749 height = width / aspectRatio; 1750 top += range.Y / 2; 1751 } 1752 1753 if (width < 0) { 1754 action = ACTION_EAST; 1755 width = 0; 1756 } 1757 1758 break; 1759 1760 case ACTION_SOUTH: 1761 if (range.y >= 0 && (bottom >= maxHeight || aspectRatio && 1762 (left <= minLeft || right >= maxWidth))) { 1763 1764 renderable = false; 1765 break; 1766 } 1767 1768 height += range.y; 1769 1770 if (aspectRatio) { 1771 width = height * aspectRatio; 1772 left -= range.X / 2; 1773 } 1774 1775 if (height < 0) { 1776 action = ACTION_NORTH; 1777 height = 0; 1778 } 1779 1780 break; 1781 1782 case ACTION_NORTH_EAST: 1783 if (aspectRatio) { 1784 if (range.y <= 0 && (top <= minTop || right >= maxWidth)) { 1785 renderable = false; 1786 break; 1787 } 1788 1789 height -= range.y; 1790 top += range.y; 1791 width = height * aspectRatio; 1792 } else { 1793 if (range.x >= 0) { 1794 if (right < maxWidth) { 1795 width += range.x; 1796 } else if (range.y <= 0 && top <= minTop) { 1797 renderable = false; 1798 } 1799 } else { 1800 width += range.x; 1801 } 1802 1803 if (range.y <= 0) { 1804 if (top > minTop) { 1805 height -= range.y; 1806 top += range.y; 1807 } 1808 } else { 1809 height -= range.y; 1810 top += range.y; 1811 } 1812 } 1813 1814 if (width < 0 && height < 0) { 1815 action = ACTION_SOUTH_WEST; 1816 height = 0; 1817 width = 0; 1818 } else if (width < 0) { 1819 action = ACTION_NORTH_WEST; 1820 width = 0; 1821 } else if (height < 0) { 1822 action = ACTION_SOUTH_EAST; 1823 height = 0; 1824 } 1825 1826 break; 1827 1828 case ACTION_NORTH_WEST: 1829 if (aspectRatio) { 1830 if (range.y <= 0 && (top <= minTop || left <= minLeft)) { 1831 renderable = false;
1832 break; 1833 } 1834 1835 height -= range.y; 1836 top += range.y; 1837 width = height * aspectRatio; 1838 left += range.X; 1839 } else { 1840 if (range.x <= 0) { 1841 if (left > minLeft) { 1842 width -= range.x; 1843 left += range.x; 1844 } else if (range.y <= 0 && top <= minTop) { 1845 renderable = false; 1846 } 1847 } else { 1848 width -= range.x; 1849 left += range.x; 1850 } 1851 1852 if (range.y <= 0) { 1853 if (top > minTop) { 1854 height -= range.y; 1855 top += range.y; 1856 } 1857 } else { 1858 height -= range.y; 1859 top += range.y; 1860 } 1861 } 1862 1863 if (width < 0 && height < 0) { 1864 action = ACTION_SOUTH_EAST; 1865 height = 0; 1866 width = 0; 1867 } else if (width < 0) { 1868 action = ACTION_NORTH_EAST; 1869 width = 0; 1870 } else if (height < 0) { 1871 action = ACTION_SOUTH_WEST; 1872 height = 0; 1873 } 1874 1875 break; 1876 1877 case ACTION_SOUTH_WEST: 1878 if (aspectRatio) { 1879 if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) { 1880 renderable = false; 1881 break; 1882 } 1883 1884 width -= range.x; 1885 left += range.x; 1886 height = width / aspectRatio; 1887 } else { 1888 if (range.x <= 0) { 1889 if (left > minLeft) { 1890 width -= range.x; 1891 left += range.x; 1892 } else if (range.y >= 0 && bottom >= maxHeight) { 1893 renderable = false; 1894 } 1895 } else { 1896 width -= range.x; 1897 left += range.x; 1898 } 1899 1900 if (range.y >= 0) { 1901 if (bottom < maxHeight) { 1902 height += range.y; 1903 } 1904 } else { 1905 height += range.y; 1906 } 1907 } 1908 1909 if (width < 0 && height < 0) { 1910 action = ACTION_NORTH_EAST; 1911 height = 0; 1912 width = 0; 1913 } else if (width < 0) { 1914 action = ACTION_SOUTH_EAST; 1915 width = 0; 1916 } else if (height < 0) { 1917 action = ACTION_NORTH_WEST; 1918 height = 0; 1919 } 1920 1921 break; 1922 1923 case ACTION_SOUTH_EAST: 1924 if (aspectRatio) { 1925 if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) { 1926 renderable = false; 1927 break; 1928 } 1929 1930 width += range.x; 1931 height = width / aspectRatio; 1932 } else { 1933 if (range.x >= 0) { 1934 if (right < maxWidth) { 1935 width += range.x; 1936 } else if (range.y >= 0 && bottom >= maxHeight) { 1937 renderable = false; 1938 } 1939 } else { 1940 width += range.x; 1941 } 1942 1943 if (range.y >= 0) { 1944 if (bottom < maxHeight) { 1945 height += range.y; 1946 } 1947 } else { 1948 height += range.y; 1949 } 1950 } 1951 1952 if (width < 0 && height < 0) { 1953 action = ACTION_NORTH_WEST; 1954 height = 0; 1955 width = 0; 1956 } else if (width < 0) { 1957 action = ACTION_SOUTH_WEST; 1958 width = 0; 1959 } else if (height < 0) { 1960 action = ACTION_NORTH_EAST; 1961 height = 0; 1962 } 1963 1964 break; 1965 1966 // Move canvas 1967 case ACTION_MOVE: 1968 this.move(range.x, range.y); 1969 renderable = false; 1970 break; 1971 1972 // Zoom canvas 1973 case ACTION_ZOOM: 1974 this.zoom((function (x1, y1, x2, y2) { 1975 var z1 = sqrt(x1 * x1 + y1 * y1); 1976 var z2 = sqrt(x2 * x2 + y2 * y2); 1977 1978 return (z2 - z1) / z1; 1979 })( 1980 abs(this.startX - this.startX2), 1981 abs(this.startY - this.startY2), 1982 abs(this.endX - this.endX2),
1983 abs(this.endY - this.endY2) 1984 ), event); 1985 this.startX2 = this.endX2; 1986 this.startY2 = this.endY2; 1987 renderable = false; 1988 break; 1989 1990 // Create crop box 1991 case ACTION_CROP: 1992 if (!range.x || !range.y) { 1993 renderable = false; 1994 break; 1995 } 1996 1997 offset = this.$cropper.offset(); 1998 left = this.startX - offset.left; 1999 top = this.startY - offset.top; 2000 width = cropBox.minWidth; 2001 height = cropBox.minHeight; 2002 2003 if (range.x > 0) { 2004 action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST; 2005 } else if (range.x < 0) { 2006 left -= width; 2007 action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST; 2008 } 2009 2010 if (range.y < 0) { 2011 top -= height; 2012 } 2013 2014 // Show the crop box if is hidden 2015 if (!this.isCropped) { 2016 this.$cropBox.removeClass(CLASS_HIDDEN); 2017 this.isCropped = true; 2018 2019 if (this.isLimited) { 2020 this.limitCropBox(true, true); 2021 } 2022 } 2023 2024 break; 2025 2026 // No default 2027 } 2028 2029 if (renderable) { 2030 cropBox.width = width; 2031 cropBox.height = height; 2032 cropBox.left = left; 2033 cropBox.top = top; 2034 this.action = action; 2035 2036 this.renderCropBox(); 2037 } 2038 2039 // Override 2040 this.startX = this.endX; 2041 this.startY = this.endY; 2042 }, 2043 2044 // Show the crop box manually 2045 crop: function () { 2046 if (!this.isBuilt || this.isDisabled) { 2047 return; 2048 } 2049 2050 if (!this.isCropped) { 2051 this.isCropped = true; 2052 this.limitCropBox(true, true); 2053 2054 if (this.options.modal) { 2055 this.$dragBox.addClass(CLASS_MODAL); 2056 } 2057 2058 this.$cropBox.removeClass(CLASS_HIDDEN); 2059 } 2060 2061 this.setCropBoxData(this.initialCropBox); 2062 }, 2063 2064 // Reset the image and crop box to their initial states 2065 reset: function () { 2066 if (!this.isBuilt || this.isDisabled) { 2067 return; 2068 } 2069 2070 this.image = $.extend({}, this.initialImage); 2071 this.canvas = $.extend({}, this.initialCanvas); 2072 this.cropBox = $.extend({}, this.initialCropBox); 2073 2074 this.renderCanvas(); 2075 2076 if (this.isCropped) { 2077 this.renderCropBox(); 2078 } 2079 }, 2080 2081 // Clear the crop box 2082 clear: function () { 2083 if (!this.isCropped || this.isDisabled) { 2084 return; 2085 } 2086 2087 $.extend(this.cropBox, { 2088 left: 0, 2089 top: 0, 2090 width: 0, 2091 height: 0 2092 }); 2093 2094 this.isCropped = false; 2095 this.renderCropBox(); 2096 2097 this.limitCanvas(true, true); 2098 2099 // Render canvas after crop box rendered 2100 this.renderCanvas(); 2101 2102 this.$dragBox.removeClass(CLASS_MODAL); 2103 this.$cropBox.addClass(CLASS_HIDDEN); 2104 }, 2105 2106 /** 2107 * Replace the image's src and rebuild the cropper 2108 * 2109 * @param {String} url 2110 * @param {Boolean} onlyColorChanged (optional) 2111 */ 2112 replace: function (url, onlyColorChanged) { 2113 if (!this.isDisabled && url) { 2114 if (this.isImg) { 2115 this.$element.attr('src', url); 2116 } 2117 2118 if (onlyColorChanged) { 2119 this.url = url; 2120 this.$clone.attr('src', url); 2121 2122 if (this.isBuilt) { 2123 this.$preview.find('img').add(this.$clone2).attr('src', url); 2124 } 2125 } else { 2126 if (this.isImg) { 2127 this.isReplaced = true; 2128 } 2129 2130 // Clear previous data 2131 this.options.data = null; 2132 this.load(url); 2133 } 2134 } 2135 }, 2136 2137 // Enable (unfreeze) the cropper 2138 enable: function () { 2139 if (this.isBuilt) { 2140 this.isDisabled = false; 2141 this.$cropper.removeClass(CLASS_DISABLED); 2142 } 2143 }, 2144 2145 // Disable (freeze) the cropper 2146 disable: function () { 2147 if (this.isBuilt) { 2148 this.isDisabled = true; 2149 this.$cropper.addClass(CLASS_DISABLED); 2150 } 2151 }, 2152 2153 // Destroy the cropper and remove the instance from the image 2154 destroy: function () { 2155 var $this = this.$element; 2156 2157 if (this.isLoaded) { 2158 if (this.isImg && this.isReplaced) { 2159 $this.attr('src', this.originalUrl); 2160 } 2161 2162 this.unbuild(); 2163 $this.removeClass(CLASS_HIDDEN); 2164 } else { 2165 if (this.isImg) { 2166 $this.off(EVENT_LOAD, this.start); 2167 } else if (this.$clone) { 2168 this.$clone.remove(); 2169 } 2170 } 2171 2172 $this.removeData(NAMESPACE); 2173 }, 2174 2175 /** 2176 * Move the canvas with relative offsets 2177 * 2178 * @param {Number} offsetX 2179 * @param {Number} offsetY (optional) 2180 */ 2181 move: function (offsetX, offsetY) { 2182 var canvas = this.canvas; 2183 2184 this.moveTo( 2185 isUndefined(offsetX) ? offsetX : canvas.left + num(offsetX), 2186 isUndefined(offsetY) ? offsetY : canvas.top + num(offsetY) 2187 ); 2188 }, 2189 2190 /** 2191 * Move the canvas to an absolute point 2192 * 2193 * @param {Number} x 2194 * @param {Number} y (optional) 2195 */ 2196 moveTo: function (x, y) { 2197 var canvas = this.canvas; 2198 var isChanged = false;
2199 2200 // If "y" is not present, its default value is "x" 2201 if (isUndefined(y)) { 2202 y = x; 2203 } 2204 2205 x = num(x); 2206 y = num(y); 2207 2208 if (this.isBuilt && !this.isDisabled && this.options.movable) { 2209 if (isNumber(x)) { 2210 canvas.left = x; 2211 isChanged = true; 2212 } 2213 2214 if (isNumber(y)) { 2215 canvas.top = y; 2216 isChanged = true; 2217 } 2218 2219 if (isChanged) { 2220 this.renderCanvas(true); 2221 } 2222 } 2223 }, 2224 2225 /** 2226 * Zoom the canvas with a relative ratio 2227 * 2228 * @param {Number} ratio 2229 * @param {jQuery Event} _event (private) 2230 */ 2231 zoom: function (ratio, _event) { 2232 var canvas = this.canvas; 2233 2234 ratio = num(ratio); 2235 2236 if (ratio < 0) { 2237 ratio = 1 / (1 - ratio); 2238 } else { 2239 ratio = 1 + ratio; 2240 } 2241 2242 this.zoomTo(canvas.width * ratio / canvas.naturalWidth, _event); 2243 }, 2244 2245 /** 2246 * Zoom the canvas to an absolute ratio 2247 * 2248 * @param {Number} ratio 2249 * @param {jQuery Event} _event (private) 2250 */ 2251 zoomTo: function (ratio, _event) { 2252 var options = this.options; 2253 var canvas = this.canvas; 2254 var width = canvas.width; 2255 var height = canvas.height; 2256 var naturalWidth = canvas.naturalWidth; 2257 var naturalHeight = canvas.naturalHeight; 2258 var originalEvent; 2259 var newWidth; 2260 var newHeight; 2261 var offset; 2262 var center; 2263 2264 ratio = num(ratio); 2265 2266 if (ratio >= 0 && this.isBuilt && !this.isDisabled && options.zoomable) { 2267 newWidth = naturalWidth * ratio; 2268 newHeight = naturalHeight * ratio; 2269 2270 if (_event) { 2271 originalEvent = _event.originalEvent; 2272 } 2273 2274 if (this.trigger(EVENT_ZOOM, { 2275 originalEvent: originalEvent, 2276 oldRatio: width / naturalWidth, 2277 ratio: newWidth / naturalWidth 2278 }).isDefaultPrevented()) { 2279 return; 2280 } 2281 2282 if (originalEvent) { 2283 offset = this.$cropper.offset(); 2284 center = originalEvent.touches ? getTouchesCenter(originalEvent.touches) : { 2285 pageX: _event.pageX || originalEvent.pageX || 0, 2286 pageY: _event.pageY || originalEvent.pageY || 0 2287 }; 2288 2289 // Zoom from the triggering point of the event 2290 canvas.left -= (newWidth - width) * ( 2291 ((center.pageX - offset.left) - canvas.left) / width 2292 ); 2293 canvas.top -= (newHeight - height) * ( 2294 ((center.pageY - offset.top) - canvas.top) / height 2295 ); 2296 } else { 2297 2298 // Zoom from the center of the canvas 2299 canvas.left -= (newWidth - width) / 2; 2300 canvas.top -= (newHeight - height) / 2; 2301 } 2302 2303 canvas.width = newWidth; 2304 canvas.height = newHeight; 2305 this.renderCanvas(true); 2306 } 2307 }, 2308 2309 /** 2310 * Rotate the canvas with a relative degree 2311 * 2312 * @param {Number} degree 2313 */ 2314 rotate: function (degree) { 2315 this.rotateTo((this.image.rotate || 0) + num(degree)); 2316 }, 2317 2318 /** 2319 * Rotate the canvas to an absolute degree 2320 * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate() 2321 * 2322 * @param {Number} degree 2323 */ 2324 rotateTo: function (degree) { 2325 degree = num(degree); 2326 2327 if (isNumber(degree) && this.isBuilt && !this.isDisabled && this.options.rotatable) { 2328 this.image.rotate = degree % 360; 2329 this.isRotated = true; 2330 this.renderCanvas(true); 2331 } 2332 }, 2333 2334 /** 2335 * Scale the image 2336 * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale() 2337 * 2338 * @param {Number} scaleX 2339 * @param {Number} scaleY (optional) 2340 */
2341 scale: function (scaleX, scaleY) { 2342 var image = this.image; 2343 var isChanged = false; 2344 2345 // If "scaleY" is not present, its default value is "scaleX" 2346 if (isUndefined(scaleY)) { 2347 scaleY = scaleX; 2348 } 2349 2350 scaleX = num(scaleX); 2351 scaleY = num(scaleY); 2352 2353 if (this.isBuilt && !this.isDisabled && this.options.scalable) { 2354 if (isNumber(scaleX)) { 2355 image.scaleX = scaleX; 2356 isChanged = true; 2357 } 2358 2359 if (isNumber(scaleY)) { 2360 image.scaleY = scaleY; 2361 isChanged = true; 2362 } 2363 2364 if (isChanged) { 2365 this.renderImage(true); 2366 } 2367 } 2368 }, 2369 2370 /** 2371 * Scale the abscissa of the image 2372 * 2373 * @param {Number} scaleX 2374 */ 2375 scaleX: function (scaleX) { 2376 var scaleY = this.image.scaleY; 2377 2378 this.scale(scaleX, isNumber(scaleY) ? scaleY : 1); 2379 }, 2380 2381 /** 2382 * Scale the ordinate of the image 2383 * 2384 * @param {Number} scaleY 2385 */ 2386 scaleY: function (scaleY) { 2387 var scaleX = this.image.scaleX; 2388 2389 this.scale(isNumber(scaleX) ? scaleX : 1, scaleY); 2390 }, 2391 2392 /** 2393 * Get the cropped area position and size data (base on the original image) 2394 * 2395 * @param {Boolean} isRounded (optional) 2396 * @return {Object} data 2397 */ 2398 getData: function (isRounded) { 2399 var options = this.options; 2400 var image = this.image; 2401 var canvas = this.canvas; 2402 var cropBox = this.cropBox; 2403 var ratio; 2404 var data; 2405 2406 if (this.isBuilt && this.isCropped) { 2407 data = { 2408 x: cropBox.left - canvas.left, 2409 y: cropBox.top - canvas.top, 2410 width: cropBox.width, 2411 height: cropBox.height 2412 }; 2413 2414 ratio = image.width / image.naturalWidth; 2415 2416 $.each(data, function (i, n) { 2417 n = n / ratio;
2418 data[i] = isRounded ? round(n) : n; 2419 }); 2420 2421 } else { 2422 data = { 2423 x: 0, 2424 y: 0, 2425 width: 0, 2426 height: 0 2427 }; 2428 } 2429 2430 if (options.rotatable) { 2431 data.rotate = image.rotate || 0; 2432 } 2433 2434 if (options.scalable) { 2435 data.scaleX = image.scaleX || 1; 2436 data.scaleY = image.scaleY || 1; 2437 } 2438 2439 return data; 2440 }, 2441 2442 /** 2443 * Set the cropped area position and size with new data 2444 * 2445 * @param {Object} data 2446 */ 2447 setData: function (data) { 2448 var options = this.options; 2449 var image = this.image; 2450 var canvas = this.canvas; 2451 var cropBoxData = {}; 2452 var isRotated; 2453 var isScaled; 2454 var ratio; 2455 2456 if ($.isFunction(data)) { 2457 data = data.call(this.element); 2458 } 2459 2460 if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) { 2461 if (options.rotatable) { 2462 if (isNumber(data.rotate) && data.rotate !== image.rotate) { 2463 image.rotate = data.rotate; 2464 this.isRotated = isRotated = true; 2465 } 2466 } 2467 2468 if (options.scalable) { 2469 if (isNumber(data.scaleX) && data.scaleX !== image.scaleX) { 2470 image.scaleX = data.scaleX; 2471 isScaled = true; 2472 } 2473 2474 if (isNumber(data.scaleY) && data.scaleY !== image.scaleY) { 2475 image.scaleY = data.scaleY; 2476 isScaled = true; 2477 } 2478 } 2479 2480 if (isRotated) { 2481 this.renderCanvas(); 2482 } else if (isScaled) { 2483 this.renderImage(); 2484 } 2485 2486 ratio = image.width / image.naturalWidth; 2487 2488 if (isNumber(data.x)) { 2489 cropBoxData.left = data.x * ratio + canvas.left; 2490 } 2491 2492 if (isNumber(data.y)) { 2493 cropBoxData.top = data.y * ratio + canvas.top; 2494 } 2495 2496 if (isNumber(data.width)) { 2497 cropBoxData.width = data.width * ratio; 2498 } 2499 2500 if (isNumber(data.height)) { 2501 cropBoxData.height = data.height * ratio; 2502 } 2503 2504 this.setCropBoxData(cropBoxData); 2505 } 2506 }, 2507 2508 /** 2509 * Get the container size data 2510 * 2511 * @return {Object} data 2512 */ 2513 getContainerData: function () { 2514 return this.isBuilt ? this.container : {}; 2515 }, 2516 2517 /** 2518 * Get the image position and size data 2519 * 2520 * @return {Object} data 2521 */ 2522 getImageData: function () { 2523 return this.isLoaded ? this.image : {}; 2524 }, 2525 2526 /** 2527 * Get the canvas position and size data 2528 * 2529 * @return {Object} data 2530 */ 2531 getCanvasData: function () { 2532 var canvas = this.canvas; 2533 var data = {}; 2534 2535 if (this.isBuilt) { 2536 $.each([ 2537 'left', 2538 'top', 2539 'width', 2540 'height', 2541 'naturalWidth', 2542 'naturalHeight' 2543 ], function (i, n) { 2544 data[n] = canvas[n]; 2545 }); 2546 } 2547 2548 return data; 2549 }, 2550 2551 /** 2552 * Set the canvas position and size with new data 2553 * 2554 * @param {Object} data 2555 */ 2556 setCanvasData: function (data) { 2557 var canvas = this.canvas; 2558 var aspectRatio = canvas.aspectRatio; 2559 2560 if ($.isFunction(data)) { 2561 data = data.call(this.$element); 2562 } 2563 2564 if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) { 2565 if (isNumber(data.left)) { 2566 canvas.left = data.left; 2567 } 2568 2569 if (isNumber(data.top)) { 2570 canvas.top = data.top; 2571 } 2572 2573 if (isNumber(data.width)) { 2574 canvas.width = data.width; 2575 canvas.height = data.width / aspectRatio; 2576 } else if (isNumber(data.height)) { 2577 canvas.height = data.height; 2578 canvas.width = data.height * aspectRatio; 2579 } 2580 2581 this.renderCanvas(true); 2582 } 2583 }, 2584 2585 /** 2586 * Get the crop box position and size data 2587 * 2588 * @return {Object} data 2589 */ 2590 getCropBoxData: function () { 2591 var cropBox = this.cropBox; 2592 var data; 2593 2594 if (this.isBuilt && this.isCropped) { 2595 data = { 2596 left: cropBox.left, 2597 top: cropBox.top, 2598 width: cropBox.width, 2599 height: cropBox.height 2600 }; 2601 } 2602 2603 return data || {}; 2604 }, 2605 2606 /** 2607 * Set the crop box position and size with new data 2608 * 2609 * @param {Object} data 2610 */ 2611 setCropBoxData: function (data) { 2612 var cropBox = this.cropBox; 2613 var aspectRatio = this.options.aspectRatio; 2614 var isWidthChanged; 2615 var isHeightChanged; 2616 2617 if ($.isFunction(data)) { 2618 data = data.call(this.$element); 2619 } 2620 2621 if (this.isBuilt && this.isCropped && !this.isDisabled && $.isPlainObject(data)) { 2622 2623 if (isNumber(data.left)) { 2624 cropBox.left = data.left; 2625 } 2626 2627 if (isNumber(data.top)) { 2628 cropBox.top = data.top; 2629 } 2630 2631 if (isNumber(data.width)) { 2632 isWidthChanged = true; 2633 cropBox.width = data.width; 2634 } 2635 2636 if (isNumber(data.height)) { 2637 isHeightChanged = true; 2638 cropBox.height = data.height; 2639 } 2640 2641 if (aspectRatio) { 2642 if (isWidthChanged) { 2643 cropBox.height = cropBox.width / aspectRatio; 2644 } else if (isHeightChanged) { 2645 cropBox.width = cropBox.height * aspectRatio; 2646 } 2647 } 2648 2649 this.renderCropBox(); 2650 } 2651 }, 2652 2653 /** 2654 * Get a canvas drawn the cropped image 2655 * 2656 * @param {Object} options (optional) 2657 * @return {HTMLCanvasElement} canvas 2658 */ 2659 getCroppedCanvas: function (options) { 2660 var originalWidth; 2661 var originalHeight; 2662 var canvasWidth; 2663 var canvasHeight; 2664 var scaledWidth; 2665 var scaledHeight; 2666 var scaledRatio; 2667 var aspectRatio; 2668 var canvas; 2669 var context; 2670 var data; 2671 2672 if (!this.isBuilt || !SUPPORT_CANVAS) { 2673 return; 2674 } 2675 2676 if (!this.isCropped) { 2677 return getSourceCanvas(this.$clone[0], this.image); 2678 } 2679 2680 if (!$.isPlainObject(options)) { 2681 options = {}; 2682 } 2683 2684 data = this.getData(); 2685 originalWidth = data.width; 2686 originalHeight = data.height; 2687 aspectRatio = originalWidth / originalHeight; 2688 2689 if ($.isPlainObject(options)) { 2690 scaledWidth = options.width; 2691 scaledHeight = options.height; 2692 2693 if (scaledWidth) { 2694 scaledHeight = scaledWidth / aspectRatio; 2695 scaledRatio = scaledWidth / originalWidth; 2696 } else if (scaledHeight) { 2697 scaledWidth = scaledHeight * aspectRatio; 2698 scaledRatio = scaledHeight / originalHeight; 2699 } 2700 } 2701 2702 // The canvas element will use `Math.floor` on a float number, so floor first 2703 canvasWidth = floor(scaledWidth || originalWidth); 2704 canvasHeight = floor(scaledHeight || originalHeight); 2705 2706 canvas = $('<canvas>')[0]; 2707 canvas.width = canvasWidth; 2708 canvas.height = canvasHeight; 2709 context = canvas.getContext('2d'); 2710 2711 if (options.fillColor) { 2712 context.fillStyle = options.fillColor; 2713 context.fillRect(0, 0, canvasWidth, canvasHeight); 2714 } 2715 2716 // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage 2717 context.drawImage.apply(context, (function () { 2718 var source = getSourceCanvas(this.$clone[0], this.image); 2719 var sourceWidth = source.width; 2720 var sourceHeight = source.height; 2721 var canvas = this.canvas; 2722 var params = [source]; 2723 2724 // Source canvas
2725 var srcX = data.x + canvas.naturalWidth * (abs(data.scaleX || 1) - 1) / 2; 2726 var srcY = data.y + canvas.naturalHeight * (abs(data.scaleY || 1) - 1) / 2; 2727 var srcWidth; 2728 var srcHeight; 2729 2730 // Destination canvas 2731 var dstX; 2732 var dstY; 2733 var dstWidth; 2734 var dstHeight; 2735 2736 if (srcX <= -originalWidth || srcX > sourceWidth) { 2737 srcX = srcWidth = dstX = dstWidth = 0; 2738 } else if (srcX <= 0) { 2739 dstX = -srcX; 2740 srcX = 0; 2741 srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX); 2742 } else if (srcX <= sourceWidth) { 2743 dstX = 0; 2744 srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX); 2745 } 2746 2747 if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) { 2748 srcY = srcHeight = dstY = dstHeight = 0; 2749 } else if (srcY <= 0) { 2750 dstY = -srcY; 2751 srcY = 0; 2752 srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY); 2753 } else if (srcY <= sourceHeight) { 2754 dstY = 0; 2755 srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY); 2756 } 2757 2758 // All the numerical parameters should be integer for `drawImage` (#476) 2759 params.push(floor(srcX), floor(srcY), floor(srcWidth), floor(srcHeight)); 2760 2761 // Scale destination sizes 2762 if (scaledRatio) { 2763 dstX *= scaledRatio; 2764 dstY *= scaledRatio; 2765 dstWidth *= scaledRatio; 2766 dstHeight *= scaledRatio; 2767 } 2768 2769 // Avoid "IndexSizeError" in IE and Firefox 2770 if (dstWidth > 0 && dstHeight > 0) { 2771 params.push(floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight)); 2772 } 2773 2774 return params; 2775 }).call(this)); 2776 2777 return canvas; 2778 }, 2779 2780 /** 2781 * Change the aspect ratio of the crop box 2782 * 2783 * @param {Number} aspectRatio 2784 */ 2785 setAspectRatio: function (aspectRatio) { 2786 var options = this.options; 2787 2788 if (!this.isDisabled && !isUndefined(aspectRatio)) { 2789 2790 // 0 -> NaN 2791 options.aspectRatio = max(0, aspectRatio) || NaN; 2792 2793 if (this.isBuilt) { 2794 this.initCropBox(); 2795 2796 if (this.isCropped) { 2797 this.renderCropBox(); 2798 } 2799 } 2800 } 2801 }, 2802 2803 /** 2804 * Change the drag mode 2805 * 2806 * @param {String} mode (optional) 2807 */ 2808 setDragMode: function (mode) { 2809 var options = this.options; 2810 var croppable; 2811 var movable; 2812 2813 if (this.isLoaded && !this.isDisabled) { 2814 croppable = mode === ACTION_CROP;
2815 movable = options.movable && mode === ACTION_MOVE; 2816 mode = (croppable || movable) ? mode : ACTION_NONE; 2817 2818 this.$dragBox. 2819 data(DATA_ACTION, mode). 2820 toggleClass(CLASS_CROP, croppable). 2821 toggleClass(CLASS_MOVE, movable); 2822 2823 if (!options.cropBoxMovable) { 2824 2825 // Sync drag mode to crop box when it is not movable(#300) 2826 this.$face. 2827 data(DATA_ACTION, mode). 2828 toggleClass(CLASS_CROP, croppable). 2829 toggleClass(CLASS_MOVE, movable); 2830 } 2831 } 2832 } 2833 }; 2834 2835 Cropper.DEFAULTS = { 2836 2837 // Define the view mode of the cropper 2838 viewMode: 0, // 0, 1, 2, 3 2839 2840 // Define the dragging mode of the cropper 2841 dragMode: 'crop', // 'crop', 'move' or 'none' 2842 2843 // Define the aspect ratio of the crop box 2844 aspectRatio: NaN, 2845 2846 // An object with the previous cropping result data 2847 data: null, 2848 2849 // A jQuery selector for adding extra containers to preview 2850 preview: '', 2851 2852 // Re-render the cropper when resize the window 2853 responsive: true, 2854 2855 // Restore the cropped area after resize the window 2856 restore: true, 2857 2858 // Check if the current image is a cross-origin image 2859 checkCrossOrigin: true, 2860 2861 // Check the current image's Exif Orientation information 2862 checkOrientation: true, 2863 2864 // Show the black modal 2865 modal: true, 2866 2867 // Show the dashed lines for guiding 2868 guides: true, 2869 2870 // Show the center indicator for guiding 2871 center: true, 2872 2873 // Show the white modal to highlight the crop box 2874 highlight: true, 2875 2876 // Show the grid background 2877 background: true, 2878 2879 // Enable to crop the image automatically when initialize 2880 autoCrop: true, 2881 2882 // Define the percentage of automatic cropping area when initializes 2883 autoCropArea: 0.8, 2884 2885 // Enable to move the image 2886 movable: true, 2887 2888 // Enable to rotate the image 2889 rotatable: true, 2890 2891 // Enable to scale the image 2892 scalable: true, 2893 2894 // Enable to zoom the image 2895 zoomable: true, 2896 2897 // Enable to zoom the image by dragging touch 2898 zoomOnTouch: true, 2899 2900 // Enable to zoom the image by wheeling mouse 2901 zoomOnWheel: true, 2902 2903 // Define zoom ratio when zoom the image by wheeling mouse 2904 wheelZoomRatio: 0.1, 2905 2906 // Enable to move the crop box 2907 cropBoxMovable: true, 2908 2909 // Enable to resize the crop box 2910 cropBoxResizable: true, 2911 2912 // Toggle drag mode between "crop" and "move" when click twice on the cropper 2913 toggleDragModeOnDblclick: true, 2914 2915 // Size limitation 2916 minCanvasWidth: 0, 2917 minCanvasHeight: 0, 2918 minCropBoxWidth: 0, 2919 minCropBoxHeight: 0, 2920 minContainerWidth: 200, 2921 minContainerHeight: 100, 2922 2923 // Shortcuts of events 2924 build: null, 2925 built: null, 2926 cropstart: null, 2927 cropmove: null, 2928 cropend: null, 2929 crop: null, 2930 zoom: null 2931 }; 2932 2933 Cropper.setDefaults = function (options) { 2934 $.extend(Cropper.DEFAULTS, options); 2935 }; 2936 2937 Cropper.TEMPLATE = ( 2938 '<div class="cropper-container">' + 2939 '<div class="cropper-wrap-box">' + 2940 '<div class="cropper-canvas"></div>' + 2941 '</div>' + 2942 '<div class="cropper-drag-box"></div>' + 2943 '<div class="cropper-crop-box">' + 2944 '<span class="cropper-view-box"></span>' + 2945 '<span class="cropper-dashed dashed-h"></span>' + 2946 '<span class="cropper-dashed dashed-v"></span>' + 2947 '<span class="cropper-center"></span>' + 2948 '<span class="cropper-face"></span>' + 2949 '<span class="cropper-line line-e" data-action="e"></span>' + 2950 '<span class="cropper-line line-n" data-action="n"></span>' + 2951 '<span class="cropper-line line-w" data-action="w"></span>' + 2952 '<span class="cropper-line line-s" data-action="s"></span>' + 2953 '<span class="cropper-point point-e" data-action="e"></span>' + 2954 '<span class="cropper-point point-n" data-action="n"></span>' + 2955 '<span class="cropper-point point-w" data-action="w"></span>' + 2956 '<span class="cropper-point point-s" data-action="s"></span>' + 2957 '<span class="cropper-point point-ne" data-action="ne"></span>' + 2958 '<span class="cropper-point point-nw" data-action="nw"></span>' + 2959 '<span class="cropper-point point-sw" data-action="sw"></span>' + 2960 '<span class="cropper-point point-se" data-action="se"></span>' + 2961 '</div>' + 2962 '</div>' 2963 ); 2964 2965 // Save the other cropper 2966 Cropper.other = $.fn.cropper; 2967 2968 // Register as jQuery plugin 2969 $.fn.cropper = function (option) { 2970 var args = toArray(arguments, 1); 2971 var result; 2972 2973 this.each(function () { 2974 var $this = $(this); 2975 var data = $this.data(NAMESPACE); 2976 var options; 2977 var fn; 2978 2979 if (!data) { 2980 if (/destroy/.test(option)) { 2981 return; 2982 } 2983 2984 options = $.extend({}, $this.data(), $.isPlainObject(option) && option); 2985 $this.data(NAMESPACE, (data = new Cropper(this, options))); 2986 } 2987 2988 if (typeof option === 'string' && $.isFunction(fn = data[option])) { 2989 result = fn.apply(data, args); 2990 } 2991 }); 2992
2993 return isUndefined(result) ? this : result; 2994 }; 2995 2996 $.fn.cropper.Constructor = Cropper; 2997 $.fn.cropper.setDefaults = Cropper.setDefaults; 2998 2999 // No conflict 3000 $.fn.cropper.noConflict = function () { 3001 $.fn.cropper = Cropper.other; 3002 return this; 3003 }; 3004 3005});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.