Canvas实现

可用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面

Posted by page on June 20, 2022

Canvas实现

绘制封装

canvas绘制海报流程

  • 后台响应绘制所需的数据,将其中的分享链接转二维码图片(base64)
  • 在页面上隐藏的canvas元素上进行绘制
  • 绘制完成后输出为自适应显示的海报图

Uasge用例

let $poster = document.querySelector('#shareCanvas');
// 初始化
let posterCavs = new Cavs({
  el: $poster,
  width: 570,
  height: 865
});
// 传入json绘制参数,调用绘制方法
posterCavs.draw(json);
// 输出为图片
posterCavs.toImage(function (img) {
  document.body.append(img);
})

封装Cavs类

  • github地址[https://github.com/KID-1912/canvas2img]

  • 特点

    • 以json对象表示绘制结构,作为绘制参数
    • 实例的this.ctx指向绘制的目标画布
    • 支持按顺序多次多步绘制
    • 资源请求失败跨过此次绘制

主要功能

  • 支持多次多步绘制

    • 第一次绘制将绘制处理赋为Promise实例存到当前Cavs实例的queue属性
    • 后续绘制处理放到Promise实例的then队列,依次绘制
  • 资源就绪后再执行绘制

    • Promise.all(arr)实现,返回结果为响应资源的列表
    • Promise.all(sourceList.map(url => {…请求url资源}))
  • 资源请求失败跨过绘制

    • 资源请求回调中将失败的资源作为undefined放到imgList
    • 绘制中判断到当前资源为undefined时,则直接跳过

完善功能

  • 默认参数自定义

    • 新建原型属性defaultOptions
    • 绘制前Object.assign({}, defaultOptions, json)
  • 支持将默认坐标基准点从左上角修改为中心位置

  • 支持圆角

    • 根据x, y, h, w等值计算关键坐标
    • 绘制通过关键坐标的path(路径) + clip(裁剪)
    • 锯齿优化:根据设备像素比放大画布绘制;弊端:输出图片大小也会根据比例增大
  • 模拟链接转为base64格式的二维码图片绘制

    • 使用qrcode.js库实现纯前端生成二维码
  • 支持跨域图片资源

    • 跨域图片可被canvas绘制,但canvas转图片会报错无效
    • 方案1:img.crossOrigin = ‘’; 禁止服务器响应匿名信息(IE11+)
    • 方案2:blob+URL.createObjectURL请求(IE10+),但IE9不支持,即IE9下无法请求同源资源
    • 解决:判断是否为跨域资源,再判断是否能成功对跨域资源处理,不能则直接跳过该资源绘制

注意点

  1. 允许设置css属性修改canvas大小,但画布大小不会自动同步,需手动修改width/height属性

  2. 绘制文本默认x,y参考点为文字左下方,ctx.textBaseline = ‘top’可设为左上方

  3. 在执行对应类型的绘制处理前,需要对关键参数进行类型检测

遇到第三方库未修复的问题?

  • 在对应github的PullRequest搜索解决方案
    • qrcode在部分安卓系统无法不生成二维码
    • 搜索到相关PullRequest
  • google搜索到添加了回调功能的respository

刮奖交互

initCanvas() {
  let guagua = this.$refs.guagua; 
  let cavs = this.$refs.cavs;
  this.ctx = cavs.getContext("2d");
  this.offsetX = guagua.offsetLeft;
  this.offsetY = guagua.offsetTop;
  this.width = cavs.clientWidth;
  this.height = cavs.clientHeight;
  cavs.width = this.width;
  cavs.height = this.height;
  // 刮奖遮罩层与抽奖结果
  let cavsImg = new Image();
  cavsImg.addEventListener("load", () => {
    this.ctx.drawImage(cavsImg, 0, 0, cavs.width, cavs.height);
    this.ctx.globalCompositeOperation = "destination-out";
    this.fetch().then(res => this.$store.commit("setPrize", res || {}));
  });
  cavsImg.setAttribute("src", maskImg);
  // 刮奖事件
  this.ctx.lineWidth = 20;
  this.ctx.lineCap = "round";
  this.ctx.lineJoin = "round";
  cavs.addEventListener("touchstart", e => {
    let toucher = e.targetTouches[0];
    let { x: startX, y: startY } = getToucherXY(toucher, this.offsetX, this.offsetY);
    this.ctx.beginPath();
    this.ctx.moveTo(startX, startY);
    cavs.addEventListener("touchmove", this.drawluck);
  });
  cavs.addEventListener("touchend", () => cavs.removeEventListener("touchmove", this.drawluck));
},
// 刮奖操作
drawluck(e) {
  e.preventDefault();
  let toucher = e.targetTouches[0];
  let { x: currentX, y: currentY } = getToucherXY(toucher, this.offsetX, this.offsetY);
  this.ctx.lineTo(currentX, currentY);
  this.ctx.stroke();
  if (this.computeAlpha() > 0.6 && this.showMask) {
    this.showMask = false;
    !this.prize.name && setTimeout(() => this.$router.replace({ name: "Noprize" }), 2000);
  }
},
// 计算实时刮开比例
computeAlpha() {
  let arr = this.ctx.getImageData(0, 0, this.width, this.height).data;
  let pixelsCount = arr.length / 4;
  let alphaCount = 0;
  for (let i = 3, len = arr.length; i < len; i += 4) {
    if (arr[i] === 0) alphaCount += 1;
  }
  return alphaCount / pixelsCount;
}
getToucherXY(toucher, offsetX, offsetY) {
  let x = toucher.pageX - offsetX;
  let y = toucher.pageY - offsetY;
  return { x, y };
}

截取视频帧

// videoUrl:视频资源 second:秒
export const generateVideoCover = function (videoUrl, second = 1) {
    let $ele = document.createElement('video');
    $ele.onloadeddata = async () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      let width = $ele.videoWidth;
      let height = $ele.videoHeight;
      while (width > 600 || height > 600) {  // 限制大小
        width = parseInt(width / 2);
        height = parseInt(height / 2);
      }
      canvas.width = width;
      canvas.height = height;
      $ele.currentTime = second;
      await $ele.play();
      await $ele.pause();
      ctx.drawImage($ele, 0, 0, width, height);
      const blob = await new Promise((resolve) => {
          canvas.toBlob((Blob) => resolve(Blob), 'image/png');
      });
      const arr = file.name.split('.');
      arr.pop();
      const name = `${arr.join('.')}_${Date.now()}.png`;
      const coverFile = new File([blob], name, { type: blob.type });
      $ele = null;
    };
    $ele.stalled = () => {
      $ele = null;
    };
    $ele.src = videoUrl;
  };
};

