canvas实现水印逻辑分析

发布于:2024-03-11 ⋅ 阅读:(79) ⋅ 点赞:(0)

效果图

在这里插入图片描述

一、相关文档

参考element-plus
canvas文档

二、分析

使用canvas将格式化后的文字转为图片
将该图片依次排布在需要添加水印的模块上

三、实现

1、将水印文字转为水印图片

首先创建一个span节点,写入水印文本,设置好字体样式后获取到文本的宽高,赋值给画布
然后创建一个canvas节点,使用canvas的 fillText(text,x,y) 以及 stokeText(text,x,y) 绘制文字,使用font、textAlign、textBaseline设置好文字的样式
最后使用canvas的toDataURL方法导出图片,将文字水印转为图片

	const {rotate, font, content, gap, image} = props;
	const {color, fontWeight, fontFamily, fontStyle, textAlign, textBaseline} = font;
	const fontSize = font.fontSize + 'px';
	let _imgSrc = '', contentLen = 0, contentHeight = 0, radian = 0;
	// 创建一个文本节点,获取水印文本长度以及高度
	const contentSpan = document.createElement('span');
	contentSpan.innerText = content;
	contentSpan.style.fontSize = fontSize;
	contentSpan.style.fontFamily = fontFamily;
	contentSpan.style.fontWeight = fontWeight;
	contentSpan.style.fontStyle = fontStyle;
	// 放到body中
	document.body.appendChild(contentSpan);
	contentLen = contentSpan.offsetWidth + 20;
	contentHeight = contentSpan.offsetHeight;
	// 销毁文本节点
	contentSpan.remove();
	
	// 创建水印 Canvas 对象实例
	const watermarkCanvas = document.createElement('canvas');
	// 放到body中
	document.body.appendChild(watermarkCanvas);
	// 初始化水印画布大小
	watermarkCanvas.width = contentLen;
	watermarkCanvas.height = contentHeight;
	// 水印 Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)
	const watermarkContext = watermarkCanvas.getContext("2d");
	// 设置填充文字样式
	watermarkContext.fillStyle = color;
	watermarkContext.textAlign = textAlign;
	watermarkContext.textBaseline = textBaseline;
	// normal bold 24px PingFang SC, PingFang SC-Medium
	const fontList = [fontStyle, fontWeight, fontSize, fontFamily].filter(item => item)
	watermarkContext.font = fontList.join(' ');
	// 居中显示水印
	watermarkContext.fillText(content, 0, 0);
	// 绘制
	watermarkContext.stroke();
	// 导出透明背景的图片
	const dataURL = watermarkCanvas.toDataURL('image/png', 1);
	_imgSrc = dataURL
	// 销毁水印 Canvas 节点
	watermarkCanvas.remove();

2、给刚生成的水印图片加入旋转以及间隔

(1)旋转位移

由于canvas并未提供直接旋转画布内容(文字/图片等)的API,故此只能通过旋转画布来实现旋转的目的(注意,不能直接旋转角度,要先将角度转为弧度Math.abs(rotate) / 360 * 2 * Math.PI)
首先旋转之后,要想放下整个水印,那画布的大小肯定是要重新设定的,如下图
经过计算得到 H = cosθ * h + sinθ * w;W = cosθ * w + sinθ * h
在这里插入图片描述

    // 创建新 Canvas 对象实例,处理间隔以及旋转,图片水印
     const myCanvas = document.createElement('canvas');
     // 放到body中
     document.body.appendChild(myCanvas);
     // 新 Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)
     const myContext = myCanvas.getContext("2d");
     const watermarkImg = new Image()
     watermarkImg.src = _imgSrc || image;
     watermarkImg.onload = () => {
      if(rotate) { // 有角度,则需计算出水印倾斜后的实际宽高
         let mergeWeight = watermarkImg.height * math.sin(radian) + watermarkImg.width * math.cos(radian);
         let mergeHeight = watermarkImg.width * math.sin(radian) + watermarkImg.height * math.cos(radian);
         if(Math.abs(rotate) === 90) { // 90°特殊处理
           mergeWeight = watermarkImg.height * 1 + watermarkImg.width * 0;
           mergeHeight = watermarkImg.width * 1 + watermarkImg.height * 0;
         }
         myCanvas.width = mergeWeight;
         myCanvas.height = mergeHeight;
     }

得到画布的宽高后,使用canvas的translate方法将水印图片的坐标原点设置到画布的正中央,如图
在这里插入图片描述

   // 旋转是以画布的左上角为原点旋转的,先将原点平移到画布中心位置
   myContext.translate(mergeWeight / 2, mergeHeight / 2);

而后使用rotate方法旋转水印图片,如图,θ < 0,逆时针旋转;θ >0 ,顺势针旋转
在这里插入图片描述在这里插入图片描述

    myContext.rotate(rotate > 0 ? radian : -radian);

如上图所示,由于旋转是以画布的中心点(即对角线中心点),且画布宽高都是按照水印图片的宽高以及角度计算出来的最小宽高,故此X轴绝对是处于对角线上的, θ < 0 处于左下到右上原点处于中心点的对角线上,θ > 0 处于左上到右下原点处于中心点的对角线上
故此想要将水印图片全部显示在画布内,那么就需要计算出水印图片要往X轴的反方向移动多少的距离,而后再使用translate位移过去即可

  let dx = 0;
  if(Math.abs(rotate) > 45) {
    dx = -((mergeHeight / math.sin(radian)) / 2);
  } else {
    dx = -((mergeWeight / 2) / math.cos(radian));
  }
  if(Math.abs(rotate) === 90) { // 90°转换为弧度后计算出来的值会存在问题
    dx = -(mergeHeight / 2);
  }
  myContext.translate(dx, -watermarkImg.height / 2);
(2)间隔位移

首先给画布添加额外宽高
然后在新的画布上对已经进行过位移的水印继续进行位移
如图所示,在添加完宽度/高度之后需要将水印向右方/下方移动1/2间隔的距离
但正如图所示,XY轴坐标不是正的是随着水印旋转了θ角度之后的,故此并不能简单的直接向Y/X移动1/2间隔的距离,而是需要沿着Y轴移动dy的距离,再沿着X轴移动dx的距离,才是我们最后想要的
计算公式如下图

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

(3)最后使用toDataURL导出为png图片
myCanvas.toDataURL('image/png', 1);

3、将生成的水印图片依次排布在需要添加水印的模块上

  • 在该模块一级的节点上添加一个节点,而后宽高100%,并定位到该模块的上方,而后将水印图片作为背景图,background-repeat设置为重复即可
 <div class="props" style="position: relative;">
   <slot></slot>
   <div :style="{
     'z-index': zIndex,
     'position': 'absolute',
     'top': 0,
     'left': 0,
     'width': '100%',
     'height': '100%',
     'pointer-events': 'none',
     'background-repeat': 'repeat',
     'background-position': '0px 0px',
     'background-image': `url(${imgSrc})`,
   }" class="watermarkBg"></div>
 </div>

