Experience in using pixi.js

Background: In order to achieve the animation effect of 2345 Weather King year-end operation activity – epidemic activity, (welcome to scan code preview)

Key features:

The main research before development is whether these key functions can be met

1) The page animation element can control the animation effect (displacement, speed and duration) according to the scrolling distance of the user in unit time of scrolling the page.

2) Page elements (sprites) support click events

3) Loading progress of page resource images (loading animation)

Stamping pit records in the development process:

1) After the picture is converted to canvas through pixi.js, the elements are blurred, and the drawn elements are jagged;

Solution: First draw canvas twice as wide and high as the current viewport, then reduce it by transform and move it to the correct position; The code is as follows:

   	let app  = new PIXI.Application({
		view:document.getElementById('canvas'),
		width:innerWidth * 2.// Double the size
		height:innerHeight * 2.// Double the size
		antialias: true.// Whether to smooth anti-aliasing, cancel font smoothing, anti-aliasing.
		forceCanvas: false.resolution: 1.clearBeforeRender: true
	}); 
Copy the code
canvas {
  transform: scale(0.5.0.5) translate(-50%, -50%);
}
Copy the code

2) How can dynamic elements, such as copywriting data, be rolled together with the user’s scrolling?

Solution: Get the scrolling distance of the current page and pan the DOM element;

3) Automatic playback of background music (almost no mobile phones support automatic playback of background music in the absence of user behavior at present)

Solution: (in the wechat environment) listen to WeixinJSBridgeReady event;

        // Play music automatically in wechat environment
        if(isWXBrowser()) {
            document.addEventListener('DOMContentLoaded'.function () {
                function audioAutoPlay() {
                    let audio = document.getElementById('bg-music')
                    audio.play()
                    document.addEventListener("WeixinJSBridgeReady".function () {
                        audio.play()
                    }, false)
                }
                audioAutoPlay()
            }); 
        } 
Copy the code

In other apps, there is still no way to support automatic music playing in the Webview environment, so you can consider the user experience. It is suggested to remove this part of the function; Or it can play automatically when the user touches the screen. The code is as follows:

        let audio = document.getElementById('bg-music')
        function musicInBrowserHandler() {
             audio.play()
             document.body.removeEventListener('touchstart', musicInBrowserHandler)
        }
        document.body.addEventListener('touchstart', musicInBrowserHandler)  
Copy the code

4) How to determine the motion parameters of animation elements with UI and restore the animation effect as far as possible;

The solution: All elements on the page that have movement, first of all: follow one principle; Has the initial position, and the end of the animation position (x, y);

The start time of animation is set to delay: distance of scrolling page/total height of page;

Animation duration is set to duration: element position at the end of animation – element position at the beginning/totalHeight; The above location information needs to be confirmed with the UI animator;

Technology selection: combined with the research program, determine PixiJS+TweenMax+TimelineMax+AlloyTouch can achieve the one-shot to the end of the set, each library play its due role, cooperate with each other;

Reference resources:

Reference: Mo Ji qingyu: Netease four character curse

Github link: Pixi Chinese translation

TweenMax: TweenMax

Introduction of the process

(1) Create pixi application, preload image resources (loader.add)

// Create an application
 let app = new PIXI.Application({
      width:1334.height:750
  });
// 
Copy the code
 // Create a resource loader to preload resources
  const loader = new PIXI.loaders.Loader();
 
  // Add the image resource
  loader.add('bg1'.'./imgs/bg1.png')
       .add('bg_desk'.'./imgs/bg_desk.png')
       .add('bg_person'.'./imgs/bg1_person.png')
 
  // Monitor the loading progress to display the loading progress
  loader.on("progress".function(target, resource) // Load progress
    document.getElementById('percent').innerText = parseInt(target.progress)+"%";
  });
 
  // The listener is loaded
  loader.once('complete'.function(target, resource) // Load complete
    document.getElementById('loading').style.display = 'none'// Hide the progress bar
    document.body.appendChild(app.view);   // Insert the pixi application into the real DOM
    initScenes(); // Initialize the scene
    initSprites();  // Initialize sprites
    initAnimation(); // Initialize the animation
    initTouch(true.'y');
  });
   
  // Start loading resources
  loader.load();
Copy the code

(2) Initialize the scene: use the new pixi.container () function to create each scene and add them to the PIXI stage. Each scene contains information such as width, height and location.

According to the design drawing, get the length, width and position of each scene. Create a new scene container and wait for the scene to be built before you can proceed to the next step and put the Sprite into the scene.

To create a scene, define the data for each scene, create a Container object, and add it to the stage. There are three main things to implement here:

ScenesOptions: Defines the data of each scenario

Container object, which is used when initializing sprites, needs to be stored for each scene object

