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下无法请求同源资源
- 解决:判断是否为跨域资源,再判断是否能成功对跨域资源处理,不能则直接跳过该资源绘制
注意点
-
允许设置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 $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
即可