使用 canvas 来实现合成头像功能

1,452 阅读3分钟

分享一次使用 canvas 来做合成头像的经验

核心API

drawImage

canvas的 drawImage,可以直接将图片绘制到 canvas 内;所绘的图像叫做 "源图像"(source image)。而绘制到的地方叫做 "目标canvas"(destination canvas)。以字母 s 来开头的变量名代表源图像,以字母 d 开头的变量用于代指目标canvas。 drawImage 方法一共可以接受一下 3 套传参方式。

  • drawImage(image, dx, dy)

    该方法将源图像同比例的放在 canvas中,通过 dx, dy 来控制,源图像在目标 canvas 中的位置

  • drawImage(image, dx, dy, dw, dh)

    该方法将源图像的宽高,修改为 dw, dh 然后绘制到目标 canvas中,位置为 dx,dy

  • drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)

    该方法将源图像需要绘制的宽高 sw, sh,以及从源图像什么位置开始 sx, sy,绘制到目标canvas 时的尺寸 dw, dh,在目标canvas 的什么位置开始绘制 dx, dy

drawImage 可以将一幅图,一个 canvas 对象或一个视频帧的整体或者某个部分绘制到canvas中。在使用时要注意一点,绘制的元素一定要加载完毕,否则在绘制的过程中会绘制出错且没有任何提示

var img = new Image();
img.src = '1.png';
canvas.drawImage(img, 0, 0); // 在此处绘制会因为图片资源还未加载完全而绘制失败
img.onload = () => {
  canvas.drawImage(img, 0, 0); // 在元素的图片资源加载完毕后才能进行正确的绘制
}

toDataURL

将 canvas 转成图片,默认为 PNG 格式。默认为 96dpi

canvas.toDataURL(type, encoderOptions)

  • type:图片格式,默认为 image/png, 可选 image/jpegimage/webp
  • encoderOptions:在指定图片格式为 image/jpegimage/webp 的情况下,可以从 0 到 1的区间内选择图片质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略

准备工作,理清实现思路

合成头像,首先得具备一个底图模板,然后用户需要上传头像,用户的上传头像需要居中,然后实现移动图片,缩放图片等功能。最后一个点击合成头像时,需要将绘制出来的 canvas 转换成图片。

设置底图

底图可以随便找一张图,但作为合成头像的模板图还是具备一定要求的,需要旁边是有底色的,中间头像区域是透明的

底图的条件的是清晰,不能模糊,然后占满整个canvas,留出中间的区域作为用户上传头像的可视区域。canvas的大小一般是根据底图设置的,我这边的设置为 2600 * 2600

// 初始化
canvas.width = 2600;
canvas.height = 2600;
ctx = canvas.getContext('2d');

上传图片

上传图片一样用 input 标签来实现上传功能

<input type="file" accept="image/*" @change="imgUpload" />
imgUpload(event) {
  var imgFile = event.target.files[0];
  var reader = new FileReader();
  // 解析图像后的回调
  reader.onload = evt => {
    var img = new Image();
    img.src = evt.target.result;
    img.onload = () => {
      ctx.globalCompositeOperation = 'destination-over';
      // 图片缩放倍数,因为我的底图是 2600 * 2600 的所以得需要大一点的放大倍数
      targetImgScale = 5;
      // 图片上传后的初始位置设置
      targetImgX = (-img.width * targetImgScale) / 2 + 1300;
      targetImgY = (-img.height * targetImgScale) / 2 + 1300;
      ctx.drawImage(
        img,
        0,
        0,
        img.width,
        img.height,
        targetImgX,
        targetImgY,
        img.width * targetImgScale,
        img.height * targetImgScale
      );
    };
  };
  reader.readAsDataURL(imgFile);
}

实现拖拽图片

实现思路:用 touchstart 记录手指按下的初始坐标值。用 touchmove 来记录移动时的变化,用 touchend 来销毁 touchmove && touchend 两个事件。

