Search code examples
htmlimagecanvascropjcrop

Kinect JS cropping rectangle


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 !

Illustration of the problem

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:

http://jsfiddle.net/wanderer/WLpXF/


Solution

  • A) Reproducing the bug:

    1. Drag a top anchor upwards (say it was dragged N pixels)
    2. Drag the entire cropper upwards; it can leave the image boundaries by N pixels

    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!