前端Js自定義相機取景框


參考文章:

效果展示

2NdSIS.gif

html部分

主要分為四部分

  • 啟動按鈕
  • 原始相機
  • 自定義相機(重點)
  • 上傳提示
<div class="uploadFacePic">
  <!-- 頭部 省略-->
 
  <!-- 啟動相機按鈕-->
  <main style="marginTop:20px">
    <div class="btn" @click="openCamera">開啟人臉采集</div>
  </main>

  <!-- 1 原始相機 不兼容時的回退方案-->
  <input id="file" type="file" accept="image/*" capture="camera" style="display:none">


  <!-- 2 自定義相機 -->
  <div style="width: 100%; position: fixed; left: 0; bottom: 0; top: 0; right: 0;" v-if="cameraShow">
    <!-- 頂部樣式 -->
    <div style="width: 100%; position: fixed; left: 0; bottom: 90vh; top: 0; right: 0;background:black">  
    </div>
    

    <!-- 中間部分 -->
    <video style="height: 65vh;width: 100vw;position: fixed;top: 10vh;left: 0;"></video>
    <div style="width: 100%; position: fixed; left: 0; bottom: 25vh; top: 10vh; right: 0;">
      <!-- a.拍攝時展示取景框。把取景框圖片換下就可以了 -->
      <img src="../../assets/qujing.png" alt="" v-if="status==1" style="width: 100%;height: 100%;opacity: 0.8">
      <!-- b.拍攝完后展示抓拍圖片 -->
      <img :src="imageUrl" alt="" v-if="status==2" style="width:100%;height:100%">
    </div>
      
    <!-- 底下控制部分-->
    <div class="control">
        
      <!-- 拍照前 -->
      <div class="control_before" v-if="status==1">
        <div class="control_before_top">照片</div>
        <div class="control_before_bottom">
          <div class="smaller" @click="cameraShow=false">取消</div>
          <i class="iconfont icon-xiangji bigger" @click="snapPhoto"></i>
          <i class="iconfont icon-zhongxin small" @click="front = !front"></i>
        </div>
      </div>
        
      <!-- 拍照后 -->
      <div class="control_after" v-if="status==2">
        <div class="smaller" @click="status=1">重拍</div>
        <div class="smaller" @click="submitPhoto('custom')">使用照片</div>
      </div>
        
    </div>
      
    <!-- 抓拍 -->
    <canvas id="mycanvas"></canvas>
  </div>
    

  <!-- 提示部分 -->
  <div class="tipinfo" v-if="tipVisible">
    <div class="successContent" v-if="tipInfo.result=='ok'">
      <van-icon name="passed"/>
      <div class="title">采集成功</div>
      <div class="info">恭喜您,完成人臉照片采集</div>
      <div class="btn" @click="tipVisible=false">{{'返回'+btntext}}</div>
    </div>
    <div class="failContent" v-else>
      <van-icon name="warning-o" />
      <div class="title">采集失敗</div>
      <div class="info">{{tipInfo.msg+',請重拍'}}</div>
      <div class="btn" @click="tipVisible=false">{{'返回'+btntext}}</div>
    </div>
  </div>

</div>

js部分

變量部分:

data() {
  return {
    type:'',//上傳類型 update|upload
    cameraShow:false,//啟動自定義相機
    status:0,//自定義相機-拍攝進度:0|未開啟 1|開啟但未拍攝 2|開啟且已拍攝
    imageUrl:'',//自定義相機-抓拍url
    front:true,// 自定義相機-前置與后置轉換(未驗證)
      
    //提示部分
    tipVisible:false,
    tipInfo:{
      result:'fail',
      msg:'采集人臉失敗'
    },//上傳結果
    btntext:'',//倒計時文本
    time:null,//計時器

    imageFile:'',//圖片對象
    
  };
},

啟動相機

openCamera:主要做了一些兼容與回退。

