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*/
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 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 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],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, 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]" :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.mzph.cn/news/182200.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu系统进入休眠后cuda初始化报错

layout: post # 使用的布局&#xff08;不需要改&#xff09; title: torch.cuda.is_available()报错 # 标题 subtitle: ubuntu系统进入休眠后cuda初始化报错 #副标题 date: 2023-11-29 # 时间 author: BY ThreeStones1029 # 作者 header-img: img/about_bg.jpg #这篇文章标题背…

解锁Jira本地部署的数据中心版高级功能,打造高效、智能、精细化的项目管理

近日&#xff0c;在龙智携手Atlassian与JFrog共同举办的“大规模开发创新&#xff1a;如何提升企业级开发效率与质量”的线下研讨会中&#xff0c;龙智高级咨询顾问、Atlassian认证专家叶燕秀为大家带来了精彩演讲&#xff0c;解锁Jira Data Center版的诸多高级功能&#xff0c…

【LeetCode刷题-字符串】--71.简化路径

71.简化路径 思路&#xff1a; 对于给定的字符串&#xff0c;先根据/分割成一个由若干字符串组成的列表&#xff0c;记为names&#xff0c;根据题意names中包含的字符串只能是以下几种&#xff1a; 空字符串一个点两个点只包含英文字母、数字或_的目录名 对于空字符串和一个…

Windows下命令行启动与关闭WebLogic的相关服务

WebLogic 的服务器类型 WebLogic提供了三种类型的服务器&#xff1a; 管理服务器节点服务器托管服务器 示例和关系如下图&#xff1a; 对应三类服务器&#xff0c; 就有三种启动和关闭的方式。本篇介绍使用命令行脚本的方式启动和关闭这三种类型的服务器。 关于WebLogic 的…

分析某款go端口扫描器之一

一、概述 进来在学go的端口检测部分&#xff0c;但是自己写遇到很多问题&#xff0c;又不知道从何入手&#xff0c;故找来网上佬们写的现成工具&#xff0c;学习一波怎么实现的。分析过程杂乱&#xff0c;没啥思路&#xff0c;勿喷。 项目来源&#xff1a;https://github.com/…

如何高效解析不定长度的协议帧

通信设计中考虑协议的灵活性&#xff0c;经常把协议设计成“不定长度”。一个实例如下图&#xff1a;锐米LoRa终端的通信协议帧。 如果一个系统接收上述“不定长度”的协议帧&#xff0c;将会有一个挑战--如何高效接收与解析。 为简化系统设计&#xff0c;我们强烈建议您采用“…

hql面试题之字符串使用split分割,并选择其中的一部分字段的问题

版本&#xff1a;20231109 1.题目&#xff1a; 有两张表,a表有id和abstringr两个字段&#xff0c;b表也有id和bstr两个字段&#xff0c;具体如下 A表&#xff1a; 1abc,bcd,cdf2123,456,789 B表: 1acddef2123456 在a表的abstring字段中用‘,’分割&#xff0c;并取出前两…

关于MySQL的66个问题

SQL基础掌握不错的小伙伴可以跳过这一部分。当然&#xff0c;可能会现场写一些SQL语句&#xff0c;SQ语句可以通过牛客、LeetCode、LintCode之类的网站来练习。 1. 什么是内连接、外连接、交叉连接、笛卡尔积呢&#xff1f; 内连接&#xff08;inner join&#xff09;&#xf…

05_MySQL主从复制架构

任务背景 ##一、真实案例 某同学刚入职公司&#xff0c;在熟悉公司业务环境的时候&#xff0c;发现他们的数据库架构是一主两从&#xff0c;但是两台从数据库和主库不同步。询问得知&#xff0c;已经好几个月不同步了&#xff0c;但是每天会全库备份主服务器上的数据到从服务…

k8s安装步骤

环境&#xff1a; 操作系统&#xff1a;win10 虚拟机&#xff1a;VMware linux发行版&#xff1a;CentOS7.9 CentOS镜像&#xff1a;CentOS-7-x86_64-DVD-2009 master和node节点通信的ip(master)&#xff1a; 192.168.29.164 0.检查配置 本次搭建的集群共三个节点&#xff0c;…

2023年【安全员-A证】考试题及安全员-A证最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 安全员-A证考试题考前必练&#xff01;安全生产模拟考试一点通每个月更新安全员-A证最新解析题目及答案&#xff01;多做几遍&#xff0c;其实通过安全员-A证模拟考试题很简单。 1、【多选题】下列关于高处作业吊篮叙…

【Redis基础】Redis基本的全局命令

✅作者简介&#xff1a;大家好&#xff0c;我是小杨 &#x1f4c3;个人主页&#xff1a;「小杨」的csdn博客 &#x1f433;希望大家多多支持&#x1f970;一起进步呀&#xff01; Redis基本的全局命令 1&#xff0c;KEYS命令 语法&#xff1a;KEYS pattern KEYS命令用来查询服…

Python内置类属性`__name__`属性的使用教程

更多Python学习内容&#xff1a;ipengtao.com Python中的__name__是一种内置的特殊属性&#xff0c;通常用于判断模块是作为主程序运行还是作为模块被导入。本文将深入讲解__name__属性的用法&#xff0c;通过丰富的示例代码展示其在不同情景下的应用。 模块作为主程序运行 当一…

统信UOS_麒麟KYLINOS上使用远程SSH连接的工具electerm

原文链接&#xff1a;统信UOS/麒麟KYLINOS上使用SSH工具electerm Hello&#xff0c;大家好啊&#xff01;在我们日常的工作和学习中&#xff0c;远程控制和管理服务器已经成为一项常见且必要的技能。尤其是对于IT专业人士和开发者来说&#xff0c;一个高效、稳定的远程SSH连接工…

一款LED段码显示屏驱动芯片方案

一、基本概述 TM1620是一种LED&#xff08;发光二极管显示器&#xff09;驱动控制专用IC,内部集成有MCU数字接口、数据锁存器、LED驱动等电路。本产品质量可靠、稳定性好、抗干扰能力强。 二、基本特性 采用CMOS工艺 显示模式&#xff08;8段6位&#xff5e;10段4位&#xff…

模拟Spring源码思想,手写源码,理解@Component,@Value,@Autowired,@Qualifier四个注解

1、BeanDefinition package com.csdn.myspring; import lombok.AllArgsConstructor; import lombok.Data; Data AllArgsConstructor public class BeanDefinition {private String beanName;private Class beanClass; }2、扫描包的工具类MyTools package com.csdn.myspring; im…

排序算法基本原理及实现2

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 &#x1f324;️冒泡排序 &#x1…

简单位运算

文章目录 求 n n n 的第 k k k 位是二进制的几lowbit(n)操作求解 n n n 的最后一个 1 1 1题目练习AcWing 801. 二进制中1的个数CODE1 原码、补码、反码 求 n n n 的第 k k k 位是二进制的几 我们需要用到&运算符&#xff1a;两位都为 1 1 1 时结果才为 1 1 1 &…

Linux小程序之进度条

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;自己能实现进度条 > 毒鸡汤&#xff1a; > …

制作心理咨询小程序的详细指南

随着科技的的发展&#xff0c;小程序已经成为了人们日常生活中不可或缺的一部分。特别是在心理咨询这个领域&#xff0c;小程序可以提供一个更为便捷、高效的服务平台。本文将通过乔拓云平台为例&#xff0c;详细介绍如何制作一个心理咨询小程序。 首先&#xff0c;我们需要注册…