Encapsulates the Vue floating pop-up picture viewer component

This article was first published in: https://github.com/bigo-frontend/blog/ Welcome to pay attention and reprint.

Encapsulates the Vue floating pop-up picture viewer component

preface

In the early years when developing the internal technology forum, in order to realize a process picture browsing experience, a picture viewer component was developed based on Vue, which briefly combed the implementation ideas, hoping to provide you with some help.

Let's take a look at the renderings:

In terms of interaction, the content is very simple. Click the picture on the page to pop up the picture floating layer from the current position of the picture, so as to achieve the purpose of picture browsing.

Principle analysis

  1. Obtain the clicked picture element according to the click event
  2. Make the current picture element invisible (through visibility or opacity)
  3. Create a shading layer
  4. Creates a picture element of the same size at the current location of the picture element
  5. Create an animation to enlarge the picture to the appropriate size (update position, scale)

When the idea is clear, it is not difficult to realize it.

Implementation scheme

Because the ultimate goal is to use it in Vue projects, the following scheme is directly encapsulated into Vue components.

Basic structure of picture viewer

The view structure of the picture viewer component is simple:

<template>
  <transition>
    <div v-if="visible" class="image-viewer">
      <img class="image" :src="src" />
    </div>
  </transition>
</template>


<script>
  export default {
    data() {
      return {
        el: null, // Picture element in mouse point
        visible: false, // Is the picture viewer visible
      };
    },
    computed: {
      src() {
        return this.el?.src;
      },
    },
    methods: {
      show(el) {
        el.style.opacity = 0; // Hide source picture

        this.el = el;
        this.visible = true;
      },
    },
  };
</script>


Briefly analyze:

  • Transition: the transition component is nested in the outer layer, which is very convenient for us to do the animation effect of picture displacement and scaling in the future
  • . image viewer: the root element is used to place picture elements and also acts as a shading layer
  • . image: the floating image displayed after clicking the image is this element, and all subsequent operations are on this image
  • show(el): after clicking a picture, this method will be called to transfer the picture element into the component and display the picture viewer

The style is also quite simple. Drawing a translucent shading is a very simple animation:

<style lang="less" scoped>
  .image-viewer {
    position: fixed;
    z-index: 99;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background: rgba(0, 0, 0, 0.6);
    cursor: move;
    transition: background 0.3s;

    /* Fade in and fade out animation */
    &.v-enter,
    &.v-leave-to {
      background: rgba(0, 0, 0, 0);
    }

    .image {
      position: absolute;
      user-select: none;
      transform-origin: center;
      will-change: transform, top, left;
    }
  }
</style>

Pop up the picture from its place and zoom in

Our picture viewer has been able to display pictures. Next, how to make the target picture element (. image) in the viewer pop up from the source picture element (el).

According to Vue's data-driven idea, its essence is to achieve the animation effect of popping the picture from its original place and putting it to the appropriate size by applying the start data and end data. Here, by maintaining a dimension data dimension, the style of the target picture element is calculated according to the dimension data.

