John
John

Reputation: 2033

Crop Functionality using FabricJs

How to implement crop tool on the image that is loaded on the canvas using fabric.js ? I have a image loaded on the canvas .Now i want to implement crop tool where the user is allowed to crop the image and reload it on to the canvas when he is done.

Upvotes: 23

Views: 39670

Answers (3)

oriongu
oriongu

Reputation: 120

1) VERSION 1 (moving the crop area)

https://jsfiddle.net/oriongu/f2yv3r7u/latest/

  1. Double click image to start the cropping process
  2. Move/Scale cropping area while preventing crop area to to leave image area
  3. Deselect crop area to finalize image cropping

var canvas = new fabric.Canvas('c');
canvas.background = '#FFFFFF';
canvas.renderAll();

var imageUrl = "https://loremflickr.com/400/260";
  
fabric.Image.fromURL(imageUrl, function (oImg) {

    // fake initial crop 
    let cropRatio = 50;
  oImg.cropX = 0;
  oImg.cropY = 0;
  oImg.width = oImg.width - cropRatio;
  oImg.height = oImg.height - cropRatio;
  oImg.top = (canvas.height / 2) - oImg.getScaledHeight() / 2;
  oImg.left = (canvas.width / 2) - oImg.getScaledWidth() / 2;
  
  canvas.add(oImg);
  
});

canvas.on({
  'mouse:dblclick': function(obj){ 
    var target = obj.target ? obj.target : null;
    if(target && target.type === 'image'){
      prepareCrop(target);
    }
  }
});

function prepareCrop(e){

  var i = new fabric.Rect({
    id: "crop-rect",
    top: e.top,
    left: e.left,
    angle: e.angle,
    width: e.getScaledWidth(),
    height: e.getScaledHeight(),
    stroke: "rgb(42, 67, 101)",
    strokeWidth: 2,
    strokeDashArray: [5, 5],
    fill: "rgba(255, 255, 255, 1)",
    globalCompositeOperation: "overlay",
    lockRotation: true,
  });

  var a = new fabric.Rect({
    id: "overlay-rect",
    top: e.top,
    left: e.left,
    angle: e.angle,
    width: e.getScaledWidth(),
    height: e.getScaledHeight(),
    selectable: !1,
    selection: !1,
    fill: "rgba(0, 0, 0, 0.5)",
    lockRotation: true,
  });

  var s = e.cropX,
      o = e.cropY,
      c = e.width,
      l = e.height;
  e.set({
    cropX: null,
    cropY: null,
    left: e.left - s * e.scaleX,
    top: e.top - o * e.scaleY,
    width: e._originalElement.naturalWidth,
    height: e._originalElement.naturalHeight,
    dirty: false
  });
  i.set({
    left: e.left + s * e.scaleX,
    top: e.top + o * e.scaleY,
    width: c * e.scaleX,
    height: l * e.scaleY,
    dirty: false
  });
  a.set({
    left: e.left,
    top: e.top,
    width: e.width * e.scaleX,
    height: e.height * e.scaleY,
    dirty: false
  });
  i.oldScaleX = i.scaleX;
  i.oldScaleY = i.scaleY;

  canvas.add(a),
    canvas.add(i),
    canvas.discardActiveObject(),
    canvas.setActiveObject(i),
    canvas.renderAll(),

    //
    i.on("moving", function () {
    (i.top < e.top || i.left < e.left) &&
      ((i.left = i.left < e.left ? e.left : i.left),
       (i.top = i.top < e.top ? e.top : i.top)),
      (i.top + i.getScaledHeight() > e.top + e.getScaledHeight() ||
       i.left + i.getScaledWidth() > e.left + e.getScaledWidth()) &&
      ((i.top =
        i.top + i.getScaledHeight() > e.top + e.getScaledHeight()
        ? e.top + e.getScaledHeight() - i.getScaledHeight()
        : i.top),
       (i.left =
        i.left + i.getScaledWidth() > e.left + e.getScaledWidth()
        ? e.left + e.getScaledWidth() - i.getScaledWidth()
        : i.left));
  });

  i.on("scaling", function () {

  });

  //
  i.on("deselected", function () {
    cropImage(i, e);
    canvas.remove(a);
  });
}

function cropImage(i, e){
    
  // remove plaeholder
  canvas.remove(i);

  //
  var s = (i.left - e.left) / e.scaleX,
    o = (i.top - e.top) / e.scaleY,
    c = (i.width * i.scaleX) / e.scaleX,
    l = (i.height * i.scaleY) / e.scaleY;

  // crop
  e.set({
    cropX: s,
    cropY: o,
    width: c,
    height: l,
    top: e.top + o * e.scaleY,
    left: e.left + s * e.scaleX,
    selectable: true,
    cropped: 1
  });

  canvas.renderAll();
  
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<canvas id="c" width="600" height="600" style=" border: 2px solid #5d9eab;"></canvas>
<small>Double click on image to start cropping</small><br>
<small>Move or scale crop area as desired</small><br>
<small>Deselect to finish cropping</small><br>

2) VERSION 2 (moving the image canva.com style)

