原理
利用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>
效果展示
使用中存在的问题
由于水印是以背景图片方式添加的,当添加水印的节点上有其他内容,水印内容会被覆盖,如下图:
这种情况下,可以利用遮罩层,给内容块添加水印。需要给遮罩层添加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
}
效果展示