cozmo / jsQR

A pure javascript QR code reading library. This library takes in raw images and will locate, extract and parse any QR code found within.
https://cozmo.github.io/jsQR/
Apache License 2.0
3.63k stars 600 forks source link

[enhancement] optimize video & animation fluency #220

Open corxit opened 2 years ago

corxit commented 2 years ago

Hi, thanks for this demo, saved me a lot of time!

I've made some changes based on my app. Hope these tips help!

<template>
  <div class="scaner" ref="scaner" @click="resume">
    <!--<div class="banner" v-if="showBanner">-->
    <!--  <i class="close_icon" @click="() => showBanner = false"></i>-->
    <!--  <p class="text">若当前浏览器无法扫码,请切换其他浏览器尝试</p>-->
    <!--</div>-->

    <div class="cover">
      <p class="line"></p>
      <!--<span class="square top left"></span>-->
      <!--<span class="square top right"></span>-->
      <!--<span class="square bottom right"></span>-->
      <!--<span class="square bottom left"></span>-->
      <p class="tips">{{ active ? '扫描二维码':'扫码暂停 点击屏幕继续扫码' }}</p>
    </div>

    <div class="video-wrapper" ref="CanvasWrapper">
      <video
          class="source"
          ref="video"
      >
        <!--v-show="showPlay"-->
        <!--controls-->
      </video>
      <canvas class="canvas-video" ref="canvas" />
      <div v-if="code != null" class="code-indicator" :style="{ top: codePosition.y+'px', left:codePosition.x+'20px', }"></div>
    </div>

    <!--<button v-show="showPlay" @click="run">开始</button>-->
  </div>
</template>

<script>

// eslint-disable-next-line no-unused-vars
import adapter from 'webrtc-adapter';
import jsQR from 'jsqr';

export default {
  name: 'Scaner',
  props: {
    // 使用后置相机
    useBackCamera: {
      type: Boolean,
      default: true
    },
    // 扫描识别后停止
    stopOnScaned: {
      type: Boolean,
      default: true
    },
    drawOnfound: {
      type: Boolean,
      default: true
    },
    // 线条颜色
    lineColor: {
      type: String,
      default: '#03C03C'
    },
    // 线条宽度
    lineWidth: {
      type: Number,
      default: 2
    },

    responsive: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      showPlay: false,
      showBanner: true,

      // videoWidth: null,
      // videoHeight: null,

      active: false,

      code:null,
    }
  },
  computed: {
    codePosition(){
      if (this.code == null){
        return null;
      }
      return {
        x: this.code.location.topLeftCorner.x + (this.code.location.bottomRightCorner.x - this.code.location.topLeftCorner.x)/2 - 25,
        y: this.code.location.topLeftCorner.y + (this.code.location.bottomRightCorner.y - this.code.location.topLeftCorner.y)/2 - 25,
      }
    },
  },
  watch: {
    // @Depracated!
    // active: {
    //   immediate: true,
    //   handler(active) {
    //     if (!active) {
    //       this.pause();
    //       //this.fullStop();
    //     }
    //   }
    // }
  },
  methods: {
    // 画线
    drawLine (begin, end) {
      this.canvas.beginPath();
      this.canvas.moveTo(begin.x, begin.y);
      this.canvas.lineTo(end.x, end.y);
      this.canvas.lineWidth = this.lineWidth;
      this.canvas.strokeStyle = this.lineColor;
      this.canvas.stroke();
    },
    // 画框
    drawBox (location) {
      if (this.drawOnfound) {
        this.drawLine(location.topLeftCorner, location.topRightCorner);
        this.drawLine(location.topRightCorner, location.bottomRightCorner);
        this.drawLine(location.bottomRightCorner, location.bottomLeftCorner);
        this.drawLine(location.bottomLeftCorner, location.topLeftCorner);
      }
    },
    tick () {
      if (this.$refs.video && this.$refs.video.readyState === this.$refs.video.HAVE_ENOUGH_DATA) {
        //.这里不设置会影响分辨率
        this.$refs.canvas.width = this.$refs.video.clientWidth;
        this.$refs.canvas.height = this.$refs.video.clientHeight;

        this.canvas.drawImage(this.$refs.video, 0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
        const imageData = this.canvas.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
        let code = false;
        try {
          code = jsQR(imageData.data, imageData.width, imageData.height);
        } catch (e) {
          console.error(e);
        }
        if (code) {
          //@Deprecated!
          //this.drawBox(code.location);

          this.found(code);
        }
      }
      this.run();
    },
    // 初始化
    setup () {
      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        this.previousCode = null;
        this.parity = 0;
        this.active = true;
        this.canvas = this.$refs.canvas.getContext("2d");
        const facingMode = this.useBackCamera ? { exact: 'environment' } : 'user';
        const handleSuccess = stream => {
           if (this.$refs.video.srcObject !== undefined) {
            this.$refs.video.srcObject = stream;
          } else if (window.videoEl.mozSrcObject !== undefined) {
            this.$refs.video.mozSrcObject = stream;
          } else if (window.URL.createObjectURL) {
            this.$refs.video.src = window.URL.createObjectURL(stream);
          } else if (window.webkitURL) {
            this.$refs.video.src = window.webkitURL.createObjectURL(stream);
          } else {
            this.$refs.video.src = stream;
          }
          this.$refs.video.playsInline = true;
          const playPromise = this.$refs.video.play();
          playPromise.catch(() => (this.showPlay = true));
          playPromise.then(this.run);
        };
        navigator.mediaDevices
          .getUserMedia({
            video: {
              facingMode,
              width: 1280,
              height: 720
            }
          })
          .then(handleSuccess)
          .catch(() => {
            navigator.mediaDevices
              .getUserMedia({ video: true })
              .then(handleSuccess)
              .catch(error => {
                this.$emit("error-captured", error);
              });
          });
      }
    },
    resume(){
      if (!this.active){
        this.code = null;
        this.active = true;
        this.$refs.video.play();
        this.run();
      }
    },
    pause(){
      this.active = false;
      this.$refs.video.pause();
    },
    run () {
      if (this.active) {
        setTimeout(()=>{
          this.tick();
        },50);

        //@Deprecated!
        //requestAnimationFrame(this.tick);
      }
    },
    found (code) {
      this.code = code;
      let codeString = code.data;
      this.$emit("code-scanned", codeString);
      if (this.stopOnScaned){
        this.pause();
      }

      // @Depracated!
      // if (this.previousCode !== codeString) {
      //   this.previousCode = codeString;
      // } else if (this.previousCode === codeString) {
      //   this.parity += 1;
      // }
      // if (this.parity > 2) {
      //   this.code = code;
      //   this.parity = 0;
      //   this.$emit("code-scanned", codeString);
      //   if (this.stopOnScaned){
      //     this.pause();
      //   }
      // }
    },
    // 完全停止
    fullStop () {
      if (this.$refs.video && this.$refs.video.srcObject) {
        this.$refs.video.srcObject.getTracks().forEach(t => t.stop());
      }
    }
  },
  mounted () {
    this.setup();
  },
  beforeDestroy () {
    this.fullStop();
  }
}
</script>

