This article was originally published at: github.com/bigo-fronte… Welcome to follow and reprint.

Encapsulate the Vue floating pop-up picture viewer component

preface

In the early years of the development of internal technical forum, in order to achieve a process of picture browsing experience, based on Vue developed a picture viewer component, briefly sorted out the implementation ideas, hoping to provide you with some help.

Take a look at the renderings:

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

The principle of analysis

  1. Gets the clicked picture element based on the click event
  2. Makes the current image element invisible (via visibility or opacity)
  3. Create a mask layer
  4. Creates a picture element of the same size at the current position of the picture element
  5. Create an animation to enlarge the image to the right size (update position, scale)

Thinking clearly, it is not difficult to achieve.

Implementation scheme

Since the ultimate purpose is to be used in Vue projects, the following scenarios are packaged directly as Vue components.

Picture viewer basic structure

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.// The image element in the mouse point
        visible: false.// Whether the image viewer is visible
      };
    },
    computed: {
      src() {
        return this.el? .src; }},methods: {
      show(el) {
        el.style.opacity = 0; // Hide the source image

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


Copy the code

A simple analysis:

  • Transition: The outer transition component is nested, which is very convenient for us to do the animation effect of moving and zooming pictures
  • Image-viewer: The root element is used to hold image elements and also acts as a mask layer
  • .image: This element is the floating image that is displayed when you click on an image, and all subsequent actions will be performed on this image
  • Show (EL) : This method is called when an image is clicked, passing the image element into the component and displaying the image viewer

The style is also quite simple, drawing a translucent mask is 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 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>
Copy the code

Pop up the image and enlarge it

Now that our image viewer is ready to display images, how to pop the target image element (.image) in the viewer from the source image element (EL).

According to Vue’s data-driven philosophy, the essence is to animate images by applying start and end data to pop them out of place and place them at the right size. Here, by maintaining a dimension data dimension, the style style of the target image element can be calculated according to the dimension data.

export default {
  data() {
    return {
      // ...
      // Image dimension information
      dimension: null}; },computed: {
    // ...
    // Target image 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: 'the transform 0.3 s'}; }},methods: {
    show(el) {
      el.style.opacity = 0;

      this.el = el;
      this.visible = true;
      this.dimension = getDimension(el); // Get dimension data from the source image,}}};Copy the code

The dimension here contains the following information about the image element:

data describe
size: { width, height } The actual width and height of the picture
position: { top, left } Absolute position of picture
scale The ratio of the actual size of a picture element to the natural size of the picture, used for subsequent picture scaling animations
translate: { x, y } Image displacement position, default to 0, for subsequent image scaling displacement animation

Get the dimension of the image element

const getDimension = (el) => { const { naturalWidth, naturalHeight } = el; const rect = el.getBoundingClientRect(); 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 }, }; };Copy the code

Now we have overlaid an image of the same size at the end of the source image, and then enlarged the image to the appropriate size based on the screen size.

We just need to modify the logic of the show section to update the value of dimension 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, i.e
          scale: 1.// Calculate the displacement to keep the image centered
          translate: {
            x: (innerWidth - size.width) / 2 - position.left,
            y: (innerHeight - size.height) / 2- position.top, }, }; }); ,}}};Copy the code

Here we use doubleRaf (Double RequestAnimationFrame) and wait for the browser to rerender before executing:

const doubleRaf = (cb) = > {
  requestAnimationFrame(() = > {
    requestAnimationFrame(cb);
  });
};
Copy the code

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

Similarly, when we click on the mask layer to close the image browser, we should shrink the image and return it to its 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 source image's dimension
        this.dimension = getDimension(this.el);
        this.visible = false;
      },
      // Completely hidden
      hidden() {
        this.el.style.opacity = null;
        document.body.style.overflow = this.bodyOverflow;
        this.$emit('hidden'); ,}}};</script>
Copy the code

Now the logic for the picture Viewer component is almost complete.

Encapsulate as a function call

To make this component easier to use, we encapsulate it as a function call:

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


const ImageViewerConstructor = Vue.extend(ImageViewer);

function showImage(el) {
  // Create the component instance and call the component's show method
  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);

  // Destruct function: removes the root element and destroys the component
  function destroy() {
    if (instance && instance.$el) {
      document.body.removeChild(instance.$el);
      instance.$destroy();
      instance = null; }}// When the component animation ends, the destruction function is executed
  instance.$once('hidden', destroy);

  // If the method is called on a parent element, the destruction 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;
Copy the code

At this point, component wrapping is complete and ready to 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>
Copy the code

conclusion

Although the function is relatively simple, but the main picture browsing function has been realized, compared with most of the picture browsing plug-ins, in the user experience is much smoother, can let users have a smoother visual transition, to provide a better immersive browsing experience.

There are a lot of ideas that haven’t been implemented yet, such as dragging images around while browsing, zooming in and out with the mouse wheel, gesture optimization, mobile experience optimization, multi-image browsing, etc.

Welcome everyone to leave a message to discuss, wish smooth work, happy life!

I’m bigO front. See you next time.