background

At JD.com, senior employees who have worked for five years are called “big bosses”, and after 10 years, they are called “super bosses”.

Starting from May 19, 2016, the date has been designated as jd.com’s “519 Senior Employee Day” every year. Is the so-called: five years of hone silver, ten years of forging gold! Employees who grow up in JINGdong for 10 years can shine like gold in any company in the industry!

In these 5 or 10 years of countless struggle day and night, we are working in what posture? Let me reveal how these poses are developed

play

First let’s review the effect with a GIF

The basic steps of gameplay are as follows

Ok, you can share the photo with your moments.

Technology selection

It can be seen that a large number of pictures are used here. By dragging and zooming the pictures, people and accessories are placed and corresponding pictures are finally synthesized. So how does this work?

First of all, we used NUTUI to build the whole project. The scaffolding can handle the image optimization and packaging well. The bottom operation menu module uses the Tab component in NUTUI to improve the development efficiency. In the main interface, the Canvas based CreatJS library and hammer. Js, a lightweight touch device gesture library, are selected to develop.

NUTUI

NUTUI is a jd style mobile component library that develops and serves enterprise-class front, middle and back end products for mobile Web interfaces. 50+ high quality components,40+ JD mobile project in use, support on demand loading, support server rendering (Vue SSR)…

Scan the code to experience it

Hammer.js

Hammer is an open source library that recognizes gestures made by touch, mouse, and pointerEvents. It has no dependencies and is small, only 7.34 kB compressed. It supports common single – and multi-touch gestures and can add custom gestures

Create.js

CreateJS is a set of modular libraries and tools developed based on HTML5. Based on these libraries, HTML5 games, animations, and interactive applications can be developed very quickly.

CreateJS contains the following sections

In this project, EaseJs is mainly used and tween.js is combined to do some small animations.

After understanding the technology used, let’s take a look at the implementation process:

Implementation scheme

This project mainly consists of three core elements: loading images, drawing gestures, and gestures, which we will discuss separately below.

1. Load the image

Since 99% of the project’s modules are made up of images, the ability to preload images is essential. There are so many images, do I have to manually list them and load them? Of course not! Now is the age of mechanization, can hand tools do not use.

const fs = require("fs");
const path = require("path");
let components = [];
const files = fs.readdirSync(path.resolve(__dirname, ".. /img/"));
files.forEach(function (item) {
  components.push(`'@/asset/img/${item}'`);
});
let data = `let imgList = [${[...components]}] module.exports = imgList; `;
fs.writeFile(path.resolve(__dirname, "./imgList.js"), data, (error) => {
  console.log(error);
});
Copy the code

Based on nodeJS ‘reading and writing of the file, the image list file can be automatically generated, and the images under the list can be loaded in turn.

2. Pose

EaselJS takes over the ‘draw’ capability in Createjs, using the draw image and text API. The usual drawing steps for EaselJS are: Create a stage -> Create an object -> Set object properties -> Add an object to the stage -> update the stage to render the next frame

this.stage = new createjs.Stage(this.canvas); // Create the stage
let bgImg = new createjs.Bitmap(imgSrc); // Create an object
this.stage.addChild(bgImg); // Add objects to the stage
Copy the code

CreateJs provides two render modes: setTimeout and requestAnimationFrame. The default is setTimeout and the number of frames is 20. Because you’re doing a lot of work on page elements, this option is smoother.

createjs.Ticker.timingMode = createjs.Ticker.RAF; // RAF stands for requestAnimationFrame
Copy the code

Createjs other basic Settings

Easeljs events do not support touch devices by default and need to be enabled manually

createjs.Touch.enable(this.stage);
Copy the code

Refresh the stage in real time

createjs.Ticker.addEventListener("tick".this.stage.update(event));
Copy the code

Hammer. Js configuration

Because hammer.js does not turn on the Rotate event by default, you need to use recognizers in your options to set up a recognizer

let bodyHandle = new Hammer.Manager(this.canvas, {
  recognizers: [[Hammer.Rotate], [Hammer.Pan]],
});
let bodyRotate = new Hammer.Rotate();
bodyHandle.add(bodyRotate);
Copy the code

