哈哈,趁着平安夜来凑凑热闹,其实很久之前就做过一个类似功能的毕业季生成邀请函小应用。这种有关图像合成的应用其实只需要熟悉h5中canvas的相关API和一点几何常识就ok了,是很容易实现的,这里就带大家探究一下实现的过程。再加上最近会有一大波节日到来,相信这个小工具值得你的拥有,如果觉得不错,不如点个赞
呗。
效果
- 用户能
上传图片
- 能将用户图片
自动调整
成合适的尺寸
- 能切换
饰品
- 能切换头像
图形边框
- 能
保存/下载
生成的图片
在线定制头像
思路
初始化
,在这个阶段会将预置的饰品和图形画到canvas上面去。
状态更改
,在这个阶段用户能更改饰品,图形边框和上传图片,我们需要将最新的用户图片,饰品图片,图形边框画到canvas中。
结果输出
,最后将canvas的内容输出成可保存下载的图片。
嗯没错,思路是非常直观
的。下面会对核心功能详细解析。
实现
首先需要明确的是,既然要做头像
,那么基本可以认为最终用户需要的图片是长和宽相等
的。但实际上用户上传的图片很可能并不规整
,这里有三种情况,宽大于高,宽小于高,宽等于高,同时又要等比放大or缩小,那应该怎么办呢?
有一个很巧妙的办法
就是,比如宽大于高时,我们可以将原图的高等比
缩放到canvas画布的高度,原图的宽也跟着等比缩放
。就像下图,这样既可以保持原图的比例
,canvas又不用留白
,虽然会丢失掉原图的一小部分,但是从实际效果来看还是可以接受的。好吧,做这一步就是为了替(tou)换(lan)掉用户自定义裁剪这个功能。
调整用户图片尺寸
// 不急,等下讲到这两个函数
const base64Url = await this.file2Base64(sourceImage);
const imgObj = await this.createImage(base64Url);
const CANVANS_SIZE = 256;
const type = imgObj.width - imgObj.height;
// 三种情况
if (type > 0) {
// 无论宽大于高还是宽小于高,都会进行等比缩放
const w = imgObj.width * CANVANS_SIZE / imgObj.height;
context.drawImage(imgObj, 0, 0, w, CANVANS_SIZE);
} else if (type < 0) {
const h = imgObj.height * CANVANS_SIZE / imgObj.width;
context.drawImage(imgObj, 0, 0, CANVANS_SIZE, h);
} else {
context.drawImage(imgObj, 0, 0, CANVANS_SIZE, CANVANS_SIZE);
}
图片转换成Base64格式
使用FileReader
对象读取用户上传的文件,并转为base64
格式。由于读取图片的过程是异步
的,这里使用Promise
封装了一下。
file2Base64(domFile) {
return new Promise((resolve, rejest) => {
const reader = new FileReader();
reader.readAsDataURL(domFile);
reader.onload = (e) => {
resolve(reader.result);
};
});
}
创建图片
由于canvas
绘制图片时只接受图片对象
,得到base64
格式的图片之后,需要再包装成图片对象才能绘制到canvas
中。由于载入成图片对象的过程也是异步
的,这里也使用Promise
封装了一下
createImage(imgUrl) {
return new Promise((resolve, rejest) => {
const imgObj = new Image();
imgObj.src = imgUrl;
imgObj.onload = (e) => {
resolve(imgObj);
};
});
}
边框处理
就是如何在canvas上绘制几何图形,觉得这个处理方式很不错,这里借鉴了一下绘制圆角和圆形的方法。
《在Canvas中绘制圆角矩形》
输出到画布
将异步操作封装成Promise的好处在这里就体现出来了,能非常直观
的使用同步的编写形式将异步操作
表达出来。要注意的是用户图片需要先绘制到canvas
中,否则饰品和图形边框就会被图片覆盖。
/**
* @param {string} imgUrl (进过处理的用户图片url)
* @param {object} decorationCurrent (饰品对象)
* @returns imageUrl
* @memberof App
*/
async handleMakeImage(imgUrl, decorationCurrent) {
if (!(imgUrl || decorationCurrent)) { return ''; }
const { border } = this.state;
const { value } = border;
const { source, style } = decorationCurrent;
const { width, height, top, left } = style;
const { canvas } = this.refs;
this.clearCanvas(canvas);
const context = canvas.getContext('2d');
if (imgUrl) {
const bgImg = await this.createImage(imgUrl);
context.drawImage(bgImg, 0, 0, bgImg.width, bgImg.height);
}
this.drawBorder(value, context);
if (decorationCurrent && source) {
const imgObj = await this.createImage(source);
context.drawImage(imgObj, left, top, width, height);
}
const targetUrl = canvas.toDataURL('image/png');
return targetUrl;
}
保存or下载
最后模拟一下a
标签的点击事件,完成图片的下载。如果是移动端用户,长按图片即可保存图片。
downloadImage(url) {
if (url) {
const aLink = document.createElement('a');
const evt = document.createEvent('HTMLEvents');
evt.initEvent('click', true, true);
// 指定图片文件名
aLink.download = 'protrait.png';
aLink.href = url;
aLink.click();
}
}
缓解图片模糊的小细节
发现如果按正常的图片比例去载入图片,最后生成出来的图片会出现模糊。有一个简易的方法就是将canvas的长宽扩大到原来的两倍,但图片还是按原来的比例来显示,简单的来说就是将大图片缩小了显示。这样就能有效缓解图片模糊的问题了。
...
<canvas width="512" height="512" className="shadow d-n" ref="canvas" />
...
<img className="w256 h256" src={targetUrl} alt="" />
ok,就这样,核心功能代码就是这些了,一共200行
不到。
有兴趣的话可以看源码了解更多细节
结束
由于接下来各种节日不断接近,这个小工具的素材也会不断更新,也欢迎大家贡献一些有趣的素材哈哈。
欢迎关注公众号
或者你感兴趣的内容
Re从零开始系列
- 《Re从零开始的组件库构建与发布流程》
- 《Re从零开始的UI库编写生活之规范制定》
- 《Re从零开始的UI库编写生活之按钮》
- 《Re从零开始的UI库编写生活之表单》
- 《Re从零开始的UI库编写生活之表格组件》
- 《Re从零开始的UI库编写生活-步骤管理组件Steps》
- 《Re从零开始的UI库编写生活-Tree组件》
- 《Re从零开始的后端学习之配置Ubuntu+Ngnix+Nodejs+Mysql环境》
- 《Re从零开始的后端学习之配置LAMP环境》