/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* $Id: ImageCropper.js 616 2013-04-22 23:48:38Z geert $
*/
"use strict";
SUI.control.ImageCropper = SUI.defineClass(
/** @lends SUI.control.ImageCropper.prototype */{
/** @ignore */ baseClass: SUI.Box,
/**
* @class
* SUI.control.ImageCropper is image cropping tool. With this tool you can
* select a rectangular area of a an image. You can send this selection
* together with the desired image width (and/or height) to a server that
* can do the actual image cropping and resizing. The tool allows you to
* set a target width or height, in that case you can make a selection that
* is not bound to a ratio. Or to set the target width and height height,
* in that case the selection will have a fixed ratio.
*
* @augments SUI.Box
*
* @description
* Create a SUI.control.ImageCropper.
*
* @constructs
* @param see base class
* @param {int} arg.outputHeight The desired height of the image to create
* by the server side procedure.
* @param {int} arg.outputWidth The desired width of the image to create
* by the server side procedure.
* @param {String} arg.image Location of the image to crop.
*/
initializer: function(arg) {
SUI.control.Date.initializeBase(this, arg);
// set the output/target dimensions for the image
this._outputHeight = arg.outputHeight || 0;
this._outputWidth = arg.outputWidth || 0;
// show the picture behind the cropper div
this.el().style.backgroundImage = "url("+arg.image+")";
// the dragger handles might move a little over the edge, display them
this.el().style.overflow = "visible";
// create a structure that holds the four rectangles that gray out the
// are to crop
this._crop = {n: null, e: null, s: null, w: null};
// create the rectangle boxes
for (var i in this._crop) {
this._crop[i] = new SUI.Box({parent: this});
this._crop[i].el().appendChild(document.createElement("SPAN"));
this._crop[i].addClass("sui-icr-crop");
this._crop[i].el().style.cursor = "crosshair";
}
// create the center 'see-through' box
this._croparea = new SUI.Box({parent: this});
// the dragger handles might move a little over the edge, display them
this._croparea.el().style.overflow = "visible";
this._croparea.el().style.cursor = "move";
this._croparea.border(new SUI.Border(1));
this._croparea.addClass("sui-icr-croparea");
// add the handler for moving this area
this._addDragHandler(this._croparea);
// create a structure that holds the eight dragger handles
this._handle = {
n: { cursor:"n-resize", top: "-5", left: "0" },
nw: { cursor:"nw-resize", top: "-5", left: "-5" },
w: { cursor:"w-resize", top: "0", left: "-5" },
sw: { cursor:"sw-resize", bottom:"-5", left: "-5" },
s: { cursor:"s-resize", bottom:"-5", right: "0" },
se: { cursor:"se-resize", bottom:"-5", right:"-5" },
e: { cursor:"e-resize", top: "0", right:"-5" },
ne: { cursor:"ne-resize", top: "-5", right:"-5" }
};
// create the handlers
for (var i in this._handle) {
// create handles on the crop area
var h = new SUI.Box({parent: this._croparea });
// set the size of the handles
h.el().style.width = this.DRAGGER_SIZE + "px";
h.el().style.height = this.DRAGGER_SIZE + "px";
// correct the position with the offset find in the data structure
if (this._handle[i].top) {
h.el().style.top = this._handle[i].top + "px";
}
if (this._handle[i].right) {
h.el().style.right = this._handle[i].right + "px";
}
if (this._handle[i].left) {
h.el().style.left = this._handle[i].left + "px";
}
if (this._handle[i].bottom) {
h.el().style.bottom = this._handle[i].bottom + "px";
}
// set the style
h.el().style.cursor = this._handle[i].cursor;
h.addClass("sui-icr-handle");
// some content for the div (what browser bug was that?)
h.el().appendChild(document.createElement("SPAN"));
// add the handler for moving this handle
this._addDragHandler(h);
// overwrite the entry in the data structure with the handle
this._handle[i] = h;
}
// initialize draw the crop area
this._rescale();
},
/**
* Size (width/height) of the dragger handlers.
*/
DRAGGER_SIZE: 7,
/**
* Minimum size (width or height) of the crop area
*/
MIN_SIZE: 16,
/**
* Get the image cropper data. This is all the data that a server side
* procedure needs to cut and resize the image as requested.
* @param {Object} Object with the following members:
* - {int} originalWidth Width of the image as shown in the cropper
* - {int} originalHeight Height of the image as shown in the cropper
* - {int} cropWidth Width of the selected region
* - {int} cropHeight Height of the selected region
* - {int} cropTop Top of the selected region
* - {int} cropLeft Left of the selected region
* - {int} newWidth Desired width of the picture to create
* - {int} newHeight Desired height of the picture to create
*/
data: function() {
var ow = this._outputWidth;
var oh = this._outputHeight;
// the desired picture has no width of height
if (ow==0 && oh==0) {
return null;
}
// if the desired width of the picture was not set calculate it
if (ow == 0) {
ow = Math.round(
this._croparea.width() * oh / this._croparea.height());
}
// if the desired height of the picture was not set calculate it
if (oh == 0) {
oh = Math.round(
this._croparea.height() * ow / this._croparea.width());
}
// fill a data structure with all relevant data and return it
return {
originalWidth: this.width(),
originalHeight: this.height(),
cropWidth: this._croparea.width(),
cropHeight: this._croparea.height(),
cropTop: this._croparea.top(),
cropLeft: this._croparea.left(),
newWidth: ow,
newHeight: oh
};
},
/**
* Set the desired output size for the image and redraw the control. If
* both are set the crop area will be resized using a fixed ratio of the
* given width and height. If the width and not the height or vice verse
* were given then you can size the crop area in all directions.
* @param {int} w The desired target width of the image
* @param {int} h The desired height width of the image
*/
targetSize: function(w, h) {
// set the output width
this._outputWidth = parseInt(w, 10);
if (isNaN(this._outputWidth)) {
this._outputWidth = 0;
}
// set the output height
this._outputHeight = parseInt(h, 10);
if (isNaN(this._outputHeight)) {
this._outputHeight = 0;
}
// and reinitialize and draw the control ...
this._rescale();
// ... show only the handles that we need
this._activateHandles();
},
// reference to the drag listener function so we can remove it later
_ehDrag: null,
// reference to the end drag listener function so we can remove it later
_ehEndDrag: null,
// object with references to the four rectangles around the crop area
_crop: null,
// start position of the mouse when starting to drag
_dragStartPos: null,
// object with references to all of the resize handlers
_handle: null,
// height of the output rectangle
_outputHeight: 0,
// width of the output rectangle
_outputWidth: 0,
// the width/height ratio to retain while dragging
_ratio: 0,
// height of the crop area when dragging starts
_startHeight: 0,
// left of the crop area when dragging starts
_startLeft: 0,
// top of the crop area when dragging starts
_startTop: 0,
// width of the crop area when dragging starts
_startWidth: 0,
// when we keep the ratio while dragging, we don't need the handles
// on the side of the crop area: remove them
_activateHandles: function() {
// should we show ...
var d = "block";
if (this._ratio) {
// ... or hide the handles
d = "none";
}
this._handle.n.el().style.display = d;
this._handle.w.el().style.display = d;
this._handle.e.el().style.display = d;
this._handle.s.el().style.display = d;
},
_addDragHandler: function(h) {
var that = this;
// h and that are the two closure variables
SUI.browser.addEventListener(h.el(), "mousedown",
function(e) {
if (!that._startDrag(h, new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
}
);
},
// recalculate the size and position of the crop area while dragging one
// of the handles
_drag: function(el, ev) {
// the current mouse position
var p = {x: SUI.browser.getX(ev.event), y: SUI.browser.getY(ev.event)};
if (el == this._croparea) {
// if the crop area was selected, move the crop area
this._moveCropArea(p);
} else if (this._ratio) {
// we're dragging the corners and should keep that ration fixed
if (el == this._handle.se) {
this._handle_se(p);
} else if (el == this._handle.ne) {
this._handle_ne(p);
} else if (el == this._handle.nw) {
this._handle_nw(p);
} else if (el == this._handle.sw) {
this._handle_sw(p);
}
} else {
// we can freely size in any direction, no ratio to worry about
if (el == this._handle.se) {
this._handle_s(p);
this._handle_e(p);
} else if (el == this._handle.ne) {
this._handle_n(p);
this._handle_e(p);
} else if (el == this._handle.nw) {
this._handle_n(p);
this._handle_w(p);
} else if (el == this._handle.sw) {
this._handle_s(p);
this._handle_w(p);
} else if (el == this._handle.e) {
this._handle_e(p);
} else if (el == this._handle.n) {
this._handle_n(p);
} else if (el == this._handle.w) {
this._handle_w(p);
} else if (el == this._handle.s) {
this._handle_s(p);
}
}
// redisplay the new crop area
this._redraw();
},
// The difference between the start position and the current position in
// horizontal direction while dragging.
_dx: function(p) {
return p.x - this._dragStartPos.x;
},
// The difference between the start position and the current position in
// vertical direction while dragging.
_dy: function(p) {
return p.y - this._dragStartPos.y;
},
// remove the listeners used for dragging
_endDrag: function(e) {
// Unregister the event handlers ...
SUI.browser.removeEventListener(document, "mousemove", this._ehDrag);
SUI.browser.removeEventListener(document, "mouseup", this._ehEndDrag);
},
// Handle dragging of the three east draggers (no fixed ratio)
_handle_e: function(p) {
// set the new croparea width
this._croparea.width(this._startWidth + this._dx(p));
// if we've gone to far, set croparea width to max/min allowed
if (this._croparea.width() < this.MIN_SIZE) {
this._croparea.width(this.MIN_SIZE);
}
if (this._croparea.left() + this._croparea.width() > this.width()) {
this._croparea.width(this.width() - this._croparea.left());
}
},
// Handle dragging of the three north draggers (no fixed _ratio)
_handle_n: function(p) {
// get the bottom first, we might need it later on
var b = this._croparea.top() + this._croparea.height();
// set the new croparea height and top
this._croparea.height(this._startHeight - this._dy(p));
this._croparea.top(this._startTop + this._dy(p));
// if we've gone to far, set croparea height to max/min allowed
if (this._croparea.height() < this.MIN_SIZE) {
this._croparea.height(this.MIN_SIZE);
this._croparea.top(b - this.MIN_SIZE);
}
if (this._croparea.top() < 0) {
this._croparea.height(b);
this._croparea.top(0);
}
},
// Handle dragging of the north-east corner while keeping a fixed _ratio
_handle_ne: function(p) {
// get the new size ...
var s = this._ratioSize(this._dx(p), -this._dy(p));
// ... and check if it's within boundaries ...
if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
&& this._croparea.left() + s.w <= this.width()
&& this._croparea.top() + this._croparea.height() - s.h >= 0) {
// ... yes: set top, width and height of crop area
this._croparea.top(
this._croparea.top() + this._croparea.height() - s.h);
this._croparea.width(s.w);
this._croparea.height(s.h);
}
},
// Handle dragging of the north-west corner while keeping a fixed _ratio
_handle_nw: function(p) {
// get the new size ...
var s = this._ratioSize(-this._dx(p), -this._dy(p));
// ... and check if it's within boundaries ...
if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
&& this._croparea.left() + this._croparea.width() - s.w >= 0
&& this._croparea.top() + this._croparea.height() - s.h >= 0) {
// ... yes: set top, left, width and height of crop area
this._croparea.left(
this._croparea.left() + this._croparea.width() - s.w);
this._croparea.top(
this._croparea.top() + this._croparea.height() - s.h);
this._croparea.width(s.w);
this._croparea.height(s.h);
}
},
// Handle dragging of the three south draggers (no fixed _ratio)
_handle_s: function(p) {
// set the new croparea height
this._croparea.height(this._startHeight + this._dy(p));
// if we've gone to far, set croparea height to max/min allowed
if (this._croparea.height() < this.MIN_SIZE) {
this._croparea.height(this.MIN_SIZE);
}
if (this._croparea.top() + this._croparea.height() > this.height()) {
this._croparea.height(this.height() - this._croparea.top());
}
},
// Handle dragging of the south-east corner while keeping a fixed _ratio
_handle_se: function(p) {
// get the new size ...
var s = this._ratioSize(this._dx(p), this._dy(p));
// ... and check if it's within boundaries ...
if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
&& this._croparea.left() + s.w <= this.width()
&& this._croparea.top() + s.h <= this.height()) {
// ... yes: set width and height of crop area
this._croparea.width(s.w);
this._croparea.height(s.h);
}
},
// Handle dragging of the south-west corner while keeping a fixed _ratio
_handle_sw: function(p) {
// get the new size ...
var s = this._ratioSize(-this._dx(p), this._dy(p));
// ... and check if it's within boundaries ...
if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
&& this._croparea.left() + this._croparea.width() - s.w >= 0
&& this._croparea.top() + s.h <= this.height()) {
// ... yes: set left, width and height of crop area
this._croparea.left(
this._croparea.left() + this._croparea.width() - s.w);
this._croparea.width(s.w);
this._croparea.height(s.h);
}
},
// Handle dragging of the three west draggers (no fixed _ratio)
_handle_w: function(p) {
// get the right first, we might need it later on
var r = this._croparea.left() + this._croparea.width();
// set the new _croparea width and left
this._croparea.width(this._startWidth - this._dx(p));
this._croparea.left(this._startLeft + this._dx(p));
// if we've gone to far, set croparea width to max/min allowed
if (this._croparea.width() < this.MIN_SIZE) {
this._croparea.width(this.MIN_SIZE);
this._croparea.left(r - this.MIN_SIZE);
}
if (this._croparea.left() < 0) {
this._croparea.width(r);
this._croparea.left(0);
}
},
// move the crop area
_moveCropArea: function(p) {
// set the new top and left
this._croparea.top(this._startTop + this._dy(p));
this._croparea.left(this._startLeft + this._dx(p));
// correct it if we've moved too far
if (this._croparea.top() < 0) {
this._croparea.top(0);
}
if (this._croparea.left() < 0) {
this._croparea.left(0);
}
if (this._croparea.left()+this._croparea.width() > this.width()) {
this._croparea.left(this.width()-this._croparea.width());
}
if (this._croparea.top()+this._croparea.height() > this.height()) {
this._croparea.top(this.height()-this._croparea.height());
}
},
// Get the (proposed) new size when resizing with a fixed _ratio.
_ratioSize: function(dx, dy) {
// get the new width an height by adding the delta ...
var r = {
w: this._startWidth + dx,
h: this._startHeight + dy
};
// .. and correct the new width and height for the given _ratio
if (r.w/r.h > this._ratio) {
r.h = Math.round(r.w / this._ratio);
} else {
r.w = Math.round(r.h * this._ratio);
}
// return the result
return r;
},
// Redraw the area: set the size of the inner rectangle and the rectangles
// that darken outer area, set the center positions of the north, south,
// west and east dragger handles.
_redraw: function() {
// set the CSS size of rectangle on the west side of the croparea
this._crop.w.left(0);
this._crop.w.top(this._croparea.top());
this._crop.w.width(this._croparea.left());
this._crop.w.height(this._croparea.height());
// set the CSS size of rectangle on the east side of the croparea
this._crop.e.top(this._croparea.top());
this._crop.e.left(this._croparea.width() + this._croparea.left());
this._crop.e.width(this.width() - this._croparea.width()
- this._croparea.left());
this._crop.e.height(this._croparea.height());
// set the CSS size of rectangle on the north side of the croparea
this._crop.n.top(0);
this._crop.n.width(this.width());
this._crop.n.height(this._croparea.top());
// set the CSS size of rectangle on the south side of the croparea
this._crop.s.top(this._croparea.height() + this._croparea.top());
this._crop.s.width(this.width());
this._crop.s.height(this.height() - this._croparea.height()
- this._croparea.top());
// set the CSS size of the rectangles
this._croparea.setDim();
this._crop.w.setDim();
this._crop.e.setDim();
this._crop.n.setDim();
this._crop.s.setDim();
// set the dragger handles on the sides to the center of the sides
// of the croparea
this._handle.n.el().style.left =
Math.round(this._croparea.width()/2) - 5 + "px";
this._handle.s.el().style.left =
Math.round(this._croparea.width()/2) - 5 + "px";
this._handle.e.el().style.top =
Math.round(this._croparea.height()/2) - 5 + "px";
this._handle.w.el().style.top =
Math.round(this._croparea.height()/2) - 5 + "px";
},
// the output width and height are changed, reinitialize draw the crop area
_rescale: function() {
// is desired output width and height larger than 0 ? ...
if (this._outputWidth > 0 && this._outputHeight > 0) {
// ... we use a fixed _ratio while resizing, calculate the _ratio
this._ratio = this._outputWidth/this._outputHeight;
// create a rectangle with the required _ratio of .9 times the
// height or width of the area, whichever direction fits.
var w, h;
if (this.width() / this.height() < this._ratio) {
w = Math.round(this.width() * .9);
h = Math.round(w / this._ratio);
} else {
h = Math.round(this.height() * .9);
w = Math.round(h * this._ratio);
}
// set the croparea size ...
this._croparea.width(w);
this._croparea.height(h);
// ... and position
this._croparea.left(Math.round(this.width()-w-4)/2);;
this._croparea.top(Math.round(this.height()-h-4)/2);
} else {
// ... no: then we don't use a _ratio: all sides and corners can
// be scaled freely ...
this._ratio = 0;
// set the croparea to a size somewhat smaller than the picture
// size
this._croparea.width(Math.round(this.width()*.9));
this._croparea.height(Math.round(this.height()*.9));
this._croparea.left(Math.round(this.width()*.05));;
this._croparea.top(Math.round(this.height()*.05));
}
// redisplay the new crop area
this._redraw();
},
// start the dragging of a handle or the crop area itself. remember
// the start position/size of the croparea and mouse click and add the
// mousemove and mouseup event listeners
_startDrag: function(el, ev) {
// store the start postion and size of the crop area
this._startWidth = this._croparea.width();
this._startHeight = this._croparea.height();
this._startTop = this._croparea.top();
this._startLeft = this._croparea.left();
// start the position of the mouse click
this._dragStartPos = {
x: SUI.browser.getX(ev.event),
y: SUI.browser.getY(ev.event)
};
// add the event listeners for dragging and stop dragging
var that = this;
SUI.browser.addEventListener(document, "mousemove",
this._ehDrag = function(e) {
if (!that._drag(el, new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
}
);
SUI.browser.addEventListener(document, "mouseup",
this._ehEndDrag = function(e) {
if (!that._endDrag(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
}
);
}
});