add new system

This commit is contained in:
2025-07-22 15:58:34 +03:00
parent d5a95eb281
commit 95e0551540
6 changed files with 446 additions and 186 deletions

19
package-lock.json generated
View File

@@ -12,7 +12,9 @@
"@sveltejs/adapter-node": "^5.2.13",
"cropperjs": "^2.0.0",
"dom-to-image": "^2.6.0",
"html2canvas": "^1.4.1"
"html2canvas": "^1.4.1",
"svelte-crop-window": "^0.1.1",
"svelte-easy-crop": "^4.0.1"
},
"devDependencies": {
"@cloudparker/easy-cropperjs-svelte": "^2.4.0",
@@ -2353,6 +2355,21 @@
"node": ">=18"
}
},
"node_modules/svelte-crop-window": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/svelte-crop-window/-/svelte-crop-window-0.1.1.tgz",
"integrity": "sha512-N5696nYO38iHuYYaafOR6xWJmh4lnCnRBwdTbMBAiAnILMAmR4LoleIlfeCyUVuxTaDtw3tG9KB2MVntQP4R9w==",
"license": "MIT"
},
"node_modules/svelte-easy-crop": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/svelte-easy-crop/-/svelte-easy-crop-4.0.1.tgz",
"integrity": "sha512-0k7vVpHVLrPyobSXqey5IJUmFVxOoCaQrobFEsFXpSCyK8N5jTkRj1VX6NuCOZK8XXcMAqUvV0MktB8D5x1oCw==",
"license": "MIT",
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",

View File

@@ -26,6 +26,8 @@
"@sveltejs/adapter-node": "^5.2.13",
"cropperjs": "^2.0.0",
"dom-to-image": "^2.6.0",
"html2canvas": "^1.4.1"
"html2canvas": "^1.4.1",
"svelte-crop-window": "^0.1.1",
"svelte-easy-crop": "^4.0.1"
}
}

40
src/lib/Alert.svelte Normal file
View File

