Canvas 绘制个人名片

422 阅读6分钟

前言

  • 在工作中接到了一个需求,要在小程序中根据用户信息生成用户名片,可下载到手机中,做完之后就在想,怎么在 HTML 中使用 canvas 实现这样一个功能。

  • 所以就通过本文来记录整体的实现过程,代码基本都有注释方便快速了解,可能不是最优实现,欢迎大家指正!

  • 获取 代码 请直接拉到最后

  • 先来看一下最终绘制出来的效果图

2022-11-28_154218.png

开始

首先我们完成两个部分,搭建基本结构和解决绘制模糊问题

基本结构

确定 canvas 标签的宽高、位置、样式

<style>
    #myCanvas {
        margin: 300px;
        border-radius: 20px;
        box-shadow: 0 0 10px #E9EAEB;
    }
</style>

<body>
    <canvas id="myCanvas"></canvas>
</body>

<script>
    // 获取节点
    const canvas = document.querySelector('#myCanvas')
    
    // 创建上下文绘图环境
    const context = canvas.getContext("2d")
    
    // 单独声明宽高,其他地方还要使用
    const width = 540, height = 325
    
    // 设置 canvas 元素宽高
    canvas.style.width = `${width}px`
    canvas.style.height = `${height}px`
</script>    

解决绘制模糊问题

canvas 在高清屏中由于 CSS 像素与物理像素的差异,会产生绘制文本和图片模糊的问题,通过以下方法解决,来源:更正 canvas 分辨率 | Web MDN

/**
 * devicePixelRatio:设备像素比 (物理像素 / css像素)
 * 即 css 的 1px 需要几个物理像素
 * */
const ratio = window.devicePixelRatio

// 将画布宽高设置为 像素比乘以元素宽高
canvas.width = Math.floor(ratio * width)
canvas.height = Math.floor(ratio * height)

// 将绘图环境按像素比缩放
context.scale(ratio, ratio)

绘制函数封装

通过效果图我们可以看出,对于名片的绘制,主要分为了绘制图片绘制矩形绘制文本绘制线条几个部分,为了便于使用,我们封装不同的绘制函数来实现。

绘制矩形

绘制矩形函数可以设置矩形的宽高边框样式圆角大小,以及是否具有裁剪功能(即矩形区域外内容将被裁剪不显示)

/**
 * @description 绘制矩形
 *
 * @param {{ left, top }} position 矩形左边距和上边距
 * @param {{ width, height }} size 矩形宽度高度
 * @param {{ width, color }} border 矩形边框宽度颜色
 * @param { string } background 矩形背景色, 默认白色
 * @param { number } round 矩形圆角, 默认无圆角
 * @param { boolean } isClip 是否裁剪矩形外区域, 默认不裁剪
 * */
function rect({ position, size, border = {}, background = 'white', round = 0, isClip = false }) {
    // 最终的 x / y 坐标
    let rectX = position.left, rectY = position.top
    // 最终的矩形宽度 / 高度
    let rectWidth = size.width, rectHeight = size.height

    // 边框宽度
    const roundWidth = border.width || 0
    /**
     * 边框颜色
     * 未设置边框宽度颜色透明
     * 设置宽度未设置颜色使用背景色
     * */
    const roundColor = roundWidth ? (border.color ? border.color : background) : 'transparent'
    // 圆角最小值 0,最大值为矩形宽度的 1/2
    if (round > rectWidth / 2) round = rectWidth / 2
    if (round < 0) round = 0
    // 设置边框宽度
    context.lineWidth = roundWidth
    // 因为 canvas 线条是从中线向两边延伸,所以矩形绘制点在边框的中间,需要取边框宽度的一半
    const roundHalf = roundWidth / 2
    // 开始绘制
    context.beginPath()
    // 起始点 (有圆角的起始点 x 坐标需要加上边框的一半)
    context.moveTo(round ? rectX + round + roundHalf : rectX + round, rectY + roundHalf)
    // 绘制的四个点
    const [rTPoint, rBPoint, lBPoint, lTPoint] = [
        { x: rectWidth + rectX - roundHalf, y: rectY + roundHalf },
        { x: rectWidth + rectX - roundHalf, y: rectHeight + rectY - roundHalf },
        { x: rectX + roundHalf, y: rectHeight + rectY - roundHalf },
        { x: rectX + roundHalf, y: rectY + roundHalf }
    ]
    // 绘制四条切线和和圆角
    context.arcTo(rTPoint.x, rTPoint.y, rBPoint.x, rBPoint.y, round)
    context.arcTo(rBPoint.x, rBPoint.y, lBPoint.x, lBPoint.y, round)
    context.arcTo(lBPoint.x, lBPoint.y, lTPoint.x, lTPoint.y, round)
    context.arcTo(lTPoint.x, lTPoint.y, rTPoint.x, rTPoint.y, round)
    // 裁剪
    if (isClip) context.clip()
    // 绘制边框颜色
    context.strokeStyle = roundColor
    context.stroke()
    // 绘制填充颜色
    context.fillStyle = background
    context.fill()
    // 结束绘制
    context.closePath()
}