(3) Loop function initScenes: Initialize the scene and add it to the stage

  const scenesOptions = [ // Scene data: define the width and height of each scene,x/y distance
    {
      name:"scene1".x:0.y:0.width:2933.height:750
    },
    {
      name:"scene2".x:2933.y:0.width:1617.height:750},... ] ;const scenes = {};  // Scene set - pixi object
   
  function initScenes(){ // Loop scene array initialization scene
    for (let i = scenesOptions.length-1; i >= 0 ; i--) {
      scenes[scenesOptions[i].name] = new PIXI.Container({
        width:scenesOptions[i].width,
        height:scenesOptions[i].height }); scenes[scenesOptions[i].name].x = scenesOptions[i].x; app.stage.addChild(scenes[scenesOptions[i].name]); }}Copy the code

(3) Initialization Sprite: define the location of each Sprite and other attribute data, and add them to the corresponding scene

The operations in this step are the same as in the previous step: data set, object set, looping function.

It’s just that here I’ve split the Sprite initialization into two functions, one for loop the Sprite array and one for loop the properties of each Sprite. Plus a function with a special property. Depending on your needs, you can do special operations on certain sprites, all in the initSpecialProp function.

  const spritesOptions = [ // Sprite data: define the coordinates of each Sprite
    { // The Sprite in the first scene
      bg1: {position: {x:0.y:0}},bg_person: {position: {x:0.y:19},
        anchor: {x:0.5.y:0.5}},... }, {// The Sprite in the second scene
      bg_desk: {position: {x:2213.y:38}},... }];const sprites = {}; // Sprite collection - pixi object
   
  function initSprites()// New all Sprite objects and give them to initSprite
    for (let i = 0; i < spritesOptions.length; i++) {
      let obj = spritesOptions[i];
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          sprites[key] = PIXI.Sprite.fromImage(key);
          initSprite(sprites[key],obj[key],i+1);
        }
      }
    }
    initSpecialProp();
  }
  function initSprite(sprite,prop,i)// Initializes the properties of a single Sprite and adds them to the scene
    for (let key in prop) {
      if (prop.hasOwnProperty(key)) {
        sprite[key] = prop[key];
      }
    }
    scenes['scene'+i].addChild(sprite);
  }
  function initSpecialProp()// If you have a special Sprite that needs to handle a special attribute, you can handle it in this function
    / / sprites. Mother_left. Pivot. Set (0 ploidy);
    / / sprites. Mother_right. Pivot. Set (50, 95)
  }
Copy the code

(4) In step 3, all sprites are drawn, but the screen can’t be dragged yet. Here, I need to use a sliding library, I used AlloyTouch. AlloyTouch can detect the sliding distance of the user, and the corresponding sliding change callback function can change the position of the stage app.stage.position.x to achieve the dragging effect of the stage. So users can swipe to see it.

new AlloyTouch({ … }) :

① Touch defines the DOM object to be touched. In this case, we will directly use the body.

② Vertical defines the direction of the touch (horizontal slide, or vertical slide). This relates to whether the device is landscape or portrait, so the value is passed in vertical. Landscape false, portrait true);

③ Maximum rolling distance Max and minimum rolling distance min. Because we’re all sliding left and up, it’s negative, so Max is 0. The minimum value is the entire width of the stage minus the width of the entire screen, and then negative;

The key point is the change function, which can return the real-time scrolling distance. We can calculate the current scrolling distance as a percentage of the total distance, and this percentage is our current progress. Get this percentage. (Default total progress is 1) You can change the progress of playback at any time by using the timeline.seek function. That’s why the slide back animation retracts. If you think about it, when we watch a video, the way we scroll is the way the user swipes the page, so that’s the key.

  let alloyTouch;
   
  function initTouch(vertical, val) {
    let scrollDis = app.stage.width-max;
    alloyTouch = new AlloyTouch({
      touch:"body".// Feedback touch dom
      vertical: vertical, // It is not required. The default is true to listen for vertical touch
      min: -app.stage.width + max, // The minimum value of the motion attribute is not required
      maxSpeed: 1.max: 0.// The maximum value of the scroll attribute is not required
      bindSelf: false.initialValue: 0.change:function(value){  
        app.stage.position[val] = value;
        let progress = -value/scrollDis;
        progress = progress < 0 ? 0 : progress;
        progress = progress > 1 ? 1: progress; timeline.seek(progress); }})}Copy the code

(5) By step 4, everything is static and our element is not moving yet. To create animations that play as the user swipes, you need a library called TimelineMax. This is the library that manages the animation playback progress bar. Directly new a primary timeline.

  const timeline = new TimelineMax({  // The timeline of the entire stage
    paused: true
  });
Copy the code

