Spaces:
Paused
Paused
Update index.html
Browse files- index.html +175 -38
index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
<title>Swaga Icon Maker PRO</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
-
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@900&display=swap" rel="stylesheet">
|
| 10 |
|
| 11 |
<style>
|
| 12 |
body {
|
|
@@ -20,7 +20,7 @@
|
|
| 20 |
min-height: 100vh;
|
| 21 |
margin: 0;
|
| 22 |
padding: 20px;
|
| 23 |
-
user-select: none;
|
| 24 |
}
|
| 25 |
|
| 26 |
h1 { margin-bottom: 10px; }
|
|
@@ -42,7 +42,6 @@
|
|
| 42 |
border-radius: 20px;
|
| 43 |
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 44 |
background-color: #E13839;
|
| 45 |
-
/* Курсор управляется через JS */
|
| 46 |
}
|
| 47 |
|
| 48 |
.controls {
|
|
@@ -57,6 +56,11 @@
|
|
| 57 |
overflow-y: auto;
|
| 58 |
}
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
.section-title {
|
| 61 |
font-size: 12px;
|
| 62 |
text-transform: uppercase;
|
|
@@ -65,6 +69,7 @@
|
|
| 65 |
border-bottom: 1px solid #444;
|
| 66 |
padding-bottom: 5px;
|
| 67 |
margin-top: 10px;
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
.drop-zone {
|
|
@@ -140,6 +145,106 @@
|
|
| 140 |
text-align: center;
|
| 141 |
margin-top: 5px;
|
| 142 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</style>
|
| 144 |
</head>
|
| 145 |
<body>
|
|
@@ -191,6 +296,36 @@
|
|
| 191 |
<input type="file" id="stickerInput" class="hidden-input" accept="image/png, image/jpeg" multiple>
|
| 192 |
|
| 193 |
<button class="btn-primary" id="downloadBtn">Скачать PNG</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
</div>
|
| 195 |
</div>
|
| 196 |
|
|
@@ -242,6 +377,35 @@
|
|
| 242 |
drawAll();
|
| 243 |
};
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
// ==========================================
|
| 246 |
// 1. ЛОГИКА БАЗОВОГО СЛОЯ (SVG/TEXT)
|
| 247 |
// ==========================================
|
|
@@ -319,11 +483,10 @@
|
|
| 319 |
};
|
| 320 |
reader.readAsDataURL(file);
|
| 321 |
});
|
| 322 |
-
stickerInput.value = '';
|
| 323 |
});
|
| 324 |
|
| 325 |
function addSticker(img) {
|
| 326 |
-
// Начальный размер: подгоняем под 1/3 канваса, но сохраняем пропорции
|
| 327 |
const maxSize = 200;
|
| 328 |
let w = img.width;
|
| 329 |
let h = img.height;
|
|
@@ -339,10 +502,9 @@
|
|
| 339 |
y: canvas.height / 2,
|
| 340 |
width: w,
|
| 341 |
height: h,
|
| 342 |
-
rotation: 0
|
| 343 |
});
|
| 344 |
|
| 345 |
-
// Выбираем только что добавленный
|
| 346 |
selectedStickerIndex = stickers.length - 1;
|
| 347 |
drawAll();
|
| 348 |
}
|
|
@@ -350,7 +512,6 @@
|
|
| 350 |
// Удаление по кнопке Delete
|
| 351 |
window.addEventListener('keydown', (e) => {
|
| 352 |
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedStickerIndex !== -1) {
|
| 353 |
-
// Не удаляем если фокус в инпуте текста
|
| 354 |
if (document.activeElement === textInput) return;
|
| 355 |
|
| 356 |
stickers.splice(selectedStickerIndex, 1);
|
|
@@ -385,7 +546,7 @@
|
|
| 385 |
ctx.restore();
|
| 386 |
});
|
| 387 |
|
| 388 |
-
// 4. Интерфейс управления
|
| 389 |
if (selectedStickerIndex !== -1) {
|
| 390 |
drawControls(stickers[selectedStickerIndex]);
|
| 391 |
}
|
|
@@ -450,14 +611,14 @@
|
|
| 450 |
ctx.lineTo(0, -halfH - 25);
|
| 451 |
ctx.stroke();
|
| 452 |
|
| 453 |
-
// Ручка вращения
|
| 454 |
ctx.fillStyle = '#E13839';
|
| 455 |
ctx.beginPath();
|
| 456 |
ctx.arc(0, -halfH - 25, 8, 0, Math.PI * 2);
|
| 457 |
ctx.fill();
|
| 458 |
ctx.stroke();
|
| 459 |
|
| 460 |
-
// Ручка изменения размера
|
| 461 |
ctx.beginPath();
|
| 462 |
ctx.arc(halfW, halfH, 8, 0, Math.PI * 2);
|
| 463 |
ctx.fill();
|
|
@@ -481,12 +642,9 @@
|
|
| 481 |
};
|
| 482 |
}
|
| 483 |
|
| 484 |
-
// Функция проверки попадания точки в повернутый прямоугольник
|
| 485 |
function isPointInRotatedRect(x, y, sticker) {
|
| 486 |
-
// Переводим координаты мыши в систему координат стикера
|
| 487 |
const dx = x - sticker.x;
|
| 488 |
const dy = y - sticker.y;
|
| 489 |
-
// Поворачиваем точку обратно
|
| 490 |
const rotatedX = dx * Math.cos(-sticker.rotation) - dy * Math.sin(-sticker.rotation);
|
| 491 |
const rotatedY = dx * Math.sin(-sticker.rotation) + dy * Math.cos(-sticker.rotation);
|
| 492 |
|
|
@@ -498,8 +656,6 @@
|
|
| 498 |
}
|
| 499 |
|
| 500 |
function getHandleHit(x, y, sticker) {
|
| 501 |
-
// Проверка попадания в ручки управления
|
| 502 |
-
// Аналогично, работаем в локальной системе координат
|
| 503 |
const dx = x - sticker.x;
|
| 504 |
const dy = y - sticker.y;
|
| 505 |
const rx = dx * Math.cos(-sticker.rotation) - dy * Math.sin(-sticker.rotation);
|
|
@@ -507,16 +663,12 @@
|
|
| 507 |
|
| 508 |
const halfW = sticker.width / 2;
|
| 509 |
const halfH = sticker.height / 2;
|
| 510 |
-
const handleRadius = 15;
|
| 511 |
|
| 512 |
-
// Rotate Handle (Top center + offset)
|
| 513 |
-
// Координаты ручки вращения в локальной системе: (0, -halfH - 25)
|
| 514 |
const rotHandleX = 0;
|
| 515 |
const rotHandleY = -halfH - 25;
|
| 516 |
if (Math.hypot(rx - rotHandleX, ry - rotHandleY) < handleRadius) return 'rotate';
|
| 517 |
|
| 518 |
-
// Resize Handle (Bottom right)
|
| 519 |
-
// Координаты: (halfW, halfH)
|
| 520 |
if (Math.hypot(rx - halfW, ry - halfH) < handleRadius) return 'resize';
|
| 521 |
|
| 522 |
return null;
|
|
@@ -525,29 +677,24 @@
|
|
| 525 |
canvas.addEventListener('mousedown', (e) => {
|
| 526 |
const {x, y} = getMousePos(e);
|
| 527 |
|
| 528 |
-
// 1. Проверяем, кликнули ли мы по ручкам текущего выделенного стикера
|
| 529 |
if (selectedStickerIndex !== -1) {
|
| 530 |
const sticker = stickers[selectedStickerIndex];
|
| 531 |
const handle = getHandleHit(x, y, sticker);
|
| 532 |
|
| 533 |
if (handle === 'rotate') {
|
| 534 |
isRotating = true;
|
| 535 |
-
// Угол между центром стикера и мышью
|
| 536 |
initialRotation = Math.atan2(y - sticker.y, x - sticker.x) - sticker.rotation;
|
| 537 |
return;
|
| 538 |
}
|
| 539 |
|
| 540 |
if (handle === 'resize') {
|
| 541 |
isResizing = true;
|
| 542 |
-
// Дистанция от центра
|
| 543 |
initialDistance = Math.hypot(x - sticker.x, y - sticker.y);
|
| 544 |
-
initialScale = sticker.width;
|
| 545 |
return;
|
| 546 |
}
|
| 547 |
}
|
| 548 |
|
| 549 |
-
// 2. Проверяем клик по телу стикера (выбор)
|
| 550 |
-
// Идем с конца (сверху), чтобы выбрать верхний
|
| 551 |
let clickedIndex = -1;
|
| 552 |
for (let i = stickers.length - 1; i >= 0; i--) {
|
| 553 |
if (isPointInRotatedRect(x, y, stickers[i])) {
|
|
@@ -558,8 +705,6 @@
|
|
| 558 |
|
| 559 |
if (clickedIndex !== -1) {
|
| 560 |
selectedStickerIndex = clickedIndex;
|
| 561 |
-
// Перемещаем выбранный стикер в конец массива (поднимаем наверх)
|
| 562 |
-
// Но только если это не тот же самый (чтобы не дергался)
|
| 563 |
if (clickedIndex !== stickers.length - 1) {
|
| 564 |
const movedSticker = stickers.splice(clickedIndex, 1)[0];
|
| 565 |
stickers.push(movedSticker);
|
|
@@ -571,7 +716,6 @@
|
|
| 571 |
startY = y;
|
| 572 |
drawAll();
|
| 573 |
} else {
|
| 574 |
-
// Клик в пустоту - снимаем выделение
|
| 575 |
selectedStickerIndex = -1;
|
| 576 |
drawAll();
|
| 577 |
}
|
|
@@ -581,9 +725,8 @@
|
|
| 581 |
const {x, y} = getMousePos(e);
|
| 582 |
const sticker = selectedStickerIndex !== -1 ? stickers[selectedStickerIndex] : null;
|
| 583 |
|
| 584 |
-
// Смена курсора
|
| 585 |
let cursor = 'default';
|
| 586 |
-
if (isRotating) cursor = 'grabbing';
|
| 587 |
else if (isResizing) cursor = 'nwse-resize';
|
| 588 |
else if (sticker) {
|
| 589 |
const handle = getHandleHit(x, y, sticker);
|
|
@@ -591,7 +734,6 @@
|
|
| 591 |
else if (handle === 'resize') cursor = 'nwse-resize';
|
| 592 |
else if (isPointInRotatedRect(x, y, sticker)) cursor = 'move';
|
| 593 |
} else {
|
| 594 |
-
// Check hover for any sticker
|
| 595 |
for (let i = stickers.length - 1; i >= 0; i--) {
|
| 596 |
if (isPointInRotatedRect(x, y, stickers[i])) {
|
| 597 |
cursor = 'move';
|
|
@@ -618,18 +760,15 @@
|
|
| 618 |
const currentDist = Math.hypot(x - sticker.x, y - sticker.y);
|
| 619 |
const ratio = currentDist / initialDistance;
|
| 620 |
|
| 621 |
-
// Сохраняем пропорции
|
| 622 |
const oldW = sticker.width;
|
| 623 |
const aspect = sticker.height / sticker.width;
|
| 624 |
|
| 625 |
-
// Новая ширина (не меньше 20px)
|
| 626 |
let newW = initialScale * ratio;
|
| 627 |
if (newW < 20) newW = 20;
|
| 628 |
|
| 629 |
sticker.width = newW;
|
| 630 |
sticker.height = newW * aspect;
|
| 631 |
|
| 632 |
-
// Обновляем базу для плавности, если нужно (здесь упрощенно)
|
| 633 |
drawAll();
|
| 634 |
}
|
| 635 |
});
|
|
@@ -644,7 +783,6 @@
|
|
| 644 |
// 5. СКАЧИВАНИЕ
|
| 645 |
// ==========================================
|
| 646 |
downloadBtn.addEventListener('click', () => {
|
| 647 |
-
// Снимаем выделение перед рендером
|
| 648 |
const prevSelection = selectedStickerIndex;
|
| 649 |
selectedStickerIndex = -1;
|
| 650 |
drawAll();
|
|
@@ -654,7 +792,6 @@
|
|
| 654 |
link.href = canvas.toDataURL('image/png');
|
| 655 |
link.click();
|
| 656 |
|
| 657 |
-
// Возвращаем выделение (опционально)
|
| 658 |
selectedStickerIndex = prevSelection;
|
| 659 |
drawAll();
|
| 660 |
});
|
|
|
|
| 6 |
<title>Swaga Icon Maker PRO</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;900&display=swap" rel="stylesheet">
|
| 10 |
|
| 11 |
<style>
|
| 12 |
body {
|
|
|
|
| 20 |
min-height: 100vh;
|
| 21 |
margin: 0;
|
| 22 |
padding: 20px;
|
| 23 |
+
user-select: none;
|
| 24 |
}
|
| 25 |
|
| 26 |
h1 { margin-bottom: 10px; }
|
|
|
|
| 42 |
border-radius: 20px;
|
| 43 |
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 44 |
background-color: #E13839;
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
.controls {
|
|
|
|
| 56 |
overflow-y: auto;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
/* Скроллбар для контролов */
|
| 60 |
+
.controls::-webkit-scrollbar { width: 8px; }
|
| 61 |
+
.controls::-webkit-scrollbar-track { background: #222; border-radius: 4px; }
|
| 62 |
+
.controls::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
|
| 63 |
+
|
| 64 |
.section-title {
|
| 65 |
font-size: 12px;
|
| 66 |
text-transform: uppercase;
|
|
|
|
| 69 |
border-bottom: 1px solid #444;
|
| 70 |
padding-bottom: 5px;
|
| 71 |
margin-top: 10px;
|
| 72 |
+
font-weight: 900;
|
| 73 |
}
|
| 74 |
|
| 75 |
.drop-zone {
|
|
|
|
| 145 |
text-align: center;
|
| 146 |
margin-top: 5px;
|
| 147 |
}
|
| 148 |
+
|
| 149 |
+
/* Стили для ссылки Disclaimer */
|
| 150 |
+
.disclaimer-link {
|
| 151 |
+
text-align: center;
|
| 152 |
+
margin-top: 10px;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.disclaimer-link span {
|
| 156 |
+
color: #555;
|
| 157 |
+
font-size: 11px;
|
| 158 |
+
cursor: pointer;
|
| 159 |
+
text-decoration: underline;
|
| 160 |
+
transition: color 0.2s;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.disclaimer-link span:hover {
|
| 164 |
+
color: #999;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* Стили модального окна */
|
| 168 |
+
.modal-overlay {
|
| 169 |
+
display: none; /* Скрыто по умолчанию */
|
| 170 |
+
position: fixed;
|
| 171 |
+
top: 0;
|
| 172 |
+
left: 0;
|
| 173 |
+
width: 100%;
|
| 174 |
+
height: 100%;
|
| 175 |
+
background: rgba(0, 0, 0, 0.8);
|
| 176 |
+
backdrop-filter: blur(5px);
|
| 177 |
+
z-index: 1000;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
opacity: 0;
|
| 181 |
+
transition: opacity 0.3s ease;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.modal-overlay.active {
|
| 185 |
+
opacity: 1;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.modal-content {
|
| 189 |
+
background: #2d2d2d;
|
| 190 |
+
width: 90%;
|
| 191 |
+
max-width: 600px;
|
| 192 |
+
padding: 30px;
|
| 193 |
+
border-radius: 20px;
|
| 194 |
+
border: 1px solid #444;
|
| 195 |
+
box-shadow: 0 20px 60px rgba(0,0,0,0.7);
|
| 196 |
+
position: relative;
|
| 197 |
+
display: flex;
|
| 198 |
+
flex-direction: column;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.modal-content h2 {
|
| 202 |
+
margin-top: 0;
|
| 203 |
+
color: #FFACC7;
|
| 204 |
+
font-size: 22px;
|
| 205 |
+
border-bottom: 1px solid #444;
|
| 206 |
+
padding-bottom: 15px;
|
| 207 |
+
margin-bottom: 15px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.legal-text {
|
| 211 |
+
font-size: 13px;
|
| 212 |
+
color: #ccc;
|
| 213 |
+
line-height: 1.6;
|
| 214 |
+
max-height: 60vh;
|
| 215 |
+
overflow-y: auto;
|
| 216 |
+
padding-right: 10px;
|
| 217 |
+
user-select: text; /* Чтобы можно было читать и выделять текст */
|
| 218 |
+
text-align: justify;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.legal-text p {
|
| 222 |
+
margin-bottom: 15px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.legal-text strong {
|
| 226 |
+
color: white;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
/* Скроллбар внутри модалки */
|
| 230 |
+
.legal-text::-webkit-scrollbar { width: 6px; }
|
| 231 |
+
.legal-text::-webkit-scrollbar-track { background: #222; }
|
| 232 |
+
.legal-text::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
|
| 233 |
+
|
| 234 |
+
.close-btn {
|
| 235 |
+
position: absolute;
|
| 236 |
+
top: 20px;
|
| 237 |
+
right: 20px;
|
| 238 |
+
background: transparent;
|
| 239 |
+
color: #777;
|
| 240 |
+
font-size: 24px;
|
| 241 |
+
width: auto;
|
| 242 |
+
padding: 0;
|
| 243 |
+
line-height: 1;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.close-btn:hover { color: white; }
|
| 247 |
+
|
| 248 |
</style>
|
| 249 |
</head>
|
| 250 |
<body>
|
|
|
|
| 296 |
<input type="file" id="stickerInput" class="hidden-input" accept="image/png, image/jpeg" multiple>
|
| 297 |
|
| 298 |
<button class="btn-primary" id="downloadBtn">Скачать PNG</button>
|
| 299 |
+
|
| 300 |
+
<!-- Кнопка вызова Disclaimer -->
|
| 301 |
+
<div class="disclaimer-link">
|
| 302 |
+
<span id="openDisclaimerBtn">Отказ от ответственности</span>
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<!-- Модальное окно -->
|
| 308 |
+
<div class="modal-overlay" id="modalOverlay">
|
| 309 |
+
<div class="modal-content">
|
| 310 |
+
<button class="close-btn" id="closeModalBtn">×</button>
|
| 311 |
+
<h2>Отказ от ответственности (Disclaimer)</h2>
|
| 312 |
+
<div class="legal-text">
|
| 313 |
+
<p><strong>1. Общие положения</strong><br>
|
| 314 |
+
Данный веб-инструмент ("Swaga Icon Maker PRO") предоставляется на условиях «как есть» (as is). Разработчик и владельцы ресурса не несут ответственности за любые прямые или косвенные убытки, возникшие в результате использования или невозможности использования данного сервиса.</p>
|
| 315 |
+
|
| 316 |
+
<p><strong>2. Пользовательский контент и Авторские права</strong><br>
|
| 317 |
+
Весь функционал сайта выполняется исключительно на стороне клиента (в вашем браузере). Мы не храним, не проверяем и не модерируем изображения, загружаемые пользователем.
|
| 318 |
+
Пользователь несет полную и единоличную ответственность за соблюдение законодательства об авторском праве и смежных правах при использовании загружаемых материалов (SVG, PNG, JPEG и других форматов). Загружая изображения, вы подтверждаете, что обладаете необходимыми правами на их использование и модификацию.</p>
|
| 319 |
+
|
| 320 |
+
<p><strong>3. Созданные материалы</strong><br>
|
| 321 |
+
Изображения, созданные с помощью данного инструмента, являются результатом творческой деятельности пользователя. Создатель сайта не претендует на права собственности созданных вами иконок, но также не несет ответственности за их дальнейшее использование, распространение или законность содержания.</p>
|
| 322 |
+
|
| 323 |
+
<p><strong>4. Технические ограничения</strong><br>
|
| 324 |
+
Разработчик не гарантирует бесперебойную работу сайта, отсутствие программных ошибок или полную совместимость с конкретными устройствами и браузерами.</p>
|
| 325 |
+
|
| 326 |
+
<p>Используя данный сайт, вы выражаете свое полное согласие с вышеуказанными условиями.</p>
|
| 327 |
+
</div>
|
| 328 |
+
<button class="btn-primary" id="acceptBtn" style="margin-top: 20px;">Всё понятно</button>
|
| 329 |
</div>
|
| 330 |
</div>
|
| 331 |
|
|
|
|
| 377 |
drawAll();
|
| 378 |
};
|
| 379 |
|
| 380 |
+
// ==========================================
|
| 381 |
+
// ЛОГИКА МОДАЛЬНОГО ОКНА
|
| 382 |
+
// ==========================================
|
| 383 |
+
const modalOverlay = document.getElementById('modalOverlay');
|
| 384 |
+
const openDisclaimerBtn = document.getElementById('openDisclaimerBtn');
|
| 385 |
+
const closeModalBtn = document.getElementById('closeModalBtn');
|
| 386 |
+
const acceptBtn = document.getElementById('acceptBtn');
|
| 387 |
+
|
| 388 |
+
function openModal() {
|
| 389 |
+
modalOverlay.style.display = 'flex';
|
| 390 |
+
// Небольшая задержка для анимации opacity
|
| 391 |
+
setTimeout(() => modalOverlay.classList.add('active'), 10);
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
function closeModal() {
|
| 395 |
+
modalOverlay.classList.remove('active');
|
| 396 |
+
setTimeout(() => modalOverlay.style.display = 'none', 300);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
openDisclaimerBtn.addEventListener('click', openModal);
|
| 400 |
+
closeModalBtn.addEventListener('click', closeModal);
|
| 401 |
+
acceptBtn.addEventListener('click', closeModal);
|
| 402 |
+
|
| 403 |
+
// Закрытие по клику вне окна
|
| 404 |
+
modalOverlay.addEventListener('click', (e) => {
|
| 405 |
+
if (e.target === modalOverlay) closeModal();
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
|
| 409 |
// ==========================================
|
| 410 |
// 1. ЛОГИКА БАЗОВОГО СЛОЯ (SVG/TEXT)
|
| 411 |
// ==========================================
|
|
|
|
| 483 |
};
|
| 484 |
reader.readAsDataURL(file);
|
| 485 |
});
|
| 486 |
+
stickerInput.value = '';
|
| 487 |
});
|
| 488 |
|
| 489 |
function addSticker(img) {
|
|
|
|
| 490 |
const maxSize = 200;
|
| 491 |
let w = img.width;
|
| 492 |
let h = img.height;
|
|
|
|
| 502 |
y: canvas.height / 2,
|
| 503 |
width: w,
|
| 504 |
height: h,
|
| 505 |
+
rotation: 0
|
| 506 |
});
|
| 507 |
|
|
|
|
| 508 |
selectedStickerIndex = stickers.length - 1;
|
| 509 |
drawAll();
|
| 510 |
}
|
|
|
|
| 512 |
// Удаление по кнопке Delete
|
| 513 |
window.addEventListener('keydown', (e) => {
|
| 514 |
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedStickerIndex !== -1) {
|
|
|
|
| 515 |
if (document.activeElement === textInput) return;
|
| 516 |
|
| 517 |
stickers.splice(selectedStickerIndex, 1);
|
|
|
|
| 546 |
ctx.restore();
|
| 547 |
});
|
| 548 |
|
| 549 |
+
// 4. Интерфейс управления
|
| 550 |
if (selectedStickerIndex !== -1) {
|
| 551 |
drawControls(stickers[selectedStickerIndex]);
|
| 552 |
}
|
|
|
|
| 611 |
ctx.lineTo(0, -halfH - 25);
|
| 612 |
ctx.stroke();
|
| 613 |
|
| 614 |
+
// Ручка вращения
|
| 615 |
ctx.fillStyle = '#E13839';
|
| 616 |
ctx.beginPath();
|
| 617 |
ctx.arc(0, -halfH - 25, 8, 0, Math.PI * 2);
|
| 618 |
ctx.fill();
|
| 619 |
ctx.stroke();
|
| 620 |
|
| 621 |
+
// Ручка изменения размера
|
| 622 |
ctx.beginPath();
|
| 623 |
ctx.arc(halfW, halfH, 8, 0, Math.PI * 2);
|
| 624 |
ctx.fill();
|
|
|
|
| 642 |
};
|
| 643 |
}
|
| 644 |
|
|
|
|
| 645 |
function isPointInRotatedRect(x, y, sticker) {
|
|
|
|
| 646 |
const dx = x - sticker.x;
|
| 647 |
const dy = y - sticker.y;
|
|
|
|
| 648 |
const rotatedX = dx * Math.cos(-sticker.rotation) - dy * Math.sin(-sticker.rotation);
|
| 649 |
const rotatedY = dx * Math.sin(-sticker.rotation) + dy * Math.cos(-sticker.rotation);
|
| 650 |
|
|
|
|
| 656 |
}
|
| 657 |
|
| 658 |
function getHandleHit(x, y, sticker) {
|
|
|
|
|
|
|
| 659 |
const dx = x - sticker.x;
|
| 660 |
const dy = y - sticker.y;
|
| 661 |
const rx = dx * Math.cos(-sticker.rotation) - dy * Math.sin(-sticker.rotation);
|
|
|
|
| 663 |
|
| 664 |
const halfW = sticker.width / 2;
|
| 665 |
const halfH = sticker.height / 2;
|
| 666 |
+
const handleRadius = 15;
|
| 667 |
|
|
|
|
|
|
|
| 668 |
const rotHandleX = 0;
|
| 669 |
const rotHandleY = -halfH - 25;
|
| 670 |
if (Math.hypot(rx - rotHandleX, ry - rotHandleY) < handleRadius) return 'rotate';
|
| 671 |
|
|
|
|
|
|
|
| 672 |
if (Math.hypot(rx - halfW, ry - halfH) < handleRadius) return 'resize';
|
| 673 |
|
| 674 |
return null;
|
|
|
|
| 677 |
canvas.addEventListener('mousedown', (e) => {
|
| 678 |
const {x, y} = getMousePos(e);
|
| 679 |
|
|
|
|
| 680 |
if (selectedStickerIndex !== -1) {
|
| 681 |
const sticker = stickers[selectedStickerIndex];
|
| 682 |
const handle = getHandleHit(x, y, sticker);
|
| 683 |
|
| 684 |
if (handle === 'rotate') {
|
| 685 |
isRotating = true;
|
|
|
|
| 686 |
initialRotation = Math.atan2(y - sticker.y, x - sticker.x) - sticker.rotation;
|
| 687 |
return;
|
| 688 |
}
|
| 689 |
|
| 690 |
if (handle === 'resize') {
|
| 691 |
isResizing = true;
|
|
|
|
| 692 |
initialDistance = Math.hypot(x - sticker.x, y - sticker.y);
|
| 693 |
+
initialScale = sticker.width;
|
| 694 |
return;
|
| 695 |
}
|
| 696 |
}
|
| 697 |
|
|
|
|
|
|
|
| 698 |
let clickedIndex = -1;
|
| 699 |
for (let i = stickers.length - 1; i >= 0; i--) {
|
| 700 |
if (isPointInRotatedRect(x, y, stickers[i])) {
|
|
|
|
| 705 |
|
| 706 |
if (clickedIndex !== -1) {
|
| 707 |
selectedStickerIndex = clickedIndex;
|
|
|
|
|
|
|
| 708 |
if (clickedIndex !== stickers.length - 1) {
|
| 709 |
const movedSticker = stickers.splice(clickedIndex, 1)[0];
|
| 710 |
stickers.push(movedSticker);
|
|
|
|
| 716 |
startY = y;
|
| 717 |
drawAll();
|
| 718 |
} else {
|
|
|
|
| 719 |
selectedStickerIndex = -1;
|
| 720 |
drawAll();
|
| 721 |
}
|
|
|
|
| 725 |
const {x, y} = getMousePos(e);
|
| 726 |
const sticker = selectedStickerIndex !== -1 ? stickers[selectedStickerIndex] : null;
|
| 727 |
|
|
|
|
| 728 |
let cursor = 'default';
|
| 729 |
+
if (isRotating) cursor = 'grabbing';
|
| 730 |
else if (isResizing) cursor = 'nwse-resize';
|
| 731 |
else if (sticker) {
|
| 732 |
const handle = getHandleHit(x, y, sticker);
|
|
|
|
| 734 |
else if (handle === 'resize') cursor = 'nwse-resize';
|
| 735 |
else if (isPointInRotatedRect(x, y, sticker)) cursor = 'move';
|
| 736 |
} else {
|
|
|
|
| 737 |
for (let i = stickers.length - 1; i >= 0; i--) {
|
| 738 |
if (isPointInRotatedRect(x, y, stickers[i])) {
|
| 739 |
cursor = 'move';
|
|
|
|
| 760 |
const currentDist = Math.hypot(x - sticker.x, y - sticker.y);
|
| 761 |
const ratio = currentDist / initialDistance;
|
| 762 |
|
|
|
|
| 763 |
const oldW = sticker.width;
|
| 764 |
const aspect = sticker.height / sticker.width;
|
| 765 |
|
|
|
|
| 766 |
let newW = initialScale * ratio;
|
| 767 |
if (newW < 20) newW = 20;
|
| 768 |
|
| 769 |
sticker.width = newW;
|
| 770 |
sticker.height = newW * aspect;
|
| 771 |
|
|
|
|
| 772 |
drawAll();
|
| 773 |
}
|
| 774 |
});
|
|
|
|
| 783 |
// 5. СКАЧИВАНИЕ
|
| 784 |
// ==========================================
|
| 785 |
downloadBtn.addEventListener('click', () => {
|
|
|
|
| 786 |
const prevSelection = selectedStickerIndex;
|
| 787 |
selectedStickerIndex = -1;
|
| 788 |
drawAll();
|
|
|
|
| 792 |
link.href = canvas.toDataURL('image/png');
|
| 793 |
link.click();
|
| 794 |
|
|
|
|
| 795 |
selectedStickerIndex = prevSelection;
|
| 796 |
drawAll();
|
| 797 |
});
|