绘制图片

绘制图片函数用于对图片进行裁剪并绘制到指定区域

/**
 * @description 绘制图片
 *
 * @param {string} imgUrl 图片路径
 * @param {number} cropImgX 裁剪图片的起始 x 坐标, 默认 0
 * @param {number} cropImgY 裁剪图片的起始 y 坐标, 默认 0
 * @param {{ left, top }} position 绘制图片的起始坐标
 * @param {{ width, height }} size 绘制图片的宽度高度
 * */
function image({ imgUrl, cropImgX = 0, cropImgY = 0, position: { left, top }, size: { width, height } }) {
    // 实例化 img 对象
    const img = new Image()
    // 设置跨域
    img.setAttribute('crossOrigin', 'anonymous')
    // 设置图片地址
    img.src = imgUrl
    // 图片加载完成
    img.onload = () => {
        // 如果未设置裁剪图片宽度高度则使用图片自身高度宽度
        let imgWidth = img.width
        let imgHeight = img.height

        let drawWidth = imgWidth
        // 如果不是正方形,则先按高度等比缩放计算绘制宽度
        if (imgWidth !== imgHeight) drawWidth = Math.floor(imgWidth / imgHeight * height)
        // 绘制图片
        context.drawImage(img, cropImgX, cropImgY, drawWidth, imgHeight, left, top, width, height)
    }
}

绘制文本

绘制文本函数可以设置文本内容样式绘制位置的信息

/**
 * @description 绘制文本
 *
 * @param {string} content 文本内容
 * @param {{ fontSize, color, bold }} style 文本大小 [默认 14]、颜色 [默认黑色]、加粗 [默认不加粗]
 * @param {{ left, top }} position 文本左边距和上边距
 * */
function text({ content, style: { fontSize = 14, color = 'black', bold = false }, position }) {
    // 文本加粗、大小、字体
    context.font = `${bold ? 'bold ' : ''}${fontSize}px YaHei`
    // 文本颜色
    context.fillStyle = color
    // 文本对齐方式
    context.textBaseline = 'top'
    // 绘制文本
    context.fillText(content, position.left, position.top)
}

绘制线条

绘制线条函数可以通过设置线条样式起始坐标路径坐标实现路径的绘制

/**
 * @description 绘制线条
 *
 * @param {{ width, color }} style 线条宽度[默认 1]、颜色[默认黑色]
 * @param {{ x, y }} startPoint 起始点坐标
 * @param {[{ x, y }]} routePoint 路径点坐标集合
 * */
function line({ style: { width = 1, color = 'black' }, startPoint, routePoint }) {
    // 开始绘制
    context.beginPath()
    // 线条大小
    context.lineWidth = width
    // 线条颜色
    context.strokeStyle = color
    // 起始点
    context.moveTo(startPoint.x, startPoint.y)
    // 遍历绘制路径点
    routePoint.forEach(({ x, y }) => context.lineTo(x, y))
    // 绘制线条
    context.stroke()
    // 结束绘制
    context.closePath()
}

其他工具函数

除了以上的绘制函数之外,我们还需要一个集中调用绘制函数的工具函数以及最终保存图片的函数。

调用绘制函数

如果通过反复调用函数的方法来实现绘制,会降低代码可读性,并且出现大量的函数调用代码,所以需要一个工具函数实现单次传参调用。

