You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue项目中Canvas适配Video标签显示比例问题问询

问题描述

我开发了一个用户注册需拍摄照片的Vue项目,客户要求video标签高度设为屏幕100%。video标签上方叠加了滤镜图片,因此video宽度与滤镜一致、高度为屏幕高度,且设置了object-fit:cover实现铺满显示。现在需要调整Canvas,让生成的图片和video的显示尺寸完全一致,但video宽度随滤镜动态变化,无法获取其实际宽度,求解决方案。

Video组件

<template>
  <div class="takePhoto__camera">
    <video ref="video" fullscreen="false" playsinline muted></video>
    <img class="takePhoto__camera__filter" src="../../assets/images/filtro.png" ref="filter" alt="Filtro">
    <button class="takePhoto__camera__btn" @click.prevent="photoTaken"></button>
  </div>
</template>

<script>
import { mapMutations } from "vuex"

export default {
  name: "ComponentCameraVideo",

  data(){
    return{
      video: null,
      localStream: null,
    }
  },

  mounted(){
    this.video = this.$refs.video;
    this.init();
  },

  methods: {
    ...mapMutations(["SET_CAMERA"]),

    init(){
      const filter = document.querySelector(".takePhoto__camera__filter");

      // 给body添加类名
      document.body.classList.add("takingPicture");

      filter.onload = () => {
        // 请求摄像头权限
        if('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices){
          navigator.mediaDevices.getUserMedia({video: true}).then((stream) => {
            this.video.srcObject = stream;
            this.localStream = stream;
            this.video.play();
            this.setWidthVideo();
          }).catch((error) => {
            console.log(error)
            alert(error);
          })
        }
        else{
          alert("无法访问摄像头!");
        }
      }
      
    },

    photoTaken(){
      // 移除body类名
      document.body.classList.remove("takingPicture");

      // 将摄像头数据存入Vuex
      this.SET_CAMERA(
        {
          "video" : {
            element: this.video,
            width: this.video.offsetWidth, 
            height: this.video.offsetHeight
          },
          "filter" : {
            element: this.$refs.filter,
            width: this.$refs.filter.offsetWidth, 
            height: this.$refs.filter.offsetHeight
          },
          "stream" : this.localStream,
        }
      );

      // 触发拍照完成事件
      this.$emit("photoTaken");

      // 停止摄像头(注释状态)
      // this.localStream.getTracks()[0].stop();
    },

    setWidthVideo(){
      setTimeout(() => {
        // 根据滤镜宽度设置video宽度
        const video = document.querySelector(".takePhoto__camera video");
        const filter = document.querySelector(".takePhoto__camera__filter");
      
        video.style.width = filter.offsetWidth + "px";
        video.style.height = innerHeight + "px";
      }, 500)
    }
  },
}
</script>

Canvas组件

<template>
  <div class="takePhoto__showPhoto">
    <canvas ref="canvas"></canvas>
    <img :src="src" alt="你的照片">
  </div>
</template>

<script>
import { mapState, mapMutations } from "vuex"

export default {
  name: "ComponentCameraCanvas",

  data() {
    return {
      canvas: null,
      src: "/",
    };
  },

  computed: {
    ...mapState(["camera"])
  },

  mounted() {
    this.canvas = this.$refs.canvas;
    this.getPhoto()
  },

  methods: {
    ...mapMutations(["SET_PHOTO"]),

    getPhoto(){
      const ratio = (window.innerHeight < window.innerWidth) ? 16 / 9 : 9 / 16;
      const ctx = this.canvas.getContext("2d");

      // 设置Canvas尺寸
      this.canvas.width = this.camera.filter.width;
      this.canvas.height = this.camera.filter.height;

      // Canvas配置
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";
      
      // 加载滤镜图片
      let filter = new Image();
      filter.src = this.camera.filter.element.src

      // 在Canvas中绘制画面和滤镜
      ctx.drawImage(this.camera.video.element, 0, 0);
      ctx.drawImage(filter, 0, 0, this.camera.filter.width, this.camera.filter.height);

      // 停止摄像头
      this.camera.stream.getTracks()[0].stop()

      // 将照片存入Vuex
      this.SET_PHOTO(this.canvas.toDataURL("image/jpeg"));

      // 在img标签中展示照片
      this.src = this.canvas.toDataURL("image/jpeg");
    },
  },
}
</script>

