80 行 JS 代码实现页面添加水印:文字水印、多行文字水印、图片水印、文字&图片水印
一、水印概括
1. 添加水印的好处
- 信息标识: 水印可以用于标识文档的所有者、保密级别、状态或其他相关信息,帮助用户更好地理解文档内容的属性
- 版权保护: 在文档中添加水印可以帮助保护内容的版权,防止他人未经授权地复制、转载或篡改内容
- 安全保护: 对于敏感信息或机密文档,添加水印可以帮助防止信息泄露,提高文档的安全性
- 提升专业性: 在一些场景下,如商业报告、合同文件等,添加水印可以增加文档的专业性和正式性
- 防止截屏或拷贝: 在网页中添加水印可以防止用户通过截屏或复制粘贴等方式非法获取文档内容
2. 添加水印的坏处
- 视觉干扰: 过多或过显眼的水印可能会影响页面的美观度和可读性,甚至干扰用户对页面内容的浏览和理解
- 性能影响: 在页面中添加水印可能会增加页面加载时间和资源消耗,特别是对于大型或复杂的水印图案或动态水印
- 用户体验: 一些用户可能会认为水印对页面内容的查看和操作产生不必要的干扰,从而降低他们的使用体验
- 不适用于移动端: 在移动设备上,屏幕空间有限,添加水印可能会占用宝贵的屏幕空间,影响用户的阅读和操作
二、技术方案
1. watermark 第三方库
优势:已有 github
库,可直接使用
劣势:
- 库比较老,多年未更新
- 只支持图片水印,不支持文案水印
- 不支持多行水印
- 包比较重,增加
SDK
负担
2. JS 简单实现水印功能
优势:
- 可支持图片和文案水印
- 封装函数即可,轻量
劣势:需要自己封装
三、水印功能实现
1. 水印功能需求
- 需要支持文案和图片
- 可设置文案颜色字体等样式
- 可支持多行文字水印
- 可动态控制水印间隔
- 能铺满全屏
- 可旋转
- 不影响页面其他模块背景展示
2. 功能实现
/**
* 水印功能代码实现
*/
interface IWarterMarkProps{
text?: string | string[] // 水印文案
imgUrl?: string // 水印图片
imageOpacity?: number // 图片透明度
fontStyle?: string // 字体样式
fontVariant?: string // 字体变体
fontWeight?: number // 字体粗细
fontFamily?: string // 字体
fontSize?: number // 字体大小
lineHeight?: number // 字体行高
fontColor?: string // 字体颜色
rotate?: number // 水印旋转角度 deg
canvasRotate?: number // canvas 旋转角度 deg
imageWidth?: number // 图片宽度
imageHeight?: number // 图片高度
xOffset?: number // 水印 x 偏移
yOffset?: number // 水印 y 偏移
width?: number // 宽度
height?: number // 高度
zIndex: number // 层级
}
interface ISetDomProps{
url: string // 图片 URL
rotate?: number // 水印旋转角度
zIndex?: number // 层级
width?: number // 宽度
}
/**
* 设置 dom CSS 属性
* @param {ISetDomProps} props dom 数据类型
*/
const setDomCSSAttr = (props: ISetDomProps) => {
const {
url, rotate = 0, zIndex = 10, width = 32
} = props
let waterMarker = document.getElementById('water-marker')
let isInitDom = true
if (!waterMarker) {
isInitDom = false
waterMarker = document.createElement('div')
waterMarker.id = 'water-marker'
}
Object.assign(waterMarker.style, {
transform: rotate ? `rotate(${rotate}deg)` : '',
backgroundSize: `${width}px`,
backgroundImage: `url(${url}), url(${url})`,
backgroundRepeat: 'repeat',
position: 'fixed',
left: 0,
top: 0,
zIndex,
width: '100vw',
height: '100vh',
pointerEvents: 'none',
mixBlendMode: 'multiply'
});
!isInitDom && document.body.appendChild(waterMarker)
}
/**
* 获取 canvas 像素比
* @param {CanvasRenderingContext2D} context
* @returns 像素比
*/
const getRatio = (context: any) => {
if (!context) {
return 1;
}
const backingStore = context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1
return (window.devicePixelRatio || 1) / backingStore
}
/**
* 设置水印
* @param {IWarterMarkProps} props 水印各种属性
*/
const setWarterMarker = (props: IWarterMarkProps) => {
const {
imgUrl = '', imageOpacity = 1, imageWidth = 0, imageHeight = 0, fontColor = 'rgba(128, 128, 128, .2)',
fontVariant = 'normal', fontWeight = 400, fontSize = 14, lineHeight = 14, fontStyle = 'normal', fontFamily = 'arial', rotate = 0, canvasRotate = 0,
yOffset = 0, xOffset = 0, width = 200, height = 200, zIndex = 10,
} = props
const text = typeof props.text === 'object' ? props.text : props.text ? [props.text] : []
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
const ratio = getRatio(ctx);
const canvasWidth = width * ratio
const canvasHeight = height * ratio
const canvasOffsetLeft = xOffset * ratio
const canvasOffsetTop = yOffset * ratio
canvas.width = canvasWidth
canvas.height = canvasHeight
ctx.rotate(canvasRotate * (Math.PI / 180));
let url = ''
if (text.length) {
ctx.font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize * ratio}px/${lineHeight * ratio}px ${fontFamily || getComputedStyle(document.body).fontFamily}`
ctx.fillStyle = fontColor
text.forEach((content, index) => {
ctx.fillText(content, canvasOffsetLeft, index * lineHeight + canvasOffsetTop + lineHeight * ratio)
})
url = canvas.toDataURL()
}
if (imgUrl) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.referrerPolicy = 'no-referrer';
img.src = imgUrl;
img.onload = () => {
ctx.globalAlpha = imageOpacity
ctx.drawImage(img, canvasOffsetLeft, canvasOffsetTop, (imageWidth || (imageHeight ? img.width * imageHeight / img.height : img.width)) * ratio, (imageHeight || (imageWidth ? img.height * imageWidth / img.width : img.height)) * ratio)
setDomCSSAttr({
url: canvas.toDataURL(),
rotate,
zIndex,
width
})
return
};
return
}
setDomCSSAttr({
url,
rotate,
zIndex,
width
})
}
3. 步骤解析
3.1. setWarterMarker 方法
setWarterMarker
是使用水印的方法,参数是一个对象,包含以下内容
interface IWarterMarkProps{
text?: string | string[] // 水印文案
imgUrl?: string // 水印图片
imageOpacity?: number // 图片透明度
fontStyle?: string // 字体样式
fontVariant?: string // 字体变体
fontWeight?: number // 字体粗细
fontFamily?: string // 字体
fontSize?: number // 字体大小
lineHeight?: number // 字体行高
fontColor?: string // 字体颜色
rotate?: number // 水印旋转角度 deg
canvasRotate?: number // canvas 旋转角度 deg
imageWidth?: number // 图片宽度
imageHeight?: number // 图片高度
xOffset?: number // 水印 x 偏移
yOffset?: number // 水印 y 偏移
width?: number // 宽度
height?: number // 高度
zIndex: number // 层级
}
- 获取各个参数值
- 创建
canvas
,获取canvas
的像素比,设置canvas
的各种参数 - 如果存在
text
属性,则进行遍历绘制 - 如果有图片,则创建
image
,再进行绘制 - 生成
url
,通过setDomCSSAttr
方法进行 dom 渲染
3.2. getRatio 方法
获取 canvas
的像素比,更清晰的绘制 canvas
3.3. setDomCSSAttr 方法
接收一个对象,包含以下内容
interface ISetDomProps{
url: string // 图片 URL
rotate?: number // 水印旋转角度
zIndex?: number // 层级
width?: number // 宽度
}
- 查找/创建
dom
元素 - 设置
style
属性transform
: 旋转backgroundSize
:背景大小backgroundImage
:背景图片backgroundRepeat
:背景重复position
:定位zIndex
:层级pointerEvents
:不能被选中mixBlendMode
:控制元素的混合模式
3.4. CSS mix-blend-mode 属性
mix-blend-mode
是 CSS
中的一个属性,用于控制元素的混合模式。它可以让元素的背景色和内容色以一种特定的方式混合在一起,从而产生各种视觉效果
以下是一些常用的 mix-blend-mode
值:
- normal:默认值,内容色完全覆盖背景色
- multiply:将内容色与背景色相乘。通常用于创建暗色效果
- screen:将内容色与背景色相除,并取反。通常用于创建亮色效果
- overlay:结合了
multiply
和screen
模式,根据背景色的亮度来决定内容色的混合模式 - darken:内容色和背景色中的每个像素值,取对应像素值的较小者
- lighten:内容色和背景色中的每个像素值,取对应像素值的较大者
- color-dodge:颜色减淡,通过减少背景色的值来增加内容色的亮度
- color-burn:颜色加深,通过增加背景色的值来降低内容色的亮度
- difference:计算内容色和背景色之间的差异,产生颜色反转的效果
- exclusion:将内容色和背景色相减,并添加其结果的两倍
- hue:保留内容色的色相,并使用背景色的饱和度和亮度
- saturation:保留内容色的饱和度,并使用背景色的色相和亮度
- color:保留内容色的色相和饱和度,并使用背景色的亮度
- luminosity:保留内容色的亮度,并使用背景色的色相和饱和度
使用 mix-blend-mode 属性,可以轻松地创建各种视觉效果,例如颜色混合、遮罩效果等
三、水印使用
1. 文字
setWarterMarker({
text: '这是水印, 这是水印, 这是水印',
width: 200,
height: 200,
fontColor: 'red',
fontSize: 8,
lineHeight: 10,
})
2. 图片
setWarterMarker({
imgUrl: 'https://sponsors.vuejs.org/images/chrome_frameworks_fund.png',
imageHeight: 50,
imageWidth: 50,
imageOpacity: 0.1,
})
3. 图片和文字结合
setWarterMarker({
text: '这是水印, 这是水印, 这是水印',
width: 200,
height: 200,
fontColor: 'red',
fontSize: 8,
lineHeight: 10,
imgUrl: 'https://sponsors.vuejs.org/images/chrome_frameworks_fund.png',
imageHeight: 50,
imageWidth: 50,
imageOpacity: 0.1,
canvasRotate: 15
})
4. 多行水印和图片
setWarterMarker({
text: ['这是水印, 这是水印, 这是水印', '第二行水印', '第三行水印', '第四行水印'],
width: 200,
height: 200,
fontColor: 'red',
fontSize: 8,
lineHeight: 40,
imgUrl: 'https://sponsors.vuejs.org/images/chrome_frameworks_fund.png',
imageHeight: 50,
imageWidth: 50,
imageOpacity: 0.1,
})
四、总结
- 水印技术调研显示,水印在保护版权、信息标识和安全保护方面具有重要作用
- 通过CSS、JavaScript或第三方库,可以实现页面水印功能
- 过多或显眼的水印可能影响用户体验,而性能和兼容性也是考虑的因素
- 综合评估水印的利弊,可结合实际需求谨慎选择适合的实现方式
- 如果需求有更复杂的水印,可以按步骤去改即可