Search code examples
javascriptcanvasfabricjscropwebcam

crop video element to fit a fabric js canvas


I am trying to get a users camera to take a snapshot, then render the final to a square fabricjs canvas.

I use the browsers native webcam functionality, and as such it comes through at 640x480 on my laptop. the way I show this to the user is to display the video feed in a square view element 'camera-view' set to "object-fit: cover" to remove the letterboxing (it does then zoom in to match the height). The picture is then placed on a square canvas 'user-photo' that also is set to "object-fit: cover" to contains the rectangular image (hiding the sides and matching the video). The image also needs to display properly on a phone camera in portrait and this seems to work.

My issues arise when trying to copy this 'user-photo' to a fabric js canvas. I want it to only take a square copy of the canvas which it does. However, it always comes through just off center. I don't want to hard code any values as the canvas and video boxes may change in size, or the video at different resolutions. I'm pretty sure I've just got some calculations off when drawing the new canvas, or that the "object-fit: cover" constraint may be causing the behavior.

I have been looking and trying for the last two days on how to achieve this properly and have got close but it is still not working exactly as it should.

I've used parts from here: fabric.js - create Image object from ImageData object of canvas API , here Crop Functionality using FabricJs and others from around Stack Overflow.

Here is the main parts of the code and the full work in progress is here: JS Fiddle

const cameraView = document.querySelector("#camera-view");
const userPhoto = document.querySelector("#user-photo");

var canWidth = 400;     //Will be calculate dynamically
var canHeight = 400;

var st = 'width:' + canWidth.toString() + 'px; height:' + canHeight.toString() + 'px';
cameraView.setAttribute("style", st);
userPhoto.setAttribute("style", st);

var canvas = new fabric.Canvas('photo-adjust', {
  width: canWidth,
  height: canHeight
});

function BuildFabricCanvas() {
const can = document.getElementById('user-photo');
var ctx = can.getContext('2d');

var landscape = can.width > can.height; //true if in landscape orientation
var leftTrim = landscape ? (can.width - can.height) : 0;    //Not sure if works on portrait
var topTrim = landscape ? (can.height - canHeight)/2 : (can.height-can.width); //Not sure if  works on portrait

var data = ctx.getImageData(leftTrim, topTrim, canWidth, canHeight); //Probably have these wrong

var c = document.createElement('canvas');
c.setAttribute('id', '_temp_canvas');
c.width = canWidth;
c.height = canHeight;
c.getContext('2d').putImageData(data, 0, 0);

fabric.Image.fromURL(c.toDataURL(), function(img) {
  img.set({
  'flipX': true,
  });
  img.left = 0;
  img.top = 0;
  canvas.add(img);
  canvas.centerObject(img);
  img.bringToFront();
  c = null;
  $('#_temp_canvas').remove();
  canvas.renderAll();
  });
}

function WebcamSetup() {
navigator.mediaDevices
.getUserMedia({
  video: {
    facingMode: "user"
  },
  audio: false
})
.then(function(stream) {
  track = stream.getTracks()[0];
  cameraView.srcObject = stream;
})
.catch(function(error) {
  console.error("Oops. Something is broken.", error);
});

cameraView.classList.remove('d-none');
userPhoto.classList.add("d-none");
DisableButton('next');
document.getElementById('retake-photo').style.visibility = 'hidden';
document.getElementById('take-photo').style.visibility = 'visible';
}

WebcamSetup();

function TakeSnapshot() {
userPhoto.classList.remove("d-none");
userPhoto.width = cameraView.videoWidth;
userPhoto.height = cameraView.videoHeight;
userPhoto.getContext("2d").drawImage(cameraView, 0, 0);
cameraView.classList.add('d-none');

EnableButton('next');
TurnOffWebcam();
document.getElementById('take-photo').style.visibility = 'hidden';
document.getElementById('retake-photo').style.visibility = 'visible';
}

