Search code examples
vuejs3cropperjs

How to solve Vue cropper flicker


This is my vue cropper code. Every time when I load a image, the cropper window will flicker, especially for large image. Do any one know how to solve this probelm? Thank you!

<template>
  <!-- other stuff.. -->
  <input
    ref="input"
    type="file"
    name="image"
    accept="image/*"
    @change="setImage"
  />
  <vue-cropper
    ref="cropper"
    class="position-relative top-0 start-50 translate-middle-x"
    center-box
    :src="imgSrc"
    preview=".preview"
    v-if="imgSrc !== ''"
    style="height: 40vh; width: 40vh"
    :aspectRatio="1"
    :zoomable="false"
    :movable="false"
  />
</template>

<script setup>

// ...
const setImage = (e) => {
  const file = e.target.files[0]
  if (typeof FileReader === 'function') {
    const reader = new FileReader()
    reader.onload = async (event) => {
      imgSrc.value = event.target.result
      cropper.value.replace(event.target.result)
    }
    reader.readAsDataURL(file)
  } else {
    alert('Sorry, FileReader API not supported')
  }
}
</script>

I tried to remove the bootstrap style, added @ready and tired nextTick. None of them work.


Solution

  • I realized that my question was not detailed enough, but I solved it somehow, using a weird method. I tried to provide a runnable code, but I am not very familiar with it. I created my project using npx create-vue folder-name. and you need to npm install --save vue-cropperjs

    I found that I can not use v-if, v-show or display: none to hide the cropper and make it reappear after loading the image. Because they will all lead to the flicker problem. And I can not use a block to cover the cropper, because this will destroy the position and other styles of the cropper. The cropper element is extremely weird.

    My solution is easy, but also a bit tricky. Fist I hide the scroll bar, which is not shown here(it's easy you can google it). Then I set a white block with a height of 9999999vh, thus nobody can scroll down to the button. Then I use v-if to hide the block when loading is false.

    But how can I know if the image is loaded? onload is useless here, because even if the file is loaded, the src image is still not loaded. I found that I can check if the image is loaded or not by adding setTimeout(() => {loading.value = false;}, 1); Await is useless. No matter how much time I set, the setTimeout will always be executed after the image is loaded.

    I'm not quite sure what is the reason, but here is my guess: When I assign the value to src, the browser loads the data from a certain position of memory to another position. Await failed because value assignment is not awaitable. If I directly assign the value without timeout, it will also fail because you can assign the value parallelly. But timeout will use the certain channel which is occupied by value assignment, and this can not be done parallelly. So, when the value assignment is finished, the timeout will be triggered. But this is only my guess.

    <template>
      <div>
        <input ref="input" type="file" name="image" accept="image/*" @change="setImage" />
        <div class="d-flex justify-content-center lower_element" v-if="loading">
          <p>loading</p>
        </div>
        <div class="white-block" v-if="loading" :style="{ left: whiteBlock.x + 'px', top: whiteBlock.y + 'px', height: '999999px' }"></div>
        <div class="container mt-4 text-center" ref="avatar">
          <h2 style="font-family: fantasy">My Avatar</h2>
          <br />
          <div class="row">
            <div class="col-md-2 col-1" />
            <div class="col-md-4 col-10">
              <vue-cropper ref="cropper" class="position-relative top-0 start-50 translate-middle-x" center-box :src="imgSrc" preview=".preview" v-if="imgSrc !== ''" style="height: 40vh; width: 40vh" :aspectRatio="1" :zoomable="false" :movable="false" />
              <button type="button" @click="showFileChooser" class="btn btn-primary my-4">Select</button>
            </div>
            <div class="col-md-4 col-12">
              <div class="position-relative">
                <div class="position-relative top-0 start-50 translate-middle-x" style="height: 40vh; width: 40vh">
                  <div class="preview position-relative top-50 start-50 translate-middle" v-if="imgSrc !== ''" />
                  <p class="fw-semibold position-absolute bottom-0 start-50 translate-middle-x">Preview</p>
                </div>
              </div>
              <button type="button" class="btn btn-primary mt-4" @click="uploadAvatar">Upload</button>
            </div>
            <div class="col-md-2 col-1" />
          </div>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref, onMounted } from 'vue';
    import VueCropper from 'vue-cropperjs';
    import 'cropperjs/dist/cropper.css';
    const input = ref(null);
    const cropper = ref(null);
    const avatar = ref(null);
    const imgSrc = ref('/avatar.jpg');
    const cropImg = ref('');
    const token = localStorage.getItem('token');
    const loading = ref(true);
    const whiteBlock = ref({ x: 0, y: 0 });
    onMounted(() => {
      whiteBlock.value.x = avatar.value.getBoundingClientRect().left;
      whiteBlock.value.y = avatar.value.getBoundingClientRect().top;
      setTimeout(() => {
        loading.value = false;
      }, 300);
    });
    
    const setImage = (e) => {
      const file = e.target.files[0];
      if (typeof FileReader === 'function') {
        const reader = new FileReader();
        reader.onload = async (event) => {
          // if (file.size > 10.5 * 1024 * 1024) {
          //   await popup('Please select an image under 10MB');
          //   return;
          // }
          loading.value = true;
          imgSrc.value = event.target.result;
          cropper.value.replace(event.target.result);
    
          setTimeout(() => {
            loading.value = false;
          }, 1);
        };
        reader.readAsDataURL(file);
      } else {
        alert('Sorry, FileReader API not supported');
      }
    };
    
    const cropImage = async () => {
      const img = new Image();
      let height, width;
      img.onload = function () {
        height = img.height;
        width = img.width;
        if (width > 60) {
          height *= 60 / width;
          width = 60;
        }
        if (height > 60) {
          width *= 60 / height;
          height = 60;
        }
      };
      img.src = cropper.value.getCroppedCanvas().toDataURL();
      cropImg.value = cropper.value.getCroppedCanvas().toDataURL();
      while (new Blob([cropImg.value]).size > 6000) {
        console.log(new Blob([cropImg.value]).size);
        cropImg.value = await compressImage(cropImg.value, width, height, 0.5);
        height = height * 0.8;
        width = width * 0.8;
      }
      console.log(new Blob([cropImg.value]).size);
      // console.log(cropImg.value);
    };
    
    const showFileChooser = () => {
      input.value.click();
    };
    
    function compressImage(base64String, maxWidth, maxHeight, quality) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.src = base64String;
        img.onload = () => {
          let width = img.width;
          let height = img.height;
    
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
    
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
    
          const canvas = document.createElement('canvas');
          canvas.width = width;
          canvas.height = height;
    
          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0, width, height);
    
          canvas.toBlob(
            (blob) => {
              const reader = new FileReader();
              reader.readAsDataURL(blob);
              reader.onloadend = () => {
                const compressedBase64 = reader.result;
                resolve(compressedBase64);
              };
            },
            'image/jpeg',
            quality
          );
        };
    
        img.onerror = (error) => {
          reject(error);
        };
      });
    }
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style>
    .white-block {
      position: 'absolute';
      background: 'white';
      z-index: 10;
    }
    input[type='file'] {
      display: none;
    }
    
    .preview {
      width: auto;
      height: 10vh;
      border: 2px solid black;
      overflow: hidden;
      border-radius: 50%;
      z-index: 1;
    }
    </style>