Middle+
Technical task: component for image cropping Goal: Develop your own component from scratch (Vue + TypeScript) that allows users to upload an image, select an area for cropping, and get the result as coordinates. Functional requirements: 1. Separate component - The component must be fully standalone, written from scratch. - Use Vue and TypeScript. 2. Upload and display image - Provide a window (view area) where an image can be inserted. - The area should have specified maximum width and height. - If the image exceeds the size of the area, it should automatically scale to fit the window (without going beyond the screen borders). 3. Select cropping area - After uploading, the image is displayed in the window. - The user should be able to select an area for cropping with the mouse. - Arbitrary area (without fixed aspect ratio). 4. Save result - The 'Save' button triggers an event or method. - The coordinates of the selected cropping area are returned. - The cropping process on the server side is not required.
sobes.tech AI
Answer from AI
Below is an example of a simple Vue 3 component in TypeScript that implements image upload, scaling to specified dimensions, selecting an arbitrary crop area, and returning the coordinates of the selected area upon saving.
<template>
<div class="crop-container" :style="containerStyle">
<input type="file" accept="image/*" @change="onFileChange" />
<div
v-if="imageSrc"
class="image-wrapper"
ref="imageWrapper"
@mousedown="startSelection"
@mousemove="moveSelection"
@mouseup="endSelection"
@mouseleave="endSelection"
>
<img :src="imageSrc" ref="image" :style="imageStyle" />
<div
v-if="selection"
class="selection"
:style="selectionStyle"
></div>
</div>
<button @click="saveCrop" :disabled="!selection">Save</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, computed } from 'vue';
export default defineComponent({
name: 'ImageCropper',
emits: ['save'],
setup(_, { emit }) {
const MAX_WIDTH = 400;
const MAX_HEIGHT = 300;
const imageSrc = ref<string | null>(null);
const image = ref<HTMLImageElement | null>(null);
const imageWrapper = ref<HTMLDivElement | null>(null);
const containerStyle = {
width: `${MAX_WIDTH}px`,
height: `${MAX_HEIGHT}px`,
border: '1px solid #ccc',
position: 'relative',
userSelect: 'none',
};
const imageStyle = reactive({
maxWidth: '100%',
maxHeight: '100%',
display: 'block',
userSelect: 'none',
pointerEvents: 'none',
});
const selection = reactive<{ x: number; y: number; width: number; height: number } | null>(null);
const isSelecting = ref(false);
const startPoint = reactive({ x: 0, y: 0 });
function onFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = e => {
imageSrc.value = e.target?.result as string;
selection.x = 0;
selection.y = 0;
selection.width = 0;
selection.height = 0;
};
reader.readAsDataURL(file);
}
}
function getRelativeCoords(event: MouseEvent) {
if (!imageWrapper.value) return { x: 0, y: 0 };
const rect = imageWrapper.value.getBoundingClientRect();
let x = event.clientX - rect.left;
let y = event.clientY - rect.top;
// Limit coordinates within container
x = Math.max(0, Math.min(x, rect.width));
y = Math.max(0, Math.min(y, rect.height));
return { x, y };
}
function startSelection(event: MouseEvent) {
if (!imageSrc.value) return;
isSelecting.value = true;
const coords = getRelativeCoords(event);
startPoint.x = coords.x;
startPoint.y = coords.y;
selection.x = coords.x;
selection.y = coords.y;
selection.width = 0;
selection.height = 0;
}
function moveSelection(event: MouseEvent) {
if (!isSelecting.value || !selection) return;
const coords = getRelativeCoords(event);
selection.x = Math.min(coords.x, startPoint.x);
selection.y = Math.min(coords.y, startPoint.y);
selection.width = Math.abs(coords.x - startPoint.x);
selection.height = Math.abs(coords.y - startPoint.y);
}
function endSelection() {
isSelecting.value = false;
}
const selectionStyle = computed(() => {
if (!selection) return {};
return {
position: 'absolute',
border: '2px dashed #007bff',
left: `${selection.x}px`,
top: `${selection.y}px`,
width: `${selection.width}px`,
height: `${selection.height}px`,
pointerEvents: 'none',
backgroundColor: 'rgba(0, 123, 255, 0.2)',
};
});
function saveCrop() {
if (!selection) return;
// Return the coordinates of the selected area
emit('save', {
x: Math.round(selection.x),
y: Math.round(selection.y),
width: Math.round(selection.width),
height: Math.round(selection.height),
});
}
return {
imageSrc,
image,
imageWrapper,
containerStyle,
imageStyle,
selection,
selectionStyle,
onFileChange,
startSelection,
moveSelection,
endSelection,
saveCrop,
};
},
});
</script>
<style scoped>
.crop-container {
font-family: Arial, sans-serif;
}
.image-wrapper {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
cursor: crosshair;
margin-top: 10px;
}
.selection {
box-sizing: border-box;
}
button {
margin-top: 10px;
padding: 6px 12px;
font-size: 14px;
}
</style>
Explanation:
- The component allows uploading an image via input.
- The image scales to fit within maximum container dimensions.
- The user can select an arbitrary area with the mouse (without fixed aspect ratio).
- When clicking the "Save" button, a
saveevent is emitted with the coordinates of the selected crop area.
This component can be used in any Vue application by listening to the save event to get the crop coordinates and further process them on the server or client side.