当前位置: 首页 > news >正文

Vue3水印(Watermark)

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

效果如下图:在线预览

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

创建水印组件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-indeximage?: 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* Only alternate layout is currently supported*/
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.valuelet positionTop = offsetTop.value - gapYCenter.valueif (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 = truewatermarkRef.value.setAttribute('style',getStyleStr({...markStyle.value,backgroundImage: `url('${base64Url}')`,backgroundSize: `${(gapX.value + markWidth) * BaseSize}px`}))containerRef.value?.append(watermarkRef.value)// Delayed executionsetTimeout(() => {stopObservation.value = false})}
}
// converting camel-cased strings to be lowercase and link it with Separato
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 followsImage: [120, 64]; Content: It's calculated by content
*/
const getMarkSize = (ctx: CanvasRenderingContext2D) => {let defaultWidth = 120let defaultHeight = 64const content = props.contentconst image = props.imageconst width = props.widthconst height = props.heightconst fontSize = props.fontSizeconst fontFamily = props.fontFamilyif (!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.contentconst fontSize = props.fontSizeconst fontWeight = props.fontWeightconst fontFamily = props.fontFamilyconst fontStyle = props.fontStyleconst color = props.colorconst mergedFontSize = Number(fontSize) * ratioctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${drawHeight}px ${fontFamily}`ctx.fillStyle = colorctx.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.imageconst rotate = props.rotate ?? -22if (ctx) {if (!watermarkRef.value) {watermarkRef.value = document.createElement('div')}const ratio = getPixelRatio()const [markWidth, markHeight] = getMarkSize(ctx)const canvasWidth = (gapX.value + markWidth) * ratioconst canvasHeight = (gapY.value + markHeight) * ratiocanvas.setAttribute('width', `${canvasWidth * BaseSize}px`)canvas.setAttribute('height', `${canvasHeight * BaseSize}px`)const drawX = (gapX.value * ratio) / 2const drawY = (gapY.value * ratio) / 2const drawWidth = markWidth * ratioconst drawHeight = markHeight * ratioconst rotateX = (drawWidth + gapX.value * ratio) / 2const rotateY = (drawHeight + gapY.value * ratio) / 2/** Alternate drawing parameters */const alternateDrawX = drawX + canvasWidthconst alternateDrawY = drawY + canvasHeightconst alternateRotateX = rotateX + canvasWidthconst alternateRotateY = rotateY + canvasHeightctx.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 nodeif (mutation.removedNodes.length) {flag = Array.from(mutation.removedNodes).some(node => node === watermarkElement)}// Whether the watermark dom property value has been modifiedif (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 } = optionslet observer: MutationObserver | undefinedconst 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] as [number, number],offset: []
})
</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, humanconsciousness and attention resources are limited. Facing this design contradiction, thepursuit 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 externalinformation is obtained through visual channels. The most important visual elements in theinterface design, including layout, colors, illustrations, icons, etc., should fullyabsorb the laws of nature, thereby reducing the user&apos;s cognitive cost and bringingauthentic and smooth feelings. In some scenarios, opportunely adding other sensorychannels 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 fullyunderstand the relationship between users, system roles, and task objectives, and alsocontextually organize system functions and services. At the same time, a series of methodssuch as behavior analysis, artificial intelligence and sensors could be applied to assistusers to make effective decisions and reduce extra operations of users, to saveusers&apos; mental and physical resources and make human-computer interaction morenatural.</p><imgstyle=" 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><Flexstyle="width: 25%;flex-shrink: 0;border-left: 1px solid #eee;padding-left: 20px;margin-left: 20px;"verticalgap="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]" placeholder="gapX" /><InputNumber v-model:value="model.gap[1]" placeholder="gapY" /></Space><p>Offset</p><Space style="display: flex" align="baseline"><InputNumber v-model:value="model.offset[0]" placeholder="offsetLeft" /><InputNumber v-model:value="model.offset[1]" 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.lryc.cn/news/246651.html

相关文章:

  • redis的性能管理、主从复制和哨兵模式
  • 排序算法:归并排序、快速排序、堆排序
  • Redis 面试题——持久化
  • Linux使用固定ip地址
  • ESP Multi-Room Music 方案:支持音频实时同步播放 实现音乐互联共享
  • java分布式锁分布式锁
  • 2. 流程控制|方法|数组|二维数组|递归
  • 22. 自动装配有哪些限制(需要注意)?
  • 14 网关实战:网关聚合API文档
  • css 固定按钮到页面顶部或者底部的实现方式
  • 【Java Spring】SpringBoot常用插件
  • GPT还远远不是真正的智能
  • 计算机网络:网络层
  • 消息队列进阶-1.消息队列的应用场景与选型
  • 浅谈堆和栈内存以及编程语言
  • SpringBootWeb案例_01
  • C语言数据结构-----栈和队列练习题(分析+代码)
  • uniapp基础-教程之HBuilderX配置篇-01
  • 【备忘录】快速回忆ElasticSearch的CRUD
  • 影响PPC广告成本预算的因素,如何计算亚马逊PPC广告预算——站斧浏览器
  • Qt 信号和槽
  • Linux基本命令二
  • isbn api开放接口
  • 助力企业实现更简单的数据库管理,ATOMDB 与 TDengine 完成兼容性互认
  • 如何通过低代码工具,提升运输行业的运营效率和服务质量
  • Vue3中调用外部iframe链接方法
  • Node——事件的监听与触发
  • 一个基于.NET Core开源、跨平台的仓储管理系统
  • 主机安全-WindowsLinux的SSH安全加固
  • pcie-2-rj45速度优化