【前端功能点】canvas绘制海报

167 阅读3分钟

分享海报核心功能

  • 画文字
  • 画图片
  • 返回最终的图片

该功能封装成一个hook,返回一个画图功能的函数,函数实现的思路

  1. 接收参数
  2. 拿到参数并绘制到canvas
  3. 全部绘制完抛出最终图

定义接收的参数

  • width 画布的宽度
  • height 画布的高度
  • dpr 绘画时的尺寸放大比例
  • nodes 要画的内容节点列表
  • onFinished 全部画完后执行的函数

处理绘画的逻辑(此处只写函数体的内容)

  1. 得到画布,定义画布的宽高(乘以放大比例),也是最终成图的宽高
 const canvas: HTMLCanvasElement = document.createElement('canvas');
 const ctx = canvas.getContext('2d');
 canvas.height = height * dpr;
 canvas.width = width * dpr;
  1. 定义绘制节点的函数,该函数接收一个参数 nodes下标
// 绘制图片
const canvasRadiusImg = (ctx, img, x, y, w, h, r = 0) => {
 if (r !== 0) {
   ctx.save();
   ctx.roundRect(x, y, w, h, r);
   ctx.clip();
   ctx.drawImage(img, x, y, w, h);
   ctx.restore();
 } else {
   ctx.drawImage(img, x, y, w, h);
 }
};
// 单个节点绘制完时执行该函数
function fishihed(cb) {
   idx++;
   if (idx >= nodes.length) {
     // 全部节点绘制完
     if (typeof onFinished === 'function') {
       onFinished(canvas.toDataURL());
     }
   } else {
     cb(idx); // cb 就是drawNode方法,未全部绘制完时接着绘制
   }
 }
// 绘制节点
function drawNode(i) {
   const {
     type, // 1:文本  2:图片
     value, // type为 1 时是文本 type为 2 时是图片路径
     dx, // 节点在画布上 x 坐标
     dy, // 节点在画布上 y 坐标
     dw, // 节点在画布上的宽度
     dh, // 节点在画布上的高度
     dr, // type 为 2 时,圆角
     fontSize, // 文本大小
     fontFamily = 'sans-serif',
     textBaseline, // 文本对齐基线
     fillStyle, // 空心文本还是实心文本
     fontType = 'fill',
     fontWeight = 'normal',
     strokeStyle,
   } = nodes[i] || {}; // 结构出单个绘画节点的数据
   if (type === 2) {
     const img = new Image();
     img.setAttribute('src', value);
     img.setAttribute('crossOrigin', 'anonymous');
     if (img.complete || img.width) {
       canvasRadiusImg(ctx, img, dx * dpr, dy * dpr, dw * dpr, dh * dpr, dr * dpr);
       fishihed(drawNode); // 当前节点绘制完传入自身
     } else {
       img.onload = () => {
         canvasRadiusImg(ctx, img, dx * dpr, dy * dpr, dw * dpr, dh * dpr, dr * dpr);
         fishihed(drawNode); // 当前节点绘制完传入自身
       };
     }
     img.onerror = (err) => {
       console.error(err);
       fishihed(drawNode); // 当前节点绘制完传入自身
     };
   } else if (type === 1) {
     ctx.beginPath();
     ctx.font = `${fontWeight} ${fontSize * dpr}px ${fontFamily}`;
     ctx.textBaseline = textBaseline as any;
     if (fontType === 'fill') {
       ctx.fillStyle = fillStyle;
       ctx.fillText(value, dx * dpr, dy * dpr, dw * dpr);
     } else {
       ctx.strokeStyle = strokeStyle;
       ctx.strokeText(value, dx * dpr, dy * dpr, dw * dpr);
     }
     ctx.closePath();
     fishihed(drawNode);
   }
 }
 drawNode(idx); // 首次执行绘制方法

demo

import { useDrawNodes } from 'hook文件地址';
import { useState } from 'react';
const src3 = '图片地址';
// 此处只是简单画了一个图片,一个文字节点
export default () => {
  const [url, setUrl] = useState(null);
  const [onDrawed] = useDrawNodes();

  const onClick = () => {
    onDrawed({
      width: 292,
      height: 175,
      nodes: [
        {
          type: 2,
          value: src3, // 图片地址
          order: 1,
          dx: 0,
          dy: 0,
          dw: 292,
          dh: 175,
          dr: 20,
        },
        {
          type: 1,
          value: 'hello word!', // 文字节点
          order: 2,
          dx: 18,
          dy: 30,
          dw: 126,
          dh: 0,
          dr: 0,
          fontSize: 24,
          textBaseline: 'center',
          fontType: 'fill',
          fillStyle: 'blue',
        },
      ],
      onFinished: (u) => {
        setUrl(u);
      },
    });
  };

  return (
    <div>
      <img style={{ width: '300px' }} src={src3} />
      {url && <img style={{ width: '300px' }} src={url} />}
      <div onClick={onClick}>
        画图
      </div>
    </div>
  );
};

总结

  1. 该方法绘制阶段使用了递归的思想,其实和链表也接近,只是数据定义时用了数组的结构
  2. 如果偶尔出现图片空白的情况,可以在图片url上拼接一个时间戳解决

注意点

  • 节点为图片时,要注意该图片不能设置后端跨域