Vue3水印(Watermark)

news/2024/7/21 18:57:47 标签: vue3, typescript, components

APIs

参数说明类型默认值必传
width水印的宽度,默认值为 content 自身的宽度numberundefinedfalse
height水印的高度,默认值为 content 自身的高度numberundefinedfalse
rotate水印绘制时,旋转的角度,单位 °number-22false
zIndex追加的水印元素的 z-indexnumber9false
image图片源,建议使用 2 倍或 3 倍图,优先级高于文字stringundefinedfalse
content水印文字内容string | string[]‘’false
color字体颜色string‘rgba(0,0,0,.15)’false
fontSize字体大小,单位pxnumber16false
fontWeight字体粗细‘normal’ | ‘light’ | ‘weight’ | number‘normal’false
fontFamily字体类型string‘sans-serif’false
fontStyle字体样式‘none’ | ‘normal’ | ‘italic’ | ‘oblique’‘normal’false
gap水印之间的间距[number, number][100, 100]false
offset水印距离容器左上角的偏移量,默认为 gap/2[number, number][50, 50]false

效果如下图:components/watermark.html">在线预览

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

创建水印组件Watermark.vue

<script setup lang="ts">
import {
  unref,
  shallowRef,
  computed,
  watch,
  onMounted,
  onBeforeUnmount,
  nextTick,
  getCurrentInstance,
  getCurrentScope,
  onScopeDispose
} from 'vue'
import type { CSSProperties } from 'vue'
interface Props {
  width?: number // 水印的宽度,默认值为 content 自身的宽度
  height?: number // 水印的高度,默认值为 content 自身的高度
  rotate?: number // 水印绘制时,旋转的角度,单位 °
  zIndex?: number // 追加的水印元素的 z-index
  image?: string // 图片源,建议使用 2 倍或 3 倍图,优先级高于文字
  content?: string|string[] // 水印文字内容
  color?: string // 字体颜色
  fontSize?: number // 字体大小
  fontWeight?: 'normal'|'light'|'weight'|number // 	字体粗细
  fontFamily?: string // 字体类型
  fontStyle?: 'none'|'normal'|'italic'|'oblique' // 字体样式
  gap?: [number, number] // 水印之间的间距
  offset?: [number, number] // 水印距离容器左上角的偏移量,默认为 gap/2
}
const props = withDefaults(defineProps<Props>(), {
  width: undefined,
  height: undefined,
  rotate: -22,
  zIndex: 9,
  image: undefined,
  content: '',
  color: 'rgba(0,0,0,.15)',
  fontSize: 16,
  fontWeight: 'normal',
  fontFamily: 'sans-serif',
  fontStyle: 'normal',
  gap: () => [100, 100],
  offset: () => [50, 50]
})
/**
 * Base size of the canvas, 1 for parallel layout and 2 for alternate layout
 */
