text:   $.extend(prototype, {
  
      // Show the crop box manually
      crop: function () {
        if (!this.built || this.disabled) {
          return;
        }
  
        if (!this.cropped) {
          this.cropped = true;
          this.limitCropBox(true, true);
  
          if (this.options.modal) {
            this.$dragBox.addClass(CLASS_MODAL);
          }
  
          this.$cropBox.removeClass(CLASS_HIDDEN);
        }
  
        this.setCropBoxData(this.initialCropBox);
      },
  
      // Reset the image and crop box to their initial states
      reset: function () {
        if (!this.built || this.disabled) {
          return;
        }
  
        this.image = $.extend({}, this.initialImage);
        this.canvas = $.extend({}, this.initialCanvas);
  
        // Required for strict mode
        this.cropBox = $.extend({}, this.initialCropBox);
  
        this.renderCanvas();
  
        if (this.cropped) {
          this.renderCropBox();
        }
      },
  
      // Clear the crop box
      clear: function () {
        if (!this.cropped || this.disabled) {
          return;
        }
  
        $.extend(this.cropBox, {
          left: 0,
          top: 0,
          width: 0,
          height: 0
        });
  
        this.cropped = false;
        this.renderCropBox();
  
        this.limitCanvas();
  
        // Render canvas after crop box rendered
        this.renderCanvas();
  
        this.$dragBox.removeClass(CLASS_MODAL);
        this.$cropBox.addClass(CLASS_HIDDEN);
      },
  
      /**
       * Replace the image's src and rebuild the cropper
       *
       * @param {String} url
       */
      replace: function (url) {
        if (!this.disabled && url) {
          if (this.isImg) {
            this.$element.attr('src', url);
          }
  
          // Clear previous data
          this.options.data = null;
          this.load(url);
        }
      },
  
      // Enable (unfreeze) the cropper
      enable: function () {
        if (this.built) {
          this.disabled = false;
          this.$cropper.removeClass(CLASS_DISABLED);
        }
      },
  
      // Disable (freeze) the cropper
      disable: function () {
        if (this.built) {
          this.disabled = true;
          this.$cropper.addClass(CLASS_DISABLED);
        }
      },
  
      // Destroy the cropper and remove the instance from the image
      destroy: function () {
        var $this = this.$element;
  
        if (this.ready) {
          if (this.isImg) {
            $this.attr('src', this.originalUrl);
          }
  
          this.unbuild();
          $this.removeClass(CLASS_HIDDEN);
        } else {
          if (this.isImg) {
            $this.off(EVENT_LOAD, this.start);
          } else if (this.$clone) {
            this.$clone.remove();
          }
        }
  
        $this.removeData(NAMESPACE);
      },
  
      /**
       * Move the canvas
       *
       * @param {Number} offsetX
       * @param {Number} offsetY (optional)
       */
      move: function (offsetX, offsetY) {
        var canvas = this.canvas;
  
        // If "offsetY" is not present, its default value is "offsetX"
        if (isUndefined(offsetY)) {
          offsetY = offsetX;
        }
  
        offsetX = num(offsetX);
        offsetY = num(offsetY);
  
        if (this.built && !this.disabled && this.options.movable) {
          canvas.left += isNumber(offsetX) ? offsetX : 0;
          canvas.top += isNumber(offsetY) ? offsetY : 0;
          this.renderCanvas(true);
        }
      },
  
      /**
       * Zoom the canvas
       *
       * @param {Number} ratio
       * @param {Event} _originalEvent (private)
       */
      zoom: function (ratio, _originalEvent) {
        var canvas = this.canvas;
        var width;
        var height;
  
        ratio = num(ratio);
  
        if (ratio && this.built && !this.disabled && this.options.zoomable) {
          if (this.trigger(EVENT_ZOOM, {
            originalEvent: _originalEvent,
            ratio: ratio
          })) {
            return;
          }
  
          if (ratio < 0) {
            ratio =  1 / (1 - ratio);
          } else {
            ratio = 1 + ratio;
          }
  
          width = canvas.width * ratio;
          height = canvas.height * ratio;
          canvas.left -= (width - canvas.width) / 2;
          canvas.top -= (height - canvas.height) / 2;
          canvas.width = width;
          canvas.height = height;
          this.renderCanvas(true);
          this.setDragMode(ACTION_MOVE);
        }
      },
  
      /**
       * Rotate the canvas
       * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate()
       *
       * @param {Number} degree
       */
      rotate: function (degree) {
        var image = this.image;
        var rotate = image.rotate || 0;
  
        degree = num(degree) || 0;
  
        if (this.built && !this.disabled && this.options.rotatable) {
          image.rotate = (rotate + degree) % 360;
          this.rotated = true;
          this.renderCanvas(true);
        }
      },
  
      /**
       * Scale the image
       * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale()
       *
       * @param {Number} scaleX
       * @param {Number} scaleY (optional)
       */
      scale: function (scaleX, scaleY) {
        var image = this.image;
  
        // If "scaleY" is not present, its default value is "scaleX"
        if (isUndefined(scaleY)) {
          scaleY = scaleX;
        }
  
        scaleX = num(scaleX);
        scaleY = num(scaleY);
  
        if (this.built && !this.disabled && this.options.scalable) {
          image.scaleX = isNumber(scaleX) ? scaleX : 1;
          image.scaleY = isNumber(scaleY) ? scaleY : 1;
          this.renderImage(true);
        }
      },
  
      /**
       * Get the cropped area position and size data (base on the original image)
       *
       * @param {Boolean} rounded (optional)
       * @return {Object} data
       */
      getData: function (rounded) {
        var options = this.options;
        var image = this.image;
        var canvas = this.canvas;
        var cropBox = this.cropBox;
        var ratio;
        var data;
  
        if (this.built && this.cropped) {
          data = {
            x: cropBox.left - canvas.left,
            y: cropBox.top - canvas.top,
            width: cropBox.width,
            height: cropBox.height
          };
  
          ratio = image.width / image.naturalWidth;
  
          $.each(data, function (i, n) {
            n = n / ratio;
            data[i] = rounded ? Math.round(n) : n;
          });
  
        } else {
          data = {
            x: 0,
            y: 0,
            width: 0,
            height: 0
          };
        }
  
        if (options.rotatable) {
          data.rotate = image.rotate || 0;
        }
  
        if (options.scalable) {
          data.scaleX = image.scaleX || 1;
          data.scaleY = image.scaleY || 1;
        }
  
        return data;
      },
  
      /**
       * Set the cropped area position and size with new data
       *
       * @param {Object} data
       */
      setData: function (data) {
        var image = this.image;
        var canvas = this.canvas;
        var cropBoxData = {};
        var ratio;
  
        if ($.isFunction(data)) {
          data = data.call(this.$element);
        }
  
        if (this.built && !this.disabled && $.isPlainObject(data)) {
          if (isNumber(data.rotate) && data.rotate !== image.rotate &&
            this.options.rotatable) {
  
            image.rotate = data.rotate;
            this.rotated = true;
            this.renderCanvas(true);
          }
  
          ratio = image.width / image.naturalWidth;
  
          if (isNumber(data.x)) {
            cropBoxData.left = data.x * ratio + canvas.left;
          }
  
          if (isNumber(data.y)) {
            cropBoxData.top = data.y * ratio + canvas.top;
          }
  
          if (isNumber(data.width)) {
            cropBoxData.width = data.width * ratio;
          }
  
          if (isNumber(data.height)) {
            cropBoxData.height = data.height * ratio;
          }
  
          this.setCropBoxData(cropBoxData);
        }
      },
  
      /**
       * Get the container size data
       *
       * @return {Object} data
       */
      getContainerData: function () {
        return this.built ? this.container : {};
      },
  
      /**
       * Get the image position and size data
       *
       * @return {Object} data
       */
      getImageData: function () {
        return this.ready ? this.image : {};
      },
  
      /**
       * Get the canvas position and size data
       *
       * @return {Object} data
       */
      getCanvasData: function () {
        var canvas = this.canvas;
        var data;
  
        if (this.built) {
          data = {
            left: canvas.left,
            top: canvas.top,
            width: canvas.width,
            height: canvas.height
          };
        }
  
        return data || {};
      },
  
      /**
       * Set the canvas position and size with new data
       *
       * @param {Object} data
       */
      setCanvasData: function (data) {
        var canvas = this.canvas;
        var aspectRatio = canvas.aspectRatio;
  
        if ($.isFunction(data)) {
          data = data.call(this.$element);
        }
  
        if (this.built && !this.disabled && $.isPlainObject(data)) {
          if (isNumber(data.left)) {
            canvas.left = data.left;
          }
  
          if (isNumber(data.top)) {
            canvas.top = data.top;
          }
  
          if (isNumber(data.width)) {
            canvas.width = data.width;
            canvas.height = data.width / aspectRatio;
          } else if (isNumber(data.height)) {
            canvas.height = data.height;
            canvas.width = data.height * aspectRatio;
          }
  
          this.renderCanvas(true);
        }
      },
  
      /**
       * Get the crop box position and size data
       *
       * @return {Object} data
       */
      getCropBoxData: function () {
        var cropBox = this.cropBox;
        var data;
  
        if (this.built && this.cropped) {
          data = {
            left: cropBox.left,
            top: cropBox.top,
            width: cropBox.width,
            height: cropBox.height
          };
        }
  
        return data || {};
      },
  
      /**
       * Set the crop box position and size with new data
       *
       * @param {Object} data
       */
      setCropBoxData: function (data) {
        var cropBox = this.cropBox;
        var aspectRatio = this.options.aspectRatio;
        var widthChanged;
        var heightChanged;
  
        if ($.isFunction(data)) {
          data = data.call(this.$element);
        }
  
        if (this.built && this.cropped && !this.disabled && $.isPlainObject(data)) {
  
          if (isNumber(data.left)) {
            cropBox.left = data.left;
          }
  
          if (isNumber(data.top)) {
            cropBox.top = data.top;
          }
  
          if (isNumber(data.width) && data.width !== cropBox.width) {
            widthChanged = true;
            cropBox.width = data.width;
          }
  
          if (isNumber(data.height) && data.height !== cropBox.height) {
            heightChanged = true;
            cropBox.height = data.height;
          }
  
          if (aspectRatio) {
            if (widthChanged) {
              cropBox.height = cropBox.width / aspectRatio;
            } else if (heightChanged) {
              cropBox.width = cropBox.height * aspectRatio;
            }
          }
  
          this.renderCropBox();
        }
      },
  
      /**
       * Get a canvas drawn the cropped image
       *
       * @param {Object} options (optional)
       * @return {HTMLCanvasElement} canvas
       */
      getCroppedCanvas: function (options) {
        var originalWidth;
        var originalHeight;
        var canvasWidth;
        var canvasHeight;
        var scaledWidth;
        var scaledHeight;
        var scaledRatio;
        var aspectRatio;
        var canvas;
        var context;
        var data;
  
        if (!this.built || !this.cropped || !SUPPORT_CANVAS) {
          return;
        }
  
        if (!$.isPlainObject(options)) {
          options = {};
        }
  
        data = this.getData();
        originalWidth = data.width;
        originalHeight = data.height;
        aspectRatio = originalWidth / originalHeight;
  
        if ($.isPlainObject(options)) {
          scaledWidth = options.width;
          scaledHeight = options.height;
  
          if (scaledWidth) {
            scaledHeight = scaledWidth / aspectRatio;
            scaledRatio = scaledWidth / originalWidth;
          } else if (scaledHeight) {
            scaledWidth = scaledHeight * aspectRatio;
            scaledRatio = scaledHeight / originalHeight;
          }
        }
  
        canvasWidth = scaledWidth || originalWidth;
        canvasHeight = scaledHeight || originalHeight;
  
        canvas = $('<canvas>')[0];
        canvas.width = canvasWidth;
        canvas.height = canvasHeight;
        context = canvas.getContext('2d');
  
        if (options.fillColor) {
          context.fillStyle = options.fillColor;
          context.fillRect(0, 0, canvasWidth, canvasHeight);
        }
  
        // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage
        context.drawImage.apply(context, (function () {
          var source = getSourceCanvas(this.$clone[0], this.image);
          var sourceWidth = source.width;
          var sourceHeight = source.height;
          var args = [source];
  
          // Source canvas
          var srcX = data.x;
          var srcY = data.y;
          var srcWidth;
          var srcHeight;
  
          // Destination canvas
          var dstX;
          var dstY;
          var dstWidth;
          var dstHeight;
  
          if (srcX <= -originalWidth || srcX > sourceWidth) {
            srcX = srcWidth = dstX = dstWidth = 0;
          } else if (srcX <= 0) {
            dstX = -srcX;
            srcX = 0;
            srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX);
          } else if (srcX <= sourceWidth) {
            dstX = 0;
            srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX);
          }
  
          if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) {
            srcY = srcHeight = dstY = dstHeight = 0;
          } else if (srcY <= 0) {
            dstY = -srcY;
            srcY = 0;
            srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY);
          } else if (srcY <= sourceHeight) {
            dstY = 0;
            srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY);
          }
  
          args.push(srcX, srcY, srcWidth, srcHeight);
  
          // Scale destination sizes
          if (scaledRatio) {
            dstX *= scaledRatio;
            dstY *= scaledRatio;
            dstWidth *= scaledRatio;
            dstHeight *= scaledRatio;
          }
  
          // Avoid "IndexSizeError" in IE and Firefox
          if (dstWidth > 0 && dstHeight > 0) {
            args.push(dstX, dstY, dstWidth, dstHeight);
          }
  
          return args;
        }).call(this));
  
        return canvas;
      },
  
      /**
       * Change the aspect ratio of the crop box
       *
       * @param {Number} aspectRatio
       */
      setAspectRatio: function (aspectRatio) {
        var options = this.options;
  
        if (!this.disabled && !isUndefined(aspectRatio)) {
  
          // 0 -> NaN
          options.aspectRatio = num(aspectRatio) || NaN;
  
          if (this.built) {
            this.initCropBox();
  
            if (this.cropped) {
              this.renderCropBox();
            }
          }
        }
      },
  
      /**
       * Change the drag mode
       *
       * @param {String} mode (optional)
       */
      setDragMode: function (mode) {
        var options = this.options;
        var croppable;
        var movable;
  
        if (this.ready && !this.disabled) {
          croppable = options.dragCrop && mode === ACTION_CROP;
          movable = options.movable && mode === ACTION_MOVE;
          mode = (croppable || movable) ? mode : ACTION_NONE;
  
          this.$dragBox.
            data('action', mode).
            toggleClass(CLASS_CROP, croppable).
            toggleClass(CLASS_MOVE, movable);
  
          if (!options.cropBoxMovable) {
  
            // Sync drag mode to crop box when it is not movable(#300)
            this.$face.
              data('action', mode).
              toggleClass(CLASS_CROP, croppable).
              toggleClass(CLASS_MOVE, movable);
          }
        }
      }
    });
  