sterepando commited on
Commit
56d22ed
·
verified ·
1 Parent(s): 7a3252a

Upload ai_studio_code (4).html

Browse files
Files changed (1) hide show
  1. ai_studio_code (4).html +664 -0
ai_studio_code (4).html ADDED
@@ -0,0 +1,664 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
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 {
13
+ font-family: 'Nunito', sans-serif;
14
+ background-color: #1e1e1e;
15
+ color: white;
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ min-height: 100vh;
21
+ margin: 0;
22
+ padding: 20px;
23
+ user-select: none; /* Чтобы текст не выделялся при драге */
24
+ }
25
+
26
+ h1 { margin-bottom: 10px; }
27
+
28
+ .container {
29
+ display: flex;
30
+ gap: 30px;
31
+ flex-wrap: wrap;
32
+ justify-content: center;
33
+ }
34
+
35
+ .preview-area {
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ }
40
+
41
+ canvas {
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 {
49
+ background: #2d2d2d;
50
+ padding: 20px;
51
+ border-radius: 15px;
52
+ width: 300px;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 15px;
56
+ max-height: 80vh;
57
+ overflow-y: auto;
58
+ }
59
+
60
+ .section-title {
61
+ font-size: 12px;
62
+ text-transform: uppercase;
63
+ color: #777;
64
+ letter-spacing: 1px;
65
+ border-bottom: 1px solid #444;
66
+ padding-bottom: 5px;
67
+ margin-top: 10px;
68
+ }
69
+
70
+ .drop-zone {
71
+ border: 3px dashed #555;
72
+ border-radius: 10px;
73
+ padding: 20px;
74
+ text-align: center;
75
+ transition: 0.3s;
76
+ cursor: pointer;
77
+ font-size: 14px;
78
+ }
79
+
80
+ .drop-zone:hover, .drop-zone.dragover {
81
+ border-color: #E13839;
82
+ background-color: rgba(225, 56, 57, 0.1);
83
+ }
84
+
85
+ label {
86
+ font-size: 14px;
87
+ color: #aaa;
88
+ margin-bottom: 5px;
89
+ display: block;
90
+ }
91
+
92
+ input[type="text"] {
93
+ width: 100%;
94
+ padding: 10px;
95
+ border-radius: 5px;
96
+ border: none;
97
+ background: #444;
98
+ color: white;
99
+ font-family: 'Nunito', sans-serif;
100
+ font-weight: 900;
101
+ font-size: 16px;
102
+ box-sizing: border-box;
103
+ }
104
+
105
+ input[type="range"] {
106
+ width: 100%;
107
+ accent-color: #E13839;
108
+ }
109
+
110
+ button {
111
+ border: none;
112
+ padding: 15px;
113
+ border-radius: 8px;
114
+ font-weight: 900;
115
+ font-size: 16px;
116
+ cursor: pointer;
117
+ transition: transform 0.1s;
118
+ width: 100%;
119
+ }
120
+
121
+ .btn-primary {
122
+ background: linear-gradient(225deg, #FFFFFF 0%, #FFACC7 100%);
123
+ color: #E13839;
124
+ margin-top: 20px;
125
+ }
126
+
127
+ .btn-secondary {
128
+ background: #444;
129
+ color: white;
130
+ font-size: 14px;
131
+ padding: 10px;
132
+ }
133
+
134
+ button:active { transform: scale(0.98); }
135
+ .hidden-input { display: none; }
136
+
137
+ .hint {
138
+ font-size: 12px;
139
+ color: #666;
140
+ text-align: center;
141
+ margin-top: 5px;
142
+ }
143
+ </style>
144
+ </head>
145
+ <body>
146
+
147
+ <h1>MandreIcon Creator PRO</h1>
148
+
149
+ <div class="container">
150
+ <!-- Область превью -->
151
+ <div class="preview-area">
152
+ <canvas id="mainCanvas" width="512" height="512"></canvas>
153
+ <p class="hint">Кликни на стикер для выделения. <br>Delete = удалить.</p>
154
+ </div>
155
+
156
+ <!-- Панель управления -->
157
+ <div class="controls">
158
+
159
+ <div class="section-title">Фон (Base Icon)</div>
160
+
161
+ <!-- Драг-н-дроп SVG -->
162
+ <div class="drop-zone" id="dropZoneBase">
163
+ <p><b>SVG</b> (Градиентный фон)<br>Клик или Drop</p>
164
+ <input type="file" id="fileInputBase" class="hidden-input" accept=".svg">
165
+ </div>
166
+
167
+ <!-- Текстовые настройки -->
168
+ <div>
169
+ <input type="text" id="textInput" placeholder="Или текст (A)">
170
+ </div>
171
+
172
+ <!-- Ползунки настроек фона -->
173
+ <div>
174
+ <label>Размер фона: <span id="scaleVal">100%</span></label>
175
+ <input type="range" id="scaleRange" min="10" max="200" value="100">
176
+ </div>
177
+ <div style="display: flex; gap: 10px;">
178
+ <div style="flex:1">
179
+ <label>X:</label>
180
+ <input type="range" id="offsetXRange" min="-256" max="256" value="0">
181
+ </div>
182
+ <div style="flex:1">
183
+ <label>Y:</label>
184
+ <input type="range" id="offsetYRange" min="-256" max="256" value="0">
185
+ </div>
186
+ </div>
187
+
188
+ <div class="section-title">Стикеры (PNG)</div>
189
+
190
+ <button class="btn-secondary" id="addStickerBtn">+ Добавить PNG</button>
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
+
197
+ <script>
198
+ const canvas = document.getElementById('mainCanvas');
199
+ const ctx = canvas.getContext('2d');
200
+
201
+ // Элементы UI
202
+ const dropZoneBase = document.getElementById('dropZoneBase');
203
+ const fileInputBase = document.getElementById('fileInputBase');
204
+ const textInput = document.getElementById('textInput');
205
+ const scaleRange = document.getElementById('scaleRange');
206
+ const offsetYRange = document.getElementById('offsetYRange');
207
+ const offsetXRange = document.getElementById('offsetXRange');
208
+ const downloadBtn = document.getElementById('downloadBtn');
209
+
210
+ const addStickerBtn = document.getElementById('addStickerBtn');
211
+ const stickerInput = document.getElementById('stickerInput');
212
+
213
+ // Константы
214
+ const BG_COLOR = '#E13839';
215
+ const GRADIENT_START = '#FFFFFF';
216
+ const GRADIENT_END = '#FFACC7';
217
+
218
+ // Состояние "Базового слоя" (градиент)
219
+ let baseLayer = {
220
+ type: 'none', // 'image' | 'text' | 'none'
221
+ object: null,
222
+ scale: 1,
223
+ x: 0,
224
+ y: 0
225
+ };
226
+
227
+ // Состояние "Стикеров" (PNG поверх)
228
+ let stickers = [];
229
+ let selectedStickerIndex = -1;
230
+
231
+ // Переменные для манипуляции мышью
232
+ let isDragging = false;
233
+ let isRotating = false;
234
+ let isResizing = false;
235
+ let startX, startY;
236
+ let initialRotation = 0;
237
+ let initialDistance = 0;
238
+ let initialScale = 1;
239
+
240
+ // Инициализация
241
+ window.onload = () => {
242
+ drawAll();
243
+ };
244
+
245
+ // ==========================================
246
+ // 1. ЛОГИКА БАЗОВОГО СЛОЯ (SVG/TEXT)
247
+ // ==========================================
248
+
249
+ function updateBaseLayerParams() {
250
+ baseLayer.scale = parseInt(scaleRange.value) / 100;
251
+ baseLayer.x = parseInt(offsetXRange.value);
252
+ baseLayer.y = parseInt(offsetYRange.value);
253
+ document.getElementById('scaleVal').innerText = scaleRange.value + '%';
254
+ drawAll();
255
+ }
256
+
257
+ scaleRange.addEventListener('input', updateBaseLayerParams);
258
+ offsetXRange.addEventListener('input', updateBaseLayerParams);
259
+ offsetYRange.addEventListener('input', updateBaseLayerParams);
260
+
261
+ textInput.addEventListener('input', () => {
262
+ if (textInput.value) {
263
+ baseLayer.type = 'text';
264
+ baseLayer.object = textInput.value;
265
+ drawAll();
266
+ } else {
267
+ baseLayer.type = 'none';
268
+ drawAll();
269
+ }
270
+ });
271
+
272
+ // Загрузка SVG для базы
273
+ dropZoneBase.addEventListener('click', () => fileInputBase.click());
274
+ dropZoneBase.addEventListener('dragover', (e) => { e.preventDefault(); dropZoneBase.classList.add('dragover'); });
275
+ dropZoneBase.addEventListener('dragleave', () => dropZoneBase.classList.remove('dragover'));
276
+ dropZoneBase.addEventListener('drop', (e) => {
277
+ e.preventDefault();
278
+ dropZoneBase.classList.remove('dragover');
279
+ handleBaseFile(e.dataTransfer.files[0]);
280
+ });
281
+ fileInputBase.addEventListener('change', (e) => handleBaseFile(e.target.files[0]));
282
+
283
+ function handleBaseFile(file) {
284
+ if (!file) return;
285
+ if (!file.type.includes('svg')) { alert('Сюда только SVG!'); return; }
286
+ const reader = new FileReader();
287
+ reader.onload = (ev) => {
288
+ const img = new Image();
289
+ img.onload = () => {
290
+ baseLayer.type = 'image';
291
+ baseLayer.object = img;
292
+ textInput.value = ''; // Очистить текст
293
+ // Сброс слайдеров
294
+ scaleRange.value = 100;
295
+ offsetXRange.value = 0;
296
+ offsetYRange.value = 0;
297
+ updateBaseLayerParams();
298
+ };
299
+ img.src = ev.target.result;
300
+ };
301
+ reader.readAsDataURL(file);
302
+ }
303
+
304
+
305
+ // ==========================================
306
+ // 2. ЛОГИКА СТИКЕРОВ (PNG)
307
+ // ==========================================
308
+
309
+ addStickerBtn.addEventListener('click', () => stickerInput.click());
310
+ stickerInput.addEventListener('change', (e) => {
311
+ Array.from(e.target.files).forEach(file => {
312
+ const reader = new FileReader();
313
+ reader.onload = (ev) => {
314
+ const img = new Image();
315
+ img.onload = () => {
316
+ addSticker(img);
317
+ };
318
+ img.src = ev.target.result;
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;
330
+ if (w > h) {
331
+ if (w > maxSize) { h *= maxSize / w; w = maxSize; }
332
+ } else {
333
+ if (h > maxSize) { w *= maxSize / h; h = maxSize; }
334
+ }
335
+
336
+ stickers.push({
337
+ img: img,
338
+ x: canvas.width / 2,
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
+ }
349
+
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);
357
+ selectedStickerIndex = -1;
358
+ drawAll();
359
+ }
360
+ });
361
+
362
+
363
+ // ==========================================
364
+ // 3. ГЛАВНАЯ ОТРИСОВКА
365
+ // ==========================================
366
+
367
+ function drawAll() {
368
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
369
+
370
+ // 1. Фон
371
+ ctx.fillStyle = BG_COLOR;
372
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
373
+
374
+ // 2. Базовый градиентный слой
375
+ if (baseLayer.type !== 'none') {
376
+ drawBaseLayer();
377
+ }
378
+
379
+ // 3. Стикеры
380
+ stickers.forEach((sticker, index) => {
381
+ ctx.save();
382
+ ctx.translate(sticker.x, sticker.y);
383
+ ctx.rotate(sticker.rotation);
384
+ ctx.drawImage(sticker.img, -sticker.width/2, -sticker.height/2, sticker.width, sticker.height);
385
+ ctx.restore();
386
+ });
387
+
388
+ // 4. Интерфейс управления (если выбран стикер и мы не скачиваем файл)
389
+ if (selectedStickerIndex !== -1) {
390
+ drawControls(stickers[selectedStickerIndex]);
391
+ }
392
+ }
393
+
394
+ function drawBaseLayer() {
395
+ const tempCanvas = document.createElement('canvas');
396
+ tempCanvas.width = canvas.width;
397
+ tempCanvas.height = canvas.height;
398
+ const tempCtx = tempCanvas.getContext('2d');
399
+
400
+ const cx = tempCanvas.width / 2;
401
+ const cy = tempCanvas.height / 2;
402
+
403
+ tempCtx.translate(cx + baseLayer.x, cy + baseLayer.y);
404
+ tempCtx.scale(baseLayer.scale, baseLayer.scale);
405
+
406
+ if (baseLayer.type === 'image') {
407
+ const w = 300;
408
+ const h = 300 * (baseLayer.object.height / baseLayer.object.width);
409
+ tempCtx.drawImage(baseLayer.object, -w/2, -h/2, w, h);
410
+ } else if (baseLayer.type === 'text') {
411
+ tempCtx.font = "900 300px 'Nunito'";
412
+ tempCtx.textAlign = "center";
413
+ tempCtx.textBaseline = "middle";
414
+ tempCtx.fillStyle = "#000";
415
+ tempCtx.fillText(baseLayer.object, 0, 20);
416
+ }
417
+
418
+ tempCtx.globalCompositeOperation = 'source-in';
419
+ const gradient = tempCtx.createLinearGradient(canvas.width, 0, 0, canvas.height);
420
+ gradient.addColorStop(0, GRADIENT_START);
421
+ gradient.addColorStop(1, GRADIENT_END);
422
+ tempCtx.fillStyle = gradient;
423
+
424
+ tempCtx.setTransform(1, 0, 0, 1, 0, 0);
425
+ tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
426
+
427
+ ctx.drawImage(tempCanvas, 0, 0);
428
+ }
429
+
430
+ function drawControls(sticker) {
431
+ ctx.save();
432
+ ctx.translate(sticker.x, sticker.y);
433
+ ctx.rotate(sticker.rotation);
434
+
435
+ const w = sticker.width;
436
+ const h = sticker.height;
437
+ const halfW = w / 2;
438
+ const halfH = h / 2;
439
+
440
+ // Рамка
441
+ ctx.strokeStyle = '#FFFFFF';
442
+ ctx.lineWidth = 2;
443
+ ctx.setLineDash([5, 5]);
444
+ ctx.strokeRect(-halfW, -halfH, w, h);
445
+
446
+ // Линия к вращалке
447
+ ctx.setLineDash([]);
448
+ ctx.beginPath();
449
+ ctx.moveTo(0, -halfH);
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();
464
+ ctx.stroke();
465
+
466
+ ctx.restore();
467
+ }
468
+
469
+
470
+ // ==========================================
471
+ // 4. ОБРАБОТКА МЫШИ (Canvas)
472
+ // ==========================================
473
+
474
+ function getMousePos(evt) {
475
+ const rect = canvas.getBoundingClientRect();
476
+ const scaleX = canvas.width / rect.width;
477
+ const scaleY = canvas.height / rect.height;
478
+ return {
479
+ x: (evt.clientX - rect.left) * scaleX,
480
+ y: (evt.clientY - rect.top) * scaleY
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
+
493
+ const halfW = sticker.width / 2;
494
+ const halfH = sticker.height / 2;
495
+
496
+ return rotatedX >= -halfW && rotatedX <= halfW &&
497
+ rotatedY >= -halfH && rotatedY <= halfH;
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);
506
+ const ry = dx * Math.sin(-sticker.rotation) + dy * Math.cos(-sticker.rotation);
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;
523
+ }
524
+
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])) {
554
+ clickedIndex = i;
555
+ break;
556
+ }
557
+ }
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);
566
+ selectedStickerIndex = stickers.length - 1;
567
+ }
568
+
569
+ isDragging = true;
570
+ startX = x;
571
+ startY = y;
572
+ drawAll();
573
+ } else {
574
+ // Клик в пустоту - снимаем выделение
575
+ selectedStickerIndex = -1;
576
+ drawAll();
577
+ }
578
+ });
579
+
580
+ canvas.addEventListener('mousemove', (e) => {
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'; // 'alias' для вращения
587
+ else if (isResizing) cursor = 'nwse-resize';
588
+ else if (sticker) {
589
+ const handle = getHandleHit(x, y, sticker);
590
+ if (handle === 'rotate') cursor = 'grab';
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';
598
+ break;
599
+ }
600
+ }
601
+ }
602
+ canvas.style.cursor = cursor;
603
+
604
+
605
+ if (!sticker) return;
606
+
607
+ if (isDragging) {
608
+ sticker.x += x - startX;
609
+ sticker.y += y - startY;
610
+ startX = x;
611
+ startY = y;
612
+ drawAll();
613
+ } else if (isRotating) {
614
+ const angle = Math.atan2(y - sticker.y, x - sticker.x);
615
+ sticker.rotation = angle - initialRotation;
616
+ drawAll();
617
+ } else if (isResizing) {
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
+ });
636
+
637
+ window.addEventListener('mouseup', () => {
638
+ isDragging = false;
639
+ isRotating = false;
640
+ isResizing = false;
641
+ });
642
+
643
+ // ==========================================
644
+ // 5. СКАЧИВАНИЕ
645
+ // ==========================================
646
+ downloadBtn.addEventListener('click', () => {
647
+ // Снимаем выделение перед рендером
648
+ const prevSelection = selectedStickerIndex;
649
+ selectedStickerIndex = -1;
650
+ drawAll();
651
+
652
+ const link = document.createElement('a');
653
+ link.download = 'SWAGA_ICON_PRO.png';
654
+ link.href = canvas.toDataURL('image/png');
655
+ link.click();
656
+
657
+ // Возвращаем выделение (опционально)
658
+ selectedStickerIndex = prevSelection;
659
+ drawAll();
660
+ });
661
+
662
+ </script>
663
+ </body>
664
+ </html>