Livestreaming has one very important interaction: likes.

In order to enhance the atmosphere of the live broadcast room, compared with ordinary video or text content, there are usually two special requirements for thumbs up:

  • Like unlimited action, guide the user crazy like
  • All the crazy likes in the studio need to be animated across all user interfaces

Let’s look at the renderings first:

There are a few other important things we can see from the renderings:

  • The likes vary in size and move randomly
  • Thumbs-up animation pictures zoom in first and then move at a constant speed.
  • Near the top, it fades away.
  • When receiving a large number of “like” requests, the “like” animation does not pile up, orderly and continued to appear.

So how do you implement these requirements? Here are two ways to implement it (with a complete demo at the bottom) :

CSS 3 implementation

Using CSS3 to implement animation, obviously, we are thinking of using animation.

First, let’s look at the animation combination method. The specific meaning will not be explained. If necessary, you can understand it by yourself.

animation: name duration timing-function delay iteration-count direction fill-mode play-state;
Copy the code

Let’s start doing it step by step.

Step 1: Fix the area and set the basic style

First of all, let’s prepare an animated picture of thumbs-up:

Take a look at the HTML structure. An outer structure holds the position of the entire display animation area. This is in a div area 100px wide by 200px high.

<div class="praise_bubble">
  <div class="bubble b1 bl1"></div>
</div>
Copy the code
.praise_bubble{
  width:100px;
  height:200px;
  position:relative;
  background-color:#f4f4f4;
}
.bubble{
    position: absolute;
    left:50%;
    bottom:0;
}
Copy the code

Step 2: Exercise

Using the frame animation of animation, define a sequence of bubble_Y frames.

.bl1{
    animation:bubble_y 4s linear 1 forwards ; 
 }
@keyframes bubble_y {
    0% {
        margin-bottom:0; 100%} {margin-bottom:200px; }}Copy the code

Set the running time here to 4s; Use linear motion, or other curves if desired, such as Ease; Each thumbs-up animation runs only once; Animations are only needed forward forward.

Step 3: Add fade in

Use opacity instead. Here we’re fixed at the end of the quarter and it starts to fade out. Modify bubble_y:

@keyframes bubble_y {
    0% {
        margin-bottom:0;
    }
    75%{
        opacity:1; 100%} {margin-bottom:200px;
        opacity:0; }}Copy the code

Step 4: Increase the animation magnification effect

For the first little while, the picture went from small to large.

So let’s add a new animation: Bubble_BIG_1.

Here we go from 0.3 to 1. Note here that the running time, as set above, is 4s from the beginning to the end of the animation, so the zoom time can be set as needed, such as 0.5s.

.bl1{
    animation:bubble_big 0.5 s linear 1 forwards; 
 }
@keyframes bubble_big_1 {
    0% {
        transform: scale(3); 100%} {transform: scale(1); }}Copy the code

Step 5: Set the offset

Let’s define the frame animation: Bubble_1 to perform the offset. When the image starts to zoom in, there is no offset set here, leaving the middle origin unchanged.

After running to 25% * 4 = 1s, that is, 1s, it is offset to the left by -8px, to the right by 8px for 2s, to the right by 15px for 3s, and finally to the right by 15px.

As you can imagine, this is the definition of a classic left-right swing trajectory, “left to right left to right” curve swing effect.

@keyframes bubble_1 {
    0% {
    }
    25% {
        margin-left: -8px; 50%} {margin-left:8px75%} {margin-left: -15px100%} {margin-left:15px}}Copy the code

The renderings are as follows:

Step 6: Complete the animation style

There is a preset curve trajectory, swing from side to side, and we are going to preset more curves to achieve the purpose of random trajectory.

For example, in bubble_1, we can modify the offset to achieve a different trajectory.

Step 7: Add node styles randomly in JS operation

Provides a method for adding likes, randomly combining the likes and rendering them to the node.

let praiseBubble = document.getElementById("praise_bubble");
let last = 0;
function addPraise() {
    const b =Math.floor(Math.random() * 6) + 1;
    const bl =Math.floor(Math.random() * 11) + 1; // bl1~bl11
    let d = document.createElement("div");
    d.className = `bubble b${b} bl${bl}`;
    d.dataset.t = String(Date.now());
    praiseBubble.appendChild(d);
}
setInterval((a)= > {
    addPraise();
},300)
Copy the code

When using CSS to implement “like”, it is usually necessary to set the bubble’s random delay, for example:

.bl2{
    animation:bubble_2 $bubble_time linear .4s 1 forwards,bubble_big_2 $bubble_scale linear .4s 1 forwards,bubble_y $bubble_time linear .4s 1 forwards;   
}
Copy the code

Here, if it is random to BL2, then it will run after 0.4s delay, and BL3 will run after 0.6s delay……

If updates are made in batches to nodes without delay, they will appear in a cluster. Random “BL” style, random delay, and then appear in batches, will automatically staggered peak display. Of course, we also need to add the animation that the current user manually likes, which requires no delay.

In addition, it is possible for others to send 40 likes at the same time. Business needs usually hope that the 40 like bubbles can appear in turn to create a continuous like atmosphere, otherwise the large number of likes will be displayed in a pile.

Then we need to split the number of likes in batches. For example, if the time of a like ($bubble_time) is 4s, how many likes do you want to have at the same time within 4S? Say 10, then 40 likes will be rendered in 4 batches.

 window.requestAnimationFrame((a)= > {
     // Continue to cycle the batch
     render();
 });
Copy the code

You also need to manually clear the node. To prevent performance problems caused by too many nodes. Below is the complete rendering.

Canvas drawing implementation

This is easy to understand. You can draw animation directly on canvas. If you don’t know canvas, you can learn it later.

Step 1: Initialize

Create a canvas tag on the page element to initialize the canvas.

You can set the width and height properties on the canvas, and you can set the width and height properties in the style property.

  • The width and height of the style on the canvas are the height and width of the canvas rendered in the browser, i.e. the actual width and height in the page.
  • The width and height of the Canvas tag are the actual width and height of the canvas.
<canvas id="thumsCanvas" width="200" height="400" style="width:100px; height:200px"></canvas>
Copy the code

A canvas canvas 200 wide by 400 high is displayed on the page, and the entire canvas is displayed in the 100 wide by 200 high area of the page. Canvas The contents of the canvas are shrunk in size to be displayed on the page.

Define a ThumbsUpAni class. The constructor reads the canvas and saves the width and height.

class ThumbsUpAni{
    constructor() {const canvas = document.getElementById('thumsCanvas');
        this.context = canvas.getContext('2d')! ;this.width = canvas.width;
        this.height = canvas.height; }}Copy the code

Step 2: Load images in advance

Preload the “like” image that needs to be rendered randomly to obtain the width and height of the image. If there is a download failure, do not display the random image. Nothing to say, easy to understand.

loadImages(){
    const images = [
        'jfs/t1/93992/8/9049/4680/5e0aea04Ec9dd2be8/608efd890fd61486.png'.'jfs/t1/108305/14/2849/4908/5e0aea04Efb54912c/bfa59f27e654e29c.png'.'jfs/t1/98805/29/8975/5106/5e0aea05Ed970e2b4/98803f8ad07147b9.png'.'jfs/t1/94291/26/9105/4344/5e0aea05Ed64b9187/5165fdf5621d5bbf.png'.'jfs/t1/102753/34/8504/5522/5e0aea05E0b9ef0b4/74a73178e31bd021.png'.'jfs/t1/102954/26/9241/5069/5e0aea05E7dde8bda/720fcec8bc5be9d4.png'
    ];
    const promiseAll = [] as Array<Promise<any>>;
    images.forEach((src) = > {
        const p = new Promise(function (resolve) {
            const img = new Image;
            img.onerror = img.onload = resolve.bind(null, img);
            img.src = 'https://img12.360buyimg.com/img/' + src;
        });
        promiseAll.push(p);
    });
    Promise.all(promiseAll).then((imgsList) = > {
        this.imgsList = imgsList.filter((d) = > {
            if (d && d.width > 0) return true;
            return false;
        });
        if (this.imgsList.length == 0) {
            logger.error('imgsList load all error');
            return; }})}Copy the code

Step 2: Create render objects

Render the image in real time to make it into a coherent animation, and most importantly: generate curvy trajectoriness. The curve should be smooth and uniform. If the resulting curve path is not smooth, the effect will be too abrupt, such as the previous one is 10px, the next one is -10px, obviously, the animation will flicker left, right, left and right.

The ideal trajectory is for the last position to be 10px, then 9px, and then smooth all the way to -10px, so that the points are consistent and the animation looks smooth.

Random smoothing X – axis offset

If you want to smooth a curve, you can actually use the familiar math.sin function to achieve a uniform curve.

Look at the sine curve below:

Here’s the curve from Math.sin(0) to Math.sin(9), and it’s a smooth curve from positive to negative and then negative to positive again, which is exactly what we want, so we need to generate a random ratio value to make the swing random.

const angle = getRandom(2.10);
let ratio = getRandom(10.30)*((getRandom(0.1)?1 : - 1));
const getTranslateX = (diffTime) = > {
    if (diffTime < this.scaleTime) {// No swing offset is performed during amplification
        return basicX;
    } else {
        return basicX + ratio*Math.sin(angle*(diffTime - this.scaleTime)); }};Copy the code

ScaleTime is how long it takes to zoom in from the beginning to the final size. Here we set 0.1, which is 10% of the total running time.

DiffTime is only how much time has elapsed from the start of the animation to the current time, as a percentage. The actual value increases gradually from 0 — 1. Difftime-scaletime = 0 ~ 0.9, diffTime = 0.4, indicating that 40% of the time has been run.

Because the math.sin (0) to Math.sin(0.9) curve is almost a straight line, it doesn’t do much wiggle work, and there is a slight change from Math.sin(0) to Math.sin(1.8), so we set the minimum Angle here to 2.

Set the Angle coefficient Angle to a maximum of 10 and run two peaks from bottom to top.

Of course, if the running distance is longer, we can increase the Angle value, such as three peaks (if the time is short and there are three peaks, it will run too fast, there will be flickering phenomenon). The diagram below:

The Y axis offset

This is easy to understand, the diffTime is 0 at the beginning, so run offset from this.height –> image.height / 2. So we go from the bottom, we go to the top and we leave, and we actually fade and hide at the top.

const getTranslateY = (diffTime) = > {
    return image.height / 2 + (this.height - image.height / 2) * (1-diffTime);
};
Copy the code

Zoom in on

When the run-time diffTime is less than the set scaleTime, scale increases proportionally with time. If the time threshold is exceeded, the final size is returned.

const basicScale = [0.6.0.9.1.2][getRandom(0.2)];
const getScale = (diffTime) = > {
    if (diffTime < this.scaleTime) {
        return +((diffTime/ this.scaleTime).toFixed(2)) * basicScale;
    } else {
        returnbasicScale; }};Copy the code

Fade out

The same as the zoom logic, except that fade-out takes effect at the end of the run.

const fadeOutStage = getRandom(14.18) / 100;
const getAlpha = (diffTime) = > {
    let left = 1 - +diffTime;
    if (left > fadeOutStage) {
        return 1;
    } else {
        return 1 - +((fadeOutStage - left) / fadeOutStage).toFixed(2); }};Copy the code

Real-time rendering

After creating the drawing object, you can draw in real time, according to the above obtained “offset value”, “zoom in” and “fade out” value, and then real-time draw the position of the like picture.

Every execution cycle, all the animation picture positions on the canvas need to be redrawn to form the effect that all the “like” pictures are moving.

createRender(){
    return (diffTime) = > {
        // The difference is full, that is, the end of 0 -- 1
        if(diffTime>=1) return true;
        context.save();
        const scale = getScale(diffTime);
        const translateX = getTranslateX(diffTime);
        const translateY = getTranslateY(diffTime);
        context.translate(translateX, translateY);
        context.scale(scale, scale);
        context.globalAlpha = getAlpha(diffTime);
        // const rotate = getRotate();
        // context.rotate(rotate * Math.PI / 180);
        context.drawImage(
            image,
            -image.width / 2,
            -image.height / 2,
            image.width,
            image.height
        );
        context.restore();
    };
}
Copy the code

The image drawn here is the width and height of the original image. We’ve set up a basiceScale, so if the image is bigger, we can make the scale smaller.

const basicScale = [0.6.0.9.1.2][getRandom(0.2)];
Copy the code

Real-time drawing scanner

Turn on the real-time drawing scanner and put the created render object into the renderList array. The array is not empty, indicating that there are still animations on the canvas, so you need to keep performing scan until there are no animations on the canvas.

scan() {
    this.context.clearRect(0.0.this.width, this.height);
    this.context.fillStyle = "#f4f4f4";
    this.context.fillRect(0.0.200.400);
    let index = 0;
    let length = this.renderList.length;
    if (length > 0) {
        requestAnimationFrame(this.scan.bind(this));
    }
    while (index < length) {
        const render = this.renderList[index];
        if(! render || ! render.render || render.render.call(null, (Date.now() - render.timestamp) / render.duration)) {
            // Delete the animation
            this.renderList.splice(index, 1);
            length--;
        } else {
            // The current animation is not complete, continueindex++; }}}Copy the code

Here is a comparison based on the execution time, determine the animation to the position:

diffTime = (Date.now() - render.timestamp) / render.duration  
Copy the code

If the start timestamp is 10000 and the current timestamp is 100100, it indicates that 100 ms has been run. If the animation should have been executed for 1000 ms, then diffTime = 0.1 indicates that 10% of the animation has been run.

Increase the animation

For each “like” or “like” received, the start method is called to generate a render instance and put it into an array of render instances. If the scanner is not currently enabled, it needs to be started. In this case, the scanning variable is used to prevent multiple scanners from being enabled.

start() {
    const render = this.createRender();
    const duration = getRandom(1500.3000);
    this.renderList.push({
        render,
        duration,
        timestamp: Date.now(),
    });
    if (!this.scanning) {
        this.scanning = true;
        requestFrame(this.scan.bind(this));
    }
    return this;
}
Copy the code

Keep out of crowds

When receiving a large number of “like” data, and many consecutive “like” (when the broadcast room is very popular). So the rendering of like data needs special attention, otherwise the page will be a lump of like animation. And the connection is not close.

thumbsUp(num: number) {
      if (num <= this.praiseLast) return;
      this.thumbsStart = this.praiseLast;
      this.praiseLast = num;
      if (this.thumbsStart + 500 < num)
        this.thumbsStart = num - 500;
      const diff = this.praiseLast - this.thumbsStart;
      let time = 100;
      let isFirst = true;
      if (this.thumbsInter ! =0) {
        return;
      }
      this.thumbsInter = setInterval((a)= > {
        if (this.thumbsStart >= this.praiseLast) {
          clearInterval(this.thumbsInter);
          this.thumbsInter = 0;
          return;
        }
        this.thumbsStart++;
        this.thumbsUpAni.start();
        if (isFirst) {
          isFirst = false;
          time = Math.round(5000 / diff);
        }
      }, time);
    },
Copy the code

Start the timer here and record the value of thumbsStart processed by the timer. If there is a new thumblike and the timer is still running, update the last praiseLast value and the timer will process all thumbsStart requests in turn.

The timer delay time is determined by how many “like” animations need to be rendered when the timer is turned on. For example, 100 “like” animations need to be rendered. We distribute 100 “like” animations within 5s to finish rendering.

  • For popular live broadcasts, many animations will be rendered at the same time, instead of being displayed in a group. Besides, the animations can be completely connected, constantly bubbling up and praising animations.
  • For the rare live broadcast, there is more than one “like” request, we can break the display within 5s, but will not show in a crowd.

End

Both ways of rendering thumbs-up animation have been completed, complete source code, source code stamp here.

Source code operation effect diagram:

Here you can also experience an online like animation. Click here

To compare

Both implementations satisfy the requirements, so which is better?

Let’s look at the data comparison. The following is the comparison of data without hardware acceleration enabled, using non-stop crazy rendering of the “like” animation:

Overall, the differences are as follows:

  • CSS3 is simple to implement
  • Canvas is more flexible and operates more delicately
  • CSS3 consumes more memory than Canvas, even more if hardware acceleration is enabled.

Welcome to follow my wechat official account: