Skip to content

Basic Cropper Component Code ​

Dependencies

This component requires:

  • Vue 3 with Composition API
  • vue-advanced-cropper library
  • useCropper composable
  • StencilCustom component
  • SCSS support

Full Component Code ​

vue
<script setup>
// vue imports
import { watch, toRefs, ref, onMounted, inject, provide, defineExpose } from 'vue';
// composables
import { useCropper } from '../composables/useCropperComposable';
// libraries
import { Cropper } from 'vue-advanced-cropper';
// components
import StencilCustom from './StencilCustom.vue';
import 'vue-advanced-cropper/dist/style.css';

const props = defineProps({
  initImage: {
    type: String,
    default: '',
    required: true,
  },
  cropImageNv: {
    type: Boolean,
    default: false,
    required: false,
  },
  width: {
    type: Number,
    default: 800,
    required: false,
  },
  height: {
    type: Number,
    default: 600,
    required: false,
  },
  fullSizeCropper: {
    type: Boolean,
    default: false,
    required: false,
  },
});

const appImages = inject('appImages');

const { cropImageNv } = toRefs(props);

const emit = defineEmits(['image:cropped']);

const cropper = ref(null);

const img = props.initImage;

const cropperData = useCropper();
const { isStencilModified } = cropperData;

provide('cropperData', cropperData);

const getCroppedImage = () => {
  const { canvas } = cropper.value.getResult();
  const base64Image = canvas.toDataURL(); // Convert to base64
  return base64Image;
};

watch(cropImageNv, (value) => {
  if (value) {
    console.log('cropping image');
    // Get the cropped image when cropImageNv becomes true
    const base64Image = getCroppedImage();
    emit('image:cropped', base64Image); // Emit event with base64 data
  } else {
    console.log('default state');
  }
});

const isInitializingStencil = ref(false);

onMounted(() => {
  try {
    isInitializingStencil.value = true;
    setTimeout(() => {
      const cropperInstance = cropper.value;
      // if (!cropperInstance || !cropperInstance.imageSize) return;

      if (props.fullSizeCropper) {
        // Use full image size
        const { width: imageWidth, height: imageHeight } = cropperInstance.imageSize;
        cropperInstance.setCoordinates({
          width: imageWidth,
          height: imageHeight,
          left: 0,
          top: 0,
        });
      } else {
        // Use custom width/height props
        const width = props.width;
        const height = props.height;

        const { width: imageWidth, height: imageHeight } = cropperInstance.imageSize;

        const left = (imageWidth - width) / 2;
        const top = (imageHeight - height) / 2;

        cropperInstance.setCoordinates({ width, height, left, top });
      }

      cropperInstance.refresh(); // make sure it applies
      isInitializingStencil.value = false;
    }, 500);
  } catch (e) {
    isInitializingStencil.value = false;
  }
});

function handleCropperClick(e) {
  if (isStencilModified.value) {
    return;
  }
  const cropperEl = cropper.value?.$el?.querySelector('img');
  if (!cropperEl) {
    console.warn('Image element not found inside cropper');
    return;
  }
  const rect = cropperEl.getBoundingClientRect();
  const offsetX = e.clientX - rect.left;
  const offsetY = e.clientY - rect.top;
  const scaleX = cropperEl.naturalWidth / rect.width;
  const scaleY = cropperEl.naturalHeight / rect.height;
  const imageX = offsetX * scaleX;
  const imageY = offsetY * scaleY;
  console.log(cropper.value, 'value');
  cropper.value?.setCoordinates({
    left: imageX - cropper.value?.coordinates.width / 2,
    top: imageY - cropper.value?.coordinates.height / 2,
  });
  console.log('Manual image coords:', { imageX, imageY });
}

defineExpose({
  getCroppedImage,
});
</script>

<template>
  <div class="basic-cropper-wrapper">
    <div class="cropper-container" :class="{ hide: isInitializingStencil }">
      <Cropper
        ref="cropper"
        :src="img"
        :stencil-component="StencilCustom"
        :resizeImage="{ wheel: true }"
        @click="handleCropperClick"
      ></Cropper>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.basic-cropper-wrapper {
  position: relative;
  height: 100%;
  width: 100%;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  .cropper-container {
    max-width: 100%;
    height: 100%;
    // max-height: 70vh;
    display: flex;
    position: relative;
    transition: all 1s ease;
    z-index: 2;

    &.hide {
      // opacity: 0;
      filter: blur(0.625rem);
    }
  }
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
<style lang="scss">
.vue-advanced-cropper__foreground {
  border-radius: 0.5rem;
}

.vue-advanced-cropper__image {
  border-radius: 0.5rem;
}

.vue-advanced-cropper__image-wrapper {
  border-radius: 0.5rem;
}

.vue-advanced-cropper__background {
  border-radius: 1rem;
  background: lightgray 0 -11.0904rem / 100% 166.949% no-repeat;
}
</style>