通过canvas去实现页面截图或者一系列dom操作的,这里首推还是html2canvas,但是在实际应用过程中会发现在某些机型上会有样式问题,图片画的比较模糊,所以这里简单记录一下用原生canvas生成一张海报遇到的问题以及常用的解决方案。
图片不显示
img.onload
这个老生常谈了,当用context.drawImage画图,如果你的图片画不出来,那十有八九就是img.onload问题了。主要的原因就是js在获取这些图片,视频资源时是异步的。这里建议封装成promise的形式,用的直接async/await
function createImg(url) {
const img = new Image();
// 跨域图片能正常裁剪(图片未转化成base64)
// 见:https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
img.crossOrigin = "Anonymous";
img.src = url;
return new Promise((resolve) => {
img.onload = () => {
resolve(img);
};
});
}
async function draw() {
const img = await createImg("url");
// do something
}
模糊
imageSmoothingEnabled
CanvasRenderingContext2D.imageSmoothingEnabled是 Canvas 2D API 用来设置图片是否平滑的属性,true表示图片平滑(默认值),false表示图片不平滑。
这个属性简单易懂,这里就不再赘述了,如果使用这个属性的话,建议加上浏览器前缀做一下兼容
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// set imageSmoothingEnabled
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
devicePixelRatio
这个属性简单来说就是屏幕越好,显示的越清晰,尤其是在视网膜屏上,下面的图片清晰的像我们展示了其区别
在devicePixelRatio为2的屏幕上我们发现canvas元素是这样的
在devicePixelRatio为1的屏幕上我们发现canvas元素是这样的
至于为什么是这样的,咱们先看一下主要的代码
function createHIDPICanvas(w = 200, h = 200) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// Set display size (css pixels).
canvas.style.width = w + "px";
canvas.style.height = h + "px";
// Set actual size in memory (scaled to account for extra pixel density).
const scale = window.devicePixelRatio || 1; // Change to 1 on retina screens to see blurry canvas.
canvas.width = Math.floor(w * scale);
canvas.height = Math.floor(h * scale);
// Normalize coordinate system to use css pixels.
ctx.scale(scale, scale);
}
可以看到,我们通过canvas.width = Math.floor(w * scale);和ctx.scale(scale, scale);来放大画布,但是实际上canvas元素的大小是由canvas.style.width = w + "px";其style的大小控制,所以就相当于其画布的宽高放大,canvas元素还是我们希望的大小,造成了一种放大镜的效果。
常用方法
ellipsics
const text = "根据产品核心卖点整理用户需求,内容标签,配合策划整理直播内容根据产品核心卖点整理用户需求,内容标签,配合策划整理直播内容根据产品核心卖点整理用户需求,内容标签,配合策划整理直播内容根据产品核心卖点整理用户需求,内容标签,配合策划整理直播内容。";
function getEllipsisShowText(ctx, text, maxWidth, maxLine = 1) {
const ellipsisText = "...";
const ellipsisTextWidth = ctx.measureText(ellipsisText).width;
// 先计算出可以画出的最大剩余文字宽度
const restTextWidth = maxWidth * maxLine - ellipsisTextWidth;
let textLine = "";
for (let n = 0; n < text.length; n++) {
textLine += text[n];
const textLineWidth = ctx.measureText(textLine).width;
// 然后遍历累加文字跟剩余宽度比较
if (textLineWidth > restTextWidth && n > 0) {
return textLine.substring(0, textLine.length - 1) + ellipsisText;
}
}
return text;
}
function fillWrapText({ text, x, y, maxOpt = {}, type }) {
// createHiDPICanvas可参考上文实现
const canvas = createHiDPICanvas(300, 500);
const ctx = canvas.getContext("2d");
// 这里可以自由设置画布的文字大小,颜色,字体啥的
ctx.font = "14px";
const { maxWidth = 300, lineHeight = 20, maxLine = 1 } = maxOpt;
let showText = text;
if (type === "ellipsis") {
showText = getEllipsisShowText(ctx, text, maxWidth, maxLine);
}
let line = "";
for (let n = 0; n < showText.length; n++) {
const textLine = line + showText[n];
const textWidth = ctx.measureText(textLine).width;
if (textWidth > maxWidth && n > 0) {
ctx.fillText(line, x, y);
line = showText[n];
y += lineHeight;
} else {
line = textLine;
}
}
ctx.fillText(line, x, y);
document.body.appendChild(canvas);
}
// 如果需要设置ellipsis,则必须要传type,否则maxLine则无效。maxWidth始终有效
fillWrapText({ text, x: 30, y: 30, maxOpt: { maxWidth: 240, maxLine: 3 }, type: "ellipsis" });
富文本
富文本比较麻烦,需要看富文本最终的内容是什么形式,这里以react-quill为例,拿到的value是简单的标签形式,这里推荐的是用svg+xml的形式,这种形式的好处是不用单独处理标签样式,还能完美还原内容。
const text = `<p><strong>岗位职责: </strong></p>
<p>1、负责财务 共享服务领域业务分析;</p>
<p> 2、结合整体规划,对于业务需求进行详细分析,并开'展解决方案及系统原型的设计工作;</p>
<p><br></p>
<p> 3、开展项&目管理,协调各方团队资源共同推进系统建设工作顺利开展,并及时预警管理项目风险;</p>`;
const div = document.createElement("div");
// 这一步是为了转义< > & 。。。
div.innerHTML = text;
const data = `<svg xmlns='http://www.w3.org/2000/svg' width='500' height='1000'>
// 可以在这里使用style样式,如果富文本的样式可枚举,也可以选择这种方案
// <style>
// .ql-center {
// text-align: center
// }
// .ql-right{
// text-align: right
// }
// </style>
<foreignObject width='100%' height='100%'>
<div xmlns='http://www.w3.org/1999/xhtml' style='font-size:14px;color:rgba(41, 44, 50, 1);white-space:pre-wrap'>
${div.innerHTML}
</div>
</foreignObject>
</svg>`;
// xml中不识别<br>,需要转成<br />,https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/br
const url = `data:image/svg+xml,${data
.replace(/#/g, "%23")
.replace(/ /g, " ")
.replace(/<br>/g, "<br />")}`;
const img = document.createElement("img");
img.src = url;
img.style.width = "500";
document.body.appendChild(img);
看一下效果,简直完美
缺点:xml的特殊字符支持的不多,所以遇到需要手动转义,没转义的话图片就挂了。
另外呢就是带class的标签了,可以使用正则匹配,class可枚举的话也可以使用上面svg+xml的形式,下面贴出正则的代码,可以根据自己的需求自行修改
const str = `<p>帮你内部推荐试试</p>
<p class=\"ql-align-center\">的法律三等奖</p>
<p class=\"ql-align-right\">是的精神动力</p>`
function patternClassAndContent(str = "") {
const res = [];
if (!str) {
return res;
}
str.match(/.*?<\/p>/g).forEach((item) => {
const patternResult = /class=['|"]?ql-align-([\w+]+)['|"]?/g.exec(item);
if (patternResult?.length > 0) {
const content = patternResult.input.replace(/<[^>]*>|/g, "");
res.push({ align: patternResult[1], text: content });
} else {
const content = item.replace(/<[^>]*>|/g, "");
res.push({ align: "left", text: content });
}
});
return res;
}
// patternClassAndContent(desc)的结果是:
// [
// { align: "left", text: "帮你内部推荐试试" },
// { align: "center", text: "的法律三等奖" },
// { align: "right", text: "是的精神动力" },
// ];
拿到上面的数组,再根据样式遍历画就行了
圆角矩形
/**
* 绘制圆角矩形
* @param {number} x x轴坐标
* @param {number} y y轴坐标
* @param {number} w 矩形宽度
* @param {number} h 矩形高度
* @param {number} radius 圆角
* @param {number} [tl] top left
* @param {number} [tr] top right
* @param {number} [br] bottom right
* @param {number} [bl] bottom left
* @param {number} [lineWidth] 线宽
* @param {string} [color] 线颜色
*/
function drawRectRadius({ x, y, w, h, radius, tl, tr, br, bl, lineWidth = 2, color = '#dddfe3' }) {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let r = x + w;
let b = y + h;
tl = tl || radius;
tr = tr || radius;
br = br || radius;
bl = bl || radius;
ctx.strokeStyle = color || "black";
ctx.lineWidth = lineWidth || 2;
ctx.beginPath();
ctx.moveTo(x + tl, y);
ctx.lineTo(r - tr, y);
ctx.quadraticCurveTo(r, y, r, y + tr);
ctx.lineTo(r, b - br);
ctx.quadraticCurveTo(r, b, r - br, b);
ctx.lineTo(x + bl, b);
ctx.quadraticCurveTo(x, b, x, b - bl);
ctx.lineTo(x, y + tl);
ctx.quadraticCurveTo(x, y, x + tl, y);
ctx.stroke();
}
drawRectRadius({ x: 10, y: 10, w: 100, h: 50, radius: 5, color: "red" });
不粘锅提示:以上的代码是基于本人浅薄的代码理解写出,不保证完全正确。