export default {
  data() {
    return {
      // ...
      // Picture dimension information
      dimension: null,
    };
  },
  computed: {
    // ...
    // Target picture style
    style() {
      if (!this.dimension) return null;

      const {
        scale,
        size: { width, height },
        position: { top, left },
        translate: { x, y },
      } = this.dimension;

      return {
        width: `${width}px`,
        height: `${height}px`,
        top: `${top}px`,
        left: `${left}px`,
        transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`,
        transition: 'transform 0.3s',
      };
    },
  },
  methods: {
    show(el) {
      el.style.opacity = 0;

      this.el = el;
      this.visible = true;
      this.dimension = getDimension(el); // Get dimension data from source picture
    },
  },
};

The dimension here contains the following information of the picture element:

datadescribe
size: { width, height }Actual width and height of picture
position: { top, left }Picture absolute position
scaleThe ratio of the actual size of the picture element to the natural size of the picture, which is used for subsequent picture scaling animation
translate: { x, y }The picture displacement position, which is 0 by default, is used for subsequent picture scaling displacement animation

How to get the dimension of a picture element:

const getDimension = (el) => {
  const { naturalWidth, naturalHeight } = el;
  const rect = el.getBoundingClientRect();


  // Enlarged picture width and height
  const height = clamp(naturalHeight, 0, window.innerHeight * 0.9);
  const width = naturalWidth * (height / naturalHeight);

  return {
    size: { width, height },
    position: {
      left: rect.left + (rect.width - width) / 2,
      top: rect.top + (rect.height - height) / 2,
    },
    scale: rect.height / height,
    translate: { x: 0, y: 0 },
  };
};

Now, we have covered a picture of the same size at the end of the source picture, and then enlarge the picture to the appropriate size according to the screen size.

We only need to modify the logic of the show part to update the dimension value at the next moment:

export default {
  // ...
  methods: {
    show(el) {
      el.style.opacity = 0;


      this.el = el;
      this.dimension = getDimension(el);
      this.visible = true;

      doubleRaf(() => {
        const { innerWidth, innerHeight } = window;
        const { size, position } = this.dimension;

        this.dimension = {
          ...this.dimension,
          // Change the scale to 1, which is the scale after zooming in
          scale: 1,
          // Calculate the displacement to keep the picture centered
          translate: {
            x: (innerWidth - size.width) / 2 - position.left,
            y: (innerHeight - size.height) / 2 - position.top,
          },
        };
      });
    },
  },
};

Double RAF (i.e Double RequestAnimationFrame ), wait for the browser to render again:

const doubleRaf = (cb) => {
  requestAnimationFrame(() => {
    requestAnimationFrame(cb);
  });
};

In this way, the animation effect of image enlargement comes out.

Similarly, when we click the shading layer to trigger the closing of the picture browser, the picture should be reduced and returned to the original position:

<template>
  <transition @afterLeave="hidden">
    <div v-if="visible" class="image-viewer" @mouseup="hide">
      <img class="image" :style="style" :src="src" />
    </div>
  </transition>
</template>


<script>
  export default {
    // ...
    methods: {
      // hide
      hide() {
        // Retrieve the dimension of the source image
        this.dimension = getDimension(this.el);
        this.visible = false;
      },
      // After completely hiding
      hidden() {
        this.el.style.opacity = null;
        document.body.style.overflow = this.bodyOverflow;
        this.$emit('hidden');
      },
    },
  };
</script>

Now, the logic of the picture viewer component is basically completed.

Encapsulated as a function call

In order to make this component more convenient and easy to use, we encapsulate it into function call mode:

import Vue from 'vue';
import ImageViewer from './ImageViewer.vue';


const ImageViewerConstructor = Vue.extend(ImageViewer);

function showImage(el) {
  // Create a component instance and call the show method of the component
  let instance = new ImageViewerConstructor({
    el: document.createElement('div'),
    mounted() {
      this.show(el);
    },
  });

  // Insert the component root element into the body
  document.body.appendChild(instance.$el);

  // Destroy function: remove the root element and destroy the component
  function destroy() {
    if (instance && instance.$el) {
      document.body.removeChild(instance.$el);
      instance.$destroy();
      instance = null;
    }
  }

  // At the end of the component animation, the destroy function is executed
  instance.$once('hidden', destroy);

  // If the method is called on a parent element, the destroy function is also executed when the parent element is destroyed (such as switching routes)
  if (this && '$on' in this) {
![preview](https://user-images.githubusercontent.com/8649710/122009053-46478400-cdec-11eb-986c-134763e15a5d.gif)
![preview](https://user-images.githubusercontent.com/8649710/122009110-55c6cd00-cdec-11eb-8fa2-6f4e9f479a1a.gif)


    this.$on('hook:destroyed', destroy);
  }
}

showImage.install = (VueClass) => {
  VueClass.prototype.$showImage = showImage;
};

export default showImage;

Here, the packaging of components has been completed and can be used happily anywhere:

// ========== main.js ==========
import Vue from 'vue';
import VueImageViewer from '@bigo/vue-image-viewer';
Vue.use(VueImageViewer);


// ========== App.vue ==========
<template>
  <div class="app">
    <img src="http://wiki.bigo.sg:8090/download/attachments/441943984/preview.gif?version=1&modificationDate=1622463742000&api=v2"  />
  </div>
</template>

<script>
export default {
  methods: {
    onImageClick(e) {
      this.$showImage(e.target);
    },
  },
};
</script>

summary

Although the function is relatively simple, the main image browsing function has been realized. Compared with most image browsing plug-ins, it is much smoother in the user experience, which can make users have a smoother transition visually and provide a better immersive browsing experience.

There are still many ideas that have not been realized, such as dragging and dropping pictures during browsing, zooming with the mouse wheel, gesture operation optimization, mobile end experience optimization, multi picture browsing and so on.

Welcome to leave a message for discussion. I wish you a smooth work and a happy life!

I'm bigo. See you next time.

Tags: Javascript html5 Vue.js

Posted on Thu, 07 Oct 2021 23:23:57 -0400 by sugarat