Now that the preparations are complete, let’s begin

Draw the scene

In order to maintain a civilized image, standing on a desk is not supported. Therefore, the scene is divided into two parts: the background and the table, which are constrained by setting the level of the table on the top of the characters.

So let’s first draw the background

let Bg = new Image();
Bg.src = require(".. /asset/img/scene" + n + ".png");
Bg.onload = (a)= > {
  let bgimg = new createjs.Bitmap(Bg);
  this.stage.addChild(bgimg);
};
Copy the code

Note that if you are not drawing for the first time, you need to clear the previous content

this.stage.removeAllChildren();
Copy the code

In the same way as drawing a table, note that once the table is drawn, its hierarchy needs to be set

. this.stage.addChild(deskImg);this.stage.setChildIndex(deskImg, 1);
Copy the code

Draw the role

Unlike drawing characters and scenes, you need to use Containers. Container is a Container that can contain Text, Bitmap, Shape, Sprite, and other EaselJS elements. For example, you can bring arms, legs, torso and head together and convert them into a group, while also moving parts relative to each other. Here we put the characters and their expressions in a Container for unified management, unified movement, zooming, rotation, etc.

Before drawing the character, we decide where to draw it: the default position is in the very middle of the canvas

let pos = {
  x: this.canvasW / 2.y: this.canvasH / 2};Copy the code

If a role has been selected and needs to be replaced, retain the original position of the role

pos = {
  x: joy.x,
  y: joy.y,
};
Copy the code

Here are the specific drawing steps:

var joy = new Image();
joy.src = require(".. /asset/img/joy" + n + ".png");
// Load the character image
joy.onload = (a)= > {
  var joyImg = new createjs.Bitmap(joy); // Create an image
  joyImg.name = "joy"; // Role name
  joyImg.regX = joy.width / 2; // Move x to the center position
  joyImg.regY = joy.height / 2; // Move y to the center position
  joyImg.x = pos.x; // Set the initial position
  joyImg.y = pos.y; // Set the initial position
  let container = new createjs.Container(); // Create a container
  container.name = "joyContainer"; // Container name
  container.addChild(joyImg); // The container adds the role
  this.stage.addChild(container); // Add the container to the stage
};
Copy the code

Drawing expression

When we drew the characters above, we created a container named joyContainer, and we drew emoticons into it

var face = newcreatejs.Bitmap(imgBg); . joyContainer.addChild(face);Copy the code

This way, when we want to move the character, we keep the integrity by moving the container. Otherwise, your head can’t keep up with your body…

Remove elements

From the moment the role is added, the activeItem of the current operation object is recorded. When the delete button is triggered, just find the activeItem and delete its related contents.

const ele = this.stage.getChildByName(this.activeItem.name);
this.stage.removeChild(ele);
Copy the code

3. Gestures

Hammer.js is a JavaScript library for detecting touch gestures, supports the most common single and multi-touch gestures, and can be fully extended to add custom gestures. NUTUI will integrate this functionality and release it in its next release.

bodyHandle.on("rotate", (e) => {
  let ctrEle = this.activeItem;
  ctrEle.scaleX = ctrEle.scaleY = e.scale * this.nowScale;
  ctrEle.rotation = this.BorderBox.rotation = e.rotation + this.nowRotate;
});
Copy the code

By listening to the rotate event, we can get the zoom and rotation data of the current operation, and combine it with the previous state to achieve the effect of various gestures.

Ok, everything is ready, start your show ~

First, choose an office setting and then role-play. Tired of standing? It’s ok to sit down in a different position, if you want to stand on a stool. Is it a little stuffy? Then stick your tongue out. Computer water cup arrangement, finally to a slogan “in jingdong fat 20 catties”.

Have you had a good time? All right, let’s get back to talking about how we did it.

Generate images

When you click “done”, we will enter the share page, the bottom picture of the share page is three colors randomly selected. Here, we need to create a temporary canvas to draw the shared picture, and then draw the shared background, customized posture scene diagram (converted into an image through canvas.toDataURL method), qr code and nickname into this temporary canvas in turn. Finally, the image is exported and assigned to the URL to share the image.

