canvas实现水印逻辑分析

目录

    • 效果图
    • 一、相关文档
    • 二、分析
    • 三、实现
      • 1、将水印文字转为水印图片
      • 2、给刚生成的水印图片加入旋转以及间隔
        • (1)旋转位移
        • (2)间隔位移
        • (3)最后使用toDataURL导出为png图片
      • 3、将生成的水印图片依次排布在需要添加水印的模块上
    • 四、完整代码

效果图

在这里插入图片描述

一、相关文档

参考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并未提供直接旋转画布内容(文字/图片等),故此只能通过旋转画布来实现旋转的目的
首先旋转之后,要想放下整个水印,那画布的大小肯定是要重新设定的,如下图
经过计算得到 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/445743.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python实现一个简单的主机-路由器结构(计算机网络)

说明 本系统模拟实现了一个路由器与两个主机节点。该路由器将接收原始以太网帧&#xff0c;并像真正的路由器一样处理它们&#xff1a;将它们转发到正确的传出接口&#xff0c;处理以太网帧&#xff0c;处理 IPv4 分组&#xff0c;处理 ARP分组&#xff0c;处理 ICMP 分组&…

Crow 编译和环境搭建

Crow与其说是编译&#xff0c;倒不如说是环境搭建。Crow只需要包含头文件&#xff0c;所以不用编译生成lib。 Crow环境搭建 boost&#xff08;可以不编译boost&#xff0c;只需要boost头文件即可&#xff09;asio &#xff08;可以不编译&#xff0c;直接包含头文件。不能直接…

事务【MySQL】

稍等更新图片。。。。 事务的概念 引入 在 A 转账 100 元给 B 的过程中&#xff0c;如果在 A 的账户已经减去了 100 元&#xff0c;B 的账户还未加上 100 元之前断网&#xff0c;那么这 100 元将会凭空消失。对于转账这件事&#xff0c;转出和转入这两件事应该是绑定在一起的…

C语言——函数指针——函数指针变量(详解)

函数指针变量 函数指针变量的作用 函数指针变量是指向函数的指针&#xff0c;它可以用来存储函数的地址&#xff0c;并且可以通过该指针调用相应的函数。函数指针变量的作用主要有以下几个方面&#xff1a; 回调函数&#xff1a;函数指针变量可以作为参数传递给其他函数&…

Docker基础教程 - 10 常用容器部署-Redis

更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 10 常用容器部署-Redis 下面介绍一下常用容器的部署。可以先简单了解下&#xff0c;用到再来详细查看。 在 Docker 中部署 Redis 容器。 10.1 搜索镜像 首先搜索镜像&#xff0c;命令&…

强大的项目管理软件:OmniPlan Pro 4 mac中文版

OmniPlan Pro 4 mac中文版是由The Omni Group为macOS和iOS操作系统开发的一款专业级项目管理软件。它允许用户创建和管理复杂的项目&#xff0c;从定义任务、分配资源到跟踪进度和生成报告&#xff0c;一应俱全。 这款软件提供了一系列强大的工具&#xff0c;帮助用户进行高效…

集合框架(一)Set系列集合

Set<E>是一个接口 特点 无序&#xff1a;添加数据的顺序和获取出的数据顺序不一致&#xff1b;不重复&#xff0c;无索引 注意&#xff1a;Set要用到的常用方法&#xff0c;基本上就是collection提供的!自己几乎没有额外新增一些常用功能! HashSet集合的底层原理 前置知…

GPU 和并行计算

还是那句话&#xff0c;互联网领域遇到的大多数问题&#xff0c;在现实世界早就有了解法&#xff0c;今天再分享一个。 视频来自安阳市最后的朋克&#xff0c;张教练的实拍&#xff0c;视频中展示的是血糕&#xff0c;安阳市特产&#xff0c;不了解的可以将其等同于 “一种必须…

【JavaScript】JavaScript 变量 ① ( JavaScript 变量概念 | 变量声明 | 变量类型 | 变量初始化 | ES6 简介 )

文章目录 一、JavaScript 变量1、变量概念2、变量声明3、ES6 简介4、变量类型5、变量初始化 二、JavaScript 变量示例1、代码示例2、展示效果 一、JavaScript 变量 1、变量概念 JavaScript 变量 是用于 存储数据 的 容器 , 通过 变量名称 , 可以 获取 / 修改 变量 中的数据 ; …

Util工具类功能设计与类设计(http模块一)

