利用Vue3指令详细实现水印背景!

您所在的位置:网站首页 canvas做背景 利用Vue3指令详细实现水印背景!

利用Vue3指令详细实现水印背景!

2024-07-12 13:18:32| 来源: 网络整理| 查看: 265

页面水印业务相信我们都有遇过,为什么需要给页面添加水印?为了保护自己的版权和知识产权,给图片加上水印一般是为了防止盗图者用于商业用途,损害原作者的权益。那么在我们开发当中有什么方法可以实现呢?一般分为前端实现和后端实现这两种方法,本文主要是学习前端实现方法:

方式一:直接将字体用块元素包裹,动态设置绝对定位,然后通过transform属性旋转。但是需要考虑一个问题,当图片过大或图片过多时会很影响性能,所以就不详细说这一方式了。 方式二:canvas上绘制出字体,设置好样式,最后以图片的样式导出,用图片作为水印层的背景图。

在学习水印层之前,我先抛出两个问题:

如果水印文字长,水印可以实现自适应吗? 能否限制用户修改并删除水印?

其实上面这两个问题是我们做页面水印需要考虑的两个核心问题,好的,话不多说,我们一起带着问题去探索🔍。

首先定义一个指令,我们要明确两点:命名(v-water-mask)和绑定值(配置值,option),实现如下:

// 配置值 const wmOption = reactive({ textArr: ['路灯下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`], deg: -35, }); 复制代码

效果如下图所示:

image.png 从上图中我们可以看出,文字有文本以及时间字符串,水印文字都是倾斜了一定角度,其实就是旋转了一定角度的。那么问题来了,我们可能问这些是怎么设置的?首先这需要使用指令的时候通过一些配置来实现一些固定值,下面这里都把这些配置都封装成一个类了,为什么要这样做?这样就不用使用的时候每次都要设定一个默认值,比如通过定义接口来引用这些配置时每次都需要设置一个默认值:

