Capturing a sectioned image from a canvas with responsive layout

Viewed 187

I have a canvas with various div elements serving as boxes that represent areas of the canvas to capture. When the capture takes place, it isn't exactly as displayed on the screen. How can I track and responsively capture the div sections as they appear?

As an example, attempting to capture the large purple section:

enter image description here

enter image description here

Here is the code from the component:

<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>
1 Answers

There are a couple of complicating factors that make it a bit hard here.

The drawImage method takes its image data from the original source. Meaning, you are not drawing a part of the video that is presented to you. You are cutting out a piece from the original image/video capture. That means that you have to calculate the difference between the video dimensions presented to you and the dimensions of the original video.

Since the video 'overflows' out of the container on either the left and right or on the top and bottom side, you will have to account for that and add an offset to either the top or left.

Lastly, to get the image as presented on screen, you will have to stretch by providing the profile-box dimensions to drawImage.

To put it all together:

<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>
Related