vue3实现水印功能
项目需求需要一个水印功能,可以自定义各个属性,调整角度不遮挡文字,网上查询资料都没有类似的角度改变不遮挡文字的,就自己来实现了,记录一下
话不多说直接上代码
import { cloneDeep } from '@/utils/common'
import { onBeforeUnmount } from 'vue'
export interface WatermarkOptions {
open: boolean // 是否开启水印
str: Array<string> // 输出文本
color?: string // 字体颜色
bgColor?: string // 背景颜色
fontFamily?: string // 字体 '微软雅黑' | '宋体' | '黑体' | '楷体' | '隶书' | '幼圆' | 'Arial' | 'Verdana' | 'Tahoma' | 'Times New Roman'
fontSize?: number // 字体大小
fontWeight?: string // 字体粗细 normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
fontStyle?: string // 字体倾斜 normal | italic | oblique
textDecoration?: string // 字体下划线 none | underline | overline | line-through
opacity?: number // 透明度 0-1
angle?: number // 倾斜角度
lineHeight?: number // 字体行高
marginRight?: number // 水印水平偏移量
marginBottom?: number // 水印垂直偏移量
}
export const getMark = () => {
/**
* 设置水印方法
* @param {HTMLElement} [container] - 设置水印的容器(如果不写则默认设置全屏水印)
*/
const setWaterMark = (container: HTMLElement, options: WatermarkOptions) => {
// console.log('setWaterMark', options.open)
// 创建唯一标识符id
const id = 'watermark1.23452384164.123412416'
if (document.getElementById(id) !== null) {
container
? container.removeChild(document.getElementById(id)!)
: document.body.removeChild(document.getElementById(id)!)
}
// 创建画布
const can = document.createElement('canvas')
options = cloneDeep(options)
options.marginRight = mmConversionPx(options.marginRight!)
options.marginBottom = mmConversionPx(options.marginBottom!)
drawCanvas(can, options)
// 创建全屏浮动全屏浮动div, 将画布作为背景
const div = document.createElement('div')
div.id = id
div.style.pointerEvents = 'none'
// div.style.top = '0'
// div.style.left = '0'
const gapXCenter = options.marginRight! / 2
const gapYCenter = options.marginBottom! / 2
const offsetLeft = 0
const offsetTop = 0
const left = offsetLeft - gapXCenter
const top = offsetTop - gapYCenter
div.style.top = top > 0 ? `${top}px` : '0'
div.style.left = left > 0 ? `${left}px` : '0'
div.style.position = 'absolute'
div.style.zIndex = '999999'
if (!options.open) {
div.style.display = 'none'
}
// div.style.width = container ? container.clientWidth + 'px' : '100%'
// div.style.height = container ? container.clientHeight + 'px' : '100%'
div.style.width = left > 0 ? `calc(100% - ${left}px)` : '100%'
div.style.height = left > 0 ? `calc(100% - ${left}px)` : '100%'
div.style.backgroundPosition = `${left > 0 ? 0 : left}px ${
top > 0 ? 0 : top
}px`
// div.style.transform = `rotate(${options.angle}deg)`
can.style.transform = `rotate(180deg)`
div.style.background =
'url(' + can.toDataURL('image/png') + ') left top repeat'
if (container) {
container.style.position = 'relative'
container.appendChild(div)
} else {
document.body.appendChild(div)
}
return id
}
// 只允许调用一次该方法
const waterMark = (
container: HTMLElement = document.body,
options: WatermarkOptions
) => {
const defaults: WatermarkOptions = {
open: true,
str: ['水印'],
color: 'rgb(0, 0, 0)',
// fontFamily: 'Microsoft YaHei',
fontFamily: '微软雅黑',
fontSize: 12,
fontWeight: 'normal',
fontStyle: 'normal',
textDecoration: 'none',
opacity: 0.5,
angle: 0,
lineHeight: 1.2,
marginRight: 20,
marginBottom: 10,
}
options = { ...defaults, ...options }
let id = setWaterMark(container, options)
// 定时检查是否存在具有相同标识符的元素
const timer = setInterval(() => {
if (document.getElementById(id) === null) {
id = setWaterMark(container, options)
}
}, 500)
// 监听窗口大小调整, 确保水印的适应性
const handleResize = () => {
setWaterMark(container, options)
}
const observer = new ResizeObserver(handleResize)
// 全屏监听
if (!container) {
observer.observe(document.documentElement, { box: 'content-box' })
}
// 指定容器监听
else {
observer.observe(container, { box: 'content-box' })
}
// 组件销毁时清除监听
onBeforeUnmount(() => {
observer.disconnect()
timer && clearInterval(timer)
console.log('销毁')
})
return {
setNewWaterMark(container: HTMLElement, newOptions: WatermarkOptions) {
setWaterMark(container, newOptions)
},
}
}
return { waterMark }
}
const drawCanvas = (c: any, options: WatermarkOptions) => {
const ctxTextBg = c.getContext('2d')
const ctx = c.getContext('2d')
const textArr = options.str.filter((text) => text.length > 0)
const maxWidthArr: Array<number> = []
const font = getFontStyle({
fontStyle: options.fontStyle,
textDecoration: options.textDecoration,
fontWeight: options.fontWeight,
fontSize: options.fontSize,
fontFamily: options.fontFamily,
})
// 遍历水印文本数组,判断每个元素的长度
textArr.forEach((text) => {
const result = measureText(ctx, text + '', font)
// 最大宽度
maxWidthArr.push(result)
})
// 最大宽度排序,最后取最大的最大宽度maxWidthArr[0]
maxWidthArr.sort((a, b) => {
return b - a
})
// 根据需要切割结果,动态改变canvas的宽和高
const maxWidth = maxWidthArr[0]
const lineHeight = options.fontSize! * (options.lineHeight ?? 1.2)
const height = textArr.length * lineHeight
// 旋转角度
const angle = options.angle! ?? 0
// 计算旋转后的矩形包围框
const radian = (Math.PI / 180) * angle
const rotatedWidth =
Math.abs(Math.sin(radian) * height) + Math.abs(Math.cos(radian) * maxWidth)
const rotatedHeight =
Math.abs(Math.sin(radian) * maxWidth) + Math.abs(Math.cos(radian) * height)
c.width = parseInt(rotatedWidth + options.marginRight! + '', 10)
c.height = parseInt(rotatedHeight + options.marginBottom! + '', 10)
// 宽高重置后,样式也需重置
// ctx.font = `${options.fontWeight} ${options.fontSize}px Vedana`
ctx.font = font
ctx.textBaseline = 'hanging' // 默认是alphabetic,需改基准线为贴着线的方式
ctx.globalAlpha = options.opacity // 透明度
ctx.save()
// 根据设备的像素比来调整画布的大小和绘制位置
const ratio = window.devicePixelRatio || 1
const canvasWidth = c.width * ratio
const canvasHeight = c.height * ratio
// 移动并旋转画布
const rotateX = canvasWidth / 2
const rotateY = canvasHeight / 2
ctx.translate(rotateX, rotateY)
ctx.rotate((Math.PI / 180) * angle)
ctx.translate(-rotateX, -rotateY)
const { left: contentLeft, top: contentTop } = calculateContentPosition(
c.width,
c.height,
maxWidth,
height
)
// 水印背景色
ctxTextBg.fillStyle = options.bgColor ?? 'transparent'
ctxTextBg.width = maxWidth
ctxTextBg.globalAlpha = options.opacity
const ctxTextBgHeight = textArr.length * lineHeight
ctxTextBg.fillRect(contentLeft, contentTop, maxWidth, ctxTextBgHeight)
// 水印文字
textArr.forEach((text, index) => {
ctx.fillStyle = options.color
const y = contentTop + index * lineHeight
ctx.fillText(text ?? '', contentLeft, y)
if (options.textDecoration === 'underline') {
ctx.beginPath()
ctx.moveTo(contentLeft, y + options.fontSize!)
ctx.lineTo(
contentLeft + ctx.measureText(text).width,
y + options.fontSize!
)
ctx.strokeStyle = options.color
ctx.stroke()
}
})
}
// 计算文字宽度
const measureText = (context: any, text: string, font: string) => {
if (font) {
context.font = font
}
return context.measureText(text).width
}
const mmConversionPx = (value: number) => {
if (value === 0) {
return 0
}
const inch = value / 25.4
return inch * conversion_getDPI()[0]
}
const conversion_getDPI = function () {
let deviceXDPI = 0
let deviceYDPI = 0
if ((window.screen as any).deviceXDPI) {
deviceXDPI = (window.screen as any).deviceXDPI
deviceYDPI = (window.screen as any).deviceYDPI
} else {
const tmpNode: any = document.createElement('DIV')
tmpNode.style.cssText =
'width:1in;height:1in;position:absolute;left:0px;top:0px;z-index:99;visibility:hidden'
document.body.appendChild(tmpNode)
deviceXDPI = parseInt(tmpNode.offsetWidth)
deviceYDPI = parseInt(tmpNode.offsetHeight)
tmpNode.parentNode.removeChild(tmpNode)
}
return [deviceXDPI, deviceYDPI]
}
function getFontStyle(options) {
let font = ''
if (options.fontWeight) {
font += options.fontWeight + ' '
}
if (options.fontStyle) {
font += options.fontStyle + ' '
}
if (options.fontSize) {
font += options.fontSize + 'px '
}
if (options.fontFamily) {
font += options.fontFamily
}
return font
}
const calculateContentPosition = function (
containerWidth: number,
containerHeight: number,
contentWidth: number,
contentHeight: number
) {
const left = (containerWidth - contentWidth) / 2
const top = (containerHeight - contentHeight) / 2
return { left, top }
}
使用
<template>
<div class="watermark-container" v-if="isShow" ref="watermarkRef"></div>
</template>
import { getMark, WatermarkOptions } from '@/utils/waterMake'
const watermarkRef = ref<HTMLElement>()
const { waterMark } = getMark()
const changeContent = (value: watermarkSetDataModel) => {
const options = getWatermarkOptions(value)
updateWatermark(waterMarkModal, watermarkRef.value!, options)
}
const updateWatermark = (
waterMarkModalValue: Ref<any>,
watermarkDom: HTMLElement,
options: WatermarkOptions
): void => {
if (waterMarkModalValue.value !== null) {
waterMarkModalValue.value.setNewWaterMark(watermarkDom, options)
} else {
waterMarkModalValue.value = waterMark(watermarkDom, options)
}
}
const getWatermarkOptions = (
value: watermarkSetDataModel
): WatermarkOptions => {
const options: WatermarkOptions = {
open: false,
str: ['我是水印'],
}
if (Object.keys(value).length > 0) {
options.open = value.open
options.str = value.showText!.split('\n')
options.color = value.fontConfig.color
options.bgColor = value.fontConfig.bgColor
options.angle = value.fontConfig.angle
options.opacity = value.fontConfig.transparency / 100
options.marginRight = value.fontConfig.marginRight
options.marginBottom = value.fontConfig.marginBottom
options.fontSize = value.fontConfig.fontSize
options.fontFamily = value.fontConfig.fontFamily
options.fontWeight = value.fontConfig.fontBold ? 'bold' : 'normal'
options.fontStyle = value.fontConfig.fontItalic ? 'italic' : 'normal'
options.textDecoration = value.fontConfig.fontUnderline
? 'underline'
: 'none'
}
return options
}
onMounted(() => {
const setting: any =
Object.keys(props.waterMarkData).length > 0
? props.waterMarkData
: waterMarkStore().waterMarkData
changeContent(setting)
})