The code that steps 4 and 5 combine

  const w = document.body.clientWidth,
      h = document.body.clientHeight;
   
  constmin = (w<h)? w:h;constmax = (w>h)? w:h;const timeline = new TimelineMax({  // The timeline of the entire stage
    paused: true
  });
   
  let alloyTouch;
   
  function initTouch(vertical, val) {
    let scrollDis = app.stage.width - max;
    alloyTouch = new AlloyTouch({
      touch:"body".// Feedback touch dom
      vertical: vertical, // It is not required. The default is true to listen for vertical touch
      min: -app.stage.width + max, // The minimum value of the motion attribute is not required
      maxSpeed: 1.max: 0.// The maximum value of the scroll attribute is not required
      bindSelf: false.initialValue: 0.change:function(value){  
        app.stage.position[val] = value;
        let progress = -value/scrollDis;
        progress = progress < 0 ? 0 : progress;
        progress = progress > 1 ? 1: progress; timeline.seek(progress); }})}Copy the code

(6) Animate sprites, using a library TweenMax that can be used to create tween animations. All we need to do is define the starting state of the Sprite, the final state, and it’s easy to transition from state to state. After creating the animation with TweenMax, add the animation to the corresponding position on the timeline. Delay :0.1 indicates that the animation starts at ten hundredths of its length.

The three functions TweenMax uses are as follows:

1) TweenMax. To (target, duration, statusObj) – target target statusObj state transition from the current state

(2) TweenMax. The from (target, duration, statusObj) – target target from statusObj state transition to the current state

(3) TweenMax. FromTo (target, duration, statusObjFrom, statusObjTo) – target target statusObjTo state transition from statusObjFrom state

Duration Indicates the transition duration. If duration=0.1, it means that the transition time accounts for 10% of the total length of the scroll, that is, 10% of the time axis.

Delay and duration calculation rules:

  1. Delay = Scrolling distance when the animation starts/total scrolling length

Delay is the time when the animation starts

  1. Duration = (scrolling distance when animation ends – scrolling distance when animation starts)/total scrolling length

Duration is the duration of the animation

 const animationsOptions = {  // Sprite animation collection
    windows: [{prop:'scale'.// There is a prop here. Some people don't notice what this is
      delay:0.05.duration:0.3.to: {x:3.y:3.ease:Power0.easeNone}  Ease :Power0. EaseNone is the ease function}, {delay:0.1.duration:0.1.to: {alpha:0}}].talk_1: [{delay:0.15.duration:0.1.from: {width:0.height:0.ease:Power0.easeNone}
    }]
  }
  function initAnimation(){
    // delay=0.1 means scroll to 10% to start animation
    // duration=0.1 indicates the percentage of movement time in scrolling
    for (let key in animationsOptions) {
      if (animationsOptions.hasOwnProperty(key)) {
        let obj = animationsOptions[key];
        for (let i = 0; i < obj.length; i++) {
          let act;
          let target;
          if (obj[i].prop) {
            target = sprites[key][obj[i].prop];
          } else {
            target = sprites[key];
          }
          if (obj[i].from & obj[i].to) {
            act = TweenMax.fromTo(target,obj[i].duration,obj[i].from,obj[i].to);
          } else if (obj[i].from) {
            act = TweenMax.from(target,obj[i].duration,obj[i].from);
          } else if (obj[i].to) {
            act = TweenMax.to(target,obj[i].duration,obj[i].to);
          }
          let tm = new TimelineMax({delay:obj[i].delay});
          tm.add(act,0);
          tm.play();
          timeline.add(tm,0); }}}// Special animation special processing
    let act = TweenMax.to(scenes.scene1,0.3, {x:2400});
    let tm = new TimelineMax({delay:0.25});
    tm.add(act,0);
    timeline.add(tm,0);
  }
Copy the code

Tip !!!!! (Guess what the prop above is for)

Note that TweenMax. The from (target, duration, statusObj) method only identify the target attribute. For example, the Sprite width changes from 0 to 100

Tweenmax. to(Sprite, 0.1, {width: 100}) //

So the question is, what about the properties that you want to change? For example, if sprite.scale. X becomes 2, target should become sprite.scale

Tweenmax. to(sprite.scale, 0.1, {x: 2}) // sprite.scale

(7) In the third step, the user’s sliding callback function plus timeline. Seek (progress) can slide to a certain position to play the corresponding animation

Here’s a summary of what each library does

  1. PixiJS: Drawing, including stage, scene, normal Sprite, animation Sprite, tile Sprite, timer, etc.

  2. TweenMax: Make transition animations

  3. TimelineMax: Manages the animation playback progress of the entire stage

  4. AlloyTouch: realize sliding effect, listen to the user slide

PixiJS+TweenMax+TimelineMax+AlloyTouch can achieve a one-shot routine, each library play its due role, and cooperate with each other