html2canvas

html2canvs实现截取DOM元素至canvas

const $ele = document.querySelector(`#ele`);
const $canvas = document.createElement('canvas');
$canvas.width = $ele.offsetWidth;
$canvas.height = $ele.offsetHeight;
const options = {
  canvas: $canvas,
  useCORS: true,
  scale: 1 // 默认根据dpr放大,此处canvas宽高固定,应固定为1
};
// fix:html2canvas莫名延迟
await new Promise((resolve) => {
  this.loading = true;
  setTimeout(() => resolve(), 200);
});
try {
  await html2canvas($ele, options);
  const blob = await new Promise((resolve) => {
    $canvas.toBlob((Blob) => resolve(Blob), 'image/png');
  });
} catch (error) {
  console.warn(error);
  this.loading = false;
  return;
}

根据dpr或放大2倍得到两倍图

const scale  = window.devicePixelRatio || 2;
$canvas.width = $widget.offsetWidth * scale;
$canvas.height = $widget.offsetHeight * scale;
const options = {
  canvas: $canvas,
  useCORS: true,
  scale
};

说明: html2canvas的canvas元素样式大小由$canvas.width/height + scale两参数计算后决定;且设置options.width/height是没有任何效果的(bug);

常见问题:

生成结果文字下移

由于 windiCSS 的base样式,对图片添加了 img { display: block }导致,

保证 生成dom的img display值为 initial 即可