https://jsfiddle.net/oriongu/0ogmnrwd/latest/

  1. Double click image to start the cropping process
  2. Move/Scale image while preventing image to leave crop area
  3. Deselect image to finalize cropping

var canvas = new fabric.Canvas('c');
canvas.background = '#FFFFFF';
canvas.renderAll();

var imageUrl = "https://loremflickr.com/400/260";
  
fabric.Image.fromURL(imageUrl, function (oImg) {

    // fake initial crop 
    let cropRatio = 50;
  oImg.cropX = 0;
  oImg.cropY = 0;
  oImg.width = oImg.width - cropRatio;
  oImg.height = oImg.height - cropRatio;
  oImg.top = (canvas.height / 2) - oImg.getScaledHeight() / 2;
  oImg.left = (canvas.width / 2) - oImg.getScaledWidth() / 2;
  
  // add to canvas
  canvas.add(oImg);
});

canvas.on({

    // listen to double click
  'mouse:dblclick': function(obj){ 
    var target = obj.target ? obj.target : null;
    if(target && target.type === 'image'){
      prepareCrop(canvas, target);
    }
  }
});

function prepareCrop(canvas){
  var img = canvas.getActiveObject();
  if (img) {

    var s = img.cropX * img.scaleX,
      o = img.cropY * img.scaleY,
      p = img.getScaledWidth(),
      q = img.getScaledHeight();

    console.log('PREPARE CROP', s, o, p, q);

    img.set({
      clipPath: null,
      cropX: null,
      cropY: null,
      left: img.left - s ,
      top: img.top - o ,
      width: img._originalElement.naturalWidth,
      height: img._originalElement.naturalHeight,
      dirty: false,
      opacity: .5,
      lockRotation: true,
      selectable: true,
    });

    // overlay bloc (gray)
    var i = new fabric.Rect({
      id: "crop-rect",
      left: img.left + s,
      top: img.top + o,
      angle: img.angle,
      width: p,
      height: q,
      stroke: "rgb(42, 67, 101)",
      strokeWidth: 1,
      strokeDashArray: [5, 5],
      fill: "rgba(255, 255, 255, 1)",
      lockRotation: true,
      selectable: false
    });

    canvas.add(i);
    canvas.bringToFront(img);
    canvas.renderAll();

    //
    img.on("moving", function () {
      // should constrain image in crop area
      // similar to canva.com crop function
      // we can still increase image scale during this phase
      if(img.left > i.left){ img.left = i.left; } // LEFT
      if(img.top > i.top){ img.top = i.top; } // TOP
      if((img.left+img.getScaledWidth()) < (i.left + i.getScaledWidth())){ img.left = i.left - (img.getScaledWidth() - i.getScaledWidth()); } // RIGHT
      if((img.top+img.getScaledHeight()) < (i.top + i.getScaledHeight())){ img.top = i.top - (img.getScaledHeight() - i.getScaledHeight()); } // RIGHT
    });

    img.on("scaling", function () {
            // to be improved, similar to the moving event
    });

    //
    img.on("deselected", function () {
      cropImage(canvas, i, img);
    });

  }
}

function cropImage(canvas, cropArea, oImg) {

  //
  var cropX = (cropArea.left - oImg.left) / oImg.scaleX,
    cropY = (cropArea.top - oImg.top) / oImg.scaleY,
    width = (cropArea.width * cropArea.scaleX) / oImg.scaleX, // correct
    height = (cropArea.height * cropArea.scaleY) / oImg.scaleY; // correct

  console.log('FINISH CROP', cropX, oImg.scaleX, cropY, oImg.scaleY);

  // crop
  oImg.set({
    cropX: cropX, //
    cropY: cropY, //
    width: width, //
    height: height, //
    top: cropArea.top,
    left: cropArea.left,
    selectable: true,
    lockRotation: false,
    cropped: 1,
    opacity: 1,
  });

  // remove events
  oImg.off('scaling');
  oImg.off('deselected');
  oImg.off('moving');

  // remove crop area
  canvas.remove(cropArea);

}
 

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<canvas id="c" width="600" height="600" style=" border: 2px solid #5d9eab;"></canvas>
<small>Double click on image to start cropping</small><br>
<small>Move or scale image as desired</small><br>
<small>Deselect to finish cropping</small><br>

Upvotes: 2

Kirby
Kirby

