canvas生成海报

1,389 阅读5分钟

通过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表示图片不平滑。

image.png

这个属性简单易懂,这里就不再赘述了,如果使用这个属性的话,建议加上浏览器前缀做一下兼容

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

这个属性简单来说就是屏幕越好,显示的越清晰,尤其是在视网膜屏上,下面的图片清晰的像我们展示了其区别

image.png

在devicePixelRatio为2的屏幕上我们发现canvas元素是这样的

image.png

在devicePixelRatio为1的屏幕上我们发现canvas元素是这样的

image.png

至于为什么是这样的,咱们先看一下主要的代码

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

image.png

富文本

富文本比较麻烦,需要看富文本最终的内容是什么形式,这里以react-quill为例,拿到的value是简单的标签形式,这里推荐的是用svg+xml的形式,这种形式的好处是不用单独处理标签样式,还能完美还原内容。

const text = `<p><strong>岗位职责: &nbsp;&nbsp;&nbsp;&nbsp;</strong></p>
<p>1、负责财务&nbsp;&nbsp;&nbsp;&nbsp;共享服务领域业务分析;</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(/&nbsp;/g, " ")
    .replace(/<br>/g, "<br />")}`;
const img = document.createElement("img");
img.src = url;
img.style.width = "500";
document.body.appendChild(img);

看一下效果,简直完美

image.png

缺点: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" });

image.png

不粘锅提示:以上的代码是基于本人浅薄的代码理解写出,不保证完全正确。