const BaseSize = 2
const FontGap = 3
// 和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。
const containerRef = shallowRef() // ref() 的浅层作用形式
const watermarkRef = shallowRef()
const stopObservation = shallowRef(false)
const gapX = computed(() => props.gap?.[0] ?? 100)
const gapY = computed(() => props.gap?.[1] ?? 100)
const gapXCenter = computed(() => gapX.value / 2)
const gapYCenter = computed(() => gapY.value / 2)
const offsetLeft = computed(() => props.offset?.[0] ?? gapXCenter.value)
const offsetTop = computed(() => props.offset?.[1] ?? gapYCenter.value)
const markStyle = computed(() => {
  const markStyle: CSSProperties = {
    zIndex: props.zIndex ?? 9,
    position: 'absolute',
    left: 0,
    top: 0,
    width: '100%',
    height: '100%',
    pointerEvents: 'none',
    backgroundRepeat: 'repeat'
  }
  /** Calculate the style of the offset */
  let positionLeft = offsetLeft.value - gapXCenter.value
  let positionTop = offsetTop.value - gapYCenter.value
  if (positionLeft > 0) {
    markStyle.left = `${positionLeft}px`
    markStyle.width = `calc(100% - ${positionLeft}px)`
    positionLeft = 0
  }
  if (positionTop > 0) {
    markStyle.top = `${positionTop}px`
    markStyle.height = `calc(100% - ${positionTop}px)`
    positionTop = 0
  }
  markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`

  return markStyle
})
const destroyWatermark = () => {
  if (watermarkRef.value) {
    watermarkRef.value.remove()
    watermarkRef.value = undefined
  }
}
const appendWatermark = (base64Url: string, markWidth: number) => {
  if (containerRef.value && watermarkRef.value) {
    stopObservation.value = true
    watermarkRef.value.setAttribute(
      'style',
      getStyleStr({
        ...markStyle.value,
        backgroundImage: `url('${base64Url}')`,
        backgroundSize: `${(gapX.value + markWidth) * BaseSize}px`
      })
    )
    containerRef.value?.append(watermarkRef.value)
    // Delayed execution
    setTimeout(() => {
      stopObservation.value = false
    })
  }
}
// converting camel-cased strings to be lowercase and link it with Separator
function toLowercaseSeparator(key: string) {
  return key.replace(/([A-Z])/g, '-$1').toLowerCase()
}
function getStyleStr(style: CSSProperties): string {
  return Object.keys(style)
    .map((key: any) => `${toLowercaseSeparator(key)}: ${style[key]};`)
    .join(' ')
}
/*
  Get the width and height of the watermark. The default values are as follows
  Image: [120, 64]; Content: It's calculated by content
*/
const getMarkSize = (ctx: CanvasRenderingContext2D) => {
  let defaultWidth = 120
  let defaultHeight = 64
  const content = props.content
  const image = props.image
  const width = props.width
  const height = props.height
  const fontSize = props.fontSize
  const fontFamily = props.fontFamily
  if (!image && ctx.measureText) {
    ctx.font = `${Number(fontSize)}px ${fontFamily}`
    const contents = Array.isArray(content) ? content : [content]
    const widths = contents.map(item => ctx.measureText(item!).width)
    defaultWidth = Math.ceil(Math.max(...widths))
    defaultHeight = Number(fontSize) * contents.length + (contents.length - 1) * FontGap
  }
  return [width ?? defaultWidth, height ?? defaultHeight] as const
}
// Returns the ratio of the device's physical pixel resolution to the css pixel resolution
function getPixelRatio () {
  return window.devicePixelRatio || 1
}
const fillTexts = (
  ctx: CanvasRenderingContext2D,
  drawX: number,
  drawY: number,
  drawWidth: number,
  drawHeight: number,
) => {
  const ratio = getPixelRatio()
  const content = props.content
  const fontSize = props.fontSize
  const fontWeight = props.fontWeight
  const fontFamily = props.fontFamily
  const fontStyle = props.fontStyle
  const color = props.color
  const mergedFontSize = Number(fontSize) * ratio
  ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${drawHeight}px ${fontFamily}`
  ctx.fillStyle = color
  ctx.textAlign = 'center'
  ctx.textBaseline = 'top'
  ctx.translate(drawWidth / 2, 0)
  const contents = Array.isArray(content) ? content : [content]
  contents?.forEach((item, index) => {
    ctx.fillText(item ?? '', drawX, drawY + index * (mergedFontSize + FontGap * ratio))
  })
}
const renderWatermark = () => {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  const image = props.image
  const rotate = props.rotate ?? -22

  if (ctx) {
    if (!watermarkRef.value) {
      watermarkRef.value = document.createElement('div')
    }

    const ratio = getPixelRatio()
    const [markWidth, markHeight] = getMarkSize(ctx)
    const canvasWidth = (gapX.value + markWidth) * ratio
    const canvasHeight = (gapY.value + markHeight) * ratio
    canvas.setAttribute('width', `${canvasWidth * BaseSize}px`)
    canvas.setAttribute('height', `${canvasHeight * BaseSize}px`)

    const drawX = (gapX.value * ratio) / 2
    const drawY = (gapY.value * ratio) / 2
    const drawWidth = markWidth * ratio
    const drawHeight = markHeight * ratio
    const rotateX = (drawWidth + gapX.value * ratio) / 2
    const rotateY = (drawHeight + gapY.value * ratio) / 2
    /** Alternate drawing parameters */
    const alternateDrawX = drawX + canvasWidth
    const alternateDrawY = drawY + canvasHeight
    const alternateRotateX = rotateX + canvasWidth
    const alternateRotateY = rotateY + canvasHeight

    ctx.save()
    rotateWatermark(ctx, rotateX, rotateY, rotate)

    if (image) {
      const img = new Image()
      img.onload = () => {
        ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight)
        /** Draw interleaved pictures after rotation */
        ctx.restore()
        rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate)
        ctx.drawImage(img, alternateDrawX, alternateDrawY, drawWidth, drawHeight)
        appendWatermark(canvas.toDataURL(), markWidth)
      }
      img.crossOrigin = 'anonymous'
      img.referrerPolicy = 'no-referrer'
      img.src = image
    } else {
      fillTexts(ctx, drawX, drawY, drawWidth, drawHeight)
      /** Fill the interleaved text after rotation */
      ctx.restore()
      rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate)
      fillTexts(ctx, alternateDrawX, alternateDrawY, drawWidth, drawHeight)
      appendWatermark(canvas.toDataURL(), markWidth)
    }
  }
}
// Rotate with the watermark as the center point
function rotateWatermark(
  ctx: CanvasRenderingContext2D,
  rotateX: number,
  rotateY: number,
  rotate: number
) {
  ctx.translate(rotateX, rotateY)
  ctx.rotate((Math.PI / 180) * Number(rotate))
  ctx.translate(-rotateX, -rotateY)
}
onMounted(() => {
  renderWatermark()
})
watch(
  () => [props],
  () => {
    renderWatermark()
  },
  {
    deep: true, // 强制转成深层侦听器
    flush: 'post' // 在侦听器回调中访问被 Vue 更新之后的 DOM
  },
)
onBeforeUnmount(() => {
  destroyWatermark()
})
// Whether to re-render the watermark
const reRendering = (mutation: MutationRecord, watermarkElement?: HTMLElement) => {
  let flag = false
  // Whether to delete the watermark node
  if (mutation.removedNodes.length) {
    flag = Array.from(mutation.removedNodes).some(node => node === watermarkElement)
  }
  // Whether the watermark dom property value has been modified
  if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
    flag = true
  }
  return flag
}
const onMutate = (mutations: MutationRecord[]) => {
  if (stopObservation.value) {
    return
  }
  mutations.forEach(mutation => {
    if (reRendering(mutation, watermarkRef.value)) {
      destroyWatermark()
      renderWatermark()
    }
  })
}
const defaultWindow = typeof window !== 'undefined' ? window : undefined
type Fn = () => void
function tryOnMounted(fn: Fn, sync = true) {
  if (getCurrentInstance()) onMounted(fn)
  else if (sync) fn()
  else nextTick(fn)
}
function useSupported(callback: () => unknown, sync = false) {
  const isSupported = shallowRef<boolean>()
  const update = () => (isSupported.value = Boolean(callback()))
  update()
  tryOnMounted(update, sync)
  return isSupported
}
function useMutationObserver(
  target: any,
  callback: MutationCallback,
  options: any,
) {
  const { window = defaultWindow, ...mutationOptions } = options
  let observer: MutationObserver | undefined
  const isSupported = useSupported(() => window && 'MutationObserver' in window)

  const cleanup = () => {
    if (observer) {
      observer.disconnect()
      observer = undefined
    }
  }

  const stopWatch = watch(
    () => unref(target),
    el => {
      cleanup()

      if (isSupported.value && window && el) {
        observer = new MutationObserver(callback)
        observer!.observe(el, mutationOptions)
      }
    },
    { immediate: true }
  )

  const stop = () => {
    cleanup()
    stopWatch()
  }

  tryOnScopeDispose(stop)

  return {
    isSupported,
    stop
  }
}
function tryOnScopeDispose(fn: Fn) {
  if (getCurrentScope()) {
    onScopeDispose(fn)
    return true
  }
  return false
}
useMutationObserver(containerRef, onMutate, {
  attributes: true // 观察所有监听的节点属性值的变化
})
</script>
<template>
  <div ref="containerRef" style="position: relative;">
    <slot></slot>
  </div>
