I am already using jQuery
plugin called jCrop
but recently I discovered KinectJs
and it really solves many problems for me. Then I stumbled upon this example:
http://www.html5canvastutorials.com/labs/html5-canvas-drag-and-drop-resize-and-invert-images/
And I decided to write my own cropping rectangle based on KinectJs
and above example.
function update(activeAnchor) {
var group = activeAnchor.getParent();
var topLeft = group.get('.topLeft')[0];
var topRight = group.get('.topRight')[0];
var bottomRight = group.get('.bottomRight')[0];
var bottomLeft = group.get('.bottomLeft')[0];
var cropper = group.get('.cropper')[0];
var leftMask = group.getParent().get('.leftMask')[0];
var rightMask = group.getParent().get('.rightMask')[0];
var topMask = group.getParent().get('.topMask')[0];
var bottomMask = group.getParent().get('.bottomMask')[0];
var anchorX = activeAnchor.getX();
var anchorY = activeAnchor.getY();
// update anchor positions
switch (activeAnchor.getName()) {
case 'topLeft':
topRight.setY(anchorY);
bottomLeft.setX(anchorX);
updateLeftMaskWidth(leftMask,activeAnchor);
updateTopMaskHeight(topMask,cropper,activeAnchor);
break;
case 'topRight':
topLeft.setY(anchorY);
bottomRight.setX(anchorX);
updateRightMaskWidthAndPos(rightMask,activeAnchor);
updateTopMaskHeight(topMask,cropper,activeAnchor);
break;
case 'bottomRight':
bottomLeft.setY(anchorY);
topRight.setX(anchorX);
updateRightMaskWidthAndPos(rightMask,activeAnchor);
updateBottomMaskHeightAndPos(bottomMask,cropper,activeAnchor);
break;
case 'bottomLeft':
bottomRight.setY(anchorY);
topLeft.setX(anchorX);
updateLeftMaskWidth(leftMask,activeAnchor);
updateBottomMaskHeightAndPos(bottomMask,cropper,activeAnchor);
break;
}
cropper.setPosition(topLeft.getPosition().x,topLeft.getPosition().y);
var width = topRight.getX() - topLeft.getX();
var height = bottomLeft.getY() - topLeft.getY();
if(width && height) {
cropper.setSize(width, height);
}
}
function updateLeftMaskWidth(mask,leftAnchor) {
mask.setWidth(leftAnchor.getAbsolutePosition().x - 100);
}
function updateRightMaskWidthAndPos(mask,rightAnchor) {
mask.setAbsolutePosition(rightAnchor.getAbsolutePosition().x,mask.getAbsolutePosition().y);
mask.setWidth(213 - (rightAnchor.getAbsolutePosition().x - 100));
}
function updateTopMaskHeight(mask,cropper,topAnchor) {
mask.setAbsolutePosition(topAnchor.getAbsolutePosition().x,mask.getAbsolutePosition().y);
mask.setHeight(topAnchor.getAbsolutePosition().y - 110);
mask.setWidth(cropper.getWidth());
}
function updateBottomMaskHeightAndPos(mask,cropper,bottomAnchor) {
mask.setAbsolutePosition(bottomAnchor.getAbsolutePosition().x, bottomAnchor.getAbsolutePosition().y);
mask.setHeight(236 - (bottomAnchor.getAbsolutePosition().y - 110));
mask.setWidth(cropper.getWidth());
}
function addAnchor(group, x, y, name) {
var stage = group.getStage();
var layer = group.getLayer();
var anchor = new Kinetic.Circle({
x: x,
y: y,
stroke: '#666',
fill: '#ddd',
strokeWidth: 1,
radius: 5,
name: name,
draggable: true,
dragBoundFunc: function(pos) {
var newX = pos.x;
var newY = pos.y;
var image = this.getParent().getParent().get('.image')[0];
var cropper = this.getParent();
// Bound horizontally
if(newX < 100) {
newX = 100;
}
else if(newX > image.getWidth() + 100 - cropper.getWidth()) {
newX = image.getWidth() + 100 - cropper.getWidth();
}
if(newY < 110) {
newY = 110;
}
else if(newY > image.getHeight() + 110 - cropper.getHeight()) {
newY = image.getHeight() + 110 - cropper.getHeight();
}
return {
x: newX,
y: newY
}
}
});
anchor.on('dragmove', function() {
update(this);
layer.draw();
});
// add hover styling
anchor.on('mouseover', function() {
var layer = this.getLayer();
document.body.style.cursor = 'pointer';
this.setStrokeWidth(2);
layer.draw();
});
anchor.on('mouseout', function() {
var layer = this.getLayer();
document.body.style.cursor = 'default';
this.setStrokeWidth(2);
layer.draw();
});
group.add(anchor);
}
function initStage(img) {
var stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 400
});
var imageGroup = new Kinetic.Group({
x: 100,
y: 110
});
var leftMaskGroup = new Kinetic.Group({
x: 100,
y: 110
});
var rightMaskGroup = new Kinetic.Group({
x: 270,
y: 110
});
var topMaskGroup = new Kinetic.Group({
x: 169.9,
y: 110
});
var bottomMaskGroup = new Kinetic.Group({
x: 169.9,
y: 150+138
});
var cropperGroup = new Kinetic.Group({
x: 170,
y: 150,
draggable: true,
dragBoundFunc: function(pos) {
var newX = pos.x;
var newY = pos.y;
var image = this.getParent().get('.image')[0];
var cropper = this.get('.cropper')[0];
// Bound horizontally
if(newX < 100) {
newX = 100;
}
else if(newX > image.getWidth() + 100 - cropper.getWidth()) {
newX = image.getWidth() + 100 - cropper.getWidth();
}
// Bound vertically
if(newY < 110) {
newY = 110;
}
else if(newY > image.getHeight() + 110 - cropper.getHeight()) {
newY = image.getHeight() + 110 - cropper.getHeight();
}
return {
x: newX,
y: newY
}
}
});
var layer = new Kinetic.Layer();
/*
* go ahead and add the groups
* to the layer and the layer to the
* stage so that the groups have knowledge
* of its layer and stage
*/
layer.add(imageGroup);
layer.add(leftMaskGroup);
layer.add(rightMaskGroup);
layer.add(topMaskGroup);
layer.add(bottomMaskGroup);
layer.add(cropperGroup);
stage.add(layer);
// cropping rectangle
var cropperRect = new Kinetic.Rect({
x: 0,
y: 0,
width: 100,
height: 138,
stroke: 'black',
name: 'cropper',
strokeWidth: 1
});
cropperGroup.add(cropperRect);
addAnchor(cropperGroup, 0, 0, 'topLeft');
addAnchor(cropperGroup, 100, 0, 'topRight');
addAnchor(cropperGroup, 100, 138, 'bottomRight');
addAnchor(cropperGroup, 0, 138, 'bottomLeft');
cropperGroup.on('dragstart', function() {
this.moveToTop();
});
cropperGroup.on('dragmove', function() {
var layer = this.getLayer();
var topLeft = this.get('.topLeft')[0];
var bottomLeft = this.get('.bottomLeft')[0];
var topRight = this.get('.topRight')[0];
var leftMask = this.getParent().get('.leftMask')[0];
var rightMask = this.getParent().get('.rightMask')[0];
var topMask = this.getParent().get('.topMask')[0];
var bottomMask = this.getParent().get('.bottomMask')[0];
updateLeftMaskWidth(leftMask,topLeft);
updateRightMaskWidthAndPos(rightMask,topRight);
updateTopMaskHeight(topMask,this.get('.cropper')[0],topLeft);
updateBottomMaskHeightAndPos(bottomMask,this.get('.cropper')[0],bottomLeft);
layer.draw();
});
// left mask
var leftMaskRect = new Kinetic.Rect({
x: 0,
y: 0,
width: 70,
height: 236,
fill: 'black',
name: 'leftMask',
strokeWidth: 0,
opacity: 0.5
});
leftMaskGroup.add(leftMaskRect);
// right mask
var rightMaskRect = new Kinetic.Rect({
x: 0,
y: 0,
width: 213-170,
height: 236,
fill: 'black',
name: 'rightMask',
strokeWidth: 0,
opacity: 0.5
});
rightMaskGroup.add(rightMaskRect);
// top mask
var topMaskRect = new Kinetic.Rect({
x: 0,
y: 0,
width: 100.2,
height: 150-110,
fill: 'black',
name: 'topMask',
strokeWidth: 0,
opacity: 0.5
});
topMaskGroup.add(topMaskRect);
// bottom mask
var bottomMaskRect = new Kinetic.Rect({
x: 0,
y: 0,
width: 100.2,
height: 236-138-(150-110),
fill: 'black',
name: 'bottomMask',
strokeWidth: 0,
opacity: 0.5
});
bottomMaskGroup.add(bottomMaskRect);
// image
var srcImg = new Kinetic.Image({
x: 0,
y: 0,
image: img,
name: 'image'
});
imageGroup.add(srcImg);
stage.draw();
}
var img = new Image();
img.onload = function() {
initStage(this);
}
img.src = 'http://www.html5canvastutorials.com/demos/assets/yoda.jpg';
It works almost perfectly. The problem is that when you resize using the circle anchors and after a few tries (just give it a few shots) when you try to drag the whole rectangle it allows you to drag it out of bounds !
From my debugging this seems like an issue with the library but if sb. sees the problem in my code or sees a way to optimize it please share your thoughts.
The result of my effort can be seen here:
A) Reproducing the bug:
Similar behaviour for the other anchors and directions.
B) The solution:
Add this function to the script:
function readjust() {
var group = this.getParent();
var topLeft = group.get('.topLeft')[0];
var topRight = group.get('.topRight')[0];
var bottomRight = group.get('.bottomRight')[0];
var bottomLeft = group.get('.bottomLeft')[0];
var cropper = group.get('.cropper')[0];
var tlx = cropper.getX(),
tly = cropper.getY(),
w = cropper.getWidth(),
h = cropper.getHeight();
group.setX(group.getX() + tlx);
group.setY(group.getY() + tly);
topLeft.setPosition(0,0);
topRight.setPosition(w,0);
bottomLeft.setPosition(0,h);
bottomRight.setPosition(w,h);
cropper.setPosition(0,0);
}
And the following handler in addAnchor()
:
anchor.on('dragend', readjust);
Fiddle: http://jsfiddle.net/BMy7b/1/
Alternatively the code from readjust()
can be included in update()
, as suggested by MarmiK. This would run more operations on every drag thus might be slower (but I am not sure; opinions?). Fiddle: http://jsfiddle.net/vUPeQ/1/
The problem was that the cropper and anchors moved relatively to the cropperGroup
, but the cropperGroup
drag bounding function wasn't taking it into account.
Have fun!