四、完整代码

<script setup>
  import * as math from 'mathjs';
  import { onMounted, reactive, ref, computed, watch, nextTick } from 'vue';

  defineOptions({name: ''});
  const props = defineProps({
    // width: {type: Number, default: 120}, // 水印的宽度,
    // height: {type: Number, default: 64}, // 水印的高度,
    rotate: {type: Number, default: -30}, // 水印的旋转角度, 单位 °
    zIndex: {type: Number, default: -1}, // 水印元素的z-index值
    image: {type: String, default: ''}, // 水印图片,建议使用 2x 或 3x 图像
    content: {type: String, default: '测试水印[123456789]'}, // 水印文本内容
    font: {
      type: Object, default: () => {
        return {
          color: 'rgba(225, 225, 225, 05)', // 字体颜色
          fontSize: '20', // 字体大小
          fontWeight: 'bold', // 字重 normal
          fontFamily: 'Georgia', // 字体 sans-serif
          fontStyle: 'normal', // 字体样式 normal
          textAlign: 'left', // 文本对齐
          textBaseline: 'top', // 文本基线 hanging\alphabetic\middle\bottom\top}
        }
      },
    },
    gap: {type: Array, default: () => [100, 100]}, // 水印之间的间距[行,纵]
    offset: {type: Array, default: () => []}, // 水印从容器左上角的偏移 默认值为 gap/2 [top, left]
  })
  const canvasWidth = ref(120);
  const canvasHeight = ref(60);
  const imgSrc = ref('');
  const emits = defineEmits(['on-ok']);
  // watch(() => foo, (newValue, oldValue) => {})
  onMounted(() => {
    initCanvas();
  });
  
  const initCanvas = () => {
    // width, height, 
    const {rotate, font, content, gap, image} = props;
    const {color, fontWeight, fontFamily, fontStyle, textAlign, textBaseline} = font;
    const fontSize = font.fontSize + 'px';
    let _imgSrc = '', contentLen = 0, contentHeight = 0, radian = 0;
    // 计算旋转弧度,弧度 = (Math.PI/180) * 角度
    if(rotate) {
      radian = Math.abs(rotate) / 360 * 2 * Math.PI
    }
    // 先将文字转为图片
    if(content) {
      // 创建一个文本节点,获取水印文本长度以及高度
      const contentSpan = document.createElement('span');
      contentSpan.innerText = content;
      contentSpan.style.fontSize = fontSize;
      contentSpan.style.fontFamily = fontFamily;
      contentSpan.style.fontWeight = fontWeight;
      contentSpan.style.fontStyle = fontStyle;
      // 放到body中
      document.body.appendChild(contentSpan);
      contentLen = contentSpan.offsetWidth + 20;
      contentHeight = contentSpan.offsetHeight;
      // 销毁文本节点
      contentSpan.remove();

      // 创建水印 Canvas 对象实例
      const watermarkCanvas = document.createElement('canvas');
      // 放到body中
      document.body.appendChild(watermarkCanvas);
      // 初始化水印画布大小
      watermarkCanvas.width = contentLen;
      watermarkCanvas.height = contentHeight;
      // 水印 Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)
      const watermarkContext = watermarkCanvas.getContext("2d");
      // 设置填充文字样式
      watermarkContext.fillStyle = color;
      watermarkContext.textAlign = textAlign;
      watermarkContext.textBaseline = textBaseline;
      // normal bold 24px PingFang SC, PingFang SC-Medium
      const fontList = [fontStyle, fontWeight, fontSize, fontFamily].filter(item => item)
      watermarkContext.font = fontList.join(' ');
      // 居中显示水印
      watermarkContext.fillText(content, 0, 0);
      // 绘制
      watermarkContext.stroke();
      // 导出透明背景的图片
      const dataURL = watermarkCanvas.toDataURL('image/png', 1);
      _imgSrc = dataURL
      // 销毁水印 Canvas 节点
      watermarkCanvas.remove();
    }
    // 而后将图片作为基本单元重新放入canvas进一步进行旋转间隔等
    if(_imgSrc || image) {
      // 创建新 Canvas 对象实例,处理间隔以及旋转,图片水印
      const myCanvas = document.createElement('canvas');
      // 放到body中
      document.body.appendChild(myCanvas);
      // 新 Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)
      const myContext = myCanvas.getContext("2d");
      const watermarkImg = new Image()
      watermarkImg.src = _imgSrc || image;
      watermarkImg.onload = () => {
        if(rotate) { // 有角度,则需计算出水印倾斜后的实际宽高
          let mergeWeight = watermarkImg.height * math.sin(radian) + watermarkImg.width * math.cos(radian);
          let mergeHeight = watermarkImg.width * math.sin(radian) + watermarkImg.height * math.cos(radian);
          if(Math.abs(rotate) === 90) { // 90°特殊处理
            mergeWeight = watermarkImg.height * 1 + watermarkImg.width * 0;
            mergeHeight = watermarkImg.width * 1 + watermarkImg.height * 0;
          }
          canvasWidth.value = myCanvas.width = mergeWeight + gap[0];
          canvasHeight.value = myCanvas.height = mergeHeight + gap[1];
          // 旋转是以画布的左上角为原点旋转的,先将原点平移到画布中心位置
          myContext.translate(mergeWeight / 2, mergeHeight / 2);
          myContext.rotate(rotate > 0 ? radian : -radian);
          // 水印是按照画布的中心点旋转的,画布的真实长宽也是按照水印的宽高以及旋转角度计算出来的
          // 所以此时水印的X轴绝对处于未添加间隔前画布的对角线上且原点处于画布的对角线的交点上
          // 故此只需要计算出来水印应该往X轴的反方向移动多少像素,以及向Y轴移动水印高的一半即可
          let dx = 0;
          if(Math.abs(rotate) > 45) {
            dx = -((mergeHeight / math.sin(radian)) / 2);
          } else {
            dx = -((mergeWeight / 2) / math.cos(radian));
          }
          if(Math.abs(rotate) === 90) { // 90°转换为弧度后计算出来的值会存在问题
            dx = -(mergeHeight / 2);
          }
          myContext.translate(dx, -watermarkImg.height / 2);
          // 间隙
          if(gap[0]) {
            let dy = math.sin(radian) * (gap[0] / 2);
            let dx = dy / math.tan(radian);
            myContext.translate(dx, rotate < 0 ? dy : -dy);
          }
          if(gap[1]) {
            let dy = math.cos(radian) * (gap[1] / 2);
            let dx = math.tan(radian) * dy;
            myContext.translate(rotate < 0 ? -dx : dx, dy);
          }
          // 绘制
          myContext.drawImage(watermarkImg, 0, 0);
        } else { // 无角度,则宽高只需要在原始水印加上对应的间隔即可
          canvasWidth.value = gap[0] + watermarkImg.width;
          canvasHeight.value = gap[1] + watermarkImg.height;
          myCanvas.width = canvasWidth.value;
          myCanvas.height = canvasHeight.value;
          // 绘制(居中显示)
          myContext.drawImage(watermarkImg, gap[0] / 2, gap[1] / 2);
        }
        // 导出透明背景的图片
        const myDataURL = myCanvas.toDataURL('image/png', 1);
        imgSrc.value = myDataURL;
        // 销毁 Canvas 节点
        myCanvas.remove();
      }
    }
  }

  // 子组件暴露
  defineExpose({});

</script>

<template>
  <div class="props" style="position: relative;">
    <slot></slot>
    <div :style="{
      'z-index': zIndex,
      'position': 'absolute',
      'top': 0,
      'left': 0,
      'width': '100%',
      'height': '100%',
      'pointer-events': 'none',
      'background-repeat': 'repeat',
      'background-position': '0px 0px',
      'background-image': `url(${imgSrc})`,
    }" class="watermarkBg"></div>
  </div>
</template>

<style lang="less" scoped>

</style>

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到