</template>

在要使用的页面引入

<script setup lang="ts">
import Watermark from './Watermark.vue'
import { reactive } from 'vue'
const model = reactive({
  content: 'Vue Amazing UI',
  color: 'rgba(0,0,0,.15)',
  fontSize: 16,
  fontWeight: 400,
  zIndex: 9,
  rotate: -22,
  gap: [100, 100],
  offset: [50, 50]
})
</script>
<template>
  <div>
    <h1>Watermark 水印</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Watermark content="Vue Amazing UI">
      <div style="height: 360px" />
    </Watermark>
    <h2 class="mt30 mb10">多行水印</h2>
    <h3 class="mb10">通过 content 设置 字符串数组 指定多行文字水印内容。</h3>
    <Watermark :content="['Vue Amazing UI', 'Hello World']">
      <div style="height: 400px" />
    </Watermark>
    <h2 class="mt30 mb10">图片水印</h2>
    <h3 class="mb10">通过 image 指定图片地址。为保证图片高清且不被拉伸,请设置 width 和 height, 并上传至少两倍的宽高的 logo 图片地址。</h3>
    <Watermark
      :height="30"
      :width="130"
      image="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*lkAoRbywo0oAAAAAAAAAAAAADrJ8AQ/original">
      <div style="height: 360px" />
    </Watermark>
    <h2 class="mt30 mb10">自定义配置</h2>
    <h3 class="mb10">通过自定义参数配置预览水印效果。</h3>
    <Flex>
      <Watermark v-bind="model">
        <p class="u-paragraph">
          The light-speed iteration of the digital world makes products more complex. However, human
          consciousness and attention resources are limited. Facing this design contradiction, the
          pursuit of natural interaction will be the consistent direction of Ant Design.
        </p>
        <p class="u-paragraph">
          Natural user cognition: According to cognitive psychology, about 80% of external
          information is obtained through visual channels. The most important visual elements in the
          interface design, including layout, colors, illustrations, icons, etc., should fully
          absorb the laws of nature, thereby reducing the user&apos;s cognitive cost and bringing
          authentic and smooth feelings. In some scenarios, opportunely adding other sensory
          channels such as hearing, touch can create a richer and more natural product experience.
        </p>
        <p class="u-paragraph">
          Natural user behavior: In the interaction with the system, the designer should fully
          understand the relationship between users, system roles, and task objectives, and also
          contextually organize system functions and services. At the same time, a series of methods
          such as behavior analysis, artificial intelligence and sensors could be applied to assist
          users to make effective decisions and reduce extra operations of users, to save
          users&apos; mental and physical resources and make human-computer interaction more
          natural.
        </p>
        <img
          style=" position: relative; z-index: 1; width: 100%; max-width: 800px;"
          src="https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/6.jpg"
          alt="示例图片"
        />
      </Watermark>
      <Flex
        style="
          width: 25%;
          flex-shrink: 0;
          border-left: 1px solid #eee;
          padding-left: 20px;
          margin-left: 20px;
        "
        vertical
        gap="middle"
      >
        <p>Content</p>
        <Input v-model:value="model.content" />
        <p>Color</p>
        <Input v-model:value="model.color" />
        <p>FontSize</p>
        <Slider v-model:value="model.fontSize" :step="1" :min="0" :max="100" />
        <p>FontWeight</p>
        <InputNumber v-model:value="model.fontWeight" :step="100" :min="100" :max="1000" />
        <p>zIndex</p>
        <Slider v-model:value="model.zIndex" :step="1" :min="0" :max="100" />
        <p>Rotate</p>
        <Slider v-model:value="model.rotate" :step="1" :min="-180" :max="180" />
        <p>Gap</p>
        <Space style="display: flex" align="baseline">
          <InputNumber v-model:value="model.gap[0]" :min="0" placeholder="gapX" />
          <InputNumber v-model:value="model.gap[1]" :min="0" placeholder="gapY" />
        </Space>
        <p>Offset</p>
        <Space style="display: flex" align="baseline">
          <InputNumber v-model:value="model.offset[0]" :min="0" placeholder="offsetLeft" />
          <InputNumber v-model:value="model.offset[1]" :min="0" placeholder="offsetTop" />
        </Space>
      </Flex>
    </Flex>
  </div>
