Appearance
Basic Cropper Component Code ​
Dependencies
This component requires:
- Vue 3 with Composition API
vue-advanced-cropperlibraryuseCroppercomposableStencilCustomcomponent- 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>