前端水印

102 阅读2分钟

vue3实现水印功能

项目需求需要一个水印功能,可以自定义各个属性,调整角度不遮挡文字,网上查询资料都没有类似的角度改变不遮挡文字的,就自己来实现了,记录一下

水印.gif 话不多说直接上代码

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)
})