<style lang="css" scoped>
.scaner {
  background: #000000;
  position: absolute;
  top: 0px;
  left: 0;
  width: 100%;
  height: 100%;
}

.scaner .video-wrapper{
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /*width: 100%;*/
  /*min-width: 100vh;*/
  /*height: 100%;*/
  /*min-height: 100vw;*/
}
.scaner .video-wrapper video.source{
  position: relative;
  min-width: 100vw;
  min-height: 100vh;
}

.scaner .video-wrapper .canvas-video{
  position: absolute;
  top: 0px;
  left: 0px;
  width: 100%;
  height: 100%;
  opacity: 0;
}
.scaner .video-wrapper .code-indicator{
  position: absolute;
  width: 50px;
  height: 50px;
  border-radius: 100px;
  background-color: #5F68E8;
  border: 3px solid #fff;
  z-index: 9999;
}

.scaner .cover {
  width: 100%;
  height: 60vh;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-60%);
  z-index: 1111;
}
.scaner .cover .tips {
  position: absolute;
  bottom: -20%;
  width: 100%;
  text-align: center;
  font-size: 14px;
  color: #FFFFFF;
  opacity: 0.8;
}
.scaner .cover .line {
  width: 80%;
  height: 6px;
  border-radius: 100%;
  margin-left: 10%;
  background: #5F68E8;
  background: linear-gradient(to right, #0000, #5F68E8, #0165FF, #5F68E8, #0000);
  position: absolute;
  animation: scan 3s infinite ease, opacity 3s infinite ease;
  animation-fill-mode: both;
}
@keyframes scan {
  0% {transform: translate3d(0,0,0)}
  100% {transform: translate3d(0,60vh,0)}
}
@keyframes opacity {
  0% {opacity:0}
  15% {opacity:1}
  45% {opacity:1}
  100% {opacity:0}
}
</style>