</template>
<style>
.u-paragraph {
  margin-bottom: 1em;
  color: rgba(0, 0, 0, .88);
  word-break: break-word;
  line-height: 1.5714285714285714;
}
</style>

http://www.niftyadmin.cn/n/5225262.html

相关文章

【数据结构】——解决topk问题

前言&#xff1a;我们前面已经学习了小堆并且也实现了小堆&#xff0c;那么我们如果要从多个数据里选出最大的几个数据该怎么办呢&#xff0c;这节课我们就来解决这个问题。我们就用建小堆的方法来解决。 首先我们来看到这个方法的时间复杂度&#xff0c;我们先取前k个数据建立…

【Java】泛型的简单使用

文章目录 一、包装类1.基本数据类型和对应的包装类2.自动装箱和自动拆箱3.手动装箱和手动拆箱 二、什么是泛型三、泛型的使用四、裸类型&#xff08;Raw Type&#xff09;五、泛型是如何编译的六、泛型的上界七、泛型方法总结 一、包装类 在了解泛型之前我们先了解什么是包装类…

安装vmware_esxi 超详细

安装vmware_esxi 超详细 </h2><div id"cnblogs_post_body" class"blogpost-body blogpost-body-html">esxi安装手册 1、esxi介绍 ESXI原生架构模式的虚拟化技术&#xff0c;是不需要宿主操作系统的&#xff0c;它自己本身就是操作系统。因此…

