I believe we have all encountered the page watermark business. Why do we need to add a watermark to the page? In order to protect your own copyright and intellectual property rights, adding watermarks to pictures is generally to prevent pirates from using them for commercial purposes and damaging the rights of the original author. So what methods can be achieved in our development? Generally divided into two methods: front-end implementation and back-end implementation. This article mainly focuses on learning the front-end implementation method:
Method 1: Directly wrap the font with block elements and dynamically set absolute positioning. Then rotate it through the transform attribute. However, there is a problem that needs to be considered. When the picture is too large or there are too many pictures, it will seriously affect the performance, so I will not go into detail about this method.
Method 2: Draw the font on the canvas, set the style, and finally export it as a picture, and use the picture as the background image of the watermark layer.
Before learning the watermark layer, I first ask two questions:
If the watermark text is long, can the watermark be adaptive?
Can users be restricted from modifying and deleting watermarks?
In fact, the above two questions are the two core issues that we need to consider when making page watermarks. Okay, without further ado, let’s explore together with the questions???? .
First define a command. We need to clarify two points: naming (v-water-mask) and binding value (configuration value, option). The implementation is as follows:
<div v-water-mask:options="wmOption"></div> // 配置值 const wmOption = reactive<WMOptions>({ textArr: ['路灯下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`], deg: -35, });
The effect is as shown in the figure below Display:
From the above picture we can see that the text contains text and time strings, and the watermark text is tilted at a certain angle, which is actually rotated at a certain angle. . So the question comes, we may ask how these are set up? First of all, this requires some configurations to achieve some fixed values when using instructions. Below, these configurations are encapsulated into a class. Why do we do this? In this way, you don’t need to set a default value every time you use it. For example, when you reference these configurations by defining an interface, you need to set a default value every time:
export class WMOptions { constructor(init?: WMOptions) { if (init) { Object.assign(this, init); } } textArr: Array<string> = ['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定位) }
If you are careful, we may find that the display location The text is an array, which is mainly for the convenience of line breaking. Smartly, we may ask: If one of them is longer, how to break the line? , Don’t worry, don’t worry, let’s first understand how the instruction is defined:
Define the instruction: First define it as an ObjectDirective object type, because the instruction is to perform some operations on the current element in different life cycles.
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 method: realizes the presentation of watermark business details, adaptive line wrapping of text, and calculates appropriate width and height values based on the size of page elements.
disablePatchWaterMask method: monitor DOM element modifications through the MutationObserver method, thereby preventing the user from canceling the presentation of the watermark.
Declaration instruction: Define the declaration instruction in the main file, so that we can use this instruction globally
app.directive('water-mask', WaterMask);
Next, let’s analyze the two watermarks one by one. Two core methods: waterMask and disablePatchWaterMask.
Achieved through the waterMask method. The waterMask method mainly does four things:
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); // 转化图像 };
Get the default value of the configuration: When the developer passes parameters You don’t necessarily need to pass in all the configurations. In fact, just follow some of the default values. By shallow copying the values bound by the instructions and fusing them together, you can update the default configuration:
Create canvas tag : Because it is implemented through canvas, we do not directly present this label in the template, so we need to create the canvas label through the document object:
function createCanvas() { const c = document.createElement('canvas'); c.style.display = 'none'; document.body.appendChild(c); return c; }
Drawing text: First traverse the watermark information that needs to be displayed, and It is the textArr text array. It traverses the array to determine whether the array element exceeds the default width and height of each watermark configured. Then it returns the text segmentation array that exceeds the text length based on the text element. At the same time, it returns the maximum width of the text. Finally, the canvas is dynamically modified through the cutting results. The width and height.
function draw(c: any, settings: WMOptions) { const ctx = c.getContext('2d'); // 切割超过最大宽度的文本并获取最大宽度 const textArr = settings.textArr || []; // 水印文本数组 let wordBreakTextArr: Array<any> = []; const maxWidthArr: Array<number> = []; // 遍历水印文本数组,判断每个元素的长度 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); }); }
From the above code we can see that the core operation of drawing text is to cut overlong text and dynamically modify the width and height of the canvas. Let's take a look at how these two operations are implemented?
The measureText() method calculates the string width based on the current font.
// 根据最大宽度切割文字 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 <= max) { // 二分字符串中点 const middle = Math.floor((min + max) / 2); // measureText()方法是基于当前字型来计算字符串宽度的 const middleWidth = context.measureText(text.substring(0, middle)).width; const oneCharWiderThanMiddleWidth = context.measureText( text.substring(0, middle + 1) ).width; // 判断当前文本切割是否超了的临界点 if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) { return middle; } // 如果没超继续遍历查找 if (middleWidth < width) { min = middle + 1; } else { max = middle - 1; } } return -1; }
So the canvas graphic width is hSinDeg wCosDeg settings.marginRight. The canvas graphic height is: wSinDeg hCosDeg settings.marginBottom.
Cut super long text:
Find the cutting point: query the position of the super long string through the binary search method:
Dynamically modify the width and height of the canvas: calculate the width and height one by one through the rotation angle value, the maximum width value and the Pythagorean theorem. First, we need to convert the rotation angle into radian values (formula: π/180×Angle, that is (Math.PI*settings.deg!) / 180), let’s take a look at the following picture first:
转化图像:通过对当前canvas配置转化为图形url,然后配置元素的style属性。
// 将绘制好的canvas转成图片 function convertCanvasToImage(canvas: any, el: HTMLElement) { // 判断是否为空渲染器 if (Util.isUndefinedOrNull(el)) { console.error('请绑定渲染容器'); } else { // 转化为图形数据的url const imgData = canvas.toDataURL('image/png'); const divMask = el; divMask.style.cssText = `position: ${defaultSettings.position}; left:0; top:0; right:0; bottom:0; z-index:9999; pointer-events:none;opacity:${defaultSettings.opacity}`; divMask.style.backgroundImage = 'url(' + imgData + ')'; divMask.style.backgroundPosition = defaultSettings.left + 'px ' + defaultSettings.top + 'px'; } }
我们都知道,如果用户需要修改html一般都会浏览器调式中的Elements中修改我们网页的元素的样式就可以,也就是我们只要监听到DOM元素被修改就可以,控制修改DOM无法生效。
由于修改DOM有两种方法:修改元素节点和修改元素属性,所以只要控制元素的相关DOM方法中进行相应操作就可以实现我们的禁止。而通过disablePatchWaterMask方法主要做了三件事情:
创建MutationObserver实例:也就是实例化MutationObserver,这样才能调用MutationObserver中的observe函数实现DOM修改的监听。
创建MutationObserver回调函数:通过传入的两个参数,一个当前元素集合和observer监听器。
监听需要监听的元素:调用observer需要传入监听元素以及监听配置,这个可以参考一下MutationObserver用法配置。
function disablePatchWaterMask(el: HTMLElement) { // 观察器的配置(需要观察什么变动) const config = { attributes: true, childList: true, subtree: true, attributeOldValue: true, }; /* MutationObserver 是一个可以监听DOM结构变化的接口。 */ const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; // 当观察到变动时执行的回调函数 const callback = function (mutationsList: any, observer: any) { console.log(mutationsList); for (let mutation of mutationsList) { let type = mutation.type; switch (type) { case 'childList': if (mutation.removedNodes.length > 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; }
从水印到取消水印(勾选到不勾选background-image):我们发现mutation.target属性中的oldValue值就是我们设置style。
从取消水印到恢复水印(不勾选到勾选background-image):我们发现mutation.target属性中的oldValue值的background-image被注释掉了。
从上面两个转化中,我们就可以直接得出直接赋值当勾选到不勾选是监听到DOM修改的oldValue(真正的style),因为这时候获取到的才是真正style,反之就不是了,由于我们不勾选时的oldValue赋值给不勾选时的style,所以当我们不勾选时再转化为勾选时就是真正style,从而实现不管用户怎么操作都不能取消水印。
The above is the detailed content of How to use Vue3 instructions to implement watermark background. For more information, please follow other related articles on the PHP Chinese website!