<style lang="scss" scoped>
#picture {
  display: block;
  width: 100%;
  height: 100vh;
  box-sizing: border-box;

  canvas {
    display: block;
    width: 100%;
    height: 100%;
  }
}
</style>
解决方案

1. 准确获取video的实际渲染尺寸

替换offsetWidth为getBoundingClientRect(),该方法能获取元素实时渲染的宽高,适配动态变化场景。修改Video组件的photoTaken方法:

photoTaken(){
  document.body.classList.remove("takingPicture");
  // 获取元素真实渲染尺寸
  const videoRect = this.video.getBoundingClientRect();
  const filterRect = this.$refs.filter.getBoundingClientRect();
  this.SET_CAMERA(
    {
      "video" : {
        element: this.video,
        width: videoRect.width, 
        height: videoRect.height,
        // 保存视频原始分辨率,用于计算裁剪比例
        naturalWidth: this.video.videoWidth,
        naturalHeight: this.video.videoHeight
      },
      "filter" : {
        element: this.$refs.filter,
        width: filterRect.width, 
        height: filterRect.height
      },
      "stream" : this.localStream,
    }
  );
  this.$emit("photoTaken");
}

2. 移除不稳定的定时器依赖

把setTimeout替换为requestAnimationFrame,确保DOM更新后再设置video尺寸,避免延迟误差:

setWidthVideo(){
  requestAnimationFrame(() => {
    const filter = this.$refs.filter;
    this.video.style.width = `${filter.offsetWidth}px`;
    this.video.style.height = `${window.innerHeight}px`;
  });
}

3. 匹配video的object-fit:cover绘制效果

直接绘制视频会导致拉伸,需计算裁剪区域,让Canvas画面和video显示完全一致。修改Canvas组件的getPhoto方法:

getPhoto(){
  const ctx = this.canvas.getContext("2d");
  const { video, filter } = this.camera;

  // 同步Canvas尺寸与滤镜(即video显示尺寸)
  this.canvas.width = filter.width;
  this.canvas.height = filter.height;

  ctx.imageSmoothingEnabled = true;
  ctx.imageSmoothingQuality = "high";

  // 计算裁剪参数,实现object-fit:cover效果
  const videoRatio = video.naturalWidth / video.naturalHeight;
  const displayRatio = video.width / video.height;
  let drawWidth, drawHeight, offsetX, offsetY;

  if (videoRatio > displayRatio) {
    // 视频宽于显示区域,裁剪左右
    drawHeight = video.naturalHeight;
    drawWidth = displayRatio * drawHeight;
    offsetX = (video.naturalWidth - drawWidth) / 2;
    offsetY = 0;
  } else {
    // 视频高于显示区域,裁剪上下
    drawWidth = video.naturalWidth;
    drawHeight = drawWidth / displayRatio;
    offsetY = (video.naturalHeight - drawHeight) / 2;
    offsetX = 0;
  }

  // 绘制裁剪后的视频画面
  ctx.drawImage(
    video.element,
    offsetX, offsetY, drawWidth, drawHeight, // 视频裁剪区域
    0, 0, filter.width, filter.height // 绘制到Canvas的区域
  );

  // 绘制滤镜(需等待图片加载完成)
  let filterImg = new Image();
  filterImg.src = filter.element.src;
  filterImg.onload = () => {
    ctx.drawImage(filterImg, 0, 0, filter.width, filter.height);
    // 保存图片到Vuex并展示
    this.SET_PHOTO(this.canvas.toDataURL("image/jpeg"));
    this.src = this.canvas.toDataURL("image/jpeg");
    // 停止摄像头
    this.camera.stream.getTracks()[0].stop();
  };
}

4. 补充CSS确保video适配生效

在Video组件的<style>中添加:

.takePhoto__camera video {
  object-fit: cover;
  display: block;
}

内容的提问来源于stack exchange,提问作者Gabriel Edu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 19:01:20