/**
 * @description 调用绘制函数
 *
 * @param {array} options 绘制配置项数组
 * */
function drawCanvas(options) {
    options.forEach(item => {
        // 根据配置项的 type 类型决定调用的绘制函数
        switch (item.type) {
            case 'rect':
                rect(item)
                break
            case 'image':
                image(item)
                break
            case 'text':
                text(item)
                break
            case 'line':
                line(item)
                break
            default:
                // 指定类型错误或未指定类型则抛错
                throw new Error('存在错误的类型 type')
        }
    })
}

保存图片

通过 a 链接的download 下载属性模拟点击 a 链接实现最终的图片保存

// 监听 canvas 点击事件
canvas.addEventListener('click', () => {
    // 创建 a 标签
    const a = document.createElement('a')
    // 设置 a 链接为图片地址
    a.href = canvas.toDataURL()
    // 设置图片名称
    a.download = new Date().toLocaleTimeString()
    // 触发点击事件
    a.click()
})

具体实现

通过调用绘制函数,传入配置项列表的方式实现绘制

示例

// 配置项列表
const options = [
    {
        type: 'rect',
        position: {
            top: 0,
            left: 0,
        },
        size: { 
            width: 540, 
            height: 325
        },
        background: 'red'
    }
]

// 调用绘制函数
drawCanvas(options)

绘制画布背景

示例

绘制与画布等宽高的白色圆角矩形,对圆角外的内容进行了裁剪

2022-11-26_151208.png

代码

{
    type: 'rect',
    // 绘制起始坐标为画布起始坐标
    position: {
        top: 0,
        left: 0,
    },
    // 宽高来源于顶部基本结构中的声明
    size: { width, height },
    // 圆角大小与 canvas 标签圆角保持一致
    round: 20,
    // 裁剪掉圆角以外区域
    isClip: true
}

绘制左侧图片

示例

2022-11-28_152750.png

代码

{
    type: 'image',
    imgUrl: './img/home.jpg',
    // 图片在 x 轴的偏移, 正值左移, 负值右移
    cropImgX: 130,
    // 图片在 y 轴的偏移, 正值下移, 负值上移
    cropImgY: 0,
    position: {
        top: 0,
        left: 0
    },
    size: {
        width: 140,
        height: 325
    }
}

绘制右侧矩形

示例

2022-11-28_153344.png

代码

{
    type: 'rect',
    position: {
        top: 0,
        left: 140,
    },
    size: {
        width: 400,
        height: 325,
    },
    background: '#F5F5FB'
}

绘制所有文本

示例

2022-11-28_153642.png

代码

// 中文公司名
{
    type: 'text',
    position: {
        top: 40,
        left: 170
    },
    content: '成都没有科技有限公司',
    style: {
        fontSize: 24,
        color: '#999999'
    }
},
// 英文公司名
{
    type: 'text',
    position: {
        top: 75,
        left: 170
    },
    content: 'Chengdu No Science And Technology Co., Ltd',
    style: {
        fontSize: 15,
        color: '#999999'
    }
},
// 姓名
{
    type: 'text',
    position: {
        top: 235,
        left: 170
    },
    content: '王二狗',
    style: {
        fontSize: 24,
        color: '#999999',
        bold: true
    }
},
// 岗位
{
    type: 'text',
    position: {
        top: 280,
        left: 170
    },
    content: '高级CV开发工程师',
    style: {
        fontSize: 16,
        color: '#999999'
    }
},

绘制右上角线条

示例

2022-11-28_153914.png

代码

{
    type: 'line',
    // 线条样式
    style: {
        width: 1,
        color: '#c3c3c3'
    },
    // 起始坐标
    startPoint: {
        x: 490,
        y: 30
    },
    // 路径坐标
    routePoint: [
        { x: 510, y: 30 },
        { x: 510, y: 50 }
    ]
}

绘制二维码

示例

2022-11-28_154218.png

代码

{
    type: 'image',
    imgUrl: './img/code.png',
    position: {
        top: 220,
        left: 435
    },
    size: {
        width: 80,
        height: 80
    }
}

总结

  • 代码都是不断优化的,如果大家发现不足之处欢迎探讨指正!
  • 获取完整代码
https://gitee.com/sanqi37/canvas-card.git