好用的canvas生成海报工具

2814

之前在移动端上做了几个cavnas生成海报的需求,发现不是插件不够好用就是自己每次手写代码去绘制cavans十分麻烦,所以抽空集中处理一下这个功能。

需要实现的功能清单

  1. 移动端的主流平台兼容:H5和微信小程序
  2. 像写CSS一样去写海报的样式。
  3. 基础的三样几何图形的绘制:盒子(包括边框)、图片、文字;有了这3个基础几何图形,基本上就可以满足日常海报的样式了。

先来看下最终预览效果 cavnas-生成海报

实现过程

第一点非常好解决,因为都是移动端,所以直接用uni-app即可,至于canvasapi在不同平台端表现不同这里已经解决了,比如CanvasContext.draw: H5端原生的getContext("2d")获取的上下文中是通过context.save()context.restore()就可以继续绘制下一个图形,但uni-app则需要在原生的基础上再多加一个context.draw(true),才能够正常绘制下一个图形,不然就有很奇怪的视图问题,还有其他的像context.fill()context.clip()等一些绘制顺序和调用的方式和网页原生canvas-api不一致就不展开细说了,因为已经处理好。

配置样式传参

第二点,我希望调用这个工具的时候,不用去记一些JSapi进行视图操作,而是像CSS一样设置widthfont-sizeborder-radiusz-index等常用属性,比起JS-apiCSS样式一看就一目了然。接下来需要用TS声明三种几何图形的CSS样式类型,在这里代码提示和约束体验十分明显,看下代码:

/** `cavans`位置类型 */
interface CavansPosition {
    /** 距离顶部偏移值 */
    top?: number
    /** 距离底部偏移值,会覆盖`top` */
    bottom?: number
    /** 距离左边偏移值 */
    left?: number
    /** 距离右边偏移值,会覆盖`left` */
    right?: number
    /** 位置层级,与`css`行为一致 */
    zIndex?: number
}

/** `cavans`矩阵尺寸类型 */
interface CavansRect {
    /** 生成的图片宽度 */
    width: number
    /** 生成的图片高度 */
    height: number
    /**
     * 边框圆角
     * - 当`width === height`,`borderRadius = width / 2`或者`borderRadius = height / 2`就会变成一个圆形
     */
    borderRadius?: number
    /** 边框厚度 */
    borderWidth?: number
    /** 边框颜色 */
    borderColor?: string
}

interface CavansImg extends CavansPosition, Omit<CavansRect, "borderWidth" | "borderColor"> {
    type: "img"
    /**
     * 生成图片的路径
     * 
     * - 网络图片地址,前提是这个图片可以跨域请求,微信小程序端需要配置`request`域名白名单
     * - (仅限H5端生效)本地相对路径地址
     * - (仅限H5端生效)`base64`图片编码,例如:`data:image/jpge;base64,xxxxxxxx`
     */
    src: string
}

interface CavansBox extends CavansRect, CavansPosition {
    type: "box"
    /** 容器背景颜色 */
    backgroundColor: string
}

interface CavansText extends CavansPosition {
    type: "text"
    /** 文字内容 */
    text: string
    /** 字体大小 */
    fontSize: number
    /** 字体颜色 */
    color: string
    // /** 指定字体的宽度,超过会被挤压 */
    // width?: number
    // /** 与`css`的`font-family`行为一致 */
    // fontFamily?: string
    /**
     * 与`css`的`text-align`行为一致
     * [参考](https://uniapp.dcloud.io/api/canvas/CanvasContext?id=canvascontextsettextalign)
     * - 默认`"left"`
     */
    textAlign?: "left" | "center" | "right"
    /**
     * 用于设置文字的水平对齐
     * [参考](https://uniapp.dcloud.io/api/canvas/CanvasContext?id=canvascontextsettextbaseline)
     * - 默认:`normal`
     */
    textBaseline?:  "top" | "bottom" | "middle" | "normal"
}

interface CavansFail {
    /** 错误信息 */
    errMsg: string
    /**
     * 错误类型
     * - `export`: canvas导出图片路径错误
     * - `load`: 图片加载失败错误
     */
    type: "export" | "load"
    /** 图片加载失败时携带的对象 */
    info?: CavansImg
}

