阅读 2731

200行代码带你制作实用的节日头像定制小工具【可在线制作,快来试试吧】

哈哈,趁着平安夜来凑凑热闹,其实很久之前就做过一个类似功能的毕业季生成邀请函小应用。这种有关图像合成的应用其实只需要熟悉h5中canvas的相关API和一点几何常识就ok了,是很容易实现的,这里就带大家探究一下实现的过程。再加上最近会有一大波节日到来,相信这个小工具值得你的拥有,如果觉得不错,不如点个呗。也可关注我的公众号BRandF(有小工具的图文信息)推荐给朋友用哈,二维码在文末。

效果

image

  1. 用户能上传图片
  2. 能将用户图片自动调整成合适的尺寸
  3. 能切换饰品
  4. 能切换头像图形边框
  5. 保存/下载生成的图片

在线定制头像

在线制作头像应用

思路

  1. 初始化,在这个阶段会将预置的饰品和图形画到canvas上面去。

image

  1. 状态更改,在这个阶段用户能更改饰品,图形边框和上传图片,我们需要将最新的用户图片,饰品图片,图形边框画到canvas中。

image

  1. 结果输出,最后将canvas的内容输出成可保存下载的图片。

image

嗯没错,思路是非常直观的。下面会对核心功能详细解析。

实现

首先需要明确的是,既然要做头像,那么基本可以认为最终用户需要的图片是长和宽相等的。但实际上用户上传的图片很可能并不规整,这里有三种情况,宽大于高,宽小于高,宽等于高,同时又要等比放大or缩小,那应该怎么办呢?

有一个很巧妙的办法就是,比如宽大于高时,我们可以将原图的高等比缩放到canvas画布的高度,原图的宽也跟着等比缩放。就像下图,这样既可以保持原图的比例,canvas又不用留白,虽然会丢失掉原图的一小部分,但是从实际效果来看还是可以接受的。好吧,做这一步就是为了替(tou)换(lan)掉用户自定义裁剪这个功能。

image

调整用户图片尺寸

// 不急,等下讲到这两个函数
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行不到。

有兴趣的话可以看源码了解更多细节

源码&&在线应用

结束

由于接下来各种节日不断接近,这个小工具的素材也会不断更新,也欢迎大家贡献一些有趣的素材哈哈。

欢迎关注公众号获取这个小工具的图文信息

image

或者你感兴趣的内容

Re从零开始系列

web安全系列