裁剪视频元素以适合织物 js canvas

crop video element to fit a fabric js canvas

我正在尝试让用户相机拍摄快照,然后将最终渲染为正方形 fabricjs canvas。

我使用浏览器的本机网络摄像头功能,因此它在我的笔记本电脑上以 640x480 分辨率显示。我向用户展示此内容的方式是在设置为“object-fit: cover”的方形视图元素 'camera-view' 中显示视频源以移除信箱(然后它会放大以匹配高度)。然后将图片放在正方形 canvas 'user-photo' 上,该正方形也设置为“object-fit: cover”以包含矩形图像(隐藏边并匹配视频)。该图像还需要在 phone 纵向相机上正确显示,这似乎可行。

我在尝试将此 'user-photo' 复制到 fabric js canvas 时出现问题。我希望它只获取它所做的 canvas 的正方形副本。但是,它总是偏离中心。我不想对任何值进行硬编码,因为 canvas 和视频框的大小可能会发生变化,或者视频的分辨率可能会发生变化。我很确定在绘制新的 canvas 时我刚刚进行了一些计算,或者“object-fit: cover”约束可能导致了该行为。

过去两天我一直在寻找并尝试如何正确地实现这一点并且已经接近了,但它仍然没有完全按照它应该的方式工作。

我使用了这里的部分:fabric.js - 从 canvas API 的 ImageData 对象创建 Image 对象 ,此处 Crop Functionality using FabricJs 和 Stack Overflow 周围的其他人。

这是代码的主要部分,完整的工作在此处: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';
}

好的,所以我现在设法解决了。

我的问题的主要原因是不同的相机提供不同的分辨率和纵横比,我在绘制 canvases 时没有正确使用这些分辨率和纵横比。我现在已经精通在 getContext("2d").drawImage() 中使用最大数量的参数。哈哈

您可以在此处查看功能正常的 3 步版本: JS Fiddle

第一步显示裁剪为方形框的矩形网络摄像头画面。 第二步采用矩形视频源,从中创建图像,然后将其绘制到新的 canvas 并计算偏移量以获得正方形 1:1 图像。 第三步将canvas重绘到fabricjscanvas上作为背景层

第二步和第三步可能会合并为一个步骤,但出于我的目的,我想要一个常规 canvas 然后是 fabricjs canvas.

这里也是 javascript 代码:

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");
}