使用响应式布局从 canvas 捕获切片图像

Capturing a sectioned image from a canvas with responsive layout

我有一个 canvas,其中包含各种 div 元素,用作表示要捕获的 canvas 区域的框。捕获发生时,它与屏幕上显示的并不完全相同。我如何跟踪并响应地捕获 div 部分出现?

举个例子,试图捕捉大的紫色部分:

这是组件的代码:

<template>
  <div>
    <div class="video-wrapper">
      <canvas ref="the-canvas" class="canvas">
      </canvas>
      <video class="video" ref="video" autoplay />
      <div :class="profileCenter.name" :style="profileToStyle(profileCenter)" />
      <div :class="profileTopLeft.name" :style="profileToStyle(profileTopLeft)" />
      <div :class="profileTopMiddle.name" :style="profileToStyle(profileTopMiddle)" />
      <div :class="profileTopRight.name" :style="profileToStyle(profileTopRight)" />
    </div>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator'
import Header from '@/components/Header.vue'

@Component({ components: { Header } })
export default class Test extends Vue {
  private userMedia: MediaStream | null = null
  private winHeight: number = window.innerHeight
  private winWidth: number = window.innerWidth

  private profileCenter: any = { name: 'profile-box', height: 350, width: 250, top: 40, left: 50, transform: [-50, -50] }
  private profileTopLeft: any = { name: 'profile-box', height: 100, width: 100, top: 20, left: 20, transform: [-50, -50] }
  private profileTopMiddle: any = { name: 'profile-box', height: 100, width: 100, top: 20, left: 50, transform: [-50, -50] }
  private profileTopRight: any = { name: 'profile-box', height: 100, width: 100, top: 20, left: 80, transform: [-50, -50] }

  async mounted () {
    this.$nextTick(() => {
      window.addEventListener('resize', () => {
        this.updateSizes()
      })
    })
    this.userMedia = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })

    setTimeout(() => {
      const video: HTMLVideoElement = document.getElementsByClassName('video')[0] as HTMLVideoElement
      const canvas: HTMLCanvasElement = document.getElementsByClassName('canvas')[0] as HTMLCanvasElement
      const targetSquare: HTMLDivElement = document.getElementsByClassName('profile-box')[0] as HTMLDivElement
      const targetDims = targetSquare.getBoundingClientRect()
      canvas.height = targetDims.height
      canvas.width = targetDims.width
      console.log(canvas.width, canvas.height)
      console.log(video.videoWidth, video.videoHeight)
      const ctx = canvas.getContext('2d')
      ctx!.drawImage(video, targetDims.left, targetDims.top, targetDims.width, targetDims.height, 0, 0, targetDims.width, targetDims.height)
      window.open(canvas.toDataURL())
    }, 3000)
  }

  private updateSizes (): void {
    this.winHeight = window.innerHeight
    this.winWidth = window.innerWidth
  }

  private profileToStyle (profile: { height: number, width: number, top: number, left: number, transform: number[] }): string {
    return `height: ${profile.height}px; min-height: ${profile.height}px; width: ${profile.width}px; min-width: ${profile.width}px; top: ${profile.top}%; left: ${profile.left}%; transform: translate(${profile.transform[0]}%, ${profile.transform[1]}%)`
  }

  @Watch('userMedia')
  private userMediaWatcher () {
    this.$refs.video.srcObject = this.userMedia
  }
}
</script>

<style>
.container--fluid {
  height: 100%;
  overflow: hidden;
}

.video-wrapper {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100%; 
  overflow: hidden;
}

.video-wrapper video {
  min-width: 100%; 
  min-height: 100%; 

  width: auto;
  height: auto;

  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
}

.canvas {
  position: absolute;
  /* width: 480px; */
  /* height: 640px; */
}

.profile-box{
  position: absolute;
  border: 2px solid purple;
}
</style>

有几个复杂的因素使这里有点困难。

drawImage 方法从原始来源获取其图像数据。意思是,您没有绘制呈现给您的视频的一部分。您正在从原始 image/video 捕获中剪下一块。这意味着您必须计算呈现给您的视频尺寸与原始视频尺寸之间的差异。

由于视频 'overflows' 在左侧和右侧或顶部和底部超出容器,您必须考虑到这一点并在顶部或左侧添加偏移量。

最后,要获得屏幕上显示的图像,您必须通过向 drawImage 提供 profile-box 尺寸来进行拉伸。

总而言之:

