canvas 生成水印图片
不到50行代码实现一个canvas
生成水印工具函数,使用时也非常简单,直接调用即可;先贴完整代码,再详解思路:
源码地址
/** * 给图片添加水印,并返回`base64` * @param {object} params * @param {string} params.img 图片地址,支持CDN和base64 * @param {string} params.title 水印标题 * @param {string=} params.desc 水印描述 * @param {"jpeg"|"png"=} params.type 生成图片的类型,默认`"jpeg"` * @param {string=} params.color 水印颜色,默认`rgba(255, 255, 255, 0.5)` * @param {number=} params.markSize 水印矩阵宽高(默认150) * @param {number=} params.fontSize 水印字体大小(默认12) * @param {number=} params.angle 水印旋转的角度(默认315则-45) * @returns {Promise<string>} */ function getWaterMarkImage(params) { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); canvas.style.cssText = "position: fixed; top: -110%; left: -110%; z-index: -10;"; document.body.appendChild(canvas); const image = new Image(); image.crossOrigin = "Anonymous"; image.src = params.img; function getDrawMark() { const mark = document.createElement("canvas"); const markSize = params.markSize || 150; const radius = markSize / 2; mark.width = markSize; mark.height = markSize; const ctx = mark.getContext("2d"); const title = params.title || ""; const desc = params.desc || ""; const fontSize = params.fontSize || 12; const angle = params.angle || 315; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = `${fontSize}px Microsoft Yahei`; ctx.fillStyle = params.color || "rgba(255, 255, 255, 0.5)"; // TODO: 必须要设置完字体样样式大小再获取字体高度 // const titleWidth = ctx.measureText(title).width; // const descWidth = ctx.measureText(desc).width; ctx.translate(radius, radius); ctx.rotate(angle * Math.PI / 180); ctx.translate(-radius, -radius); if (desc) { ctx.fillText(title, radius, radius - fontSize); ctx.fillText(desc, radius, radius + fontSize * 0.6); } else { ctx.fillText(title, radius, radius); } return mark; } return new Promise(function (resolve) { image.onload = function () { canvas.width = image.width; canvas.height = image.height; context.drawImage(image, 0, 0); context.fillStyle = context.createPattern(getDrawMark(), "repeat"); context.fillRect(0, 0, canvas.width, canvas.height); const base64 = canvas.toDataURL(`image/${params.type || "jpeg"}`, 1); resolve(base64); canvas.remove(); } image.onerror = function () { resolve(""); canvas.remove(); } }); } 复制代码
步骤1-处理传入的图片
生成水印原理都知道,就是在图片上层贴一层自定义的标识(牛皮癣),至于怎么个贴法就百花齐放了;所以这里先要加载传入的图片,然后用canvas
绘制出来,像下面这样:
方法需要配置的参数参考说明上面,这里不重复了
function getWaterMarkImage(params) { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); canvas.style.cssText = "position: fixed; top: -110%; left: -110%; z-index: -10;"; document.body.appendChild(canvas); const image = new Image(); image.crossOrigin = "Anonymous"; image.src = params.img; function getDrawMark() { ...// 下面分解说明 } return new Promise(function (resolve) { image.onload = function () { canvas.width = image.width; canvas.height = image.height; context.drawImage(image, 0, 0); context.fillStyle = context.createPattern(getDrawMark(), "repeat"); context.fillRect(0, 0, canvas.width, canvas.height); const base64 = canvas.toDataURL(`image/${params.type || "jpeg"}`, 1); resolve(base64); canvas.remove(); } image.onerror = function () { resolve(""); canvas.remove(); } }); } 复制代码
生成图片并绘制的时候,注意要给图片对象添加crossOrigin = "Anonymous"
,不然非同域的图片会生成失败!接着看context.fillStyle = context.createPattern(getDrawMark(), "repeat");
这行代码,与css
中的背景图功能类似,不同的在canvas
中,它是贴在上面的;所以省去了计算水印并铺满图片的操作,而第一个参数则可以为canvas
对象或者图片对象传入生成多个并重复铺满,所以水印的生成只需要绘制一个canvas
就可以了。
步骤2-绘制自定义水印文字
因为水印平铺已经一行代码搞定,所以接下来只需要把单个水印绘制出来即可;绘制之前,先单独写一个方法并绘制到浏览器中,方便调试和查看效果,像这样:
function rotateTest() { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const size = 200; const radius = size / 2; canvas.width = size; canvas.height = size; canvas.style.cssText = "border: 1px solid orange"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = `20px Microsoft Yahei`; function run() { ctx.fillText("content", radius, radius); } document.body.appendChild(canvas); run(); } rotateTest(); 复制代码
效果图:
使用fillText
可以绘制文字,让文字居中显示则使用盒子的半径去作为x
、y
坐标即可,由于水印是带有旋转角度的,接着再加个旋转效果看看:
// 省略上面代码 function run() { ctx.rotate(30 * Math.PI / 180); // 先旋转,再设置文字,不然不生效 ctx.fillText("content", radius, radius); } 复制代码
效果图:
可以看到不是想要的效果,和我理解的rotate
不一样,后面去查了一下,原来canvas
的旋转是以左上角为中心点进行旋转的,和css
中的transform: rotate()
不一样。为了方便调试,我把字体先去掉,换成矩阵盒子来做个动态的角度变化,像这样:
let angle = 0; function run() { ctx.clearRect(0, 0, size, size);//清屏 ctx.save(); ctx.beginPath(); ctx.fillStyle = "orange"; ctx.rotate(angle * Math.PI / 180)//设置矩形旋转 ctx.fillRect(0, 0, radius, radius);//fillrect(矩形宽度的一半,矩形高度的一半,矩形长度,矩形宽度) ctx.restore(); angle += 10; requestAnimationFrame(run); } 复制代码
效果图:
有了视觉效果,那么实现基于中心点旋转就好调试了;既然没有相关api
设置旋转中心,那么就换一个思路,先将矩阵移动到cavans
矩阵的中心点,然后旋转到指定的角度,再设置负数的原来偏移值,最后再绘制文字,这样逻辑上就达到了基于矩阵中心旋转的效果了,改造一下代码之后:
function run() { ctx.clearRect(0, 0, size, size);//清屏 ctx.save(); ctx.beginPath(); ctx.fillStyle = "orange"; ctx.translate(radius, radius);//围绕矩形的中心点旋转 ctx.rotate(angle * Math.PI / 180)//设置矩形旋转 ctx.translate(-radius, -radius); ctx.fillText("content", radius, radius); ctx.strokeRect(0, 0, size, size); ctx.restore(); angle++; requestAnimationFrame(run); } 复制代码
效果图:
这样,就可以从外面传入旋转的角度,去自定水印的旋转位置,同时大小也可以根据图片实际大小去设置水印的尺寸和间距,是我想要的,比较完美的最终效果。
最后一步,把思路代码改写到getDrawMark
函数中就大功告成了:
function getDrawMark() { const mark = document.createElement("canvas"); const markSize = params.markSize || 150; const radius = markSize / 2; mark.width = markSize; mark.height = markSize; const ctx = mark.getContext("2d"); const title = params.title || ""; const desc = params.desc || ""; const fontSize = params.fontSize || 12; const angle = params.angle || 315; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = `${fontSize}px Microsoft Yahei`; ctx.fillStyle = params.color || "rgba(255, 255, 255, 0.5)"; // TODO: 必须要设置完字体样样式大小再获取字体高度 // const titleWidth = ctx.measureText(title).width; // const descWidth = ctx.measureText(desc).width; ctx.translate(radius, radius); ctx.rotate(angle * Math.PI / 180); ctx.translate(-radius, -radius); if (desc) { ctx.fillText(title, radius, radius - fontSize); ctx.fillText(desc, radius, radius + fontSize * 0.6); } else { ctx.fillText(title, radius, radius); } return mark; }
作者:黄景圣
链接:https://juejin.cn/post/7171759354520010765