我正在参加「掘金·启航计划」
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) 清除为透明矩形
-
绘制路径 路径:颜色,宽度的线条形成形状的点的集合
- 路径绘制图形步骤
- 创建路径(ctx.beginPath())
- 调用绘制方法去绘制路径(ctx.moveTo(),ctx.lineTo(),ctx.arc())
- 主动闭合路径(ctx.closePath())
- 一旦路径生成,通过描边或填充路径区域来渲染图形(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下无法请求同源资源
- 解决:判断是否为跨域资源,再判断是否能成功对跨域资源处理,不能则直接跳过该资源绘制
注意点
-
允许设置css属性修改canvas大小,但画布大小不会自动同步,需手动修改width/height属性
-
绘制文本默认x,y参考点为文字左下方,ctx.textBaseline = 'top'可设为左上方
-
在执行对应类型的绘制处理前,需要对关键参数进行类型检测
遇到第三方库未修复的问题?
- 在对应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);