目录 类功能 类定义 类实现 编译测试 Split分割字符串测试 ReadFile读取测试 WriteFile写入测试 UrlEncode编码测试 UrlDecode编码测试 StatuDesc状态码信息获取测试 ExtMime后缀名获取文件mime测试 IsDirectory&IsRegular测试 VaildPath请求路径有效性判断测…

Day33-计算机基础3

Day33-计算机基础3 1.根据TCP/IP进行Linux内核参数优化1.1 例1&#xff1a;调整访问服务端的【客户端】的动态端口范围 &#xff0c;LVS&#xff08;10-50万并发&#xff09;&#xff0c;NGINX负载&#xff0c;SQUID缓存服务,1.2 企业案例&#xff1a;DOS攻击的案例&#xff1a…

工资低适合下班做的6大副业,每一个都值得尝试!

2024年是最适合发展个人副业的时候&#xff01;无论你是否有全职工作&#xff0c;如果你的主业还不能满足你的成就感&#xff0c;还不能满足你的生活需求&#xff0c;这6个下班可以做的副业都很值得尝试&#xff01; 千金宝库做简单的网络任务 近年来&#xff0c;随着互联网技…

算法详解——leetcode150(逆波兰表达式)

欢迎来看博主的算法讲解 博主ID&#xff1a;代码小豪 文章目录 逆波兰表达式逆波兰表达式的作用代码将中缀表达式转换成后缀表达式文末代码 逆波兰表达式 先来看看leetcode当中的原题 大多数人初见逆波兰表达式的时候大都一脸懵逼&#xff0c;因为与平时常见的表达式不同&am…

C语言学习笔记,学懂C语言,看这篇就够了!(中)

附上视频链接&#xff1a;X站的C语言教程 目录 第8章、函数8.1 函数是什么8.2 函数的分类8.2.1 库函数8.2.1.1 如何使用库函数 8.2.2 自定义函数 8.3 函数参数8.3.1 实际参数(实参)8.3.2 形式参数(形参) 8.4 函数调用8.4.1 传值调用8.4.2 传址调用8.4.3 练习 8.5 函数的嵌套调…

如何使用ArcGIS Pro进行坡度分析

坡度分析是地理信息系统中一种常见的空间分析方法&#xff0c;用于计算地表或地形的坡度&#xff0c;这里为大家介绍一下如何使用ArcGIS Pro进行坡度分析&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的DEM数据&#xff0c;除了DEM数据&…

Python爬虫:http和https介绍及请求

HTTP和HTTPS 学习目标&#xff1a; 记忆 http、https的概念和区别记忆 浏览器发送http请求的过程记忆 http请求头的形式记忆 http响应头的形式了解 http响应状态码 1 为什么要复习http和https 在发送请求&#xff0c;获取响应的过程中 就是发送http或https的请求&#xff0c…

自然语言发展历程

一、基础知识 自然语言处理&#xff1a;能够让计算理解人类的语言。 检测计算机是否智能化的方法&#xff1a;图灵测试 自然语言处理相关基础点&#xff1a; 基础点1——词表示问题&#xff1a; 1、词表示&#xff1a;把自然语言中最基本的语言单位——词&#xff0c;将它转…

中国电子学会2021年9月份青少年软件编程Sc ratch图形化等级考试试卷四级真题

【 单选题 】 1.下面哪个选项程序可以交换下图列表中第2项和第3项的位置&#xff1f; A&#xff1a; B&#xff1a; C&#xff1a; D&#xff1a; 2.雷峰塔景区的门票价格政策是&#xff1a;成人40元/人&#xff1b;6周岁&#xff08;含6周岁&#xff09;以下的实行免票&#…

常用MII接口详解

开放式系统互连 (OSI) 模型 七层开放系统互连 (OSI) 模型中&#xff0c;以太网层 位于最底部两层 - 物理层和数据链路层。 从百兆以太网接口开始 首先是百兆以太网规定的两种接口 介质无关接口 (MII) Media Independent Interface 介质相关接口 (MDI) Medium Depen…

manjaro 安装 wps 教程

内核: Linux 6.6.16.2 wps-office版本&#xff1a; 11.10.11719-1 本文仅作为参考使用, 如果以上版本差别较大不建议参考 安装wps主体 yay -S wps-office 安装wps字体 &#xff08;如果下载未成功看下面的方法&#xff09; yay -S ttf-waps-fonts 安装wps中文语言 yay …