前端水印方案

index.html
复制

>
<html>
  <head>
    <meta charset="utf-8" />
    <title>水印生成器title>
  head>
  <body>
    <div id="root">
      <div>
        <div>
          <label for="text">水印内容:label>
        div>

        <textarea name="text" id="text" rows="4" cols="50"> textarea>
      div>

      <div>
        <button onclick="addWatermark()">生成水印button>
        <button onclick="cancelWatermark()">取消水印button>
      div>

      <div>
        refs:

        <ul>
          <li>
            <a
              href="https://newspaint.wordpress.com/2014/05/22/writing-rotated-text-on-a-javascript-canvas/"
              >https://newspaint.wordpress.com/2014/05/22/writing-rotated-text-on-a-javascript-canvas/a
            >
          li>
          <li>
            <a
              href="https://stackoverflow.com/questions/25172501/html5-rotate-text-around-its-centre-point"
              >https://stackoverflow.com/questions/25172501/html5-rotate-text-around-its-centre-pointa
            >
          li>
        ul>
      div>
    div>
    <script type="module">
      import { Watermark } from "./watermark.js";

      const waterMark = new Watermark(document.body);

      const $text = document.querySelector("#text");

      $text.value = "法外狂徒张三\n13377801021";

      window.addWatermark = function addWatermark() {
        waterMark.show({
          words: [
            {
              value: "法外狂徒张三",
              color: "rgba(255,0,0,0.5)",
              fontSize: 22,
            },
            {
              value: "13377801021",
              color: "rgba(0,255,0,0.5)",
              fontSize: 16,
            },
            {
              value: "2022-08-20",
              color: "rgba(0,0,255,0.5)",
              fontSize: 10,
            },
          ],
          position: "fixed",
          rotate: 45,
          padding: 20,
          debug: true,
          textAlign: "center",
        });
      };

      window.cancelWatermark = function cancelWatermark() {
        waterMark.dispose();
      };

      addWatermark();
    script>
  body>
html>

watermark.js
复制

const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver;

/**
 * 水印实例
 */
export class Watermark {
  /**
   * @type {HTMLElement} container
   * */
  constructor(container = document.body) {
    /**
     * @type {HTMLElement}
     */
    this.container = container;
    /**
     * @type { MutationObserver }
     */
    this.ob = null;
    /**
     * @type { MutationObserver }
     */
    this.obContainer = null;
  }

