如何优雅地实现一个通用水印组件

330 阅读5分钟

如何优雅地实现一个通用水印组件

代码来自Ant Design,已改写为通用js组件,轻松接入项目😉

在前端开发中,水印组件广泛应用于保护网页内容不被非法复制或未经授权的使用。通过 Canvas 实现的水印组件具有高度的灵活性与性能优势。在本文中,我将详细介绍如何使用 Canvas 构建一个通用的水印组件,并重点讨论如何测量文本宽高来确保水印精确显示。

先看效果

PixPin_2024-09-11_09-31-25.png

核心思路:通过 Canvas 渲染水印

我们要做的就是在 Canvas 上绘制水印,并将生成的图片用作目标容器的背景。Canvas 的高自由度让我们可以轻松控制水印的样式、位置和透明度等属性。

为什么选择 Canvas 实现水印?

相比其他方法(如图片叠加或 DOM 方式),Canvas 具有以下优势:

  1. 灵活且高可控:可以精确控制文本的旋转角度、透明度、字体样式等,甚至支持图像作为水印。
  2. 较难篡改:由于水印直接渲染到 Canvas 中,不容易被简单的 DOM 操作或 CSS 样式删除。

接下来我们将探讨 Canvas 的关键实现部分。

渲染水印的关键:测量文本宽高

在实现水印渲染时,我们首先需要知道文本的宽度和高度,以确保排布的水印不会重叠、超出边界,或影响页面的整体布局。为此,Canvas 提供了 measureText 方法,它可以用来精确测量文本的宽度。

为什么测量文本宽高如此重要?

  1. 防止文本重叠:在多行文本的水印中,确保每行有足够的间距,以避免水印重叠。
  2. 动态适应文本长度:水印内容长度不固定时,动态测量宽高,确保所有文本都能显示完整。
  3. 精准排布:通过精确测量文本尺寸,可以确保水印内容均匀分布,不会显得杂乱无章。

如何使用 measureText 测量文本尺寸?

measureText 方法可以返回文本的宽度,而为了获取文本的高度,我们结合字体的上升(ascent)和下降(descent)信息来计算。

getMarkSize(ctx) {
  const { content, font = {}, width, height } = this.options;
  let defaultWidth = 120;
  let defaultHeight = 64;

  // 如果没有图片,则测量文本的宽高
  if (!this.options.image && ctx.measureText) {
    ctx.font = `${Number(font.fontSize || 16)}px ${font.fontFamily || 'sans-serif'}`;
    const contents = Array.isArray(content) ? content : [content];

    // 通过 measureText 获取文本的宽度和高度
    const sizes = contents.map(item => {
      const metrics = ctx.measureText(item);
      return [
        metrics.width, // 文本的实际宽度
        metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent + metrics.ideographicBaseline // 文本的实际高度
      ];
    });

    // 确保水印内容不会超出宽高边界
    defaultWidth = Math.ceil(Math.max(...sizes.map(size => size[0])));
    defaultHeight = Math.floor(Math.max(...sizes.map(size => size[1]))) * contents.length + (contents.length - 1) * getFontGap();
  }

  return [width ?? defaultWidth, height ?? defaultHeight];
}

代码解读:

  • ctx.measureText(item).width 用于获取文本的宽度。
  • metrics.fontBoundingBoxAscentmetrics.fontBoundingBoxDescent 是 Canvas 提供的用于计算文本高度的属性。
  • getFontGap() 计算行间距,确保多行文本有足够的间隔。

通过这种方式,我们可以轻松获取每段文本的准确宽度和高度,并确保渲染时不发生重叠或遮挡。

核心实现:Canvas 水印渲染

在测量文本尺寸后,我们使用 Canvas 来渲染水印,并将生成的水印图片应用到目标元素。

javascript
复制代码
function renderWatermark() {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // 获取设备的像素比率,确保在高分辨率设备上水印效果良好
  const ratio = getPixelRatio();
  const [markWidth, markHeight] = this.getMarkSize(ctx);

  // 根据测量到的文本大小进行绘制
  const [nextClips, clipWidth] = renderClips(
    this.options.content,
    this.options.rotate,
    ratio,
    markWidth,
    markHeight,
    this.options.font,
    this.options.gap[0],
    this.options.gap[1],
    this.options.globalAlpha,
    this.options.isDebug
  );

  // 将渲染出的水印应用到目标容器中
  this.appendWatermark(nextClips, clipWidth, this.options.container);
}

渲染过程的详细步骤:

  1. 创建 Canvas:为水印创建一个新的 Canvas 元素,并获取其 2D 渲染上下文。
  2. 计算像素比率:为了兼容高清屏幕设备,使用 getPixelRatio 函数获取设备的像素比率,以调整画布的分辨率。
  3. 获取文本大小:通过之前定义的 getMarkSize 方法获取水印文本的宽度和高度。
  4. 绘制水印:调用 renderClips 函数,根据测量到的文本宽高及其他设置(如旋转角度、透明度等)进行绘制。
  5. 应用水印:将生成的水印图像作为背景图应用到目标元素中。

renderClips 函数的实现

renderClips 函数负责实际的绘制过程。它将文本内容或者图片按照设定的样式绘制到 Canvas 上,然后通过重复背景实现整个水印的排布。

javascript
复制代码
function renderClips(content, rotate, ratio, width, height, font, gapX, gapY, globalAlpha, isDebug = false) {
  const [ctx, canvas, contentWidth, contentHeight] = prepareCanvas(width, height, ratio, isDebug);
  
  if (content instanceof HTMLImageElement) {
    ctx.drawImage(content, 0, 0, contentWidth, contentHeight);
  } else {
    const { color, fontSize, fontStyle, fontWeight, fontFamily, textAlign } = font;
    const mergedFontSize = Number(fontSize) * ratio;
    ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${height}px ${fontFamily}`;
    ctx.fillStyle = color;
    ctx.textAlign = textAlign;
    ctx.textBaseline = 'top';
    const contents = Array.isArray(content) ? content : [content];

    contents.forEach((item, index) => {
      ctx.fillText(item ?? '', contentWidth / 2, index * (mergedFontSize + getFontGap() * ratio) + patchWordRenderHeight(item));
    });
  }

  // 绘制水印的旋转、透明度及间隔逻辑
  // ...省略部分代码
}

总结

通过本文,我们详细展示了如何通过 Canvas 实现一个高效的水印组件,并重点介绍了 文本宽高测量 在水印排版中的重要性。使用 measureText 和其他 Canvas API,我们可以精确控制文本的宽度和高度,从而确保水印的美观与功能性。

使用 Canvas 实现水印是一种高效、灵活且安全的方式,特别适用于需要动态内容和复杂排版的场景。通过这种方式,前端开发者可以轻松地构建出强大且优雅的水印组件。完整代码点此进入在线预览npm