Canvas实现水印效果

1,113 阅读3分钟

原理

利用canvas绘制水印,通过canvas的toDataURL方法导出图片,并将图片作为背景填充。

实现(vue)

vue项目中,可通过全局指令方式实现水印效果,给绑定元素添加背景图片。

指令写法

Vue.directive('watermark', {
    mounted: function (el, binding) {
        // binding.value结构为 { text: string, font: string, textColor: string }
        // text: 水印文字  font: 字体  textColor: 文字颜色
        const { text, font, textColor } = binding.value
        // 创建画布
        const canvas = document.createElement('canvas')
        // 获取画笔
        const ctx = canvas.getContext('2d')
        canvas.style.display = 'none'
        if (ctx) {
            // 获取文字宽度
            const offsetX = Math.ceil(ctx.measureText(text).width*3)
            // 宽度会影响列与列之间的距离
            canvas.setAttribute('width', String(offsetX))
            // 高度会影响行与行之间的距离
            canvas.setAttribute('height', String(offsetX/2 + 5))
            // 旋转画笔,负数代表逆时针旋转,Math.PI/6代表30°
            ctx.rotate(-Math.PI/6)
            ctx.font = font || '16px 微软雅黑'
            ctx.fillStyle = textColor || '#ddd'
            ctx.textAlign = 'left'
            ctx.textBaseline = 'middle'
            // 绘制文本
            ctx.fillText(text, 0, offsetX/2 + 5)
        }
        el.style.backgroundImage = 'url(' + canvas.toDataURL('image/png') + ')'
    }
})

使用方法

<div v-watermark="{ text: '测试' }"><div>

效果展示

image.png

使用中存在的问题

由于水印是以背景图片方式添加的,当添加水印的节点上有其他内容,水印内容会被覆盖,如下图:

image.png

这种情况下,可以利用遮罩层,给内容块添加水印。需要给遮罩层添加pointer-event: none;,才能保证原有鼠标事件的触发。

<div
      style="position: absolute;z-index: 999;height: 100%;width: 100%;top: 0;left: 0;pointer-events: none;" 
      v-watermark="{ text: '测试' }">
</div>

基于上述问题,可以对指令做一些调整优化。

// 优化一下
export default {
    mounted: function (el, binding) {
        // binding.value结构为 { text: string, font: string, textColor: string, zIndex: number }
        // text: 水印文字  font: 字体  textColor: 文字颜色
        const { text, font, textColor, zIndex } = binding.value
        // 创建画布
        const canvas = document.createElement('canvas')
        // 获取画笔
        const ctx = canvas.getContext('2d')
        canvas.style.display = 'none'
        if (ctx) {
            // 获取文字宽度
            const offsetX = Math.ceil(ctx.measureText(text).width*3)
            // 宽度会影响列与列之间的距离
            canvas.setAttribute('width', String(offsetX))
            // 高度会影响行与行之间的距离
            canvas.setAttribute('height', String(offsetX/2 + 5))
            // 旋转画笔
            ctx.rotate(-Math.PI/6)
            ctx.font = font || '16px 微软雅黑'
            ctx.fillStyle = textColor || '#ddd'
            ctx.textAlign = 'left'
            ctx.textBaseline = 'middle'
            // 绘制文本
            ctx.fillText(text, 0, offsetX/2 + 5)
        }
        // 创建遮罩层节点
        const container = document.createElement('div')
        container.style.position = 'absolute'
        container.style.width = '100%'
        container.style.height = '100%'
        container.style.zIndex = zIndex || '999'
        container.style.top = '0'
        container.style.left = '0'
        // 去除鼠标事件
        container.style.pointerEvents = 'none'
        // 背景图片添加到遮罩层节点上
        container.style.backgroundImage = 'url(' + canvas.toDataURL('image/png') + ')'
        // 绑定指令的元素末尾追加遮罩层节点
        el.appendChild(container)
    }
}

完成!这样就可以在任意节点直接使用,并且水印内容不会被节点中的其他内容遮挡。

多行水印

水印内容有时候并不是一行展示的,当水印需要分行显示时,上述指令就无法满足需求。因此,需要对指令再做一些修改。

// 支持多行水印
export default {
    mounted: function (el, binding) {
        // binding.value结构为 { text: string | string[], font: string, textColor: string, zIndex: number }
        // text: 水印文字  font: 字体  textColor: 文字颜色
        const { text, font, textColor, zIndex } = binding.value
        // 创建画布
        const canvas = document.createElement('canvas')
        // 获取画笔
        const ctx = canvas.getContext('2d')
        canvas.style.display = 'none'
        if (ctx) {
            const textArr = text.split(',')
            // 根据数组长度判断单行/多行水印
            if (textArr.length === 1) {
                // 获取文字宽度
                const offsetX = Math.ceil(ctx.measureText(textArr[0]).width*3)
                // 宽度会影响列与列之间的距离
                canvas.setAttribute('width', String(offsetX))
                // 高度会影响行与行之间的距离
                canvas.setAttribute('height', String(offsetX/2 + 5))
                // 旋转画笔
                ctx.rotate(-Math.PI/6)
                ctx.font = font || '16px 微软雅黑'
                ctx.fillStyle = textColor || '#ddd'
                ctx.textAlign = 'left'
                ctx.textBaseline = 'middle'
                // 绘制文本
                ctx.fillText(textArr[0], 0, offsetX/2 + 5)
            } else {
                // 找到长度最长的字符串索引
                let lastIndex = findMaxLengthStr(textArr)
                // 获取文字宽度
                const offsetX = Math.ceil(ctx.measureText(textArr[lastIndex]).width*4)
                // 宽度会影响列与列之间的距离
                canvas.setAttribute('width', String(offsetX))
                // 高度会影响行与行之间的距离
                canvas.setAttribute('height', String(offsetX/2 + 25 * textArr.length))
                // 旋转画笔
                ctx.rotate(-Math.PI/6)
                ctx.font = font || '16px 微软雅黑'
                ctx.fillStyle = textColor || '#ddd'
                ctx.textAlign = 'center'
                ctx.textBaseline = 'middle'
                textArr.forEach((textItem, index) => {
                    // 绘制文本
                    ctx.fillText(textItem, 0, offsetX/2 + 5 + 20*index)
                })
            }
            
        }
        const container = document.createElement('div')
        container.style.position = 'absolute'
        container.style.width = '100%'
        container.style.height = '100%'
        container.style.zIndex = zIndex || '999'
        container.style.top = '0'
        container.style.left = '0'
        container.style.pointerEvents = 'none'
        container.style.backgroundImage = 'url(' + canvas.toDataURL('image/png') + ')'
        el.appendChild(container)
    }
}

// 找到字符串数组中,最长字符串所在索引
const findMaxLengthStr = (strArr) => {
    let lastIndex = 0
    let maxLength = 0
    strArr.forEach((item, index) => {
        if (item.length > maxLength) {
            maxLength = item.length
            lastIndex = index
        }
    })
    return lastIndex
}

效果展示

image.png