openCamera() {
  // 1. 先展示,因為要從這里獲取video標簽
  this.cameraShow=true
    
  // 2. constraints:指定請求的媒體類型和相對應的參數
  var constraints={
    audio: false,
    video: {
      facingMode: (this.front? "user" : "environment") 
    }
  }
	
  // 3. 兼容部分:
  // 老的瀏覽器可能根本沒有實現 mediaDevices,所以我們可以先設置一個空的對象
  if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
  }
  // 一些瀏覽器部分支持 mediaDevices。我們不能直接給對象設置 getUserMedia
  // 因為這樣可能會覆蓋已有的屬性。這里我們只會在沒有getUserMedia屬性的時候添加它。
  if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia = function(constraints) {
      // 首先,如果有getUserMedia的話,就獲得它
      var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.oGetUserMedia;
      // 一些瀏覽器根本沒實現它 - 那么就返回一個error到promise的reject來保持一個統一的接口
      if (!getUserMedia) {
        return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
      }
      // 否則,為老的navigator.getUserMedia方法包裹一個Promise
      return new Promise(function(resolve, reject) {
        getUserMedia.call(navigator, constraints, resolve, reject);
      });
    }
  }

    
  // 4. 獲取視頻流
  let that=this
  navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    // 進來這里表示能夠兼容
    let video=document.querySelector('video');
    video.srcObject = stream;
    video.onloadedmetadata = function(e) {
      video.play();
    };
    // 進入自定義拍攝模式
    that.status=1 
  })
  .catch(function(err) {
    // 進來這里表示不能兼容
    console.log('nonono',err)
    // 調用原始攝像頭
    that.originCamera()
  });
},

方案一:兼容

snapPhoto:抓拍

snapPhoto(){
  var canvas = document.querySelector('#mycanvas');
  var video = document.querySelector('video');
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  canvas.getContext('2d').drawImage(video,0,0);
    
  // 保存為文件,用於后續的上傳至服務器(尚未實踐)——>后續提交
  this.imageFile=this.canvasToFile(canvas)
    
  // blob轉url:用於展示
  let p=new Promise((resolve,reject)=>{
      canvas.toBlob(blob=>{
        let url=URL.createObjectURL(blob)
        resolve(url)
    });
  })
  let that=this
  p.then(value=>{
    that.imageUrl=value
    that.status=2//表示拍攝完成
  })
},

canvasToFile:canvas轉為文件格式,用於上傳服務器

canvasToFile(canvas){
  var dataurl = canvas.toDataURL("image/png");
  var arr = dataurl.split(','),
      mime = arr[0].match(/:(.*?);/)[1],
      bstr = atob(arr[1]),
      n = bstr.length,
      u8arr = new Uint8Array(n);
  while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
  }
  var file = new File([u8arr], "phone.png", {type: mime});
  return file
},

方案二:不兼容

originCamera:調用原始攝像機

originCamera(){
  let that=this
  
  //關閉自定義相機
  that.cameraShow=false
  let promise= new Promise(function (resolve, reject) {
      let file=document.getElementById('file')
      file.click()
      file.onchange = function (event) {
          if (!event) {
              reject('empty')
          }
          //當選中或者拍攝后確定圖片后,保存圖片文件——>后續提交
          let file=event.target.files[0]
          resolve(file)
      }
  })
  promise.then((value)=> {
      that.submitPhoto('origin',value)
    }
  )
},

提交與上傳提示

submitPhoto(type,file) {
  if(type=='origin'){
      this.imageFile=file
  }
  console.log("提交",this.imageFile);
    
  //在這里進行上傳操作
  // let fd=new FormData()
  // fd.append("face_image",this.imageFile)
  //...
    
  //上傳成功時:
  this.tipInfo.result='ok'
  // this.tipInfo.result='fail'
    
  this.cameraShow=false
  
  //開始提示
  this.countdown()
},

//倒計時與提示
countdown(){
  clearInterval(this.time);
  this.tipVisible=true
  let coden = 3;
  this.btntext = '('+coden+'s)';
  this.time = setInterval(() => {
    coden--
    this.btntext = '('+coden+'s)';
    if (coden == 0) {
      clearInterval(this.time);
      this.tipVisible = false;
      this.btntext = "";
    }
  }, 1000);
},

css部分