  /**
   * 显示水印
   * @returns {number}
   */
  show({
    fillStyle = "rgba(184, 184, 184, 0.6)",
    opacity = 0.5,
    padding = 60,
    textAlign = "center",
    image = null, // 内嵌的图片
    words = [], // [{"value": "xxx", "color": "red"}]
    rotate = 45, // 0 - 90
    zIndex = 10000,
    debug = false,
    position = "absolute",
  } = {}) {
    this.dispose();

    const fontFamily = "verdana";

    const container = this.container;
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");

    const maxWidth =
      Math.round(
        Math.max(
          ...words.map((v) => {
            ctx.font = `${v.fontSize}px ${fontFamily}`;
            const metrics = ctx.measureText(v.value);

            return metrics.width;
          })
        )
      ) +
      padding * 2;

    const lineSpace = 4;

    /**
     * 每行文字高度
     */
    const lineHeights = words.map((v, index) => {
      ctx.textBaseline = "top";
      ctx.font = `${v.fontSize}px ${fontFamily}`;

      const metrics = ctx.measureText(v.value);

      const actualHeight =
        Math.abs(metrics.actualBoundingBoxAscent) + // 边界可能超出基线,会是负数
        metrics.actualBoundingBoxDescent;

      const lineHeight = Math.round(actualHeight);

      return lineHeight;
    });

    const linesText = lineHeights.map((h, index) => {
      const word = words[index];

      // 跟上一行字的间距
      const space = index === 0 ? 0 : index * lineSpace;

      return {
        ...word,
        x: 0,
        y:
          padding + // 容器的 padding
          lineHeights.slice(0, index).reduce((a, b) => a + b, 0) +
          space,
        lineHeight: h,
      };
    });

    const maxHeight =
      Math.round(lineHeights.reduce((a, b) => a + b, 0)) + // 每行字的高度
      (linesText.length - 1) * lineSpace + // 每行字的间隙
      padding * 2; // 容器的 padding

    canvas.width = maxWidth;
    canvas.height = maxHeight;

    ctx.save();

    const actualWidth =
      Math.cos((rotate * Math.PI) / 180) * maxWidth +
      Math.cos(((90 - rotate) * Math.PI) / 180) * maxHeight;
    const actualHeight =
      Math.sin((rotate * Math.PI) / 180) * maxWidth +
      Math.sin(((90 - rotate) * Math.PI) / 180) * maxHeight;

    canvas.width = actualWidth;
    canvas.height = actualHeight;

    ctx.fillStyle = "black";
    ctx.font = `20px ${fontFamily}`;

    if (debug) {
      ctx.translate(0.5, 0.5);
      ctx.beginPath();
      ctx.moveTo(canvas.width / 2, 0);
      ctx.lineTo(canvas.width / 2, canvas.height);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(0, canvas.height / 2);
      ctx.lineTo(canvas.width, canvas.height / 2);
      ctx.stroke();
    }

    ctx.textAlign = textAlign;
    ctx.textBaseline = "top";

    /**
     * 图中的中心点做吧
     */
    const center = {
      x:
        canvas.width / 2 +
        Math.cos(((90 - rotate) * Math.PI) / 180) * (maxHeight / 2),
      y:
        canvas.height / 2 -
        Math.cos((rotate * Math.PI) / 180) * (maxHeight / 2),
    };

    if (ctx.textAlign === "center") {
      ctx.translate(center.x, center.y);
    } else if (ctx.textAlign === "left") {
      ctx.translate(
        center.x - (Math.cos((rotate * Math.PI) / 180) * maxWidth) / 4,
        center.y - (Math.sin((rotate * Math.PI) / 180) * maxWidth) / 4
      );
    } else if (ctx.textAlign === "right") {
      ctx.translate(
        center.x + (Math.cos((rotate * Math.PI) / 180) * maxWidth) / 4,
        center.y + (Math.sin((rotate * Math.PI) / 180) * maxWidth) / 4
      );
    }

    ctx.rotate((rotate * Math.PI) / 180);
    ctx.fillStyle = fillStyle;
    ctx.globalAlpha = opacity;

    linesText.forEach((word, index) => {
      ctx.fillStyle = word.color;
      ctx.font = `${word.fontSize}px ${fontFamily}`;
      ctx.fillText(word.value, word.x, word.y);
    });

    ctx.restore();

    const base64Url = canvas.toDataURL();
    const $watermarkBox = container.querySelector(".watermark-box");

    const id = "id-" + parseInt(Math.random() * 100000000);

    let watermarkDiv = $watermarkBox || document.createElement("div");

    const styleStr = `
              position:${position};
              display: block;
              opacity: 1;
              top: 0;
              left: 0;
              bottom: 0;
              right: 0;
              width: 100%;
              height: 100%;
              z-index: ${zIndex};
              pointer-events: none;
              background-repeat: repeat;
              background-image: url('${base64Url}')`;

    watermarkDiv.className = "watermark-box";
    watermarkDiv.setAttribute("style", styleStr);
    watermarkDiv.id = watermarkDiv.id || id;

    Watermark.Map[id] = this;

    let clonedWatermarkDiv = watermarkDiv.cloneNode(true);

    const containerPosition = getComputedStyle(container).position;

    if (!containerPosition || containerPosition === "static") {
      container.style.position = "relative";
    }

    if (!$watermarkBox) {
      container.insertBefore(watermarkDiv, container.firstChild);
    }

    if (MutationObserver) {
      // 防止属性被修改而隐藏了水印
      const startObserve = () => {
        this.ob && this.ob.disconnect();
        this.ob = null;

        this.ob = new MutationObserver((nodes) => {
          if (!watermarkDiv) {
            container.insertBefore(watermarkDiv, container.firstChild);
          } else {
            watermarkDiv.remove();

            this.ob.disconnect();
            this.ob = null;

            watermarkDiv = clonedWatermarkDiv;
            clonedWatermarkDiv = clonedWatermarkDiv.cloneNode(true);
            container.insertBefore(watermarkDiv, container.firstChild);
            startObserve();

            requestAnimationFrame(() => startObserveContainer());
          }
        });

        this.ob.observe(watermarkDiv, { attributes: true });
      };

      startObserve();

      // 防止节点本删除而隐藏水印
      const startObserveContainer = () => {
        this.obContainer && this.obContainer.disconnect();
        this.obContainer = null;

        this.obContainer = new MutationObserver((nodes) => {
          watermarkDiv = document.getElementById(watermarkDiv.id);

          if (!watermarkDiv) {
            this.obContainer.disconnect();
            this.obContainer = null;

            watermarkDiv = clonedWatermarkDiv;
            clonedWatermarkDiv = clonedWatermarkDiv.cloneNode(true);
            container.insertBefore(watermarkDiv, container.firstChild);
            startObserveContainer();

            requestAnimationFrame(() => startObserve());
          }
        });

        this.obContainer.observe(this.container, { childList: true });
      };

      startObserveContainer();
    }

    return watermarkDiv.id;
  }

  dispose() {
    if (this.ob) {
      this.ob.disconnect();
    }

    if (this.obContainer) {
      this.obContainer.disconnect();
    }

    const $watermarkBox = this.container.querySelector(".watermark-box");

    if ($watermarkBox) {
      $watermarkBox.remove();
      delete Watermark.Map[$watermarkBox.id];
    }
  }
}

/**
 * 存储所有的水印实例
 * @private
 * @type { { [key:string]: Watermark } }
 */
Watermark.Map = {};

/**
 * 指定隐藏某个水印
 * @public
 * @param {string} id
 * */
Watermark.dispose = (id) => {
  const instance = Watermark.Map[id];

  if (instance) {
    instance.dispose();
  }
};

/**
 * 销毁所有的水印
 * @public
 * */
Watermark.destroy = () => {
  for (const id in Watermark.Map) {
    Watermark.dispose(id);
  }
};

大牛们的评论:朕有话说

还没有人评论哦,赶紧抢沙发!