canvas.addEventListener('touchstart', handlerTouchstart); //  给canvas 添加手指按下监听事件
function handlerTouchstart(event) {
  event.preventDefault();
  moveIMG(event.touches[0]); // 正常移动是一个触碰点,所有采用 0 的位置
}
// 移动图片
function moveIMG(targetTouch) {
  // 获取起始坐标
  let top = this.$refs.canvas.offsetParent.offsetTop;
  let startX = targetTouch.pageX - 10;
  let startY = targetTouch.pageY - top - 10;
  // 定义两个变量 作为记录移动时位置的变化
  let nowX = 0;
  let nowY = 0;
  // 获取底图 移动时需要每次都清除画板,但底图位置是不变的
  var baseImg = new Image();
  baseImg.src = '1.png';
  // 等待图片初始化完毕后
  baseImg.onload = () => {
    // 添加一个 touchmove 事件
    canvas.ontouchmove = e => {
      ctx.clearRect(0, 0, 2600, 2600); //  清除画布
      // 获取原图的宽高 并将其绘制在目标 canvas 上
      let baseWidth = baseImg.width;
      let baseHeight = baseImg.height;
      ctx.drawImage(baseImg, 0, 0, 2600, 2600);
      // 得到移动时触碰点的坐标
      let moveX = e.touches[0].pageX - 10;
      let moveY = e.touches[0].pageY - top - 10;
      let img = '2.png';
      // 设置两张图片的叠加方式
      ctx.globalCompositeOperation = 'destination-over';
      // 当前的图像位置等于 初始化绘制时的偏移度 - 移动时的差值
      nowX = targetImgX - (startX - moveX);
      nowY = targetImgY - (startY - moveY);
      ctx.drawImage(
        img,
        0,
        0,
        img.width,
        img.height,
        nowX,
        nowY,
        img.width * targetImgScale,
        img.height * targetImgScale
      );
    };
    
    canvas.ontouchend = () => {
      // 当用户手指抬起时,将 当前的 偏移度 覆盖原有的偏移度,并且移除两个事件
      targetImgX = nowX;
      targetImgY = nowY;
      canvas.ontouchmove = null;
      canvas.ontouchend = null;
    };
  };
}

实现缩放图片

缩放思路:一个触碰点实现不了缩放,需要两个,当 touchstart 执行时,需要记录初始的坐标点,touchmove 执行时,需要将初始的和当前的进行计算得到一定的比例值,在乘以原有的比例,就是当前的缩放比例

canvas.addEventListener('touchstart', handlerTouchstart); //  给canvas 添加手指按下监听事件
function handlerTouchstart(event) {
  event.preventDefault();
  // 缩放需要两个触碰点,所以需要判断触碰点的个数
  if (event.touches.length > 1) {
    scaleImg(event.touches);
  }
}
// 缩放图片
function scaleImg(targetTouch) {
  // 存放初始坐标点
  let preTouchesClientx1y1x2y2 = [];
  let one = targetTouch[0];
  let two = targetTouch[1];
  preTouchesClientx1y1x2y2 = [one.clientX, one.clientY, two.clientX, two.clientY];
  
  const distance = (x1, y1, x2, y2) => {
    let a = x1 - x2;
    let b = y1 - y2;
    return Math.sqrt(a * a + b * b);
  };
  
  var baseImg = new Image();
  baseImg.src = base.imgUrl;
  let nowScale = this.targetImgScale;
  baseImg.onload = () => {
    // 记录老的图片宽高
    let oldWidth = null;
    let oldHeight = null;
    canvas.ontouchmove = e => {
      // 记录两个触碰点移动的位置 并 计算与起始点的比例
      if (e.touches.length > 1) {
        let ones = e.touches['0'];
        let twos = e.touches['1'];
        nowScale = (
          distance(ones.clientX, ones.clientY, twos.clientX, twos.clientY) /
          distance(...preTouchesClientx1y1x2y2)
        ).toFixed(2);
      }
      this.ctx.clearRect(0, 0, 2600, 2600);
      let baseWidth = baseImg.width;
      let baseHeight = baseImg.height;
      ctx.drawImage(baseImg, 0, 0, 2600, 2600);
      let img = '2.png';
      if (!oldWidth) {
        oldWidth = img.width * targetImgScale;
        oldHeight = img.height * targetImgScale;
      }
      // 获取放大后的图片宽高
      let width = img.width * nowScale * targetImgScale;
      let height = img.height * nowScale * targetImgScale;
      // 计算边距 old - new  = 多出来的边距 / 2 = 定位需要移动的距离
      // 这样就可以保证是根据图片中心来进行缩放
      targetImgX = targetImgX + (oldWidth - width) / 2;
      targetImgY = targetImgY + (oldHeight - height) / 2;
      oldWidth = width;
      oldHeight = height;
      ctx.globalCompositeOperation = 'destination-over';
      // 将当前的缩放比例 乘以原有的缩放比例
      ctx.drawImage(
        img,
        0,
        0,
        img.width,
        img.height,
        targetImgX,
        targetImgY,
        width,
        height
      );
    };
    canvas.ontouchend = () => {
      // 更新缩放值,并且移除事件
      targetImgScale = nowScale * targetImgScale;
      canvas.ontouchmove = null;
      canvas.ontouchend = null;
    };
  };
}

合成头像

最简单的点击按钮将canvas 转换成图片, 在赋值给页面上准备好的 img 元素

let img = this.canvas.toDataURL('image/png');
this.$refs.img.src = img;

以上就是使用本次分享的全部内容,在实现的过程以及在书写该文章的过程也对其他内容进行了引用

核心API drawImage ---- HTML5 Canvas核心技术

缩放功能借鉴了该篇文章 ---- 一步步实现网页图片的手势拖拽与缩放