h5手机移动端实现图片截取逻辑

代码记录如下

主要是结合vuejs开发的小活动里面,需要用到截图的功能,开始自己以为实现起来很麻烦,经过同事的帮忙,居然有人实现过,就直接拿过来使用了,目前测试结果,没有发现什么兼容性的问题,不过需要按照自己的需求更改下具体的实现逻辑,代码如下

clip库的js代码段(这里是我做了部分改动的,因为最终的图片结果我认为还是直接返回比较好)

class Clip{
  //因为要保存到vue实例中,所以直接传一个$vm当闭包使用了
  constructor(wpId,$vm){
    this.regional = document.getElementById(wpId);
    this.getImage = document.createElement('canvas');
    this.getImage.id = 'image-box';
    this.editBox = document.createElement('canvas');
    this.editBox.id = 'cover-box';

    this.regional.appendChild(this.getImage);
    this.regional.appendChild(this.editBox);
    this.$vm = $vm;
  }

  init(file){
    this.sx = 0; //裁剪框的初始x
    this.sy = 0; //裁剪框的初始y
    this.sWidth = 233; //裁剪框的宽
    this.sHeight = 233; //裁剪框的高
    this.chooseBoxScale = 233/233;

    this.handleFiles(file);
  }

  handleFiles(file){
    let t = this;
    let reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = function() {
      t.imgUrl = this.result;
      t.paintImg(this.result);
    }
  }

  paintImg(picUrl){
    let t = this;
    let cxt = t.getImage.getContext('2d');

    //先清空画布
    cxt.clearRect(0, 0, this.getImage.width, this.getImage.height);

    let img = new Image();
    img.src = picUrl;

    img.onload = function() {

      let imgScale = img.width / img.height;
      let boxScale = t.regional.offsetWidth / t.regional.offsetHeight;

      //判断盒子与图片的比列
      if (imgScale < boxScale) {
          //设置图片的像素
          t.imgWidth = t.regional.offsetHeight * imgScale;
          t.imgHeight = t.regional.offsetHeight;
      } else {
          //设置图片的像素
          t.imgWidth = t.regional.offsetWidth;
          t.imgHeight = t.regional.offsetWidth / imgScale;
      }

      //判断图片与选择框的比例大小,作出裁剪
      if (imgScale < t.chooseBoxScale) {
          //设置选择框的像素
          t.sWidth = t.imgWidth;
          t.sHeight = t.imgWidth / t.chooseBoxScale;

          //设置初始框的位置
          t.sx = 0;
          t.sy = (t.imgHeight - t.sHeight) / 2;
      } else {
          //设置选择框的像素
          t.sWidth = t.imgHeight * t.chooseBoxScale;
          t.sHeight = t.imgHeight;

          t.sx = (t.imgWidth - t.sWidth) / 2;
          t.sy = 0;
      }

      //高分屏下图片模糊,需要2倍处理
      t.getImage.height = 2 * t.imgHeight;
      t.getImage.width = 2 * t.imgWidth;
      t.getImage.style.width = t.imgWidth + 'px';
      t.getImage.style.height = t.imgHeight + 'px';

      let vertSquashRatio = t.detectVerticalSquash(img);

      cxt.drawImage(img, 0, 0,2 * t.imgWidth * vertSquashRatio, 2 * t.imgHeight * vertSquashRatio)

      t.cutImage();
      t.drag();
    }
  }

  cutImage(){
    let t = this;

    //绘制遮罩层:
    t.editBox.height = 2 * t.imgHeight;
    t.editBox.width = 2 * t.imgWidth;

    t.editBox.style.display = 'block';
    t.editBox.style.width = t.imgWidth + 'px';
    t.editBox.style.height = t.imgHeight + 'px';

    let cover = t.editBox.getContext("2d");
    cover.fillStyle = "rgba(0, 0, 0, 0.7)";

    cover.fillRect(0, 0, 2 * t.imgWidth, 2 * t.imgHeight);
    cover.clearRect(2 *t.sx, 2 * t.sy, 2 * t.sWidth, 2 * t.sHeight);
  }

