Canvas之于图片妙用
使用Canvas给图片增加水印
在C端业务中,我们经常需要对用户上传的图片添加水印,这里提供一个灵活便捷的文字水印添加工具:
调用时仅需要:
waterMarkImg = waterMarkGen.init({ textArr: ['自定义水印文字'] });
源码:
const HALF_DEG = 180;
const TWO = 2;
export default {
settings:{ // 默认配置
textArr: ['默认水印文字', '多行的,我是第二行'],
textAlign: 'left',
font: '12px "Microsoft Yahei"',
fillStyle: 'rgba(0,0,0,0.2)', // 文字填充颜色
maxWidth: 100, // 单行文字最大宽度,超出会自动换一行
minWidth: 60,
lineHeight: 16, // 文字行高
deg: -45, // 文字旋转角度
marginRight: 10, // 水印区块右边距(这里其实对内是边距,输出后对外其实是内边距padding)
marginBottom: 10,
left: 0,
top: 0,
},
init: funciton (options) { // 水印生成:输出一张水印database64图片
let textArr;
this.settings = Object.assign(this.settings, options || {}); // 合并选项
this.settings.minWidth = Math.min(this.settings.maxWidth, this.settings.minWidth); // 防止出现maxWidth < minWidth的奇怪情况,做兼容处理
textArr = this.settings.textArr;
if (Object.prototype.toString.call(textArr) !== '[Object Array]') { // 当需要渲染的水印文字非数组时返回错误
throw Error('textArr 水印文本必须是数组类型');
}
const c = this._createCanvas(); // 创建Canvas对象
this._draw(c, this.settings); // 按照配置绘制水印内容
return this._convertCanvasToImage(c); // 将当前Canvas对象输出为图片database64 - url
},
_createCanvas: function () { // 创建一个不可见的canvas对象并返回
let c = document.createElement('canvas');
c.style.display = 'none';
document.body.appendChild(c);
return c;
},
_draw: function (c, settings) {
let ctx = c.getContext('2d'),
textArr = settings.textArr || [],
wordBreakTextArr = [],
maxWidthArr = [];
textArr.forEach(function (text) {
// 计算在当前所设定最大宽度/字体下 当前文字需要的行数,与最大宽度
let result = this._breakLinesForCanvas(ctx, text + '', settings.maxWidth, settings.font);
wordBreakTextArr.push(...result);
maxWidthArr.push(result.maxWidth);
});
maxWidthArr.sort(function (a, b) { return b - a; });
// 根据切割结果动态修改canvas宽高
const maxWidth = Math.max(maxWidthArr[0], settings.minWidth),
lineHeight = settings.lineHeight,
height = workBreakTextArr.length * lineHeight, // 高度 = 渲染文字行数 × 行高
degToPI = Math.PI * settings.deg / HALF_DEG, // 角度换算
absDeg = Math.abs(degToPI),
hSinDeg = height * Math.sin(absDeg),
hCosDeg = height * Math.cos(absDeg),
wSinDeg = maxWidth * Math.sin(absDeg),
wCosDeg = maxWidth * Math.cos(absDeg);
c.width = parseInt(hSinDeg + wCosDeg + settings.marginRight, 10); // 计算canvas宽度
c.height = parseInt(wSinDeg + hCosDeg + settings.marginBottom, 10);
// 同时重绘样式
ctx.font = settings.font;
ctx.fillStyle = settings.fillStyle;
ctx.textBaseline = 'hanging'; // 默认为alphabetic,这里改为基准线为贴着线的方式
// 移动并旋转画布
ctx.translate(0, wSinDeg);
ctx.rotate(degToPI);
// 绘制文本到画布
wordBreakTextArr.forEach(function (wordInfo, index) {
// 居中情况需要动态的去计算文本起始点的位置(x, y)
let x = null;
switch (settings.textAlign) {
case 'left':
x = 0; // 从最左开始
break;
case 'right':
x = maxWidth - wordInfo.maxWidth; // 最右边 - 本身文字内容的宽度
break;
case 'center':
x = (maxWidth - wordInfo.maxWidth) / TWO;
break;
default:
x = 0;
break;
};
ctx.fillText(wordInfo.textArr[0], x, lineHeight * index); // 绘制到画布指定位置
});
},
//转换canvas --> dabase64URL
_convertCanvasToImage: function (canvas, type = 'image/png') {
return canvas.toDataURL(type);
},
// 根据给定的每行最大宽度,切割文字(自动换行功能)
_breakLinesForCanvas: function (ctx, text, width, font) {
let textArr = [],
maxWidth = 0;
font && (ctx.font = font);
let breakPoint = this._findBreakPoint(text, width, ctx); // 寻找换行断点,返回indexindex
while (breakPoint !== -1) { // 循环,知道当前text全部都无需换行
textArr.push(text.substr(0, breakPoint)); // 将单行最大宽度可容纳 文本 截取,然后继续循环换行
text = text.substr(breakPoint);
maxWidth = width;
breakPoint = this._findBreakPoint(text, width, ctx);
}
// (最后剩下的部分无需换行文本 | 当前文本无需换行未走循环情况),加入数组
if (text) {
textArr.push(text);
const lastTextWidth = context.measureText(text).width;
!maxWidth && (maxWidth = lastTextWidth); // maxWidth为0时(未换过行),最大宽度即为当前文本宽度
}
return {
textArr,
maxWidth
};
},
_findBreakPoint: function (text, width, ctx) {
let min = 0,
max = text.length - 1;
// 二分查找
while (min <= max) {
const middle = Math.floor((min + max) / TWO),
middleWidth = ctx.measureText(text.substr(0, middle)).width,
onCharWiderThanMiddleWidth = ctx.measureText(text.substr(0, middle + 1)).width; // 计算比middle多一个字符的宽度
// 已找到断点
if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
return middle;
}
if (middleWidth < width) {
min = middle + 1; // 后移middle中点,继续寻找
} else {
max = middle - 1; // 前移middle中点,继续查找
}
}
return -1; // 无需分割
}
};


