Files
imagegenerator/src/routes/+page.svelte
2025-08-26 23:09:07 +03:00

278 lines
8.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import domtoimage from "dom-to-image";
import Cropper, { type OnCropCompleteEvent } from "svelte-easy-crop";
import gradiImage from "$lib/images/image.png"
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;
let fontSize: number = 2.5;
async function validateAndLoadImage(file: File): Promise<void> {
error = "";
const img = new Image();
const reader = new FileReader();
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 as string;
} else {
incorrectImage = e.target?.result as string;
isModalOpen = true;
}
};
};
reader.readAsDataURL(file);
}
function handleDrop(e: DragEvent): void {
e.preventDefault();
isDragging = false;
const file = (e.dataTransfer as DataTransfer).files[0];
if (file && file.type.startsWith("image/")) {
validateAndLoadImage(file);
}
}
function handleDragOver(e: DragEvent): void {
e.preventDefault();
isDragging = true;
}
function handleDragLeave(): void {
isDragging = false;
}
async function saveImage(): Promise<void> {
if (!image) return;
let node = document.getElementById("result");
if (node) {
domtoimage
.toPng(node, {
style: {
border: "none",
padding: "0",
margin: "0",
outline: "none",
},
})
.then((dataUrl: string) => {
var link = document.createElement("a");
link.download = "avatar.png";
link.href = dataUrl;
link.click();
});
}
}
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;
ctx.drawImage(img, x, y, width, height, 0, 0, 512, 512);
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 mt-3"
>
Обрезать
</button>
</div>
</div>
{:else}
<div class="bg-white rounded-xl shadow-lg p-6">
<div
class="preview-container relative bg-gray-50 mb-4 overflow-hidden border-2 transition-colors h-[512px] w-[512px] rounded-full"
ondrop={handleDrop}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
style="cursor: pointer"
onclick={() => document.getElementById("fileInput").click()}
>
<!-- Состояние без изображения -->
{#if !image}
<div
class="absolute inset-0 flex flex-col items-center justify-center cursor-pointer"
>
<svg
class="w-16 h-16 text-gray-400 mb-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-width="2"
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">Перетащите изображение</p>
<p class="text-sm text-gray-400 mt-1">или кликните для выбора</p>
</div>
{/if}
<!-- Загруженное изображение -->
{#if image}
<div id="result" class="w-full h-full">
<svg viewBox="0 0 512 512" class="w-full h-full" style="--font-size: {fontSize}rem">
<defs>
<clipPath id="circle-clip">
<circle cx="256" cy="256" r="256" />
</clipPath>
<pattern id="imagePattern" patternUnits="userSpaceOnUse" width="512" height="512" >
<image href={gradiImage} width="512" height="512"/>
</pattern>
</defs>
<image href={image} width="512" height="512" clip-path="url(#circle-clip)" />
<!-- Текст -->
{#if text}
<path
id="text-path"
d="M 256,26 a 230,230 0 0,0 0,460 a 230,230 0 0,0 0,-460"
fill="none"
/>
<text class="circle-text-bg">
<textPath href="#text-path" startOffset="50%" text-anchor="middle">{text}</textPath>
</text>
<text class="circle-text">
<textPath href="#text-path" startOffset="50%" text-anchor="middle">{text}</textPath>
</text>
{/if}
</svg>
</div>
{/if}
<input
id="fileInput"
type="file"
accept="image/*"
class="hidden"
onchange={(e) =>
validateAndLoadImage((e.target as HTMLInputElement).files[0])}
/>
</div>
{#if error}
<p class="text-red-500 text-sm mb-4 text-center">{error}</p>
{/if}
<div class="flex flex-col sm:flex-row gap-3 mb-4">
<input
type="text"
bind:value={text}
placeholder="Должность"
class="flex-grow px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onclick={saveImage}
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
</button>
</div>
<div class="flex items-center gap-3 mb-4">
<label for="font-size-slider" class="text-sm text-gray-600 whitespace-nowrap"
>Размер текста:</label
>
<input
id="font-size-slider"
type="range"
min="1.5"
max="7"
step="0.1"
bind:value={fontSize}
class="w-full"
/>
<div class="min-w-[25px] min-h-[25px]">{fontSize}</div>
</div>
<p class="text-sm text-gray-500 text-center">
Размер изображения 512×512 пикселей
</p>
</div>
{/if}
</div>
<style>
.circle-text-bg {
fill: none;
stroke: rgb(0, 0, 0, 0.7);
stroke-width: 5px;
stroke-linejoin:round;
stroke-linecap:round;
paint-order: stroke;
font-size: var(--font-size);
font-weight: 700;
letter-spacing: 6px;
}
.circle-text {
fill: url(#imagePattern);
stroke: #000;
stroke-width: 1.5px;
paint-order: stroke;
letter-spacing: 6px;
font-size: var(--font-size);
font-weight: 700;
}
</style>