let tmpStage = new createjs.Stage(tmpCanvas);
tmpStage.addChild(bg, share, code, text);
Copy the code

Since the display elements of the shared picture and the shared page are not exactly the same, what is displayed to the user is the shared page, while the transparency of the shared picture is set to 0, which can only be saved but not seen.

However,, things are not so simple, a wave of bugs are running amok.

Problems encountered

Route bottom navigation remove

As mentioned earlier, this project consists of two pages: the load page and the main screen, with a route jump in the middle (history mode). But in some mobile phones, when you jump to another page through the route, the navigation module will automatically appear at the bottom, which is what we do not want to see. In the already tight space, there is so much space out of thin air, which is intolerable.

Therefore, after weighing the replace mode, the user can’t go back to the loading page after entering the main screen, so it can’t have it both ways.

Input fields in ios do not automatically return, there are white blocks

After loading, the input box with a nickname is completed under ios. After the keyboard is folded up, there will be a large blank at the bottom of the page, which is stuck.

But when we swipe around the page, the white block disappears. This is because the ios keyboard pops up and puts the page on top, so we need to use the scrollTo function to scroll the page when the blur keyboard falls to put the page back in place.

blur() {
    window.scrollTo(0.0);
}
Copy the code

After the system is updated, the white block becomes transparent, which makes people even more elusive. Obviously you can’t see anything, but the input box is not selected. Don’t think you don’t know who you are just because you take off your vest. The solution above is still valid.

Pictures of cross-domain

After the local development is complete and the code is uploaded to the server, the original world is completely quiet and is replaced by a dazzling red:

A search turned up the following statement: Although it is possible to use images on a canvas that are not approved by CORS, doing so would pollute the canvas. Once the canvas is contaminated, data can no longer be extracted from the canvas. For example, you can no longer use the Canvas toBlob(), toDataURL(), or getImageData() methods; Doing so raises a security error. This prevents users from using images to obtain information from remote sites without permission, thereby exposing private data. This explains the error, so how to solve the problem?

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

This turns on CORS during image loading, bypassing the error.

Click on the error

The image loads, but when I try to do drag and drop, I get an error again…

Createjs provides hitArea click areas. You can set another object, objB, to be the hitArea of the display object objA. Clicking on objB is equivalent to clicking on objA. This objB does not need to be added to the list of display objects, nor does it need to be visible, but it replaces objA in the firing of interactive events.

var hitArea = new createjs.Shape();
hitArea.graphics.beginFill("# 000").drawRect(0.0, imgBg.width, imgBg.height); // The size here is the size of the picture, please adjust yourself
img.hitArea = hitArea;
Copy the code

Bind the object to a click area so that the drag is on the area, not the original image, so that no errors are reported

Hierarchy problems

In this project, the role is at the bottom of all other elements, and when the element is toggled, the currently selected element is placed at the top, using createJS’s setChildIndex method

The setChildIndex method allows you to move a display object up or down in the display list. The display list can be viewed as an array, indexed at position 0. If three elements are created, their positions are layers 0, 1, and 2. Layer 2 objects are on the outside, layer 0 objects are on the inside.

If you want to move an element above all other elements, you use the getNumChildren property, which means the number of objects to display in the container. The outermost depth is the numChildren-1 layer. Other elements whose level is higher than that of the top element are reduced by one level.

if (ele.name === "joy") {
  this.stage.setChildIndex(ele, 1);
} else {
  this.stage.setChildIndex(ele, this.stage.getNumChildren() - 2);
}
Copy the code

When we select or add an element, the hierarchy is triggered because we want to ensure that the current element is at the upper level. Because there is a top element, when setting the hierarchy, if it is a character element, set it at level 2, just above the scene background; If it is any other element, it is set to the sub-top-level.

The ios version base64 onload is faulty

In the testing phase found that ios10 below the phone, can not drag, what a bolt from the blue!

In the process of investigation, I found something strange. The reason why I couldn’t drag and drop was that the delete button on the selected box was not loaded. What’s special about this button? The problem was solved, but not explored.

The next result was worse, the shared image disappeared, only the background box was left!

As mentioned in the section of “Generating pictures” above, pictures are exported to Canvas through toDataURL, and the exported format is the base64 format with problems above.

