布景

在京东,上任满五年的老员工被称作“大佬”,假如满了十年,那就要被称之为“超级大佬”了。

从 2016 年 5 月 19 日开端,每一年的这一天都被定为京东集团的“519 老员工日”。正所谓:五年砺银,十年M Q t w | *锻金!在京东成长 10 年的员工,放在行业里/ c J 3 Q 0 d的任何一家公司,都能够像金子般发光!

在这 5 年或 1z K 3 s0 年无数个斗争的日夜里,咱们是以怎样的姿态在工作呢?下面由我揭晓这些姿态是怎样修炼而成的吧~

玩法

首要咱们用一张 gi# ! Z vf 图来回忆一下作用

如何用Canvas拍出 JDer's工作照

玩法根本的过程如下

如何用Canvas拍出 JDer's工作照

ok,拍完照就能够共享到朋友圈T H . d j t 1 p了。

技术选型

能够看到这儿用到了大量的图片,经过对图片的拖拽缩放等操作,摆放人物及配件,终究组成相应的图片。那么这一过程是怎样完结的呢?

首要咱们采用 NUTUI 来搭建整个项目,其脚手架能够很好地处理图片优化打包等。底部操作菜单模块运用了 NUTUI 中的 Tab 组件,提升了开发功率。在主界面的部分选用了根据 canvas 的 creatjs 库,以m 2 n v g及一个轻量级的触屏设备手势库 hammer.js 来开发。

如何用Canvas拍出 JDer's工作照

