preface

You can see a cool animation effect in the transition animation section of Vue’s official website

At first glance, this logic should be very complicated to write out by hand, but let’s take a look at the end of this article. It’s very similar to this case.

preview

You can also go directly to the preview site:

Sl1673495. Gitee. IO/flip – animat…

What is it like to have a beautiful girlfriend? To delete, invasion.

Analyze requirements

Given this requirement, what’s your first instinct? Suppose the first image in the first row moves to the second row and the third column. Do you want to calculate the height of the first row, then the width of the first two elements in the second row, and then move from the original coordinate point through CSS or some animation API? This is fine, but it can be very complicated when the image varies in height and width and you have to move many images at once. In this case, we need to manually manage the coordinates of the picture, which is very unfavorable for maintenance and expansion.

Another way to think about it, can we just naturally add DOM elements to the DOM tree via the native API, then ask the browser to help us with the end value, and then we can animate it?

We found a noun in the document: FLIP, which gave us a clue as to whether it was possible to write this animation.

The answer is yes. Follow this lead to an article in the Aerotwist community called flip-your-animations, and use this article as a starting point to achieve a similar effect in animations.

FLIP

What exactly is a FLIP? Let’s take a look at its definition:

First

The initial state of the element to be animated (such as position, transparency, etc.).

Last

The final state of the element to be animated.

Invert

This step is critical, assuming that our initial position of the image is left: 0, up: 0, and the final position of the element after animating is left: 100, up 100, it is clear that the element has moved 100px to the lower right corner.

However, instead of calculating its final position and then ordering the element to move from 0, 0 to 100, 100, we let the element move by itself (for example, in Vue, data-driven, append a few images to the front of the array, and the previous image moves to the bottom).

There’s one key point to note here, and one I mentioned in my previous post about EventLoop and Browser Rendering, Frame animation, and Idle Callbacks you don’t Know:

Changes to DOM element attributes (such as left, right, transform, etc.) are collectively deferred until the browser renders the next frame, so we get an intermediate point in time when the DOM state (position information) changes before the browser renders.

With this precondition, we can make sure that Vue handles the DOM changes first, before the browser renders, and we can get the position of the DOM state changes.