Reputation: 15875

(This answer is an iteration on the fiddle in Tom's answer. Thank you, Tom, for getting me down the road.)

You can crop in Fabric.js using either fabric.Object.clipTo() or fabric.Object.toDataURL(). The clipTo() method retains the original image and displays a crop via a mask. The toDataURL() method creates a new image.

My full example uses the clipTo() method. I have included a small chunk of code at the end showing the toDataURL() method.

Solution 1

Summary

  1. Prepare an invisible rectangle. Have it draw when the user clicks and drags mouse across canvas.
  2. When user releases mouse click, clip the underlying image

Difference's from Tom's answer

In the fiddle in Tom's answer, there are a couple of minor things I wanted to change. So in my example the differences are

  1. The crop box work from left to right and right to left (Tom's works from right to left only)

  2. You have more than one chance to draw the crop box (attempting to re-draw the crop box in Tom's causes box to jump)

  3. Works with Fabric.js v1.5.0

  4. Less code.

The Code

 // set to the event when the user pressed the mouse button down
var mouseDown;
// only allow one crop. turn it off after that
var disabled = false;
var rectangle = new fabric.Rect({
    fill: 'transparent',
    stroke: '#ccc',
    strokeDashArray: [2, 2],
    visible: false
});
var container = document.getElementById('canvas').getBoundingClientRect();
var canvas = new fabric.Canvas('canvas');
canvas.add(rectangle);
var image;
fabric.util.loadImage("http://fabricjs.com/lib/pug.jpg", function(img) {
    image = new fabric.Image(img);
    image.selectable = false;
    canvas.setWidth(image.getWidth());
    canvas.setHeight(image.getHeight());
    canvas.add(image);
    canvas.centerObject(image);
    canvas.renderAll();
});
// capture the event when the user clicks the mouse button down
canvas.on("mouse:down", function(event) {
    if(!disabled) {
        rectangle.width = 2;
        rectangle.height = 2;
        rectangle.left = event.e.pageX - container.left;
        rectangle.top = event.e.pageY - container.top;
        rectangle.visible = true;
        mouseDown = event.e;
        canvas.bringToFront(rectangle);
    }
});
// draw the rectangle as the mouse is moved after a down click
canvas.on("mouse:move", function(event) {
    if(mouseDown && !disabled) {
        rectangle.width = event.e.pageX - mouseDown.pageX;
        rectangle.height = event.e.pageY - mouseDown.pageY;
        canvas.renderAll();
    }
});
// when mouse click is released, end cropping mode
canvas.on("mouse:up", function() {
    mouseDown = null;
});
$('#cropB').on('click', function() {
    image.clipTo = function(ctx) {
        // origin is the center of the image
        var x = rectangle.left - image.getWidth() / 2;
        var y = rectangle.top - image.getHeight() / 2;
        ctx.rect(x, y, rectangle.width, rectangle.height);
    };
    image.selectable = true;
    disabled = true;
    rectangle.visible = false;
    canvas.renderAll();
});
<head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" type="text/javascript"></script>
</head>
<body>
<canvas style="border: 1px solid black" id="canvas"></canvas>
<button id=cropB>crop</button>
</body>

Solution 2

Or, instead of using clipTo() like above, we could generate a new image using toDataURL(). Something like this

$('#cropB').on('click', function() {
    image.selectable = true;
    disabled = true;
    rectangle.visible = false;
    var cropped = new Image();
    cropped.src = canvas.toDataURL({
        left: rectangle.left,
        top: rectangle.top,
        width: rectangle.width,
        height: rectangle.height
    });
    cropped.onload = function() {
        canvas.clear();
        image = new fabric.Image(cropped);
        image.left = rectangle.left;
        image.top = rectangle.top;
        image.setCoords();
        canvas.add(image);
        canvas.renderAll();
    };
});

Upvotes: 19

Tom
Tom

Reputation: 4662

In Summary

  1. set el.selectable = false
  2. draw a rectangle on it with mouse event
  3. get rectangle left/top and width/height
  4. then use the following function

Sorry, let me explain. The ctx.rect will crop the image from the center point of the object. Also the scaleX factor should be taken into account.

x = select_el.left - object.left;
y = select_el.top - object.top;

x *= 1 / scale;
y *= 1 / scale;

width = select_el.width * 1 / scale;
heigh = select_el.height * 1 / scale;

object.clipTo = function (ctx) {
    ctx.rect (x, y, width, height);
}

Complete example: http://jsfiddle.net/hellomaya/kNEaX/1/

And check out this http://jsfiddle.net/hellomaya/hzqrM/ for generating the select box. And a reference for the Fabric events: https://github.com/kangax/fabric.js/wiki/Working-with-events

Upvotes: 13

Related Questions