NUTD 8 [UI

NUTUI 是一套京东风格的移动端组V . + jF k 7 A 2 D库,开发2 l !和服务于移动 Web 界面的企业级前中后台产品N 9 P O , b *。50+ 高质量组件,40+ 京东移动端项目正在运用,支撑按需加载,支撑服务端x u f q 7 / R烘托(Vue SSR)…

快扫码体会起来吧

如何用Canvas拍出 JDer's工作照

Hammer.js

Hammer 是一个开源代码库,能够识v ( % L 5 o ~别由接触,鼠t W :标和 pointerEvents 做出的手势。它没有任何依赖性,而且很小,紧缩后只有 7.34 kB。
它支撑常见的单点和多点接触手势,而且能够增加自定义手势

如何用Canvas拍出 JDer's工作照

Create.js

CreateJS 是根据 HTML5 开发的一套模块化的库和东西。根据这些库,能够十分便利地开宣布根据 HTML5 的游戏、动画和交互使用。

Cre: ( 1 b HateJS 包括如下几部分

如何用Canvas拍出 JDer's工作照

在本项目中首要是运用了 EaseJs,并结合 Tw– ~ s R c Geen.js 做了一些小动画。

了解完所用到的技术后,咱们来看看详细的完结过程:

完结方案

这个项目首t Z t ( ! : ^ y R要包括了三大核心:加载图片、制作姿态、手势操作,下面咱们分别来讨论一下。

1. 加载图片

因为这个项目 99%的模块是由图片构成,因而预加载图片这一功用必不行少。图L N S –片那么多,要一个个手动列出来去加载吗?当然不必!i c – G 5 e & M现在是机械化年代了,能交给东西的就不着手。A w . v

const fs = require("fs");
constM 3 e p = path = require("path");
let componentsE F ; ~ - ) W = [];
c% Z 9 7 h 5 } S ;onst fils H 4 P S $ Ies = fs.readdirSync(path.resolve(__dirname, ".! & C B V A | e./img/a % , C S 0 . t"));
files.forEj ( j ~ 7ach(functi* H * V K . |on (item) {
components.push(`'@/asset/img/$5 ] G z +  4 A E{item}'`);
});
let data = `let imgList = [${R j 5 0 - Z  | D[...components]}]
module.exports = imgList;`;
fs.writeFile(path.resolve(__dirname, "./imgList.js"), data, (eT ^ : E arror) =>} _ E 7 _ I (; {
console.log(error);
});

依托于 nodejs 对文件的读写来完结主动生成图片列表文件,加载时对这个列表下的图片顺次 load 即可。

2. 制作姿态

EaselJS 在 Createjs 中承当 ‘画’ 的能力,这儿用到了画图片和画文字的 API。Easel? n [ ~ b N –JS 一般的制作过程是:创立舞台 -&j W & _ *gt; 创立方针 -&gtq _ e k ( ^ O u; 设置方针特点 -> 增加方针到舞台 -> 更新舞台出现下一帧

this.stage = new createjs.Stage(th% m P 2is.canvas); // 创立舞台
let bgImg = new createjs.Bitmap(imgSrc); // 创立方针
thij a U = [ ~ D J $s.stage.addChild(bgImg); // 增加? m R k %方针到舞2 s Z , L ` X F

CreateJs 供给了两种烘托形式,一种是用 setTimeout,一种是用 requestAnimationFrame,默许是 setTimeout,帧数是 20,这儿咱们选用 requestAnimationFrame 形式,因为要对页面元素进行大量7 I k % h q E $的操作,选此种办法会愈加流6 , f z y通。

crea^ / l % t 7tejs.Ticker.timingMode = createjs.Ticker.RAF; // RAF为requestAniC p I 4 % H k j 3mationFrame缩写

createjs 其他根本设置

easeljs 事情默许是不支a 3 V , f l Q撑 touch 设备的,需求手动敞开

createjs.Touchi D X p .enable(this.stage);

实时刷新舞台

createjs.Ticker.addEventLii ) N F g 9stener("tick", this.stage.update(event));{ / f

hammer.js 装备

因为 hammer.js 默许是不敞开 rotate 事情的,因而需求在选项中运用 recognizers 来设置一个识别器

let bodyHandle = new Hammer.Manager(this.canvas, {
recognizers: [[Hammer.RotM | K 3ate], [Hammer.Pan]],
});
let body9 9 @  e O -Rotate = newd # % HammerO f ] s K l w 0.RotaD x _te();
bodyHandleW P A C {.add(bodyRotate);

准备工作完结,下面正式开端

制作场景

为了坚持文明的形象,就J u 不支撑站在桌子上工作了。因而场景分为布景和桌子两部分,经过设置桌子的层级在人物的上层来进行束缚。

首要制作布景

let Bg = new Image();
Bg.src = require("../asset/img/scene" +u o } 9 q n + ".png");
Bg.onload = () => {
let bgY ] U p Y U %img = new createjs.Bitmap(Bg);
this.stag0 k r y |  O ve.addChild(bgimg);
};

注意,假如不是初次制作,需求将之前的内容清空

this.stage.removeAllChildren();

同理制作桌子,需求注意的是,桌子制作完今后,需求设置其) z 9 y t m R层级

...
this.stage.addChild(deskImg* O A 0 S [ 8);
this.stage.setChildIndek & 1 Y J cx(deskImg, 1);

制作人物

制作人物与场景不同,这儿需求用到 Container。
Container 是一个容器,能够包括g r z [ 5 Text、Bitmap、j q ) V { QShape、Sprite 等其他的 EaselJS 元素。例如,你能够将手臂、腿部、躯干和头部聚在一起,把它们转换为一组,一起还能够将各u Y n s Y : e个部分相对互相移动。在这儿咱们将人物及其表情放0 F j ^在一个 Container 中便利一致管理,一致移动缩放旋转5 ^ Z (等。

制作人物前,咱们先确认制作的方位@ k ? W:默许方位在画布的最中心

let pos = {
x: this.canvV . 0 FasW / 2,
y: this.canvasH / 2,
};

假如现已挑选过人物,需求替换时,需求坚持之前人物的方位

pos = {
x: joy.x,
y: joy.y,
};

下面是详细制作过程:

var joy = new Image();
joy.src = require("../asset/img/joy" + nd q - M R l + ".png");
// 加载人物图片
joy.onload = () => {
var jo, 6 j DyI` + = ~ w ; p [ jmg = new createjs.Bitmap(joy); // 创立图画
joyImg.name = "joy"; // 人物命名
jw } 4 S @oyImg.regX = joy.width / 2; //v : 2 , T o 移动x方向到中心点方位
joyImg.regY = joy.height / 2; // 移动y方向到中心点方位
joyImg.x = G c pos.x; // 设置初始方位
joyImg.y = pos.y; // 设置初始方位
let container = new createjs.Z [ } X # c -Container(); // 创立容器
container.name = "joyContainer"; // 容器命名
container.addChild(joyImg); // 容器增加人物
this.stage.addChild(container); // 增加容器到舞台
};

制作表情

在上面制作人物时,创立了一个 name 为 joyContainer 的容器,咱们将A * J 3表情也制作进去

var fat 7 Kce = new createjs.Bitmap(imgBg);
...
joy5 * J c X w o EContainer.addChild(face);

这样当咱们想移动这个人物时,经过移动容器,y l . – k n , 1 V来确保全体性。不然会出现脑袋跟不上身体移动的状况。。。

删去元素

从增加人物开端,就会记录下当时的F I & – } ^操作方针 activeItem,当触发删去按钮时,只需找到 activeIte; Q W 5 )m,并将其相关内容删去即Q . Q y F可。

const ele = this.stage.getCh7 l & pildByName(this.activeIteR } * 8 L Q d @m.name);
this.stage.removeChilE J 4 F Ad(ele);

3. 手势操作

hammer.js 是用于检测接触手势的 JavaScript 库,支撑最常见的单点和多点接触手势,而且能够完全扩展以增加自定义手势。NUTUI中将会集成此功用并在下个版别中正式发布。

bodyHande G 1 6le.on("rotate"J K ?, (e) => {
let ctrEle = this.activeItem;
ctrEle.scaG r 2leXd ~ W = ctrO v 9Ele.scaleY = e.scale * this.nowScale;
ctrEle.~ U -  Rrot| I O Aation = this.BorderBox.rotation = e.rotation + this.nowRotate;
});

经过监听 rotate 事情,能够得到当次操作的缩放及旋转的数据,咱们再将其与之前的状态相结合,就能达到_ C i Y各种手势操作的作用了。

好了,一切准备就绪,开端你的表演吧~

首要,挑选l # ? d Q V * [一个工作场景,然后来个人物扮演,站着有点累?不要紧,换个姿态坐下来吧,当然你想站着凳子上也不要紧。。表情是不是有点古板?那就吐吐舌头吧。电脑水杯安排上,终究再来个标语“在京东胖个 20 斤”。。

如何用Canvas拍出 JDer's工作照

玩过瘾了吗?好了,收收心咱们持续聊怎么完结的吧。

生成? } ~ | , n {图片

当你点击“完结时”,咱们会进入共享页,共享页的底图是三种颜色随机挑选。这儿咱们需求创立一个暂时的 canvas 来制作共享图片,将共享的布景,定制好的姿态场景图(经过 canvas.toDataURL 办法转成图片),还有二维码,以及昵称,顺次制作到这个暂时的 canvas 中c $ {,终究导出图片后S i ] . 赋值给共享图片的 url。

let0 t . f tmpSr } - /tage = new createjs.Stage(tmpCanvas)c 4 3;
tmpStage.a/ j 2 g % j K 1 SddChild(bg, share, code, tex? E & Kt);

因为共享图片与共享页展现元素不完全相同,因而展现给用户看到的是共享页,而共享图片设置了通明度为 0,只能保存不能被看到。

可是,事情没有这么简略,一大波 bJ C Zug 正在马不停蹄的狂奔袭来。。

V f N 0 H到的问题

路由 底部导航去除

前面介绍过,这个项目是由加载页和主界面两个页面组成,中心是经过路由跳转(history 形式)。可是在一些手机中a } : z z M U 3,经过路由跳转到另一个页面时,底部会主动出现导航模块,这是咱们所不希望看K ] P z到的,本就捉襟见肘的空间里,凭空多了这么大一块,这是a # + %不行容忍的存在。

如何用Canvas拍出 JDer's工作照

因而在权衡之后,挑选了 replace 形式,可是这样用户在进入主界} : ; V : ,面今后,就不能回到加载页了,鱼与熊掌不行兼: D #得。

如何用Canvas拍出 JDer's工作照

ios 中输入框不主动收回,有白块

在加载完结后,有个昵称的输入框,在 ios 下输入. m v V 4完结,键盘? ; d ) ( a收起后页面底部会有一大片空白,呈卡死状。

如何用Canvas拍出 JDer's工作照

可是当咱们在页面上随意滑动0 S , V q一下,这个白块就会消失。这是Q l H O a M [ V因为 ios 键盘弹出后,会把页面全体顶上去,因而咱们需求运用 scrollTo 函数O d 8 | I T,在 blur 键盘落下时滚动页面,使页c 2 J ` G W 2面归位。

blur() {
w_ ! ! Gindow.scrollTo(0, 0);
}

因为体系更新后,白块变成了通明状态,这使得人愈加琢磨不透,明明看不到任何东西,可是输入框便是无法选中。别以为脱了马甲就不# 8 S P知道你了,上面的处理方案依旧是有效的。

图片跨域

本地开发完结,上传代码到服务器后,本来的世界静好全都消失不见,取而代之的是扎眼的红:

如何用Canvas拍出 JDer's工作照

一番查阅后找到了如下这段话:
虽然能够在画布中运用未经CORS批准的图画,但这样做会{ F x v 2污染画布。一旦画布被污染,就不能再从画布中提取数据。例如,不能再运用canvas toBlob()、toDataURL()或getImageData()办法;这样做将引发安全错误。这能够避免用户在未经允许的状况下运用图画从长途网站获取信息,然后揭露私有数据。
这就解说了上面报错的由来,那么怎么处理呢?

var bg = new Image();
bg.crossOrigin = "Anonymous";

这就敞开了图片c P X p Z y加载过程中的 C5 / # 3 W X g 6ORS 功用,然后绕过了报错。

点击报错

图片能够加载了,可是当我想做拖拽等操作时,又又又报错了。。。

如何用Canvas拍出 JDer's工作照

createjs 供给了 hitArea 点击区域。能够设置另一个方针 objB 作为W K Y [ [显现方针 o2 / 9 N ubjA 的 hitArea,当点击到 objB 时就相当于点? m t V F W W o击到了 objA。 这个 o( s G Q S E ; . 6bjB 不需求增加到显现方针列表,也不需求可见,但它会在交互事情的触发中替代E ( ~ } d objA。

var hitArea = new createjs.Shape();
hitArea.grat = tphics.beginFill("#000").drawRect(0, 0, imgBg.width, imgBg.height); //这儿的大% d Q 8 ` 9 3 % +小为图片大小,请自己调整
img.hitArea = hitArea;

给方针绑定一个点击区域,这样拖拽是操作这个区域,而不是本来的图画,这样就能够不报错了

层级问题

在这个项目中的设定,人物在一切其他元素的底层,而元素切换Z G d选中时,也需求将当时选中元素置顶,} ~ e : S这儿用到了 createjs 的 setChild, t b 0 _Index 办法

setChildIndex 办法允许你向上或向下移动显现方针在显现列表内的方位。显现列表能够看作为一个数组,它的= b n V B + q索引方位是从第 0 开端的。假如创立了 3 个元素,那么他们的 0 I a M h g方位便是第 0,1,2 层。第二层的方针在外面,第 0 层的在最里面。

假如想把某一元素移到一切元素的上面,这时就要用到 getNumChildren 特点,它的意义便是该容器内显现方针的数目。最外层的层深便是第 numChildren-1 层。其他本来层级高于置顶元素的元素,^ X I相应层级会减少i K / : G (一级。

if (ele.name === "joy") {
this.stage.setChildIndex(ele, 1);
} else {
this.stage.setChildIndex(ele, this.stage.getNumChB 6 N F 8 E Gildren() - 2);
}

在咱们选中或者新增一个元素时,触发层( C 3 S c S ( {级设置,因为要确保当时操作的元素层级在上。因为有置顶的元o X 8 . W素,因而在设置层级o . D时,假如是人物元素,那么设置在第 2 层,只是高于场景布景层;假如是其他元素,则设置为次顶层。

ios 低版别 base64 onload 有问题

在测d t ) ? d试阶段发现,ios10 以下的手机,不能拖拽,真是个平地风波!

在排查过程中发现了奇怪,不能拖拽竟然是因为选中框上面的删去按钮没有加载到,这个按钮有什么特别之处呢,哦,原来是 webpackc o n w ^ ( { 装备中的 url-loader 主动将小图片转成了 base64 格局,顺着这个思路,将这个功用~ b R [去掉今后,问题得以处理,但并没有深究。S a ! j [

接下来的结果更糟,共享图片不知去向了,只剩下个布景框!

如何用Canvas拍出 JDer's工作照

上面“生成图片”部分就讲过,图片都是将 canvas 经过 toDataURL 导出,导出格局正是上面有问题的 base64 格局。

咱们发现 base64 在 ios10 以下版别中,无法触发 onload 事情,而是走了 onerror。那么 base64 图片还能转成什么格局呢?答C | af R p 4 . J @就在这儿:

dataURLToBlob(dataurl) {
//dataS { Q x ` : =url: data:image/webp;base64,UklGRvAIAABXRUJQVlA4WAoAAAAQAAAAXwAE v D a B # eAXwAAQUxQSC4CAAABkAXbtml$ T D m s gH+xmxn...
var arr = dataurl.split(','); // ['data:image/webp;base64','UklGRvAIAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQ7 C D ? _ /UxQSC4CAAABkAXbtmlH+xmxn..J k # {.']
var mimeU t s r & = arr[0].match(/:(.*?);/)[1]; // 别离出mime类型 ——> image/webp
var bstr = atob(arr[1]); // aS ) 3 1 gtob() 办法用于解码运用 base64 编码的字符串,转换为字符串中保存的原始二进制数据。
var n = bstr.lengA s t ;th;
var u8aw / w p qrr = new Uint8Array(n); // Uint8Array表示一个8位无符号整型数组,创立时内容被初始化为0。创立完后,能够以方针的办法或运用数组下标索引的办法引证数组中的元素。
whilex ; V ` F O # & O (n--) {
u8arr[n] = bstr.charCodeAt(n); // 顺次存储Unicode 编码
}
return new Blob([u8arr], {type: mime})k 6 J _ s;  //} _ 0 H a type:代表了将会被放入到blob中的数组内容的MIME类型
}

咱们先将 b, 4 g l B G I Nase64 图片转为 blob 格局

sharePhoto.srcO a ? % = window.URL.createObj( t w 0 TectURL(this.dataURLToBlob(photo));

然后经过 URL.createObjectURL 办法生成6 d + $ K = + ObjectURL

window.URL.revokeObjectURL(shareP0 H Bhoto);

因为 createObjectUt _ x | yRL 回来的 url 一向存储在内存中,直到K z { d^ p [ 2 bocument 触发了 unload 事情(例如:document close)。所以咱们养成a n p v & ^好习惯,在运用完结今后要记住顺手释放一下哦~

那么 createObjectURL 到底是何方神圣呢?咱们一起来学习下:

createObjectURL

定义:URL.createObjectURL()办法会根据传入的参数创立一个指向该参数方针的 URL。这个 URL 的生命仅存在于它被创立的这个文档里。新的方针 URL 指向执行的 File 方针或者是 Blob 方针。+ o o h

createObjectURL 回来一段带 hash 的 url,而且一向存储在内存中,直到 doc6 H 7 !umen+ 7 u g H V ] mt 触发了 unload 事情(例如:document close)或者执行 revokeObjectURL 来释放。

浏览器支撑P ! u n p状况如下,移动端根本能够放心运用~

如何用Canvas拍出 JDer's工作照

阻挠长按事情

在即将上线时,因为内部 app 对长按保存图片支撑不太充沛,因而暂时决议在其中屏蔽此功用,这儿尝试了三种办法:

  1. 加通明 div 盖在最顶层
    因为长按保存时刻是在 img 标签上触发,因而 div 能阻挡住
  2. touchstart 时阻挠 contextmenu
    究其实质,长按是触发了 contextmenu 上n t 5 1 ?下文菜单,那么咱们只需阻挠这个事情即可
document.oncontextmenu = (e) => {
e.preventDefault();
};

在 wJ R # Y l 1eb 浏览器中收效,可是在移动端无效

  1. 加款式
* {
-webkit-touch-callout: none; /* 体系默许菜单$ _ 3 Q #被禁用*/
-webkit-user-select: none; /* webkiI Y ` 9 ( M [ 4t浏览器*/
-moz-user-select: none; /* 火狐*/
-ms-user-select: none;` g v { c  p * ^ /* IE10*/
user-select: none; /* 用户是否能够选中文本*/
}

实践证明这种办法不行行,咱们顺次来剖析一下:
user-select 操控用户能否选中文本,而咱们这儿需求的是操控图片。
-webkit-touchR Q W-callout:当你接触并按x N } { w k住接触方针时分,禁止或显现体系默许菜单。适用于:链接元素比方新窗口打开,img 元素比方保存图画等等
乍一看,这不便是咱们所需求的吗?
可是,-webk* & r C ? O W eit-touch-callout 是一个 不; 4 e N z c b标准的特点(unsupporteE g Z ^ v 7 #d WebKit property),它没有出现在 CSS 标准草案中。$ [ 4 E
看一下支撑状况就k = s e = U 6理解了:

如何用Canvas拍出 JDer's工作照

终究挑选了第一种办法,简略直接,不必考虑兼容性。

图片优化

在处理了上面一系0 W Q g列的问题之后,要回到最初的剖析& 8 W V & e g ):不论项目用了何种技术,终究出现的实质都是图片。所以图片的大小不只影响加载速度,一起也影响着烘托速度,为了供给更优的用户体会,挑选运用 NUTUI 中z J X v的图片紧缩功用,它能够供给高紧缩比的图片优化,而且能够主动转化成 webp 格局。咱们都知道,webp 格局的图片比一般紧缩过的图片还要小许多,依托于这么强壮的靠山,想不出色都难!

总结

不论你现^ j k p H在是大佬 – , A、超级大佬,还是刚刚加入京东的 freshI L S blood,51U ) R n9 老员工日便是归于每一位 JDer 共同的节日!

在做项目的过程中,从零开端学习 createjs,项目H V K v中心不断试错,不断去处理问题,学习新知识,收获良多。在今后的工作中,还要注重基础知识的广度,不断; – k t 1 j [积累,也许学习K i x的时分并不清楚使用场景,可是终有一天会发现,每个知识都有其存在的理由。