@@ -0,0 +1,40 @@
<script lang="ts">
export let isOpen = false;
function closeModal() {
isOpen = false;
close();
}
</script>
<div class="inline-flex items-center text-base font-semibold text-gray-900">
<div>
{#if isOpen}
<div
class="relative z-10"
aria-labelledby="dialog-title"
role="dialog"
aria-modal="true"
>
<!-- Затемнение фона -->
<div
class="fixed inset-0 bg-gray-700/20 bg-opacity-75 transition-opacity"
aria-hidden="true"
on:click={closeModal}
></div>
<!-- Центрирование по вертикали и горизонтали -->
<div class="fixed inset-0 z-10 flex items-center justify-center p-4">
<div
class="w-full max-w-md transform rounded-lg bg-white p-6 shadow-xl transition-all"
>
<!-- Центрированное содержимое -->
<div class="flex flex-col items-center space-y-4 text-center">
<slot></slot>
</div>
</div>
</div>
</div>
{/if}
</div>
</div>

View File

@@ -1,63 +1,99 @@
<script lang="ts">
import EasyCropperjs from "@cloudparker/easy-cropperjs-svelte";
<script>
import Cropper from "svelte-easy-crop";
let easyCropperjsRef: EasyCropperjs | null = $state(null);
let file: File = $props();
let image = $props(); // начальное изображение
let crop = { x: 0, y: 0 };
let zoom = 1;
let cropSize = { width: 512, height: 512 };
let maxZoom = 10;
let minZoom = 1;
let restrictPosition = true;
let showGrid = false;
let croppedImage = null; // хранение вырезанного изображения
let imageLoaded = false;
// Функция для обработки обрезанного изображения
async function handleCrop() {
const data = await easyCropperjsRef?.crop({
outputWidth: 512, // Ширина обрезанного изображения
outputFormat: "png", // Формат обрезанного изображения
outputQuality: 0.6, // Качество
outputType: "file", // Тип данных
});
// Если данные обрезки получены и это Blob
if (data instanceof Blob) {
const downloadLink = document.createElement("a"); // Создаем элемент <a> для скачивания
downloadLink.href = URL.createObjectURL(data); // Создаем URL для обрезанного изображения
downloadLink.download = "cropped_image.png"; // Имя файла для скачивания
downloadLink.click(); // Имитируем клик для скачивания
} else {
console.log("Нет данных для скачивания");
// Функция для обработки загрузки изображения
function handleImageUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
image = e.target.result; // Set the image
imageLoaded = true; // Mark image as loaded
};
reader.readAsDataURL(file); // Convert to base64
}
}
// Получение результата обрезки
function handleCropResult(ev: CustomEvent) {
let base64ImageUrl: string = ev.detail;
// Используйте base64ImageUrl по вашему усмотрению
// Функция для обрезки изображения
function cropImage(x, y, width, height) {
const img = new Image();
img.src = image;
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
// Обрезаем изображение с помощью canvas
ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
// Получаем обрезанное изображение в формате base64
croppedImage = canvas.toDataURL("image/png");
};
}
// Функция для получения файла
async function getFile(url: string) {
const fileName = url.split("/").pop() || "file.png";
const blob = await (await fetch(url)).blob();
const file = new File([blob], fileName, { type: blob.type });
return file;
// Функция для скачивания обрезанного изображения
function downloadImage() {
const link = document.createElement("a");
link.href = croppedImage;
link.download = "cropped-image.png";
link.click();
}
// Обработчик события cropcomplete
function handleCropComplete(event) {
if (!imageLoaded) return; // Prevent cropping if image is not loaded yet
const { x, y, width, height } = event.detail;
cropImage(x, y, width, height);
}
</script>
<div class="p-4">
<div
class="w-[512px] h-[512px] overflow-hidden relative border-2 border-gray-300"
>
{#if file}
<EasyCropperjs
bind:this={easyCropperjsRef}
outputAspectRatio={1}
inputImageFile={file}
onCrop={handleCropResult}
<div class="flex flex-col">
<div class="h-[1024px] w-[1024px] relative">
<Cropper
{image}
{cropSize}
{maxZoom}
{minZoom}
{restrictPosition}
{showGrid}
bind:crop
bind:zoom
on:cropcomplete={handleCropComplete}
/>
{/if}
</div>
<!-- Кнопка для обрезки изображения -->
<button
onclick={handleCrop}
class="mt-4 p-2 bg-blue-500 text-white rounded hover:bg-blue-700"
>
Crop
</button>
<button on:click={downloadImage}> Скачать вырезанное изображение </button>
</div>
<style>
.upload-btn {
margin: 10px 0;
}
.download-btn {
margin-top: 10px;
padding: 10px 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.download-btn:hover {
background-color: #45a049;
}
</style>

View File

@@ -1,28 +1,41 @@
<script>
<script lang="ts">
import domtoimage from "dom-to-image";
import Scropper from "$lib/Scropper.svelte";
import Cropper, {
type CropArea,
type OnCropComplete,
type OnCropCompleteEvent,
} from "svelte-easy-crop";
let image = null;
let text = "";
let error = "";
let isDragging = false;
let btnDisabled = image != null;
let isModalOpen = $state(false);
let crop = { x: 0, y: 0 };
let zoom = 1;
let cropSize = { width: 512, height: 512 };
let maxZoom = 10;
let minZoom = 1;
let restrictPosition = true;
let showGrid = false;
let croppedImage = null; // хранение вырезанного изображения
let image: string;
let incorrectImage: string;
let text: string = "";
let error: string = "";
let isDragging: boolean = false;
let isModalOpen: boolean = false;
// Проверка и загрузка изображения
async function validateAndLoadImage(file) {
async function validateAndLoadImage(file: File): Promise<void> {
error = "";
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
reader.onload = (e: ProgressEvent<FileReader>) => {
img.src = e.target?.result as string;
img.onload = () => {
if (img.width === 512 && img.height === 512) {
image = e.target.result;
image = e.target?.result as string;
} else {
incorrectImage = e.target?.result as string;
isModalOpen = true;
error = "Изображение должно быть 512×512 пикселей";
}
};
};
@@ -30,30 +43,31 @@
}
// Обработка Drag and Drop
function handleDrop(e) {
function handleDrop(e: DragEvent): void {
e.preventDefault();
isDragging = false;
const file = e.dataTransfer.files[0];
const file = (e.dataTransfer as DataTransfer).files[0]; // Explicitly casting to DataTransfer
if (file && file.type.startsWith("image/")) {
validateAndLoadImage(file);
}
}
function handleDragOver(e) {
function handleDragOver(e: DragEvent): void {
e.preventDefault();
isDragging = true;
}
function handleDragLeave() {
function handleDragLeave(): void {
isDragging = false;
}
async function saveImage() {
async function saveImage(): Promise<void> {
if (!image) return;
let node = document.getElementById("result");
// Убираем лишние стили
if (node) {
node.style.border = "none";
node.style.padding = "0";
node.style.margin = "0";
@@ -68,24 +82,80 @@
outline: "none", // Убираем outline
},
})
.then(function (dataUrl) {
.then((dataUrl: string) => {
var link = document.createElement("a");
link.download = "avatar.png";
link.href = dataUrl;
link.click(); // Скачать изображение
});
}
}
const openModal = () => {
const openModal = (): void => {
isModalOpen = true;
};
// Закрыть модальное окно
const closeModal = () => {
const closeModal = (): void => {
isModalOpen = false;
};
function cropImage(x: number, y: number, width: number, height: number) {
const img = new Image();
img.src = incorrectImage;
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 512;
// Обрезаем изображение с помощью canvas
ctx.drawImage(img, x, y, width, height, 0, 0, 512, 512);
// Получаем обрезанное изображение в формате base64
croppedImage = canvas.toDataURL("image/png");
};
}
const handleCropComplete = (event: OnCropCompleteEvent) => {
const { x, y, width, height } = event.pixels;
cropImage(x, y, width, height);
};
function saveToMainImage() {
image = croppedImage; // Ссылка на обрезанное изображение
isModalOpen = false;
}
</script>
<div>
{#if isModalOpen}
<div class="bg-white rounded-xl shadow-lg p-6">
<div class="flex flex-col">
<div class="h-[700px] w-[700px] relative rounded-2xl">
<Cropper
image={incorrectImage}
{cropSize}
{maxZoom}
{minZoom}
{restrictPosition}
{showGrid}
bind:crop
bind:zoom
oncropcomplete={handleCropComplete}
/>
</div>
<button
onclick={saveToMainImage}
class="px-6 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-medium rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
Сохранить
</button>
</div>
</div>
{:else}
<div class="bg-white rounded-xl shadow-lg p-6">
<div
class="preview-container relative bg-gray-50 rounded-lg mb-4 overflow-hidden border-2 transition-colors h-[512px]"
@@ -111,7 +181,7 @@
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
></path>
</svg>
<p class="text-lg text-gray-500">Перетащите изображение 512×512</p>
<p class="text-lg text-gray-500">Перетащите изображение</p>
<p class="text-sm text-gray-400 mt-1">или кликните для выбора</p>
</div>
{/if}
@@ -122,7 +192,9 @@
<img src={image} class="object-cover" />
<!-- Текст -->
{#if text}
<div class="absolute bottom-0 left-0 right-0 py-5 bg-black/70">
<div
class="absolute bottom-0 left-0 right-0 py-5 bg-black/70 rounded-t-4xl"
>
<h1
class="text-center text-5xl font-bold text-transparent bg-clip-text bg-cover bg-right leading-17 tracking-tight"
style="background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAADICAYAAACeXFkKAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADs0lEQVR42u3WTU7DMBCA0RkOySU4HScMi4bW/06zYcF7EhKV09SeptKXn1/fR0RExhEREZkR8ft/RFRrERFZrh3Pax7vna2d/+drrbsu6/d098ij20+5p/Faeab2/vvzjs7UXbc9UzO/6Zmae+RgfuU9Fmcq97Q/U3NdDuZ34UzlLG+dafLsrM5UP1d3zrQ+b/nsDJ+r7kzXfivP++fN30q37/1vpbp/c6bjuXL+Zdavu/XXTofr5wcexe67e+Xo3lm/zv7e1edmrPd5DnG+/viCp2vVPt6byWoet2eSq31enEnz3Q6v3c0ki/kuZjb9Xqf7zM3Mms/NzTkiNzO78Kw/93FvJvNn+f2ZrJ/lizNpvtuPAAD+HQEAAAIAABAAAIAAAAAEAAAgAAAAAQAACAAAQAAAAAIAABAAAIAAAAAEAAAgAAAAAQAACAAAQAAAAAIAAAQAACAAAAABAAAIAABAAAAAAgAAEAAAgAAAAAQAACAAAAABAAAIAABAAAAAAgAAEAAAgAAAAAQAAAgAAEAAAAACAAAQAACAAAAABAAAIAAAAAEAAAgAAEAAAAACAAAQAACAAAAABAAAIAAAAAEAAAgAABAAAIAAAAAEAAAgAAAAAQAACAAAQAAAAAIAABAAAIAAAAAEAAAgAAAAAQAACAAAQAAAAAIAABAAACAAAAABAAAIAABAAAAAAgAAEAAAgAAAAAQAACAAAAABAAAIAABAAAAAAgAAEAAAgAAAAAQAACAAAAABAAACAAAQAACAAAAABAAAIAAAAAEAAAgAAEAAAAACAAAQAACAAAAABAAAIAAAAAEAAAgAAEAAAAACAAAEAAAgAAAAAQAACAAAQAAAAAIAABAAAIAAAAAEAAAgAAAAAQAACAAAQAAAAAIAABAAAIAAAAAEAAAIAABAAAAAAgAAEAAAgAAAAAQAACAAAAABAAAIAABAAAAAAgAAEAAAgAAAAAQAACAAAAABAAAIAAAQAACAAAAABAAAIAAAAAEAAAgAAEAAAAACAAAQAACAAAAABAAAIAAAAAEAAAgAAEAAAAACAAAQAAAgAAAAAQAACAAAQAAAAAIAABAAAIAAAAAEAAAgAAAAAQAACAAAQAAAAAIAABAAAIAAAAAEAAAgAAAAAQAAAgAAEAAAgAAAAAQAACAAAAABAAAIAABAAAAAAgAAEAAAgAAAAAQAACAAAAABAAAIAABAAAAAAgAABAAAIAAAAAEAAAgAAEAAAAACAAAQAACAAAAA/tYPVhsMy8Jj9qoAAAAASUVORK5CYII=');"
@@ -139,7 +211,8 @@
type="file"
accept="image/*"
class="hidden"
onchange={(e) => validateAndLoadImage(e.target.files[0])}
onchange={(e) =>
validateAndLoadImage((e.target as HTMLInputElement).files[0])}
/>
</div>
@@ -158,7 +231,6 @@
/>
<button
onclick={saveImage}
disabled={btnDisabled}
class="px-6 py-3 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-medium rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
Сохранить 512×512
@@ -166,28 +238,8 @@
</div>
<p class="text-sm text-gray-500 text-center">
Поддерживаются только изображения размером 512×512 пикселей
Размер изображения 512×512 пикселей
</p>
<!-- Модальное окно -->
{#if isModalOpen}
<div
class="fixed inset-0 bg-gray-900 bg-opacity-50 flex justify-center items-center z-50"
>
<div class="bg-white p-4 rounded shadow-lg w-[512px] h-[512px]">
<h2 class="text-xl font-semibold mb-4">Crop your image</h2>
{#if file}
<Scropper {file}></Scropper>
{/if}
<div class="mt-4 flex justify-between">
<button
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-700"
onclick={closeModal}
>
Close
</button>
</div>
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,113 @@
<script>
import Cropper from "svelte-easy-crop";
let image = ""; // начальное изображение
let crop = { x: 0, y: 0 };
let zoom = 1;
let cropSize = { width: 512, height: 512 };
let maxZoom = 10;
let minZoom = 1;
let restrictPosition = true;
let showGrid = false;
let croppedImage = null; // хранение вырезанного изображения
// Функция для обработки загрузки изображения
function handleImageUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
image = e.target.result; // Обновляем изображение
};
reader.readAsDataURL(file); // Преобразуем файл в Data URL
}
}
// Функция для обрезки изображения
function cropImage(x, y, width, height) {
const img = new Image();
img.src = image;
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
// Обрезаем изображение с помощью canvas
ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
// Получаем обрезанное изображение в формате base64
croppedImage = canvas.toDataURL("image/png");
};
}
// Функция для скачивания обрезанного изображения
function downloadImage() {
if (croppedImage) {
const link = document.createElement("a");
link.href = croppedImage;
link.download = "cropped-image.png";
link.click();
} else {
alert("Сначала выполните обрезку изображения!");
}
}
// Обработчик события cropcomplete
function handleCropComplete(event) {
const { x, y, width, height } = event.detail;
cropImage(x, y, width, height);
}
</script>
<div class="flex flex-col">
<div class="h-[1024px] w-[1024px] relative">
<Cropper
{image}
{cropSize}
{maxZoom}
{minZoom}
{restrictPosition}
{showGrid}
bind:crop
bind:zoom
on:cropcomplete={handleCropComplete}
/>
</div>
<div>
<!-- Кнопка для загрузки изображения -->
<input
type="file"
accept="image/*"
on:change={handleImageUpload}
class="upload-btn"
/>
</div>
{#if croppedImage}
<!-- Кнопка для скачивания вырезанного изображения -->
<button on:click={downloadImage}> Скачать вырезанное изображение </button>
{/if}
</div>
<style>
.upload-btn {
margin: 10px 0;
}
.download-btn {
margin-top: 10px;
padding: 10px 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.download-btn:hover {
background-color: #45a049;
}
</style>