爬虫http代理有什么用处?怎么高效使用HTTP代理?

在进行网络爬虫工作时&#xff0c;我们有时会遇到一些限制&#xff0c;比如访问频率限制、IP被封等问题。这时&#xff0c;使用HTTP代理可以有效地解决这些问题&#xff0c;提高爬虫的工作效率。本文将介绍爬虫HTTP代理的用处以及如何高效地使用HTTP代理。 一、爬虫HTTP代理的用…

夸克大模型助力学术科研提效 四大优势提升知识正确性

当严谨的学术科研与创新的大模型技术结合在一起&#xff0c;会擦出什么样的火花&#xff1f;日前&#xff0c;夸克大模型甫一推出便以优秀的性能成为国产大模型中的“学霸”。在中国科学技术协会近期主办的“大模型应用场景研讨会”上&#xff0c;夸克大模型在快速阅读、创作润…

Springboot的excel导出

这里导出excel用到的是 阿里巴巴的easyexcel 1、首先导入依赖 <!--alibaba easyexcel--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.1.6</version> </dependency> 2、…

短视频账号矩阵系统开发--saas源头技术开发(手机版)

目前PC端网页版基本上已经很倦市场了&#xff0c;所以在这种情况下 &#xff0c;我们已经专注开发短视频矩阵系统pc版3年了&#xff0c;目前我们这边核心技术优势就是都是自己一手搭建开发的并且我们的剪辑算法也是自己一手源头开发的&#xff0c;剪辑成本后期运营成本低&#…

单片机霍尔测速系统设计+源程序

一、系统方案 1、本设计采用52单片机作为主控器。 2、霍尔测速送到液晶1602。 3、蜂鸣器报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 void lcd_init()//液晶初始化函数* { write_1602com(0x38);//设置液晶工作模式&#xff0c;意思…