Say specific point, assuming that our picture is a line of two arrangement, image array initialization state is [img1, img2, at this time we are two elements to an array of additional head [img3 img4, img1, img2], then img1 and img2 will naturally be pushed to the next line.

Assuming img1’s initial position is 0, 0, squeezed out by data-driven DOM changes to 100, 100, then the browser hasn’t rendered yet, We can point at this time the img1. The style.css. Transform = translate (- 100 px, 100 px), make it to Invert the position of the displacement of the horse back to the former.

Play

After the inversion, it is easy to animate it, and then return it to the position of 0, 0. This paper will use the latest Web Animation API to realize the final Play.

MDN document: Web Animation

implementation

First of all, rendering the image is very simple. Just arrange the image in 4 columns:

.wrap {
  display: flex;
  flex-wrap: wrap;
}

.img {
  width: 25%;
}

<div v-else class="wrap">
  <div class="img-wrap" v-for="src in imgs" :key="src">
    <img ref="imgs" class="img" :src="src" />
  </div>
</div>
Copy the code

So the key is how to append elements to the IMGS array and do a smooth path animation.

Let’s implement the method add:

async add() {
  const newData = this.getSister()
  await preload(newData)
}
Copy the code

Firstly, take out several random images as elements to be put into the array, and use new Image to preload these images to avoid rendering a bunch of blank images to the screen.

We then define a function getRects that calculates the position of a set of DOM elements. GetBoundingClientRect is used to get the latest location information. This method will be used to retrieve both the old and new positions of picture elements.

function getRects(doms) {
  return doms.map((dom) = > {
    const rect = dom.getBoundingClientRect()
    const { left, top } = rect
    return { left, top }
  })
}

// An existing image
const prevImgs = this.$refs.imgs.slice()
const prevPositions = getRects(prevImgs)
Copy the code

After recording the old position of the image, we can append the new image to the array:

this.imgs = newData.concat(this.imgs)
Copy the code

Then comes the key point. We know that Vue is rendered asynchronously, meaning that changing the IMGS array does not immediately change the DOM. In this case, we will use the nextTick API, which puts the callback you pass into the microTask queue. As mentioned in the event loop article above, the execution of the microTask queue must occur before the browser rerenders.

Imgs = newdata.concat (this.imgs) is called first, which triggers the responsive dependent update of Vue. At this time, Vue will put the rendering function of the DOM update in the microTask queue first. The queue at this point is [changeDOM].

After nextTick(callback) is called, the callback function is appended to the queue, which is [changeDOM, callback].

Now you’re smart enough to understand why nextTick’s callback must get the latest DOM element, since the new image is already in the DOM tree, but the screen hasn’t been drawn yet. A call to getBoundingClientRect triggers a forced synchronous layout, and the following code gets the exact location where the DOM is expected to appear on the screen. (Note that the location information is up to date, but no drawing has taken place on the screen.)

Since we saved the prevImgs array of the image element nodes, we call getRect again in nextTick to get the latest location of the old image.

async add() {
  // Latest DOM status
  this.$nextTick((a)= > {
    // Call the same method again to get the latest element position
    const currentPositions = getRects(prevImgs)
  })
},
Copy the code

Now that we have the key information for the Invert step, new and old, it’s easy to loop through the array and animate the Invert.

prevImgs.forEach((imgRef, imgIndex) = > {
  const currentPosition = currentPositions[imgIndex]
  const prevPosition = prevPositions[imgIndex]

  // After the inverted position, although the picture moved to the latest position, but you go back to me first, wait for me to let you do the animation.
  const invert = {
    left: prevPosition.left - currentPosition.left,
    top: prevPosition.top - currentPosition.top,
  }

  const keyframes = [
    // The initial position is the inverted position
    {
      transform: `translate(${invert.left}px, ${invert.top}px)`,},// Where the image should be after the update
    { transform: "translate(0)"},]const options = {
    duration: 300.easing: "Cubic - the bezier (,0,0.32 0, 1)",}// Start exercising!
  const animation = imgRef.animate(keyframes, options)
})
Copy the code

A very smooth path animation is now complete.

The complete implementation is as follows:

async add() {
  const newData = this.getSister()
  await preload(newData)

  const prevImgs = this.$refs.imgs.slice()
  const prevPositions = getRects(prevImgs)

  this.imgs = newData.concat(this.imgs)

  this.$nextTick((a)= > {
    const currentPositions = getRects(prevImgs)

    prevImgs.forEach((imgRef, imgIndex) = > {
      const currentPosition = currentPositions[imgIndex]
      const prevPosition = prevPositions[imgIndex]

      const invert = {
        left: prevPosition.left - currentPosition.left,
        top: prevPosition.top - currentPosition.top,
      }

      const keyframes = [
        {
          transform: `translate(${invert.left}px, ${invert.top}px)`}, {transform: "translate(0)"},]const options = {
        duration: 300.easing: "Cubic - the bezier (,0,0.32 0, 1)",}const animation = imgRef.animate(keyframes, options)
    })
  })
},
Copy the code

out-of-order

Now we want to implement the shuffle effect of the demo on the official website. Now that we have the logic of the appended image, do you feel that the idea is coming to a head? That’s right, even if the image is badly scrambled, it’s easy to animate the path as long as we have “where the image started” and “where the image ended”.

Now what we need to do is pull the logic out of the animation, and let’s analyze the whole link:

Save the old location -> change the data-driven view update -> Get the new location -> Animate the FLIP

All we have to do is pass in an update method that tells us how to update the image array, and we can completely abstract this logic into a function.

scheduleAnimation(update) {
  // Get the location of the old image
  const prevImgs = this.$refs.imgs.slice()
  const prevSrcRectMap = createSrcRectMap(prevImgs)
  // Update data
  update()
  // DOM is updated
  this.$nextTick((a)= > {
    const currentSrcRectMap = createSrcRectMap(prevImgs)
    Object.keys(prevSrcRectMap).forEach((src) = > {
      const currentRect = currentSrcRectMap[src]
      const prevRect = prevSrcRectMap[src]

      const invert = {
        left: prevRect.left - currentRect.left,
        top: prevRect.top - currentRect.top,
      }

      const keyframes = [
        {
          transform: `translate(${invert.left}px, ${invert.top}px)`}, {transform: ""},]const options = {
        duration: 300.easing: "Cubic - the bezier (,0,0.32 0, 1)",}const animation = currentRect.img.animate(keyframes, options)
    })
  })
}
Copy the code

The function to append images and disorder them becomes very simple:

// Append images
async add() {
  const newData = this.getSister()
  await preload(newData)
  this.scheduleAnimation((a)= > {
    this.imgs = newData.concat(this.imgs)
  })
},
// Out of order
shuffle() {
  this.scheduleAnimation((a)= > {
    this.imgs = shuffle(this.imgs)
  })
}
Copy the code

The source address

Github.com/sl1673495/f…

conclusion

FLIP

FLIP can animate not only position changes, but also transparency, width and height.

For example, there is often an animation in e-commerce platforms. After clicking a picture of a commodity, the commodity slowly expands from its original position into a complete page.

After mastering the idea of FLIP, as long as you know the state before and after the animation of elements, you can easily “invert the state” and make them do a smooth animation to reach the destination, and the DOM state at this time is very clean. Rather than forcing it to move from 0, 0 to 100, 100 in a computationally intensive way and leaving transform: Translate (100px, 100px) on the DOM style.

Web Animation

Using the Web Animation API allows us to use JavaScript to more intuitively describe the Animation we need elements to do. Imagine using CSS to do this requirement, we would like to accomplish this requirement:

const currentImgStyle = currentRect.img.style
currentImgStyle.transform = `translate(${invert.left}px, ${invert.top}px)`
currentImgStyle.transitionDuration = "0s"

this._reflow = document.body.offsetHeight

currentRect.img.classList.add("move")

currentImgStyle.transform = currentRect.img.style.transitionDuration = ""

currentRect.img.addEventListener("transitionend", () => {
  currentRect.img.classList.remove("move")})Copy the code

This is a FLIP animation that has chosen to control CSS styles in a more native way. This code makes me uncomfortable:

  1. Need to pass throughclassThe overall flow of CSS additions and deletions is not intuitive.
  2. Need to listen for animation completion events, and do some cleaning operations, easy to miss.
  3. Need to usedocument.body.offsetHeightIt’s triggered in this wayForced synchronous layoutI’m a hack.
  4. Need to usethis._reflow = document.body.offsetHeightThis way you can add a meaningless attribute to the element instance and prevent it from being rolled up by packaging tools like Rolluptree-shakingMistakenly deleted. Hack +1

The code using the Web Animation API becomes intuitive and easy to maintain:

const keyframes = [
  {
    transform: `translate(${invert.left}px, ${invert.top}px)`}, {transform: ""},]const options = {
  duration: 300.easing: "Cubic - the bezier (,0,0.32 0, 1)",}const animation = currentRect.img.animate(keyframes, options)
Copy the code

As for compatibility, W3C has provided Web Animation API Polyfill, which can be used freely.

Hopefully, in the near future, we will be able to throw away the old animation mode and embrace this newer and better API.

I hope this article will give you something to learn if you are worried about animation. Thank you!

❤️ thank you

1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.

2. Follow the public account “front-end from advanced to hospital” to add my friends, I pull you into the “front-end advanced communication group”, we communicate and progress together.