We found that base64 could not trigger onLoad event in ios10 or later, but went onError instead. So what else can a Base64 image be converted to? Here’s the answer:

dataURLToBlob(dataurl) {
    //dataurl: data:image/webp; base64,UklGRvAIAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSC4CAAABkAXbtmlH+xmxn...
    var arr = dataurl.split(', '); // ['data:image/webp;base64','UklGRvAIAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSC4CAAABkAXbtmlH+xmxn...']
    var mime = arr[0].match(/ : (. *?) ; /) [1]; // Isolate the MIME type -- > image/webp
    var bstr = atob(arr[1]); The atob() method is used to decode a base64-encoded string into the raw binary data stored in the string.
    var n = bstr.length;
    var u8arr = new Uint8Array(n); // Uint8Array represents an array of 8-bit unsigned integers that are initialized to 0 when created. Once created, you can refer to the elements of the array as objects or by using an array subscript index.
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n); // Store the Unicode encoding in turn
    }
    return new Blob([u8arr], {type: mime});  // type: represents the MIME type of the array content to be put into the BLOb
}
Copy the code

Let’s start by converting the Base64 image to BLOB format

sharePhoto.src = window.URL.createObjectURL(this.dataURLToBlob(photo));
Copy the code

The ObjectURL is then generated using the url.createObjecturl method

window.URL.revokeObjectURL(sharePhoto);
Copy the code

Because the URL returned by createObjectURL is stored in memory until Document fires the Unload event (for example: Document Close). So let’s develop a good habit, after the completion of use to remember to release oh ~

So who exactly is createObjectURL? Let’s learn:

createObjectURL

Definition: The url.createObjecturl () method creates a URL to that parameter object based on the parameter passed in. The URL lives only in the document in which it was created. The new object URL points to the executing File or Blob object.

CreateObjectURL returns a hash URL and is stored in memory until Document fires an Unload event (for example, Document Close) or revokeObjectURL is released.

The browser support is as follows, and the mobile terminal can use it safely

Block long press events

When it was about to be launched, the internal app did not support long press to save pictures sufficiently, so we temporarily decided to block this function. Here we tried three methods:

  1. Since the save time is triggered on the IMG tag, div can be blocked
  2. Blocking contextmenu when touchstart essentially triggers the contextmenu contextmenu, so we simply block this event
document.oncontextmenu = (e) = > {
  e.preventDefault();
};
Copy the code

This works in web browsers, but not on mobile devices

  1. Add the style
* {
  -webkit-touch-callout: none; /* System default menu is disabled */
  -webkit-user-select: none; /* WebKit browser */
  -moz-user-select: none; / * * / firefox
  -ms-user-select: none; /* IE10*/
  user-select: none; /* Whether the user can select text */
}
Copy the code

User-select controls whether the user can select text, whereas we need to control images. -webkit-touch-callout: Disables or displays the system default menu when you touch and hold the touch target. Applies to: link elements such as opening new Windows, img elements such as saving images, etc. At first glance, isn’t that what we need? However, -webkit-touch-callout is a noncanonical property (unsupported webkit property) that does not appear in the draft CSS specification. Just look at the support:

In the end, I chose the first method, which is simple and direct, regardless of compatibility.

Image optimization

Having solved the above series of problems, it’s time to go back to the original analysis: no matter what technology the project uses, the essence of the final presentation is graphics. Therefore, image size not only affects the loading speed, but also the rendering speed. To provide a better user experience, choose to use the image compression feature in NUTUI, which provides high compression ratio optimization of images and can be automatically converted to WebP format. As we all know, webP format pictures than the general compressed picture is much smaller, relying on such a powerful backer, not good all difficult!

conclusion

Whether you’re a big shot, a big shot, or just joined Jd Fresh Blood, 519 Senior Employee Day is a day for everyone in JDer!

In the process of doing the project, I learned CreateJS from scratch. During the project, I kept trying and trying to solve problems and learn new knowledge. I learned a lot. In the future work, we should pay attention to the breadth of basic knowledge and keep accumulating it. Maybe we are not clear about the application scenarios when learning, but one day we will find that every knowledge has its reason for existence.