  drag(){
    let t = this;
    let draging = false;

    //记录初始点击的pageX,pageY。用于记录位移
    let pageX = 0;
    let pageY = 0;

    //初始位移
    let startX = 0;
    let startY = 0;

    t.editBox.addEventListener('touchmove', function(ev) {
      let e = ev.touches[0];

      let offsetX = e.pageX - pageX;
      let offsetY = e.pageY - pageY;
      if (draging) {
        if (t.imgHeight == t.sHeight) {
          t.sx = startX + offsetX;
          if (t.sx <= 0) {
            t.sx = 0;
          } else if (t.sx >= t.imgWidth - t.sWidth) {
            t.sx = t.imgWidth - t.sWidth;
          }
        } else {
          t.sy = startY + offsetY;
          if (t.sy <= 0) {
            t.sy = 0;
          } else if (t.sy >= t.imgHeight - t.sHeight) {
            t.sy = t.imgHeight - t.sHeight;
          }
        }
        t.cutImage();
      }
    });

    t.editBox.addEventListener('touchstart', function(ev) {
      let e = ev.touches[0];
      draging = true;

      pageX = e.pageX;
      pageY = e.pageY;

      startX = t.sx;
      startY = t.sy;

    })

    t.editBox.addEventListener('touchend', function() {
      draging = false;
    })
  }

  save(callback) {
    let t = this;
    let saveCanvas = document.createElement('canvas');
    let ctx = saveCanvas.getContext('2d');

    //图片裁剪后的尺寸
    saveCanvas.width = 466;
    saveCanvas.height = 466;

    let images = new Image();
    images.src = t.imgUrl;

    images.onload = function(){
      //计算裁剪尺寸比例,用于裁剪图片
      let cropWidthScale = images.width/t.imgWidth;
      let cropHeightScale = images.height/t.imgHeight;

      t.drawImageIOSFix(
        ctx,
        images,
        cropWidthScale * t.sx ,
        cropHeightScale* t.sy,
        t.sWidth * cropWidthScale,
        t.sHeight * cropHeightScale,
        0,
        0,
        466,
        466);

      // t.$vm.clipUrl = saveCanvas.toDataURL();
      t.regional.removeChild(t.getImage);
      t.regional.removeChild(t.editBox);
      callback(saveCanvas.toDataURL());
    }
  }

  remove() {
    let t = this;
    t.regional.removeChild(t.getImage);
    t.regional.removeChild(t.editBox);
  }

  getImageUrl() {
    let t = this;
    return t.$vm.clipUrl;
  }

  //用于修复ios下的canvas截图问题
  //详情可以看这里http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios
  detectVerticalSquash(img) {
    if(/png$/i.test(img.src)) {
      return 1;
    }
    let iw = img.naturalWidth, ih = img.naturalHeight;
    let canvas = document.createElement('canvas');
    canvas.width = 1;
    canvas.height = ih;
    let ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    let data = ctx.getImageData(0, 0, 1, ih).data;

    let sy = 0;
    let ey = ih;
    let py = ih;
    while (py > sy) {
      const alpha = data[(py - 1) * 4 + 3];
      if (alpha === 0) {
        ey = py;
      } else {
        sy = py;
      }
      py = (ey + sy) >> 1;
    }
    const ratio = (py / ih);
    return (ratio===0)?1:ratio;
  }
  drawImageIOSFix(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) {
    const vertSquashRatio = this.detectVerticalSquash(img);
    ctx.drawImage(
      img,
      sx * vertSquashRatio,
      sy * vertSquashRatio,
      sw * vertSquashRatio,
      sh * vertSquashRatio,
      dx,
      dy,
      dw,
      dh);
  }
}

css代码段(这里我只用到了截图图片一小部分的UI,就是弹窗后截取图片的UI部分,这一部分如果自己写的话,就要看源码了。其实截图后的展示部分的功能,完全可以根据自己的需求来实现,而且原作者是用的less)

