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-Mediumconst 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', // 字重 normalfontFamily: 'Georgia', // 字体 sans-seriffontStyle: 'normal', // 字体样式 normaltextAlign: '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-Mediumconst 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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Python数据结构与判断2/7】数据和判断小结

目录 序言 print() 变量 赋值 四种数据类型 字符串 格式化输出 四则运算 取整与取模 比较运算 逻辑运算 判断 if语句 if-else语句 if-elif-else语句 Tips 空值、0、非0非空值 实战案例 输入密码 短信模板 总结 序言 今天将对前面学过的内容进行一个复习小结…

科技引领品质:飞利浦智能锁“12年免费换新机”重塑行业新标杆

随着智能锁行业的竞争愈发火热&#xff0c;各大品牌在技术创新和服务升级方面不断推陈出新。售后服务的形态正发生深刻变化&#xff0c;从传统的保修维修到如今的技术支持、24小时在线客服等&#xff0c;各大品牌都在不断地提升售后服务水平&#xff0c;以创新的服务理念和先进…

配置与管理DNS服务器

配置与管理DNS服务器 **1&#xff0c;什么是DNS&#xff1f;**负责将域名转换成实际想对应的ip地址&#xff0c;这个过程交域名解析。 **2&#xff0c;域名解析的方法&#xff1a;**分布式&#xff0c;层次结构的数据库系统。根域&#xff0c;顶级域&#xff0c;二级域&#…

sql注入基础学习

1.常用SQL语句 01、显示数据库 show databases&#xff1b; 02、打开数据库 use db name&#xff1b; 03、显示数据表 show tables&#xff1b; 04、显示表结构 describe table_name&#xff1b; 05、显示表中各字段信息&#xff0c;即表结构 show columns from table_nam…

面向对象的编程语言是什么意思?——跟老吕学Python编程

面向对象的编程语言是什么意思&#xff1f;——跟老吕学Python编程 面向对象是什么意思&#xff1f;面向对象的定义面向对象的早期发展面向对象的背景1.审视问题域的视角2.抽象级别3.封装体4.可重用性 面向对象的特征面向对象的开发方法面向对象程序设计基本思想实现 面向对象的…

数据结构-稀疏数组

文章目录 1、什么是稀疏数组&#xff1f;2、稀疏数组的存储流程3、代码实现4、运行结果 1、什么是稀疏数组&#xff1f; 当一个数组中大部分元素为0&#xff0c;或者为同一个值的数组时&#xff0c;可以使用稀疏数组来保存该数组。 2、稀疏数组的存储流程 记录数组一共有几行…

活体检测(点头,摇头,张嘴等动态识别)

活体检测&#xff08;点头&#xff0c;摇头&#xff0c;张嘴等动态识别&#xff09; 某本书里有一句话&#xff0c;等我去读、去拍案。 田间的野老&#xff0c;等我去了解、去惊识。 山风与发&#xff0c;冷泉与舌&#xff0c; 流云与眼&#xff0c;松涛与耳&#xff0c; 他们等…

21、状态模式(行为性模式)

版本一、get状态指针 #include <iostream> using namespace std;//前置声明 class Context;//状态 class State{ public://4个状态virtual void toUp (Context& context){ }virtual void toDown (Context& context){ }virtual void toLeft (Context& cont…

架构学习总结:企业架构=业务+数据+技术+应用架构

最近再次研读DAMA数据管理知识体系,结合工作对什么是企业架构?如何开展企业架构设计工作有一些新的认识,供大家参考。企业架构包括企业的业务架构、数据架构、技术架构和应用架构,要想做好企业的信息化数字化建设规划,这四个架构都不可缺少,这四个方面的内容共同组成了企…

[云原生] k8s配置资源管理

一、Secret的资源配置 1.1 Secret配置的相关说明 Secret 是用来保存密码、token、密钥等敏感数据的 k8s 资源&#xff0c;这类数据虽然也可以存放在 Pod 或者镜像中&#xff0c;但是放在 Secret 中是为了更方便的控制如何使用数据&#xff0c;并减少暴露的风险。 Secret 有…

【JavaSE】抽象类与接口

Object 类 类 java.lang.Object是类层次结构的根类&#xff0c;即所有类的父类。 除Object类之外的任何一个Java类&#xff0c;全部直接或间接的继承于Object类。由此&#xff0c;Object类也被称为根父类。Object类中声明的成员具有通用性&#xff0c;并且Object类中没有声明…

300W-500W-700W-1000W超薄制动电阻

EAK制动电阻&#xff0c;最大连续功率&#xff1a;300 W--1000W 制动电阻器&#xff0c;用于带有中低功率变频器 或作为充电电阻器的驱动器。 安装在变频器附近。 防护等级 IP 20 / IP 54 可根据要求提供更高的防护等级 测试电压 2.5 kV AC 可根据要求提供其他容量和安装…

SMART PLC自适应低通滤波器(收放卷线速度滤波)

一阶低通滤波器更多内容请参考信号处理专栏相关文章,常用链接如下: 1、SMART PLC 低通滤波器和模拟量采集应用 https://rxxw-control.blog.csdn.net/article/details/136595982https://rxxw-control.blog.csdn.net/article/details/1365959822、SMART PLC双线性变换和后向差…

Docker笔记-进入运行中的镜像,查看日志等操作

docker搭建好后&#xff0c;查看运行的docker镜像&#xff1a; docker ps -a 进入运行的容器&#xff0c;命令如下&#xff1a; docker exec -it <容器ID> /bin/bash # 或者&#xff0c;直接用容器里面的命令&#xff0c;比如mysql镜像 docker exec -it <容器ID>…

网络协议常见问题

网络协议常见问题 OSI&#xff08;Open Systems Interconnection&#xff09;模型OSI 封装 TCP/IP协议栈IP数据报的报头TCP头格式UDP头格式TCP (3-way shake)三次握手建立连接&#xff1a;为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。每次建立TCP连接…

蓝桥杯单片机---第十二届省赛题目解析

文章目录 比赛题目一、代码相关定义、声明1.头文件声明2.变量声明 二、主要函数1.main函数2.按键扫描3.数码管显示4.电压模式1、2输出 & LED显示5.定时器中断6.消除85C显示 三、次要函数1.初始化函数Init2.按键函数Key3.LED函数Led4.数码管函数Seg5.iic函数中6.onewire函数…

【LeetCode】17.电话号码的字母组合

题目 链接&#xff1a;17. 电话号码的字母组合 - 力扣&#xff08;LeetCode&#xff09; 给定一个仅包含数字2-9的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按任意顺序返回 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何…

Vue ECharts line3D点击空白处重置图表视角- 附完整示例

ECharts&#xff1a;一个基于 JavaScript 的开源可视化图表库。 目录 效果 一、介绍 1、官方文档&#xff1a;Apache ECharts 2、官方示例 二、准备工作 1、安装依赖包 2、示例版本 三、使用步骤 1、在单页面引入 echarts 2、指定容器并设置容器宽高 3、数据处理&…

leetcode 热题 100_反转链表

题解一&#xff1a; 迭代&#xff1a;逐步修改节点指针&#xff0c;注意在修改前要保存下一个节点指针。 class Solution {public ListNode reverseList(ListNode head) {ListNode pre null;while (head! null) {ListNode temp head.next;head.next pre;pre head;head te…

使用Canvas绘制一个自适应长度的折线图

要求x轴根据数据长度自适应 y轴根据数据最大值取长度值 <template><div ref"cvsContainer" class"cvs-container"><canvas ref"cvs" class"canvas"></canvas></div> </template><script set…