Character relationship diagram of PIXIJs combat
preface
Recently, I was working on a project to refurbish an old character diagram page. I needed to optimize the presentation of character relationships and highlight people and their relationships. Let’s take a look at the designer’s results.
When I first saw this, I was like this.
It’s not so hard to calm down and analyze a wave.
Here’s a step-by-step explanation of how to implement such a page.
1. Holistic analysis
First of all, in the face of a complex content can not blindly start. Break it down to see what the page contains and decide on the overall direction.
Divided from the hierarchy, including three parts: background layer, role relationship layer, role information layer.
Character information: no excessive dynamic effects and repeated rendering, direct use of Dom elements to achieve, convenient and simple.
Character relationship layer & background layer: just looking at the cloud background, the first feeling can use 3D engine to achieve the overall background, such as THREEJS, but for a single page THREEJS volume is too heavy, the page content itself is more in line with 2D. Therefore, PIXIJS, a 2D rendering library, was chosen as the technical selection. PIXIJS is the fastest in 2D rendering speed, and PIXIJS encapsulates a very rich API that can be used to facilitate the property setting and modification of different rendering objects. On the dynamic side, you can use TweenJS to set the properties you want to modify to easily achieve a variety of animation effects.
After disassembly and analysis of the requirements, we chose to use PIXIJS. Sort out the overall implementation idea: introduce PIXIJS to the page, create app and stage, split the display content according to different levels, achieve the visual objects of each layer, and finally gather them to render and display on the stage. See the following figure for details:
2. The specific implementation
2.1 Creating a Stage
Application (app) : Canvas or WebGL is automatically selected to render graphics, depending on browser support.
Stage: All visual objects to be rendered need to be added to the stage to be displayed. The stage is the root container of the visual object and the lowest layer of the tree rendering structure. Details are as follows:
const app = new PIXI.Application({
width: windowWidth,
height: windowHeight,
antialias: true,
resolution: devicePixelRatio,
transparent: true,
})
app.renderer.autoDensity = true
app.renderer.resize(windowWidth, windowHeight)
document.getElementById('container').appendChild(app.view)
app.stage.sortableChildren = true
Copy the code
Concept explanation:
- Antialias: Smoothes font and graphic edges.
- Resolution: Sets the resolution, which determines the actual Canvas width/height.
- AutoDensity: attribute set to true, CSS size automatically adjusts the Canvas view to the screen size (namely Canvas. Style. The width/height).
- SortableChildren: With the property set to true, the children of the stage will be sorted by their zIndex value (the default zIndex is 0).
* Note that the larger the resolution is, the larger the Canvas will need to draw, which will slow down the overall loading and rendering speed. Even if this provides sufficient clarity, it is still up to developers to measure and optimize.
2.2 Creating a Background
The background is mainly divided into three layers: clouds, stars and earth.
2.2.1 clouds
For cloud backgrounds, writing with shaders works best by looking up network examples.
PIXIJS, on the other hand, also provides a special shader, Filter, which applies post-processing effects to the input texture and writes to the output render target. In short, we can completely customize the texture effect and easily display it.
First create render target and Filter:
const background = new PIXI.Sprite() background.width = this.app.screen.width background.height = this.app.screen.height this.app.stage.addChild(background) this.filter = new PIXI.Filter(null, fogFragment, { uResolution: { x: this.app.screen.width * devicePixelRatio, y: this.app.screen.height * devicePixelRatio, }, uTime: 0, }) background.filters = [this.filter]Copy the code
Once the Filter is created, it is important to pass in the update time and render it over time to achieve the cloud fluttering effect.
Update () {this. Filter. Uniforms. UTime + = 0.01}Copy the code
The shader code is as follows:
Void main() {const vec3 c1 = vec3(0.110, 0.110, 0.137); Const vec3 c2 = vec3(0.133, 0.149, 0.247); Vec2 p = gl_fragcoord.xy * 8.0 / uresolution.xx; Float q = FBM (p-utime * 0.1); float q = FBM (p-utime * 0.1); Vec2 r = VEC2 (FBM (p + q + uTime * 0.4-p.x-p.y), FBM (P + q-utime * 0.7)); vec3 c = mix(c1, c2, fbm(p + r)); float grad = gl_FragCoord.y / uResolution.y; Gl_FragColor = vec4(c * cos(1.4 * gl_fragcoord.y/uresolution. y), 1.0); Gl_fragcolor. xyz *= 0.8 + grad; }Copy the code
The basic principle is to use noise to simulate the cloud texture and mix the two colors to achieve the effect of cloud floating. More details are not covered here. (If you are interested, you can learn about Shader, which is a beautiful new land.)
At this point our page has the first layer. As you can see, it’s still very natural and silky.
2.2.2 the starry sky
The realization idea for the starry sky is slightly more complicated: multi-sprite reuse + radial distribution + camera motion.
The first step is to create multiple sprites, which are easy to create in a loop.
// loop create Sprite for (let I = 0; i < this.starAmount; i++) { const star = { sprite: new PIXI.Sprite(starTexture), z: 0, x: 0, y: Star.sprite.anchor. Set (0.5) this.randomizestar (star, This.app.stage.addchild (star.sprite) // Keep object this.stars.push(star)}Copy the code
Once each star is created, the same random radians are computed using sines and cosines to determine their x and y positions, ensuring that no objects overlap with the camera position (i.e., 0,0). This is called a radial distribution.
randomizeStar(star, initial) { star.z = initial ? Math.random() * 2000 : This.cameraz + math.random () * 1000 + 2000 0) overlapping object const deg = math.random () * math.pi * 2 // 0 ~ 2PI const distance = math.random () * 60 + 1 // Distance to center Star. x = math. cos(deg) * distance star.y = math. sin(deg) * distance}Copy the code
Next, let’s get the stars moving.
Camera motion, simply speaking, is to let the camera advance uniformly with time (z-axis coordinates increase) and project the coordinates of stars from three-dimensional coordinates to two-dimensional. At the same time, we need to calculate whether the current star has been out of the mirror (that is, the z-axis coordinates are less than the camera coordinates), and then re-initialize the position to play a role of reuse. The specific code is as follows:
update(delta) { this.cameraZ += delta * 10 * this.baseSpeed for (let i = 0; i < this.starAmount; I ++) {const star = this.stars[I] // If (star.z < this.cameraz) {this.randomizestar (star)} const z = star.z - this.cameraz star.sprite.x = star.x * (this.fov / z) * this.app.renderer.screen.width + this.app.renderer.screen.width / 2 Star. Sprite. Y = star. * y (enclosing fov/z). * this app. The renderer. Screen. The width + enclosing app. The renderer. Screen. The height / 2 / / computing scale const distanceScale = Math.max(0, (2000 - z) / 2000) * this.starBaseSize star.sprite.scale.x = distanceScale star.sprite.scale.y = distanceScale } }Copy the code
Let’s have a look at the actual effect, or a sense of substitution ~
* The number of stars in the video is 100, which can be adjusted according to actual use. However, be aware that too many render objects can increase rendering cost and drag down performance.
The earth background does not have a lot of dynamic effects, just use the Sprite map directly
Let’s look at the overall effect.
It feels like half the battle to get here
2.3 Creating a Role and its association
2.3.1 Role Profile picture
For characters, there are two points of entry: positioning and drawing.
Start with drawing first, considering the dynamic effect and interaction, and then it will be more convenient to process. The head is merged and drawn into an object, which is composed of four parts: background, head picture, shadow mask and name.
Considering the existence of profile picture cutting, text long omission and other problems. Canvas drawing is more flexible and convenient. Draw different avatars and names through Canvas. Then convert Canvas to Texture and load it into Sprite map, which is more flexible and convenient. Finally, add to the background gold circle to form a whole.
const canvas = document.createElement('canvas')
const texturePIXI.Texture.from(canvas)
const roleSprite = new PIXI.Sprite(texture)
Copy the code
Secondly, there is the positioning problem. According to the designer’s description, all the roles are divided into two parts: leading role and related role, among which the related role is divided into inner circle and middle circle role 6 each, and outer circle role up to 24.
-
Protagonist: Positioned in the center of the stage.
-
Inner circle and middle circle: it is not difficult to see that they are evenly distributed on the circle, that is, the radian between each character is 1/3 PI;
-
Outer ring: the outer ring is special. Although there are 24 at most, there are only 12 in one screen, which are distributed on the upper and lower half arcs. All we need to do is to calculate the position of the first point, and the rest can be deduced through it.
Here is a simple diagram:
Through the arctan function (arctangent function) to calculate a radian value, this radian value can determine the initial position of the first role, and it also represents the total radian occupied by the first three roles, and then can calculate the fixed radian interval, in turn to calculate the radian of each role.
Const h = this.screenHeight * 0.3 const w = this.screenWidth const atan = math.atan (w / 2 / h)Copy the code
2.3.2 Role Relationships
Once the roles are defined, the relationships are easy to deal with. It can be split into: wire and relationship name.
The center line for each character is the relationship line, which is easy to do by calculating the distance between two centers in the drawing line.
const widthTarget =
Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2)) -(this.ROLE_SIZE / 2)
Copy the code
Note: The line here needs to go into the center of the associated character, so only the radius of the protagonist is cut out. As for the hierarchy, the zIndex attribute for each object mentioned above can be easily solved.
For the relationship name, use Canvas to draw the text as well, and then convert to Texture to load. The advantage of this is that we can use the CTx. measureText method to measure the actual width of the text while drawing, and then use the line length to calculate the text’s center position relative to the line.
relation.position.set(
line.x + ((widthTarget - (v.effectiveWidth / 2) * operation) / 2) * Math.cos(rotation),
line.y + ((widthTarget - (v.effectiveWidth / 2) * operation) / 2) *Math.sin(rotation),
)
Copy the code
If you’re careful, you’ll notice that there’s an extra operation. What is it?
Let’s see what it looks like without it.
The text on the left is inverted. You can’t see if the experience is too bad until you break your neck
Operation handles this case by mirroring the text drawn in the second and third quadrants of radians. Set the text scale to -1, which means to scale the x and y axes of the rendered object by -1 times, that is, to achieve mirror inversion.
Also, set operation to -1. When the text is inverted by mirror image, the original position calculation also changes accordingly.
In this way, the text is placed properly but still centered.
If (rotation < -math.pi / 2 && rotation > (-math.pi / 2) * 3) {// If (rotation < -math.pi / 2 && Rotation > (-math.pi / 2) * 3) {// If (rotation < -math.pi / 2 && Rotation > (-math.pi / 2) * 3)Copy the code
3. Animation effects
Once we’ve created the stage, the background, and the characters and their relationships, the next step is animation.
The animation of the character mainly includes zooming, displacement and rotation. TweenJS makes it very easy to animate each object.
Tween.js, similar to jQuery’s animate method and CSS3 animation. Modify element attribute values in a smooth manner. Just tell TweenJS what you want to change, what its final value will be at the end of the animation, how long the animation will take, and so on. The Tween engine calculates the value between the beginning of the animation and the end of the animation and produces a smooth animation. There are also many moderating functions to help create better animations.
3.1 Animation to achieve
Concrete implementation as introduced above as simple, the following is a leading role of the animation, a look to understand:
const enterTween = new TWEEN.Tween(bg)
.group(this.innerGroup)
.to({ scale: { x: scaleTarget, y: scaleTarget }, rotation: 0 }, 300)
Copy the code
For example, the outer circle role rotation animation, is just a simple rotation, to slow function assisted implementation, very simple and easy to understand.
const tween = new TWEEN.Tween(this.outer[i])
.to({ rotation: ro }, du)
.easing(TWEEN.Easing.Sinusoidal.InOut)
.delay(5000 - du)
Copy the code
The other animation effects are similar and are not described here.
Finally, take a look at the overall effect
Parents are welcome to click on the qr code or click on the link to experience online finished products:
H5.if.qidian.com/h5/relation…
4. Climbed some pits
4.1 Adaptation Problems
At the beginning, the adaptation of all global objects is based on equal ratio enlargement. However, on large-screen devices such as iPad Pro, the overall beautification degree is greatly reduced due to adaptation problems such as the excessively large display of the character’s head image and the excessively large offset position of the role in the inner circle.
To solve this problem, a Benchmark value is calculated based on the current device screen width and the width provided by the design draft before stage loading.
getBenchmark(w, h) {
return (
(innerWidth * h > innerHeight * w
? (innerHeight * w) / h
: innerWidth) / 360
)
}
Copy the code
This coefficient can be added to all subsequent calculations to limit scaling, position calculations, and so on to maximize the overall effect.
Role. width = base * this.app.benchmark role.height = base * this.app.benchmark... Const scaleTarget = s[I] * this.app.benchmark... const offsetX = Math.cos(rotation) * (this.app.benchmark * this.REFER_SHOW_SIZE) * coefficient const offsetY = Count (rotation) * (this.app.benchmark * this.refer_show_size) * coefficient... const widthTarget = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2)) - (this.ROLE_SIZE / 2) * this.app.benchmarkCopy the code
4.2 Compatibility of shader Noise algorithm
In the testing phase, some iOS models were found to have cloud background rendering errors. Apparently, it’s the iOS Safari version.
The test machine tested the Shader code line by line with this assumption. It turned out that it wasn’t one of the Shader built-in algorithms that was not supported, but that in the noisy computation, the set coefficient of 43758.5453 was not supported in the lower version of Safari. Changing to 537.5453 fixed the problem. In essence, this parameter only affects the pseudo-random number density generated by the sine function and does not affect the overall effect display.
Float rand(vec2 n) {return fract(cos(dot(n, vec2(12.9898, 4.1414)) * 437.5453); float rand(vec2 n) {return fract(cos(dot(n, vec2(12.9898, 4.1414)) * 437.5453); }Copy the code
4.3 The outer ring character does not scroll naturally
The original implementation of the outer rotation was to modify the x and y coordinates. But if you make the animation longer you will find that the animation is very unnatural. The root cause is that the x and y offsets are different at the same time. In order to solve this problem, re-formulate the outer circle realization idea, by calculating radian to position, rotation radian to achieve animation effect. (See above for details)
4.4 Performance Correlation
In the testing phase, there are two main performance problems:
- Low – end machine slightly graphics slow
- Some models run for a long time
Below, two real computers with significantly different configurations are found for GPU presentation mode analysis.
Yellow — indicates how long it takes to process a task, or how long it takes the CPU to wait for the GPU to complete a task. The higher the line, the more things the GPU does
Red — indicates the time the task was executed, and the higher the line, the more views need to be drawn
Blue – indicates how long it takes to draw the view list. Higher lines indicate that the current view is complex or invalid and needs to be redrawn, affecting the frame rate
Green — measures, layouts, animations, input events, main thread tasks, and more
Green horizontal line – indicates the 16ms line, ideally with each column height less than this
From the overall trend, it can be seen that the rendering of both phones is stable (the graphics have no obvious mutation fluctuation). But the overall performance of the low-end machine is slightly worse.
The tasks in red and yellow are processed by CPU and GPU. Even if the line exceeds 16ms, it does not completely mean that the page is stuck. As can be seen from the above figure, in addition to the configuration differences of mobile phones, there are still slightly more problems in the views drawn. In the later stage, we can choose to merge rendering objects and reduce the number of rendering objects to optimize. The blue and green parts are what we need to focus on. The poor performance of low-end computers can still be considered as the reason for the multiple elements layout levels and low configuration efficiency.
In addition, because the page always exists animation effect, always non-stop in the rendering, long time high frequency rendering is part of the model long time running heat root cause. Optimization can be started by stopping rendering at idle time, no longer extended here.
5. conclusion
After such a complex, detailed page. I learned a lot of knowledge that I had never touched before. At the same time, I also have some insights:
- No matter how complex things are, they can be simplified and solved one by one.
- Details, details, details.
Related references:
pixijs.com/
Github.com/tweenjs/twe…
thebookofshaders.com/13/?lan=ch
En.wikipedia.org/wiki/Fracti…
patriciogonzalezvivo.github.io/glslEditor/
Copy the code