<template>
<section class="home">
  <div>
    <div class="video-wrapper">
      <canvas ref="the-canvas" class="canvas">
      </canvas>
      <video class="video" ref="video" autoplay/>
      <div :class="profileCenter.name" :style="profileToStyle(profileCenter)" />
      <div :class="profileTopLeft.name" :style="profileToStyle(profileTopLeft)" />
      <div :class="profileTopMiddle.name" :style="profileToStyle(profileTopMiddle)" />
      <div :class="profileTopRight.name" :style="profileToStyle(profileTopRight)" />
    </div>
  </div>
  </section>
</template>

<script lang="ts">
import { Vue, Component, Watch } from 'vue-property-decorator'
import Header from '@/pages/Header.vue'

@Component({components: Header})
export default class Test extends Vue {
    $refs!: {
    video: HTMLVideoElement
  }
  private userMedia: MediaStream | null = null
  private winHeight: number = window.innerHeight
  private winWidth: number = window.innerWidth

  private profileCenter: any = { name: 'profile-box', height: 350, width: 250, top: 40, left: 50, transform: [-50, -50] }
  private profileTopLeft: any = { name: 'profile-box', height: 100, width: 100, top: 20, left: 20, transform: [-50, -50] }
  private profileTopMiddle: any = { name: 'profile-box', height: 100, width: 100, top: 20, left: 50, transform: [-50, -50] }
  private profileTopRight: any = { name: 'profile-box', height: 100, width: 100, top: 20, left: 80, transform: [-50, -50] }

  async mounted () {
    this.$nextTick(() => {
      window.addEventListener('resize', () => {
        this.updateSizes()
      })
    })
    this.userMedia = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })

    setTimeout(() => {
      const video: HTMLVideoElement = document.getElementsByClassName('video')[0] as HTMLVideoElement
      const canvas: HTMLCanvasElement = document.getElementsByClassName('canvas')[0] as HTMLCanvasElement
      const targetSquare: HTMLDivElement = document.getElementsByClassName('profile-box')[0] as HTMLDivElement
      const targetDims = targetSquare.getBoundingClientRect()
      
      canvas.height = targetDims.height
      canvas.width = targetDims.width
      const ctx = canvas.getContext('2d')
    const originalVideoWidth = video.videoWidth;
    const originalVideoHeigth = video.videoHeight;  
    const videoActualWidth: number = video.getBoundingClientRect().width;
    const videoActualHeight: number = video.getBoundingClientRect().height;
    const videoContainer: HTMLDivElement = document.getElementsByClassName('video-wrapper')[0] as HTMLDivElement;
    const containerWidth: number = videoContainer.getBoundingClientRect().width;
    const containerHeight: number = videoContainer.getBoundingClientRect().height;
    let videoOffsetLeft = 0;
    let videoOffsetTop = 0;
    if(containerWidth === videoActualWidth){
        videoOffsetTop = ((videoActualHeight - containerHeight) / 2) * (originalVideoHeigth / videoActualHeight)
    }
    else{
        videoOffsetLeft = ((videoActualWidth - containerWidth) / 2) * (originalVideoWidth / videoActualWidth)
    }
    const left = videoOffsetLeft + ((originalVideoWidth / videoActualWidth) * targetDims.left);
    const top = videoOffsetTop + ((originalVideoHeigth / videoActualHeight) * targetDims.top);
    const width = (originalVideoWidth / videoActualWidth  ) * targetDims.width;
    const height = (originalVideoHeigth / videoActualHeight ) * targetDims.height;
    ctx!.drawImage(video,
        left, top, width, height, 0, 0, targetDims.width,targetDims.height)
  
      //window.open(canvas.toDataURL())
    }, 3000)
  }

  private updateSizes (): void {
    this.winHeight = window.innerHeight
    this.winWidth = window.innerWidth
  }

  private profileToStyle (profile: { height: number, width: number, top: number, left: number, transform: number[] }): string {
    return `height: ${profile.height}px; min-height: ${profile.height}px; width: ${profile.width}px; min-width: ${profile.width}px; top: ${profile.top}%; left: ${profile.left}%; transform: translate(${profile.transform[0]}%, ${profile.transform[1]}%)`
  }

  @Watch('userMedia')
  private userMediaWatcher () {
    
    this.$refs.video.srcObject = this.userMedia
  }
}
</script>

<style>
.container--fluid {
  height: 100%;
  overflow: hidden;
}

.video-wrapper {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100%; 
  overflow: hidden;
}

.video-wrapper video {
  min-width: 100%; 
  min-height: 100%; 

  width: auto;
  height: auto;

  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
}
img{
    height: 100%;
    width: 100%;
}
.canvas {
  position: absolute;
  /* width: 480px; */
  /* height: 640px; */
}

.profile-box{
  position: absolute;
  border: 2px solid purple;
}
</style>