interface CavansCreaterParams {
    /**
     * `cavans`节点`id`
     * @example
     * ```html
     * <cavans id="xxx" canvas-id="xxx"></cavans>
     * ```
    */
    cavansId: string
    /** `cavans`整体宽度 */
    width: number
    /** `cavans`整体高度 */
    height: number
    /** 生成的内容列表 */
    list: Array<CavansImg | CavansBox | CavansText>
    /**
     * 生成的图片类型
     * - 默认`"png"`
     */
    fileType?: "jpg" | "png"
    /** 成功回调 */
    success?: (res: UniApp.CanvasToTempFilePathRes) => void
    /** 图片加载失败回调 */
    fail?: (error: CavansFail) => void
}

最后看下调用的样子

cavansCreater({
    cavansId: "the-cavans",
    width: 300,
    height: 500
    list: [
        {
            type: "box",
            width: 40,
            height: 40,
            backgroundColor: "#07c160",
            borderRadius: 1000,
            borderColor: "orange",
            borderWidth: 10,
            left: 20,
            top: 20,
            zIndex: 10,
        },
        {
            type: "box",
            width: 40,
            height: 40,
            backgroundColor: "#07c160",
            borderRadius: 1000,
            borderColor: "orange",
            borderWidth: 10,
            right: 20,
            top: 20,
            zIndex: 10,
        },
        {
            type: "box",
            width: 40,
            height: 40,
            backgroundColor: "orange",
            borderRadius: 1000,
            borderColor: "#07c160",
            borderWidth: 10,
            left: 20,
            bottom: 20,
            zIndex: 10,
        },
        {
            type: "box",
            width: 40,
            height: 40,
            backgroundColor: "orange",
            borderRadius: 1000,
            borderColor: "#07c160",
            borderWidth: 10,
            right: 20,
            bottom: 20,
            zIndex: 10,
        },
        {
            type: "box",
            width: 300,
            height: 500,
            backgroundColor: "#eee",
            borderRadius: 60
        },
        {
            type: "img",
            src: "https://muse-ui.org/img/img3.6e264e66.png",
            // src: "../static/logo.png",
            width: 300,
            height: 217,
            // borderRadius: 100
        },
        {
            type: "img",
            src: "https://game.gtimg.cn/images/lol/act/img/champion/Talon.png",
            // src: "../static/logo.png",
            width: 60,
            height: 60,
            borderRadius: 10,
            bottom: 50,
            left: 50,
            // zIndex: 12
        },
        {
            type: "img",
            src: "https://game.gtimg.cn/images/lol/act/img/champion/Zed.png",
            // src: "../static/logo.png",
            width: 60,
            height: 60,
            borderRadius: 50,
            bottom: 50,
            right: 50,
            // zIndex: 12
        },
        {
            type: "text",
            text: "向右对齐的文字",
            color: "green",
            textAlign: "right",
            fontSize: 16,
            top: this.cavansSize.height / 2,
            right: 10,
            zIndex: 20,
        },
        {
            type: "text",
            text: "底部居中文字",
            fontSize: 16,
            color: "orange",
            textAlign: "center",
            textBaseline: "middle",
            bottom: 20,
            left: this.cavansSize.width / 2
        }
    ],
    success(res) {
        console.log("生成的图片信息 >>", res);
    },
    fail(err) {
        console.log("错误信息 >>", err);
    }
});

基础几何图形的绘制

代码片段

配置好传参字段,最后就是实现绘制的步骤了;这里不放代码,只说思路:

  1. 首先是在方法里面拿到配置列表list后,先根据zIndex去排序一次,这样就实现了层级的概念;
  2. 然后就是分别对【3】种几何图形的绘制,分别封装成三个方法,然后数组遍历逐个给它绘制出来,最后生成本地路径导出即可;

像普通盒子需要圆角和边框就要用三角函数的计算裁剪出来,坐标的计算等方法也不展开细说了,因为都是比较精简的就没啥好说,具体看代码。

需要一提的是:微信小程序中,图片的加载和H5的图片加载是不同的,这个问题也是摸索了不少时间,所以代码里面条件编译了两种处理图片的方法,在设置图片时,注意看代码提示的注释即可,问题不大