우리 모두 페이지 워터마크 사업을 접해 본 적이 있을 것입니다. 페이지에 워터마크를 추가해야 하는 이유는 무엇입니까? 자신의 저작권과 지적 재산권을 보호하기 위해 사진에 워터마크를 추가하는 것은 일반적으로 불법 복제자가 상업적 목적으로 사용하고 원저작자의 권리를 손상시키는 것을 방지하기 위한 것입니다. 그렇다면 우리의 개발에서는 어떤 방법을 달성할 수 있습니까? 일반적으로 프론트 엔드 구현과 백엔드 구현의 두 가지 방법으로 나뉩니다. 이 기사는 주로 프론트 엔드 구현 방법 학습에 중점을 둡니다.
방법 1: 글꼴을 블록 요소로 직접 래핑하고 절대 위치를 동적으로 설정합니다. 그런 다음 변환 속성을 통해 회전합니다. 다만, 사진이 너무 크거나 너무 많으면 성능에 심각한 영향을 미치게 되므로 이 방법에 대해서는 자세히 다루지 않겠습니다.
방법 2: 캔버스에 글꼴을 그리고 스타일을 설정한 후 마지막으로 그림으로 내보내고, 그 그림을 워터마크 레이어의 배경 이미지로 사용합니다.
워터마크 레이어를 배우기 전에 먼저 두 가지 질문을 합니다.
워터마크 텍스트가 길면 워터마크가 적응할 수 있나요?
사용자가 워터마크를 수정하고 삭제하는 것을 제한할 수 있나요?
사실 위의 두 가지 문제는 페이지 워터마크를 만들 때 고려해야 할 두 가지 핵심 문제입니다. 자, 더 이상 고민하지 말고 질문을 가지고 함께 살펴보겠습니다.
먼저 명령을 정의해야 합니다. 이름 지정(v-water-mask) 및 바인딩 값(구성 값, 옵션)은 다음과 같습니다.
<div v-water-mask:options="wmOption"></div> // 配置值 const wmOption = reactive<WMOptions>({ textArr: ['路灯下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`], deg: -35, });
효과는 다음과 같습니다.
From 위의 그림을 보면 텍스트에 텍스트와 시간 문자열이 포함되어 있고 워터마크 텍스트가 특정 각도로 기울어져 있는데 실제로는 특정 각도로 회전되어 있는 것을 볼 수 있습니다. 그래서 질문이 옵니다. 이것이 어떻게 설정되어 있는지 물어볼 수 있습니까? 우선, 명령어를 사용할 때 일부 고정된 값을 달성하려면 몇 가지 구성이 필요합니다. 아래에서는 이러한 구성이 클래스에 캡슐화되어 있습니다. 이렇게 하면 사용할 때마다 기본값을 설정할 필요가 없습니다. 예를 들어 인터페이스를 정의하여 이러한 구성을 참조하는 경우 매번 기본값을 설정해야 합니다.
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定位) }
주의하면. , 표시된 텍스트가 배열이라는 것을 알 수 있습니다. 이는 주로 분기의 편의를 위한 것입니다. 그 중 하나가 더 길면 어떻게 분리할 수 있습니까? , 걱정하지 마십시오. 먼저 명령이 정의되는 방식을 이해합시다.
명령 정의: 먼저 이를 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 요소 수정을 모니터링하여 사용자가 워터마크 표시를 취소하는 것을 방지합니다.
선언 명령: 이 명령을 전역적으로 사용할 수 있도록 기본 파일에 선언 명령을 정의합니다.
app.directive('water-mask', WaterMask);
다음으로 워터마킹의 두 가지 핵심 방법인 waterMask와 비활성화PatchWaterMask를 살펴보겠습니다.
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); // 转化图像 };
구성의 기본값 가져오기: 개발자가 매개변수를 전달할 때 반드시 모든 구성을 전달할 필요는 없으므로 실제로는 일부 기본값이면 충분합니다. 얕은 복사를 통해 지침에 바인딩된 값이 전달되고 함께 병합되어 기본 구성을 업데이트합니다.
캔버스 태그 만들기: 캔버스를 통해 구현되므로 이 라벨을 템플릿에 직접 추가하려면 문서 개체를 통해 캔버스 라벨을 만들어야 합니다.
function createCanvas() { const c = document.createElement('canvas'); c.style.display = 'none'; document.body.appendChild(c); return c; }
텍스트 그리기: 먼저 표시해야 하는 워터마크 정보(textArr 텍스트 배열)를 탐색하고 탐색합니다. 배열 요소가 각 워터마크 너비와 높이에 대해 구성된 기본값을 초과하는지 여부를 확인하는 배열, 그런 다음 텍스트 요소에 따라 텍스트 길이를 초과하는 텍스트 분할 배열을 반환하고 동시에 텍스트의 최대 너비를 반환하고, 마지막으로 절단 결과를 통해 캔버스의 너비와 높이를 동적으로 수정합니다.
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); }); }
위의 코드에서 텍스트 그리기의 핵심 작업은 매우 긴 텍스트를 자르고 캔버스의 너비와 높이를 동적으로 수정하는 것임을 알 수 있습니다. 이 두 가지 작업이 어떻게 구현되는지 살펴보겠습니다.
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 <= 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; }
그래서 캔버스 그래픽 너비는 hSinDeg + wCosDeg + settings.marginRight입니다. 캔버스 그래픽 높이는 wSinDeg + hCosDeg + settings.marginBottom입니다.
초장거리 텍스트 자르기:
절단점 찾기: 이진 검색 방법을 통해 초장 문자열의 위치 쿼리:
캔버스의 너비와 높이를 동적으로 수정: 회전 각도를 통해 값, 최대 너비 값 및 피타고라스 정리를 사용하여 너비와 높이를 하나씩 계산해야 합니다. 먼저 회전 각도를 라디안 값으로 변환해야 합니다(공식: π/180° 각도, 즉 (Math.PI). *settings.deg!) / 180), 먼저 아래 사진을 살펴보세요:
转化图像:通过对当前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,从而实现不管用户怎么操作都不能取消水印。
위 내용은 Vue3 지침을 사용하여 워터마크 배경을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!