.file{
    height: 40px;
    display: block;
    margin: 40px auto 0;
  }

  .clip-img{
    width: 300px;
    height: 225px;
    margin: 20px auto 0;
    border: 1px solid #999;
    overflow: hidden;
  }

  .clip-img img{
      width: 100%;
  }

  .upload-wp {
    text-align: center;
    width: 300px;
    margin: 20px auto 0;
  }

  .upload-wp button {
    padding: 5px 10px;
  }

  .upload-wp p {
    word-wrap: break-word;
    font-size: 12px;
  }

  .clip-wp {
    position: fixed;
    width: 100%;
    top: 0;
    bottom: 0;
    z-index: 11;
    background-color: #000;
    text-align: center;
  }

  .clip-wp #container{
    background-color: #000;
    text-align: center;
    width: 100%;
    left: 0;
    right: 0;
    top: 20px;
    bottom: 80px;
    margin: 0 auto;
    position: absolute;
  }

  .clip-wp #save-img{
    position: absolute;
    bottom: 20px;
    width: 40%;
    left: 5%;
    height: 42px;
    line-height: 42px;
    color: #fff;
    background-color: #32c47c;
    border-radius: 20px;
  }

  .clip-wp #cancel-img{
    position: absolute;
    bottom: 20px;
    width: 40%;
    left: 55%;
    height: 42px;
    line-height: 42px;
    color: #fff;
    background-color: #32c47c;
    border-radius: 20px;
  }

  .clip-wp #image-box {
    position: absolute;
    left: 0px;
    right: 0px;
    bottom: 0px;
    top: 0px;
    margin: auto;
  }

  .clip-wp #cover-box {
    position: absolute;
    z-index: 9999;
    display: none;
    left: 0px;
    right: 0px;
    bottom: 0px;
    top: 0px;
    margin: auto;
  }

  .preview-wp {
    text-align: left;
    position: fixed;
    top: 0;
    bottom: 0;
    width: 100%;
    background-color: #000;
    overflow:auto;
  }

  .preview-wp .preview-img{
    position: absolute;
    top: 50%;
    width: 100%;
    transform: translate(0 , -50% );
    -webkit-transform: translate(0 ,-50%);
  }

html代码实现部分,这个就要根据具体的情况,我是页面上有两个图片按钮,点击后替换图片 所以我就直接用一个file类型的input,上传完之后将图片替换到指定的按钮上,然后重置input,这样就可以重复不断的更新上传图片

<!-- 上传图片的隐藏元素 -->
<div style="width:9vw;height:9vw;background:red;position:absolute;top:2vw;display:none">
  <form id="fileElem1">
    <input ref=fileElem type="file" class="file" accept="image/*;capture=camera" name="img" @change="clipImg($event)" style="display: none">
  </form>
</div>
<span style="display:inline-block;position:relative;">
<span style="display:inline-block;width:9vw;height:9vw;line-height:9vw;text-align:center;border:solid .2vw #fff;background-color:#f7e9d4;box-shadow:0 1vw 1vw 0 #e0cfb3;overflow:hidden;border-radius:50%" @click='choiceMeAvatar()'>
  <img v-bind:src="images.left_avatar_kuang" style="width: 100%">
</span>
<span style="display: inline-block;width: 9vw;height: 9vw;line-height:9vw;text-align:center;border: solid 0.2vw #ffffff;background-color: #f7e9d4;box-shadow: 0vw 1vw 1vw 0vw #e0cfb3;overflow: hidden;border-radius: 50%;margin-left: -2vw" @click='choiceTaAvatar()'>
  <img v-bind:src="images.right_avatar_kuang" style="width: 100%">
</span>

vue js代码methods实现部分

clipImg: function(event){
  this.clip = new Clip('container', this);
  this.clip.init(event.target.files[0]);
  this.isClip = true;
  document.body.addEventListener('touchmove', this.noScoll, false);
  // main-container 固定
  document.getElementById('main-container').style.position = 'fixed';
},

saveImg: function(){
  this.isClip = false;
  var self = this;
  var imageData = this.clip.save(function(imageData){
    console.log(self.currentUploadFieldName);
    console.log(imageData);

    if (self.currentUploadFieldName == 'left_avatar_kuang') {
      self.images.left_avatar_kuang = imageData;
    } else if (self.currentUploadFieldName == 'right_avatar_kuang') {
      self.images.right_avatar_kuang = imageData;
    }
  });

  document.body.removeEventListener('touchmove', this.noScoll, false);
  // form中input reset
  document.getElementById("fileElem1") && document.getElementById("fileElem1").reset();
  // main-container 解除固定
  document.getElementById('main-container').style.position = 'relative';
},

cancelImg: function() {
  this.isClip = false;
  this.clip.remove();
  this.clip = null;
  document.body.removeEventListener('touchmove', this.noScoll, false);
  // form中input reset
  document.getElementById("fileElem1") && document.getElementById("fileElem1").reset();
  // main-container 解除固定
  document.getElementById('main-container').style.position = 'relative';
},

vue变量部分代码如下

data: {
  currentUploadFieldName: '',
  images: {
    'left_avatar_kuang': '/xxx/avatar_default.png',
    'right_avatar_kuang': '/xxx/avatar_default.png',
  },
  isClip: false,
  clipUrl:'',
  noScoll: function(evt){
    this.isClip && evt.preventDefault();
  },
  clip:{},
}

我这里只是给出了基本上80%实现部分,还有20%具体细节的部分要具体细化了。

参考文章:链接1