export class WMOptions { constructor(init?: WMOptions) { if (init) { Object.assign(this, init); } } textArr: Array = ['test', '自定义水印']; // 需要展示的文字,多行就多个元素【必填】 font?: string = '16px "微软雅黑"'; // 字体样式 fillStyle?: string = 'rgba(170,170,170,0.4)'; // 描边样式 maxWidth?: number = 200; // 文字水平时最大宽度 minWidth?: number = 120; // 文字水平时最小宽度 lineHeight?: number = 24; // 文字行高 deg?: number = -45; // 旋转的角度 0至-90之间 marginRight?: number = 120; // 每个水印的右间隔 marginBottom?: number = 40; // 每个水印的下间隔 left?: number = 20; // 整体背景距左边的距离 top?: number = 20; // 整体背景距上边的距离 opacity?: string = '.75'; // 文字透明度 position?: 'fixed' | 'absolute' = 'fixed'; // 容器定位方式(值为absolute时,需要指定一个父元素非static定位) } 复制代码

细心的地我们可能会发现显示地文本是一个数组,这样主要是为了方便分行,聪明地我们可能会问:假如其中一个比较长怎么换行?,别急别急,我们先了解一下指令是怎么定义的:

定义指令:首先定义为一个ObjectDirective对象类型,因为指令也就是通过在不同生命周期中对当前元素做一些操作。

const WaterMask: ObjectDirective = { // el为当前元素 // bind是当前绑定的属性,注意地,由于是vue3实现,这个值是一个ref类型 beforeMount(el: HTMLElement, binding: DirectiveBinding) { // 实现水印的核心方法 waterMask(el, binding); }, mounted(el: HTMLElement, binding: DirectiveBinding) { nextTick(() => { // 禁止修改水印 disablePatchWaterMask(el); }); }, beforeUnmount() { // 清除监听DOM节点的监听器 if (observerTemp.value) { observerTemp.value.disconnect(); observerTemp.value = null; } }, }; export default WaterMask; 复制代码 waterMask方法:实现水印业务细节呈现,对文字的自适应换行,根据页面元素大小来计算合适宽高值。 disablePatchWaterMask方法:通过MutationObserver方法监听DOM元素修改,从而阻止用户取消水印的呈现。

声明指令:在main文件中定义声明指令,这样我们就可以全局使用这个指令了

app.directive('water-mask', WaterMask); 复制代码

接下来我们来看一一分析水印的两个核心方法:waterMask和disablePatchWaterMask。

实现水印功能

通过waterMask方法实现,waterMask方法主要是做了四件事情:

let defaultSettings = new WMOptions(); const waterMask = function (element: HTMLElement, binding: DirectiveBinding) { // 合并默认值和传参配置 defaultSettings = Object.assign({}, defaultSettings, binding.value || {}); defaultSettings.minWidth = Math.min( defaultSettings.maxWidth!, defaultSettings.minWidth! ); // 重置最小宽度 const textArr = defaultSettings.textArr; if (!Util.isArray(textArr)) { throw Error('水印文本必须放在数组中!'); } const c = createCanvas(); // 动态创建隐藏的canvas draw(c, defaultSettings); // 绘制文本 convertCanvasToImage(c, element); // 转化图像 }; 复制代码

获取配置的默认值:由于开发者传参的时候不一定需要把所有配置的传进来,其实按照本身默认的一些值就行,通过浅拷贝把指令绑定的值传进来的一起融合一起就可以更新默认的配置:

创建canvas标签:因为是通过canvas实现的,我们本身是没有直接在template中呈现这个标签,所以需要通过document对象创建canvas标签:

function createCanvas() { const c = document.createElement('canvas'); c.style.display = 'none'; document.body.appendChild(c); return c; } 复制代码

绘制文本:首先遍历传入需要显示的水印信息,也就是textArr文本数组,遍历数组判断数组元素是不是超出了配置的每个水印默认宽高,然后根据文本元素返回超出文本长度的文本分割数组,同时把文本最大宽度返回,最后通过切割结果动态修改canvas的宽高。

function draw(c: any, settings: WMOptions) { const ctx = c.getContext('2d'); // 切割超过最大宽度的文本并获取最大宽度 const textArr = settings.textArr || []; // 水印文本数组 let wordBreakTextArr: Array = []; const maxWidthArr: Array = []; // 遍历水印文本数组,判断每个元素的长度 textArr.forEach((text) => { const result = breakLinesForCanvas(ctx,text + '',settings.maxWidth!,settings.font!); // 合并超出最大宽度的分割数组 wordBreakTextArr = wordBreakTextArr.concat(result.textArr); // 最大宽度 maxWidthArr.push(result.maxWidth); }); // 最大宽度排序,最后取最大的最大宽度maxWidthArr[0] maxWidthArr.sort((a, b) => { return b - a; }); // 根据需要切割结果,动态改变canvas的宽和高 const maxWidth = Math.max(maxWidthArr[0], defaultSettings.minWidth!); const lineHeight = settings.lineHeight!; const height = wordBreakTextArr.length * lineHeight; const degToPI = (Math.PI * settings.deg!) / 180; const absDeg = Math.abs(degToPI); // 根据旋转后的矩形计算最小画布的宽高 const hSinDeg = height * Math.sin(absDeg); const hCosDeg = height * Math.cos(absDeg); const wSinDeg = maxWidth * Math.sin(absDeg); const wCosDeg = maxWidth * Math.cos(absDeg); c.width = parseInt(hSinDeg + wCosDeg + settings.marginRight! + '', 10); c.height = parseInt(wSinDeg + hCosDeg + settings.marginBottom! + '', 10); // 宽高重置后,样式也需重置 ctx.font = settings.font; ctx.fillStyle = settings.fillStyle; ctx.textBaseline = 'hanging'; // 默认是alphabetic,需改基准线为贴着线的方式 // 移动并旋转画布 ctx.translate(0, wSinDeg); ctx.rotate(degToPI); // 绘制文本 wordBreakTextArr.forEach((text, index) => { ctx.fillText(text, 0, lineHeight * index); }); } 复制代码

从上面代码中我们可以看出绘制文本的核心操作是切割超长文本和动态修改canvas的宽高。我们接下来看看这两个操作是如何实现的?

切割超长文本:

measureText()方法是基于当前字型来计算字符串宽度的。

// 根据最大宽度切割文字 function breakLinesForCanvas(context: any,text: string,width: number,font: string) { const result = []; let maxWidth = 0; if (font) { context.font = font; } // 查找切割点 let breakPoint = findBreakPoint(text, width, context); while (breakPoint !== -1) { // 切割点前的元素入栈 result.push(text.substring(0, breakPoint)); // 切割点后的元素 text = text.substring(breakPoint); maxWidth = width; // 查找切割点后的元素是否还有切割点 breakPoint = findBreakPoint(text, width, context); } // 如果切割的最后文本还有文本就push if (text) { result.push(text); const lastTextWidth = context.measureText(text).width; maxWidth = maxWidth !== 0 ? maxWidth : lastTextWidth; } return { textArr: result, maxWidth: maxWidth, }; } 复制代码 寻找切割点:通过二分查找方法查询字符串超长的位置在哪里: // 寻找切换断点 function findBreakPoint(text: string, width: number, context: any) { let min = 0; let max = text.length - 1; while (min 0) { // 删除节点,直接从删除的节点数组中添加回来 mutation.target.append(mutation.removedNodes[0]); } break; case 'attributes': // 为什么是这样处理,我们看一下下面两幅图 mutation.target.setAttribute('style', mutation.target.oldValue); break; default: break; } } }; // 创建一个观察器实例并传入回调函数 const observer = new MutationObserver(callback); // 以上述配置开始观察目标节点 observer.observe(el, config); observerTemp.value = observer; } 复制代码

image.png 从水印到取消水印(勾选到不勾选background-image):我们发现mutation.target属性中的oldValue值就是我们设置style。 image.png 从取消水印到恢复水印(不勾选到勾选background-image):我们发现mutation.target属性中的oldValue值的background-image被注释掉了。

从上面两个转化中,我们就可以直接得出直接赋值当勾选到不勾选是监听到DOM修改的oldValue(真正的style),因为这时候获取到的才是真正style,反之就不是了,由于我们不勾选时的oldValue赋值给不勾选时的style,所以当我们不勾选时再转化为勾选时就是真正style,从而实现不管用户怎么操作都不能取消水印。

本文正在参加「金石计划」



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