用 Vue + Canvas 做一个奥利奥图片生成器
用 Vue + Canvas 做一个奥利奥图片生成器

用 Vue + Canvas 做一个奥利奥图片生成器

一张沙雕梗图是如何产生的。

偶然发现了一张沙雕图:

奥利利利利利奥

感觉很有意思啊,来做一个生成器好了。

成果

你可以从 这里 访问这个生成器。

全部源代码已经在 Github 开源,项目地址 ddiu8081/oreooo ,喜欢的话欢迎加个星星。

思路

因为功能整体比较简单——根据输入字符串生成对应图片,并且可以以 png 格式保存。整体不涉及后端操作,所以选择轻巧的 Vue 来实现。

关于生成图片的部分,有两个主流选择:直接创建对应的 DOM 元素,再将 DOM 转化为 png 图片;或者直接将图片绘制在 canvas 画布上。在这里我们选择后者方案。

素材准备

找了一张分层的源文件(图左),用 Illustrator 打开之后处理成立体的样子,分别导出。

(绘图水平并不高…只是把原图拉扁,拼了一个椭圆+矩形,看起来像一个圆柱…至于光滑的感觉实在做不出来...)

核心操作:Canvas 部分

canvas 基本概念及图片绘制

canvas 是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素。例如,它可以用于绘制图表、制作图片构图或者制作简单的(以及不那么简单的)动画。

通过 HTML 和 JavaScript 声明一个 canvas,然后使用到 drawImage 方法来绘制图像。:

<canvas width="240px" height="300px" id="oreo_canvas"></canvas>
var canvas = document.getElementById('oreo_canvas');
var ctx = canvas.getContext('2d');

context.drawImage 的基本语法如下:

context.drawImage(img,x,y);
context.drawImage(img,x,y,width,height);
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);

参数描述img规定要使用的图像、画布或视频。sx可选。开始剪切的 x 坐标位置。sy可选。开始剪切的 y 坐标位置。swidth可选。被剪切图像的宽度。sheight可选。被剪切图像的高度。x在画布上放置图像的 x 坐标位置。y在画布上放置图像的 y 坐标位置。width可选。要使用的图像的宽度。(伸展或缩小图像)height可选。要使用的图像的高度。(伸展或缩小图像)

向画布中叠放图片

例如,在 canvas 中插入一张”奥“:

// 创建图片
var O = new Image(); // 顶部饼干层
var R = new Image(); // 中部奶油层
var Ob = new Image(); // 底部饼干层
O.src = 'assets/O.png';
R.src = 'assets/R.png';
Ob.src = 'assets/Ob.png';

// 从下往上堆叠图片,防止层级错乱
O.onload = function () {
    R.onload = function () {
        Ob.onload = function () {
            ctx.drawImage(O,0,0,300,200);
            ctx.drawImage(R,0,52,300,200);
            ctx.drawImage(Ob,0,22,300,200);
        }
    }
}

注意和 DOM 中可以根据 z-index 属性设置层次不同,canvas 中的对象没有层级,也不能修改,类似于现实中的画油画。要根据从后向前的顺序依次覆盖,否则可能会出现下图状况:

但是这样三个 onload 相互嵌套,不够优雅,所以可以实现一个图片预加载的方法。传入一个预加载对象,然后再进行叠放。

loadImages: function (sources, callback) {
    var images = {};
    var index = 0;
    var attCount = Object.getOwnPropertyNames(sources).length;
    for (imgItem in sources) {
        images[imgItem] = new Image();
        images[imgItem].onload = function () {
            index++;
            if (index == attCount) {
                callback(images);
            }
        }
        images[imgItem].src = sources[imgItem];
    }
}

var sources = {
    O: "assets/image/O.png",
    R: "assets/image/R.png",
    Ob: "assets/image/Ob.png"
};
this.loadImages(sources, function (images) {
    // foo
});

然后一张”奥利奥“就成型了。至于做成”奥利利奥奥奥“这种图片,依次叠放即可......

进阶:其他一些优化

实现旋转 loading

想在网页初次加载的时候放一个全屏 loading,用到了 CSS 的动画。

首先创建一个占满浏览器窗口的 div,将图片置于中心,使用 Vue 的 data 来控制 loading 的显隐。

然后对图片设置动画,先创建一个旋转动作:

@keyframes rotate{
    0%{
        transform: rotate(0deg);
        -webkit-transform: rotate(0deg);
        -moz-transform: rotate(0deg);
    }
    100%{
        transform: rotate(360deg);
        -webkit-transform: rotate(360deg);
        -moz-transform: rotate(360deg);
    }
}

然后对图片设置 animation 属性,调节动画速度,设置为循环播放,线性速度。

.loading-img {
    animation: rotate 6s infinite linear;
}

WebFont 自定义字体

在 Web 中使用自定义字体是个麻烦事,因为中文字体实在太大,全部加载的话体积可观,于是采用按需加载是一个可行的方案:计算页面中使用了哪些字,再实时生成一个字体子集进行挂载。目前 有字库justfontAdobe Fonts(Typekit) 均提供了中文字体的在线加载方案。

由于这个项目非常简单,用到的字也不多,干脆直接生成一个仅包含这些字的字体用在网页中。

百度前端团队提供了一套 Fontmin 解决方案,可以将 ttf 字体进行子集化,提供了开发方案和客户端。

正巧发现了一套免费可商用的 瀨戶字型,和这种中二的风格很搭(?)

所以通过 Fontmin 工具,创建了一份瀨戶字型的子集,并且自动生成了所需的 CSS 代码:

@font-face {
    font-family: "Seto";
    src: url("assets/font/Seto.eot"); /* IE9 */
    src: url("assets/font/Seto.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
            
    url("assets/font/Seto.woff") format("woff"), /* chrome、firefox */
    url("assets/font/Seto.ttf") format("truetype"), /* chrome、firefox、opera、Safari, Android, iOS 4.2+ */
            
    url("assets/font/Seto.svg#Seto") format("svg"); /* iOS 4.1- */
    font-style: normal;
    font-weight: normal;
}

国际化

要实现一个国际友好的项目,多语言切换是不能少的,下面使用 Vue I18n 来实现一下。

安装详见文档 Installation 部分,这里因为没有用到模块化所以直接用了 script 引入方式,然后使用 Vue.use 引入 VueI18n。

定义字典:

const messages = {
    en: {
        output: {
            meta: "Here's your",
            save: "Save Image",
            back: "Back"
        }
    },
    zh_CN: {
        output: {
            meta: "這是你的",
            save: "保存图片",
            back: "返回"
        }
    }
}
const i18n = new VueI18n({
    locale: 'zh_CN', // set locale
    messages, // set locale messages
})

// create root Vue instance
new Vue({
  i18n,
  ...
}).$mount('#app')

然后在 HTML 中通过 $t 就可以读取到当前语言的字符串了。

<div @click="downloadImage" class="btn">{{ $t("output.save") }}</div>
<div @click="backToInput" class="btn">{{ $t("output.back") }}</div>

然后可以看到语言已经被替换成对应的文字。