Solution

  • Ok, so I managed to sort it out now.

    My main cause of issue was that different cameras were providing different resolutions and aspect ratios that I wasn't properly using when drawing the canvases. I am now well versed in using the maximum amount of arguments in getContext("2d").drawImage(). Ha.

    You can see a functioning 3 step versions here: JS Fiddle

    The first step shows the rectangular webcam feed cropped to a square box. The second step takes the rectangular video feed, crates an image from it then draws it to a new canvas with calculated offsets to get a square 1:1 image. The third step redraws the canvas onto a fabricjs canvas as the background layer.

    The second and third steps could probably be consolidated into a single one but for my purposes I wanted a regular canvas and then a fabricjs canvas.

    Here is the javascript code as well:

    var canvasSquare; //Used for our image sizing
    var boxWidth; //Used for our resonsive div sizing
    var vidW, vidH; //Calculate our webcame feeds width and height
    //Canvases
    const cameraView = document.querySelector("#camera-view");
    const userPhoto = document.querySelector("#user-photo");
    var canvasFab = new fabric.Canvas('photo-adjust', {});
    //Div setup for buttons
    const cameraDiv = document.querySelector("#camera");
    const resultDiv = document.querySelector("#result");
    const fabricDiv = document.querySelector("#fabric");
    
    //Webcam Setup and usage
    var constraints = {
      video: {
        width: {
          ideal: 4096
        },
        height: {
          ideal: 4096
        },
        facingMode: "user"
      },
      audio: false
    };
    
    //Sets up all the divs to be the same size 
    function SetupSizes() {
      boxWidth = document.getElementById("box-width").offsetWidth;
      var st = 'width:' + boxWidth.toString() + 'px; height:' + boxWidth.toString() + 'px';
      document.getElementById('camera-view').setAttribute("style", st);
      document.getElementById('user-photo').setAttribute("style", st);
      document.getElementById('photo-adjust').setAttribute("style", st);
      canvasFab.setWidth(boxWidth);
      canvasFab.setHeight(boxWidth);
    }
    SetupSizes();
    
    //Resizes the canvases
    function ResizeCanvases() {
      var cvs = document.getElementsByTagName("canvas");
      for (var c = 0; c < cvs.length; c++) {
        cvs[c].height = canvasSquare;
        cvs[c].width = canvasSquare;
      }
      canvasFab.width = canvasSquare;
      canvasFab.height = canvasSquare;
    }
    
    function WebcamSetup() {
      navigator.mediaDevices
        .getUserMedia(constraints)
        .then(function(stream) {
          let track = stream.getTracks()[0];
          if (track.getSettings) {
            let {
              width,
              height
            } = track.getSettings();
            vidW = width;
            vidH = height;
            console.log(`${width}x${height}`);
            canvasSquare = (vidW > vidH) ? vidH : vidW;
            cameraView.width = (vidW > vidH) ? vidW : vidH;
            cameraView.height = (vidH > vidW) ? vidH : vidW;
            ResizeCanvases();
          }
          cameraView.srcObject = stream;
        })
        .catch(function(error) {
          console.error("Oops. Something is broken.", error);
        });
    
      cameraDiv.classList.remove("d-none");
      resultDiv.classList.add("d-none");
      fabricDiv.classList.add("d-none");
    }
    WebcamSetup();
    
    function TakeSnapshot() {
    
      var landscape = vidW > vidH; //Is the video in landscape?
      var boxSize = canvasSquare;
      var ratio = landscape ? vidW / vidH : vidH / vidW;
      var offset = ((boxSize * ratio) - boxSize) / 2;
      userPhoto.getContext("2d").drawImage(cameraView, landscape ? offset : 0, landscape ? 0 : offset, canvasSquare, canvasSquare, 0, 0, userPhoto.width, userPhoto.height);
    
      cameraDiv.classList.add("d-none");
      resultDiv.classList.remove("d-none");
      fabricDiv.classList.add("d-none");
    
      TurnOffWebcam();
    }
    
    //Removes the video and stops the stream
    function TurnOffWebcam() {
      var videoEl = document.getElementById('camera-view');
      stream = videoEl.srcObject;
      if (stream != null) {
        stream.getTracks().forEach(track => track.stop());
        videoEl.srcObject = null;
      }
    }
    
    function UseImage() {
    
      const photo = document.getElementById('user-photo');
    
      fabric.Image.fromURL(photo.toDataURL(), function(img) {
        img.set({
          'flipX': true,
        });
        canvasFab.centerObject(img);
        canvasFab.setBackgroundImage(img, canvasFab.renderAll.bind(canvasFab));
      });
    
      cameraDiv.classList.add("d-none");
      resultDiv.classList.add("d-none");
      fabricDiv.classList.remove("d-none");
    }