.uploadFacePic{
  .img{
    width:100%;
    height:100%
  }
  
  .bigger{
    font-weight: 600;
    font-size: 3em;
  }
  .small{
    font-size: 2em;
  }
  .smaller{
    font-size: 1.2em;
  }
  // 控制台
  .control{
    width: 100%;
    position: fixed; 
    left: 0; 
    bottom: 0; 
    top: 75vh; 
    right: 0;
    background:black;
    .control_before{
      position:relative;
      width:100%;
      height:100%;
      display: flex;
      flex-direction: column;
      .control_before_top{
        z-index: 1;
        flex: 1;
        color: orange;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .control_before_bottom{
        flex: 2;
        display: flex;
        justify-content: space-around;
        color: white;
        align-items: center;
        margin-bottom: 1.5em;
      }
    }
    .control_after{
      position:relative;
      width:100%;
      height:100%;
      display: flex;
      color: white;
      align-items: center;
      justify-content: space-around;
    }
  }
  main{
    text-align: center;
  }
  .tipinfo{
    z-index: 2;
    position: fixed;
    top:46px;
    left: 0;
    right: 0;
    bottom: 0;
    background: white;
    display: flex;
    justify-content: center;
    align-items: center;
    .successContent{
      .van-icon{
        color: #f68618;
        font-size: 5em;
      }
      .title{
        color: #f68618;
        font-size: 1.8em;
      } 
    }
    .failContent{
      .van-icon{
        color: red;
        font-size: 5em;
      }
      .title{
        color: red;
        font-size: 1.8em;
      } 
    }
    .info{
      margin: 1em 0 3em;
    }
  }
  .btn{
    height: 34px;
    width: 80vw;
    background: #f68618;
    color: white;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: .3em;
    margin: 0 auto;
  }
}

后記

  • 部分icon是從阿里導入的,這里沒有寫出來。
  • 目前在微信開發者工具能夠看到自定義相機的效果,在真機上(http)還啟動不了,查閱資料說navigator.mediaDevices.getUserMedia服務器需要用https(尚未驗證)
  • 自定義相機自拍時會出現鏡像效果,暫時不知道怎么處理。
  • 中間上傳時估計要做一個加載ui進行過渡

優化:video自適應大小

在html中,屏幕中顯示的可見視圖大小其實我們一開始就規定好了,但是不同設備有它的大小,那怎么統一且自適應呢?

  • video標簽原始的object-fit的屬性值為contain,這就造成了問題:沒有辦法填充我們定義的可見視圖大小——解決:object-fit:cover

    <video style="object-fit: cover"></video>
    
  • 然額還沒結束。了解object-fit:cover的原理后,canvas也應該對應變化。cover的情況下,當寬高不一致時,超出部分會被剪切。此外,cover會自適應,即它不會只從一邊裁剪,而是端水從兩邊等份裁剪。

    var canvas = document.querySelector('#mycanvas');
    var video = document.querySelector('video');
    
    // 基於object-fit:cover
    
    // 原始寬高
    let width=video.videoWidth
    let height=video.videoHeight
    
    //判斷寬高是否一致
    const WIDTH_UNEQUAL_HEIGHT=width!=height 
    
    //被裁減掉的一邊的大小
    const SPAN=Math.abs(width-height)/2	
    
    //根據差值定義裁剪起點x,y。
    //例:如果高比寬高出10,那么上面會被裁減掉5,那么起點y就等於5,這樣就保證了裁剪到的是可視圖片
    let cut_x=WIDTH_UNEQUAL_HEIGHT?SPAN:0  //裁剪起點x
    let cut_y=WIDTH_UNEQUAL_HEIGHT?0:SPAN  //裁剪起點y
    
    //如果裁剪過,寬度要把裁剪部分去掉。
    let cut_after_width=cut_x!=0?width-2*SPAN:width 
    let cut_after_height=cut_y!=0?height-2*SPAN:height
    
    //繪制畫布(裁剪后的)
    canvas.width = cut_after_width;
    canvas.height = cut_after_height;
    canvas.getContext('2d').drawImage(video,cut_x,cut_y,cut_after_width,cut_after_height,0,0,cut_after_width,cut_after_height)
    

    ok完成。

優化:video鏡像翻轉

受前面的啟發。

<video style="transform: rotateY(180deg);"></video>

稍微改寫一下畫布

let context=canvas.getContext('2d')
context.drawImage(video,cut_x,cut_y,cut_after_width,cut_after_height,0,0,cut_after_width,cut_after_height)

// 如果是前置 需要鏡像翻轉
if(this.front){
  context.scale(-1, 1);
  context.drawImage(video,cut_x,cut_y, width*-1,height);
}

但是查到資料說ios的scale無法接收負數。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM