The article is the first personal blog

In the new version of Now live lottery, users can click on any place after entering the room (gold, wood, soil, sun, moon and thunder) to enter the lottery. As shown below:

The animation is divided into two parts: the lightning and the prize part.

The lightning part of the animation is about 25 frames, while the prize part is about 500 frames due to the complexity of the animation. What kind of way to play animation for such a large number of frames is a problem we need to discuss.

#Common way to play animation

Those of you who have done animation should know that there are common ways to play animation

  • CSS animation or transition;

    • Advantages: Relatively simple to use
    • Disadvantages: for 500+ frames, animation playback depends on whether the picture is downloaded successfully, 500+ picture download, is bound to cause animation lag
  • Use Sprite +background-position+@keframesPlay animation;

    • Advantages: Combining multiple images into one image reduces the number of HTTP requests
    • Disadvantages:
      1. The size of each frame must be consistent and the position must be controlled precisely
      2. Animation control is more complex
      3. Maintenance is cumbersome, and adding/deleting frames involves code changes
  • usegifPlay up to

    • Advantages: simple to use, only need to design students to a picture
    • Disadvantages:
      1. GIF only supports 256 color palettes, so detailed images and realistic photographic images lose color information and look toned
      2. GIF supports limited transparency with no translucency or fading effects
      3. GIF animations don’t play as smoothly when compressed
      4. There are jagged edges around the edges
  • usevideoThe TAB plays the animation as a video

    • Advantages: easy to use, the use of its own video tag and API can control the playback of animation, pause, etc
    • Disadvantages:
      1. Mobile compatibility is not good, especially under Android, may be blocked by various systems, and then to implement the player plug-in will have compatibility problems;
      2. Compatibility problems such as automatic video playing and shadowing control bar need to be solved
  • Play animations using Lottie – Web

    • Advantages:
    1. With Lottie, JSON files are much smaller than GIF files and have better performance;
    2. The front end can easily call the animation, and control the animation, reduce the front-end animation workload
    • disadvantages
    1. The Lottie-Web file itself is still large, lottie.js is 513K, the lightweight version is 144K compressed, and gzip is 39K.
    2. There are a very small number of AE animations that Lottie cannot implement, some due to performance issues and some that are not done. For example: Stroke animation and so on;
    3. The designer needs to export THE JSON content in AFTER Effects. The JSON content is large in the case of complicated animation
  • useapngPlay up to

    • Advantages:
    1. Can hold more colors than GIF; And backward compatible with PNG images;
    2. Alpha transparent channels are supported
    3. The image size is smaller than GIF
    • disadvantages
    1. Poor compatibility
  • javascriptDraw an animation with requestAnimationFrame+ Canvas

    • Advantages: you can control the playback of animation;
    • Cons: Need to find a suitable partner, play frame by frame

For a more intuitive sense of the difference between GIF and APNG, interested students can click here to view the comparison.

#Our plan

First, we need to reject the frame-by-frame loading method, which will result in 500+ images being loaded, which is obviously not reasonable.

Use GIF play with miscellaneous edges and less color support; So reject it for now;

The introduction of Lottie-Web will increase the project size, and if the animation needs to be changed in the future, the animation will fail to play if it is designed to an animation that Lottie does not support; The exported JSON is also large.

So the alternative is to use APNG or Sprite images to solve the problem. In order to facilitate the subsequent maintenance and the simplicity of the code logic, APNG is selected in the project to solve the problem of animation frames.

However, it should be noted that for compatibility and controllability, the project does not play apNG images directly.

So our final solution is: APng +requestAnimationFrame+ Canvas

The advantages of using this scheme are:

  1. [Fixed] ApNG support is less strict on the position/width/height of each frame than Sprite
  2. Apng is smaller than GIF and supports transparent channels. And support more colors;
  3. userequestAnimationFrame+canvasCan you control when to play the animation
  4. requestAnimationFrame+canvasNo compatibility issues

#Idea of animation Playback

The idea of the whole animation is as follows:

  1. To obtainapngThe picture
  2. fromapngParse each frame in
  3. usecanvas+requestAnimationFrameThe animation

#1. Obtain apNG pictures

export const fetchApngData = (id: number) = > new Promise<ArrayBuffer> ((resolve, reject) = > {
  const xhr = new XMLHttpRequest();
  xhr.onload = () = > {
    resolve(xhr.response as ArrayBuffer);
  };
  xhr.onerror = (err) = > {
    reject(err);
  };
  xhr.responseType = 'arraybuffer';
  xhr.open('get', apngs[id]);
  xhr.send();
}));

Copy the code

#2. Apng frame parsing

Before parsing frames, let’s take a quick look at the structure of APNG and PNG:

PNG mainly includes PNG Signature ‘ ‘, IHDR, IDAT, IEND and some auxiliary blocks:

  • PNG SignatureIs the file identifier used to check whether the file format isPNG

Const PNGSignature = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);

  • IHDRIs a file header data block, containing basic image information, such as image width and height information;
  • IDATIs an image data block, which stores specific image data. A PNG file may have one or more IDAT blocks.
  • IENDIs the end data block, marking the end of the image; The secondary block comes after the IHDR and before the IEND

APNG added acTL, fcTL and fdAT blocks on the basis of PNG:

  • acTL: animation control block, which contains the number of frames of the image and the number of cycles (0 means infinite cycles)
  • fcTL: Frame control block, belonging to the auxiliary block in PNG specification, contains the serial number of the current frame, the width and height of the image, the horizontal and vertical offset, the frame playing time and drawing method (dispose_OP and BLend_OP), etc., and only one fcTL block for each frame
  • fdAT: Frame data block, contains the frame sequence number and image data, only more than IDAT frame sequence number, each frame can have one or more fcTL blocks. The serial number of fdAT is shared with fcTL to detect APNG sequence errors and can be selectively corrected.

So the general steps of parsing are as follows:

Apng-js (Opens New Window)

export default function parseAPNG(buffer) {
    const bytes = new Uint8Array(buffer);
    // 1. Verify PNGSignature. If it is not PNG, return it directly
    if (Array.prototype.some.call(PNGSignature, (b, i) = >b ! == bytes[i])) {return errNotPNG;
    }

    // fast animation test
    let isAnimated = false;
    // 1. Use acTL to check whether the format is APNG
    eachChunk(bytes, type= >! (isAnimated = (type ==='acTL')));
    if(! isAnimated) {return errNotAPNG;
    }

    const
        preDataParts = [],
        postDataParts = [];
    let
        headerDataBytes = null,
        frame = null,
        frameNumber = 0,
        apng = new APNG();
    // 3. Process different types of data separately
    eachChunk(bytes, (type, bytes, off, length) = > {
        const dv = new DataView(bytes.buffer);
        switch (type) {
            case 'IHDR':
                headerDataBytes = bytes.subarray(off + 8, off + 8 + length);
                apng.width = dv.getUint32(off + 8);
                apng.height = dv.getUint32(off + 12);
                break;
            case 'acTL': // Use acTL to get the number of APNG frames
                apng.numPlays = dv.getUint32(off + 8 + 4);
                break;
            case 'fcTL': // Use fcTL to fetch all frames
                if (frame) {
                    apng.frames.push(frame);
                    frameNumber++;
                }
                frame = new Frame();
                frame.width = dv.getUint32(off + 8 + 4);
                frame.height = dv.getUint32(off + 8 + 8);
                frame.left = dv.getUint32(off + 8 + 12);
                frame.top = dv.getUint32(off + 8 + 16);
                var delayN = dv.getUint16(off + 8 + 20);
                var delayD = dv.getUint16(off + 8 + 22);
                if (delayD === 0) {
                    delayD = 100;
                }
                frame.delay = 1000 * delayN / delayD;
                if (frame.delay <= 10) {
                    frame.delay = 100;
                }
                apng.playTime += frame.delay;
                frame.disposeOp = dv.getUint8(off + 8 + 24);
                frame.blendOp = dv.getUint8(off + 8 + 25);
                frame.dataParts = [];
                if (frameNumber === 0 && frame.disposeOp === 2) {
                    frame.disposeOp = 1;
                }
                break;
            case 'fdAT':
                if (frame) {
                    frame.dataParts.push(bytes.subarray(off + 8 + 4, off + 8 + length));
                }
                break;
            case 'IDAT':
                if (frame) {
                    frame.dataParts.push(bytes.subarray(off + 8, off + 8 + length));
                }
                break;
            case 'IEND':
                postDataParts.push(subBuffer(bytes, off, 12 + length));
                break;
            default:
                preDataParts.push(subBuffer(bytes, off, 12+ length)); }});if (frame) {
        apng.frames.push(frame);
    }

    if (apng.frames.length == 0) {
        return errNotAPNG;
    }

    const preBlob = new Blob(preDataParts),
        postBlob = new Blob(postDataParts);
    // Assemble each frame in bloB format
    apng.frames.forEach(frame= > {
        var bb = [];
        bb.push(PNGSignature);
        headerDataBytes.set(makeDWordArray(frame.width), 0);
        headerDataBytes.set(makeDWordArray(frame.height), 4);
        bb.push(makeChunkBytes('IHDR', headerDataBytes));
        bb.push(preBlob);
        frame.dataParts.forEach(p= > bb.push(makeChunkBytes('IDAT', p)));
        bb.push(postBlob);
        frame.imageData = new Blob(bb, {'type': 'image/png'});
        delete frame.dataParts;
        bb = null;
    });

    return apng;
}

Copy the code

#3, use,canvas+requestAnimationFrameThe animation

  const [frames, setFrames] = useState<Frame[]>([]);

  const tick = useCallback((ptr: number) = > {
    // Check the existence of canvas instance and animation frame in memory
    if(! canvasRef.current)return;
    const { current } = canvasRef;
    const ctx = current.getContext('2d');
    if(! ctx)return;
    const frame = frames[ptr];
    if(! frame.imageElement)return;
    constscaling = (current.parentElement? .clientWidth ??0) / 230; // 230 is the standard frame size
    ctx.clearRect(0.0, current.clientWidth, current.clientHeight);
    ctx.drawImage(
      frame.imageElement,
      frame.left * scaling,
      frame.top * scaling,
      frame.width * scaling,
      frame.height * scaling
    );
  }, [frames]);

  /** * is executed when the first load and the parameter ID change to load the animation frame into memory *@function* /
  useEffect(() = > {
    let timer = -1;
    // Get the apng frames at the specified location
    getOrbApngFrames(id).then(images= >{
        setFrames(images)
    }).catch(() = >{
        timer = window.setTimeout(() = > {
          events.launchSubject.complete();
          events.breakSubject.complete();
        }, 1000);
    })
    return () = > {
      clearTimeout(timer);
    };
  }, [id, events]);

    useEffect(() = > {
    / /... Some other processing
    let handle;
    handle = window.setTimeout(() = > {
      handle = requestAnimationFrame(function animationTick(t) {
        try {
          if ((t - lastFrameAt) >= (1000 / 30)) { // 30 fps
            tick(ptr);
            // Let the static orb image fade out after the first frame of the animation is drawn. In case the orb disappears but the animation hasn't loaded yet
            / /... All other treatments
          }
          if (ptr < frames.length) {
            handle = requestAnimationFrame(animationTick);
          } else {
            ptr = 0; events.breakSubject.complete(); }}catch (err) {
            // ...}}); },300); // Wait for the laser
    return () = > {
      clearTimeout(handle);
      cancelAnimationFrame(handle);
    };
  }, [frames.length, tick, events]);

Copy the code

#reference

  • The structure of APNG in this paper refers to the implementation principle of APNG playback from the Web

The article is the first personal blog