ホームページ > 記事 > ウェブフロントエンド > Vue3 命令を使用して透かしの背景を実装する方法
誰もがページの透かしビジネスに遭遇したことがあると思いますが、なぜページに透かしを追加する必要があるのでしょうか?自分の著作権と知的財産権を保護するために、画像に透かしを追加するのは一般に、海賊版が画像を商業目的で使用したり、元の作者の権利を侵害したりするのを防ぐためです。では、私たちの開発ではどのような方法が実現できるのでしょうか?フロントエンド実装とバックエンド実装の 2 つの方法に大別されますが、この記事では主にフロントエンド実装方法の学習に焦点を当てます:
方法 1: フォントをブロックで直接囲む要素を作成し、絶対位置を動的に設定してから、transform 属性を使用して回転させます。ただし、考慮する必要がある問題があり、画像が大きすぎる場合や画像の数が多すぎる場合は、パフォーマンスに重大な影響を与えるため、この方法については詳しく説明しません。
方法 2: キャンバス上にフォントを描画し、スタイルを設定し、最後にそれを画像としてエクスポートし、その画像を透かしレイヤーの背景画像として使用します。
ウォーターマーク レイヤーについて学ぶ前に、まず 2 つの質問をします。
ウォーターマーク テキストが長い場合、ウォーターマークは適応可能ですか?
ユーザーによるウォーターマークの変更と削除を制限できますか?
実際、上記の 2 つの質問は、ページの透かしを作成するときに考慮する必要がある 2 つの中心的な問題です。それでは、早速、質問を一緒に見ていきましょう??? 。
最初にコマンドを定義します。名前付け (v-water-mask) とバインド値 (設定値、オプション) の 2 点を明確にする必要があります。実装は次のとおりです:
<div v-water-mask:options="wmOption"></div> // 配置值 const wmOption = reactive<WMOptions>({ textArr: ['路灯下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`], deg: -35, });
The効果は次の図に示すとおりです。 表示:
上の図から、テキストにはテキストと時間の文字列が含まれており、透かしテキストは一定の角度で傾いていることがわかります。角度、実際には特定の角度で回転します。そこで疑問が生じます。これらはどのように設定されているのでしょうか?まず第一に、これには命令を使用するときにいくつかの固定値を達成するためにいくつかの設定が必要です。以下では、これらの設定はクラスにカプセル化されています。なぜこれを行うのでしょうか?この方法では、使用するたびにデフォルト値を設定する必要はありません。たとえば、インターフェイスを定義してこれらの構成を参照する場合、毎回デフォルト値を設定する必要があります:
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);
次に、2 つのウォーターマークを 1 つずつ分析しましょう。 2 つのコア メソッド:waterMask と disablePatchWaterMask。
waterMask メソッドによって達成されます。waterMask メソッドは主に 4 つのことを行います:
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); }); }
上記のコードから、テキスト描画の中心的な操作は、長すぎるテキストを切り取り、キャンバスの幅と高さを動的に変更することであることがわかります。これら 2 つの操作がどのように実装されるかを見てみましょう。
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 です。
非常に長いテキストを切り取る:
切り取りポイントを見つける: 二分検索メソッドを使用して、非常に長い文字列の位置をクエリします:
キャンバスの幅と高さを動的に変更する: 回転角度の値、最大幅の値、およびピタゴラスの定理を使用して、幅と高さを 1 つずつ計算します。回転角度をラジアン値に変換します (式: π/180×Angle、つまり (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 中国語 Web サイトの他の関連記事を参照してください。