Canvas简易开发

745 阅读5分钟

我正在参加「掘金·启航计划」

Canvas

什么是canvas

canvas是HTML5中的新增元素,支持使用js在该元素上绘制图像并对其控制;canvas绘制基于像素

canvas元素

可选属性

  • canvas只有两个可选属性:height/width,数值单位为px,默认为width300,height150;
  • 允许设置css属性修改canvas大小,但画布大小不会自动同步,需手动修改width/height属性
  • 允许css宽高与画布宽高不一致,但尽量按比例设置,否则造成扭曲

渲染上下文对象(Rending Context)

canvas元素会创建画布并向外提供渲染上下文对象,以此来绘制/处理画布要展示的内容

  • let ctx = $cavs.getContext('2d/experimental-webgl') 2D渲染/3D渲染

绘制形状

  • 绘制矩形

    • ctx.fillRect(x, y, w, h) 绘制填充矩形
    • ctx.strokeRect(x, y, w, h) 绘制边框矩形
    • ctx.clearRext()x, y, w, h) 清除为透明矩形
  • 绘制路径 路径:颜色,宽度的线条形成形状的点的集合

    • 路径绘制图形步骤
      1. 创建路径(ctx.beginPath())
      2. 调用绘制方法去绘制路径(ctx.moveTo(),ctx.lineTo(),ctx.arc())
      3. 主动闭合路径(ctx.closePath())
      4. 一旦路径生成,通过描边或填充路径区域来渲染图形(ctx.fill())
    • 绘制线
      • ctx.moveTo(x, y)
      • ctx.lineTo(x, y)
    • 绘制圆弧路径
      • ctx.arc(x, y, r, startPi, endPi, direct) 中心绘法
      • ctx.arcTo(xQie, yQie, xEnd, yEn, r) 切线绘法:切点坐标,中点坐标,圆弧半径
    • 绘制矩形
      • ctx.rect(x, y, w, y)

颜色与样式

  • ctx.fillStyle 填充色
  • ctx.strokeStyle 边框色
  • ctx.globalAlpha = 0.8 全局不透明度
  • ctx.lineWidth = 10 线宽,,默认1px
  • ctx.lineCap = butt/round/square 线条以方形/半圆/额外拓长方形结束
  • ctx.lineJoin = round/bevel/miter 线条之间以圆弧,三角形地,方形衔接
  • ctx.setLineDash([20, 5]); 虚线样式:[实线长度, 间隙长度]
  • ctx.lineDashOffset = -0; 虚线绘制起始偏移

绘制文本

  • ctx.fillText('txt', x, y, 可选最大宽度)
    • x,y默认基于文字左下方
    • 超出设置的最大宽度会自动压缩文字
  • ctx.strokeText('txt', x, y, 可选最大宽度)
  • 文本样式
    • ctx.font = '10px sans-serif'(默认) 用来绘制文本的css样式
    • ctx.textAlign文本对齐:start(默认), end, left, right or center
    • ctx.textBaseline基线对齐选项:top, hanging, middle, alphabetic(默认), ideographic, bottom
    • 以上两个属性仅决定文本绘制的基准坐标参考位置
    • ctx.direction文本方向。可能的值包括:ltr, rtl, inherit(默认)

绘制图片

  • ctx.drawImage($img, x, y, w可选, h可选, dx, dy, dw, dh) img元素,位置,大小,切点位置,切区大小

状态的保存与恢复

  • ctx.save()与ctx.restore(),用于保存/恢复状态栈中状态
  • 状态即画布当前状态,包括变形,各个Style值,裁切路径

变形

  • translate(x, y) 移动canvas原点至指定坐标
  • rotate(angle) 旋转画布坐标轴,在进行绘制:顺时针弧度
  • scale(x, y) 增减绘制像素数目
  • transform(a, b, c, d, e, f) 变形矩阵

合成层控制

  • ctx.globalCompositeOperation = copy
    • 默认新层覆盖旧层
    • source-in 仅显示新层的重叠部分
    • source-out 仅显示新层的未重叠部分
    • source-atop 仅显示老层,和新层覆盖部分
    • destination-over 老层覆盖新层
    • destination-in 仅显示老层的重叠部分
    • destination-out 仅显示老层之外的新层
    • destination-atop 仅显示新层,和老层覆盖部分
    • lighter\darken\lighten 重叠部分合成色
    • xor 重叠透明
    • copy 仅显示新图层

裁剪

  • ctx.clip() 将当前的绘制路径path转为裁剪路径,此时仅裁剪路径内可显示绘制

canvas动画

  • requestAnimateFrame

canvas转换

转base64图片

  • let src = $canvas.toDataURL('image/png')

转blob转blob链接

  • let blob = $canvas.toBlob(blob => let url = URL.createObjectURL(blob) ,'image/png')

绘制封装

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地址[github.com/KID-1912/ca…]

  • 特点

    • 以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 video = document.createElement('video');
  video.setAttribute('crossOrigin', 'anonymous'); // 处理跨域
  video.setAttribute('muted', 'muted'); // 静音,防止播放失败
  video.setAttribute('src', videoUrl);
  return new Promise((resolve) => {
    // 视频加载
    video.addEventListener('canplay', async () => {
      let canvas = document.createElement('canvas');
      let { videoWidth, videoHeight } = video;
      // 控制截取大小
      while (videoWidth > 600 || videoHeight > 600) {
        videoWidth /= 2;
        videoHeight /= 2;
      }
      canvas.width = videoWidth;
      canvas.height = videoHeight;
      video.currentTime = second;
      // 播放到时间的帧
      await video.play();
      await video.pause()
      canvas.getContext('2d')?.drawImage(video, 0, 0, videoWidth, videoHeight);
      canvas.toBlob((Blob) => resolve(Blob), 'image/png');  // 返回blob
      //resolve(canvas.toDataURL('image/png')); // 返回base64图片
      video = null;
      canvas = null;
    });
  });
};

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);