diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts index ebbe3ed3be0e56fdce1636eaad2ef8174fcd42fe..173d041bf8dfe0217432c101743530959f02ac35 100644 --- a/src/common/interfaces/componentsProps.ts +++ b/src/common/interfaces/componentsProps.ts @@ -183,7 +183,13 @@ export interface IPopupProps { } export interface ICropperProps { - size?: TSize; + src?: string; + file?: File; + width?: number; + height?: number; + menuPosition?: 'top' | 'right' | 'bottom' | 'left'; + theme?: TThemeColor; + darknessTheme?: TDarkness; } export interface IColorPickerProps { diff --git a/src/components/Cropper/Cropper.stories.ts b/src/components/Cropper/Cropper.stories.ts index 2cc8135d819f115f020600909394e797bab7ff57..30c424faed71603a2ecbaecb0cc6020f98ed6de2 100644 --- a/src/components/Cropper/Cropper.stories.ts +++ b/src/components/Cropper/Cropper.stories.ts @@ -14,10 +14,11 @@ const meta: Meta = { }, }, argTypes: { - buttonProps: { control: 'object' }, - sameButtonColor: { control: 'boolean' }, + menuPosition: { control: 'select', options: ['top', 'right', 'bottom', 'left'] }, + src: { control: 'text' }, + width: { control: 'number' }, + height: { control: 'number' }, disabled: { control: 'boolean' }, - size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, }, } satisfies Meta<typeof Cropper>; @@ -26,7 +27,9 @@ export default meta; type Story = StoryObj<typeof meta>; export const Simple: Story = { - args: {}, + args: { + src: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQoFRQjM-wM_nXMA03AGDXgJK3VeX7vtD3ctA&s', + }, }; export const Full: Story = { diff --git a/src/components/Cropper/Cropper.vue b/src/components/Cropper/Cropper.vue index 07f9948b225afb547a7ec4cfc97198d1745a6ed2..946c3aab13a667f1638259b7b6754b418d749b74 100644 --- a/src/components/Cropper/Cropper.vue +++ b/src/components/Cropper/Cropper.vue @@ -1,17 +1,119 @@ <script setup lang="ts"> import type { ICropperProps } from '@interfaces/componentsProps'; +import { computed, ref, watch } from 'vue'; +import Button from '@components/Button/Button.vue'; +import { convertThemeToTextColor } from '@helpers/common'; +import SaveIcon from '@icons/Mono/SaveIcon.vue'; +import CornerLeftTopIcon from '@icons/Mono/CornerLeftTopIcon.vue'; const props = withDefaults(defineProps<ICropperProps>(), { - size: 'normal', - disabled: false, + width: 300, + height: 300, + menuPosition: 'top', + theme: 'black', + darknessTheme: '500', }); + +const canvas = ref(); + +const ctx = computed(() => canvas.value && canvas.value.getContext('2d')); +const imageSource = computed(() => props.src ?? props.file); +const width = computed(() => props.width); +const height = computed(() => props.height); +const color = computed(() => convertThemeToTextColor(props.theme, props.darknessTheme)); + +watch( + [imageSource, ctx, width, height], + () => { + if (!imageSource.value) return; + + const img = new Image(); + img.src = props.src ?? URL.createObjectURL(props.file); + + img.onload = () => { + canvas.value.width = width.value ?? 0; + canvas.value.height = height.value ?? 0; + ctx.value?.drawImage(img, 0, 0, width.value ?? 0, height.value ?? 0); + }; + }, + { immediate: true }, +); </script> <template> - <section class="container"></section> + <section + :class="[ + 'container', + { + flexVertical: menuPosition === 'top' || menuPosition === 'bottom', + }, + ]" + > + <div class="canvas-container"> + <canvas ref="canvas" id="cropper-canvas"> </canvas> + <div class="crop-border left top"> + <CornerLeftTopIcon size="16" /> + </div> + <div class="crop-border right top"></div> + <div class="crop-border left bottom"></div> + <div class="crop-border right bottom"></div> + </div> + <div + v-show="imageSource" + :class="[ + 'buttons', + { + order1: menuPosition === 'top' || menuPosition === 'left', + flexVertical: menuPosition === 'right' || menuPosition === 'left', + }, + ]" + > + <Button :theme="theme" :darknessTheme="darknessTheme" label="Reset" /> + <Button :theme="theme" :darknessTheme="darknessTheme" label="Save"> + <SaveIcon :color="color" size="16" /> + </Button> + </div> + </section> </template> <style scoped> .container { + display: flex; + align-items: center; + width: max-content; +} +.canvas-container { + position: relative; + line-height: 0; +} +#cropper-canvas { + border: 1px solid black; +} +.buttons { + display: flex; + align-items: center; + gap: 20px; +} +.crop-border { + position: absolute; + z-index: 50; + width: 20px; + height: 20px; + background-color: gray; +} +.top { + top: 0; +} +.right { + right: 0; +} +.bottom { + bottom: 0; +} +.flexVertical { + flex-direction: column; +} +.order1 { + order: -1; } </style> diff --git a/src/components/Cropper/helpers.ts b/src/components/Cropper/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..99c4fe7cd837fd9aabb774446a3752741175d8ce --- /dev/null +++ b/src/components/Cropper/helpers.ts @@ -0,0 +1,7 @@ +export const getImageInfo = (image: string | File, instance: HTMLImageElement) => { + instance.src = typeof image === 'string' ? image : URL.createObjectURL(image); + + instance.onload = function () { + return [instance, 0, 0, instance.width, instance.height]; + }; +}; diff --git a/src/icons/Mono/CornerLeftTopIcon.vue b/src/icons/Mono/CornerLeftTopIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..fe4378b829e409314d58ccf922179dc8ab1431e3 --- /dev/null +++ b/src/icons/Mono/CornerLeftTopIcon.vue @@ -0,0 +1,26 @@ +<script setup lang="ts"> +interface Props { + color?: string; + size?: string | number; +} +defineProps<Props>(); +</script> + +<template> + <svg + :width="`${size ?? 40}px`" + :height="`${size ?? 40}px`" + viewBox="3 3 10 10" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M9.8774 3c-1.101 0-1.9579 0-2.6453.0562-.6979.057-1.265.1744-1.7751.4343-.8467.4314-1.5351 1.1198-1.9665 1.9665-.2599.5101-.3773 1.0772-.4343 1.7751C3 7.9195 3 9 3 10V9.9v2.1c0 .2761.2239.5.5.5s.5-.2239.5-.5V9.9c0-1.1284.0004-1.9446.0528-2.5865.052-.6361.1526-1.0569.3287-1.4025.3355-.6585.871-1.194 1.5295-1.5295.3456-.1761.7664-.2767 1.4025-.3287C7.9554 4.0004 8.7716 4 9.9 4h2.1c.2761 0 .5-.2239.5-.5s-.2239-.5-.5-.5H9.9 9.8774Z" + :fill="color ?? '#000000'" + /> + </svg> +</template> + +<style scoped></style>