Spaces:
Paused
Paused
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Swaga Icon Maker</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| font-family: 'Nunito', sans-serif; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background-color: #1e1e1e; | |
| color: white; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 100vh; | |
| margin: 0; | |
| padding: 20px; | |
| user-select: none; | |
| } | |
| h1 { margin-bottom: 10px; font-weight: 900; } | |
| .container { | |
| display: flex; | |
| gap: 30px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .preview-area { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| canvas { | |
| border-radius: 20px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| background-color: #E13839; | |
| } | |
| .controls { | |
| background: #2d2d2d; | |
| padding: 20px; | |
| border-radius: 15px; | |
| width: 300px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| } | |
| .controls::-webkit-scrollbar { width: 8px; } | |
| .controls::-webkit-scrollbar-track { background: #222; border-radius: 4px; } | |
| .controls::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; } | |
| .section-title { | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| color: #777; | |
| letter-spacing: 1px; | |
| border-bottom: 1px solid #444; | |
| padding-bottom: 5px; | |
| margin-top: 10px; | |
| font-weight: 900; | |
| } | |
| .drop-zone { | |
| border: 3px dashed #555; | |
| border-radius: 10px; | |
| padding: 20px; | |
| text-align: center; | |
| transition: 0.3s; | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| .drop-zone:hover, .drop-zone.dragover { | |
| border-color: #E13839; | |
| background-color: rgba(225, 56, 57, 0.1); | |
| } | |
| label { | |
| font-size: 14px; | |
| color: #aaa; | |
| margin-bottom: 5px; | |
| display: block; | |
| } | |
| input[type="text"] { | |
| width: 100%; | |
| padding: 10px; | |
| border-radius: 5px; | |
| border: none; | |
| background: #444; | |
| color: white; | |
| font-weight: 900; | |
| font-size: 16px; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| accent-color: #E13839; | |
| } | |
| button { | |
| border: none; | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-weight: 900; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: transform 0.1s; | |
| width: 100%; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(225deg, #FFFFFF 0%, #FFACC7 100%); | |
| color: #E13839; | |
| margin-top: 20px; | |
| } | |
| .btn-secondary { | |
| background: #444; | |
| color: white; | |
| font-size: 14px; | |
| padding: 10px; | |
| } | |
| button:active { transform: scale(0.98); } | |
| .hidden-input { display: none; } | |
| .hint { | |
| font-size: 12px; | |
| color: #666; | |
| text-align: center; | |
| margin-top: 5px; | |
| } | |
| .disclaimer-link { | |
| text-align: center; | |
| margin-top: 10px; | |
| } | |
| .disclaimer-link span { | |
| color: #555; | |
| font-size: 11px; | |
| cursor: pointer; | |
| text-decoration: underline; | |
| transition: color 0.2s; | |
| } | |
| .disclaimer-link span:hover { | |
| color: #999; | |
| } | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(5px); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .modal-overlay.active { | |
| opacity: 1; | |
| } | |
| .modal-content { | |
| background: #2d2d2d; | |
| width: 90%; | |
| max-width: 600px; | |
| padding: 30px; | |
| border-radius: 20px; | |
| border: 1px solid #444; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.7); | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .modal-content h2 { | |
| margin-top: 0; | |
| color: #FFACC7; | |
| font-size: 22px; | |
| border-bottom: 1px solid #444; | |
| padding-bottom: 15px; | |
| margin-bottom: 15px; | |
| font-weight: 900; | |
| } | |
| .legal-text { | |
| font-size: 13px; | |
| color: #ccc; | |
| line-height: 1.6; | |
| max-height: 60vh; | |
| overflow-y: auto; | |
| padding-right: 10px; | |
| user-select: text; | |
| text-align: justify; | |
| } | |
| .legal-text p { | |
| margin-bottom: 15px; | |
| } | |
| .legal-text strong { | |
| color: white; | |
| font-weight: 900; | |
| } | |
| .legal-text::-webkit-scrollbar { width: 6px; } | |
| .legal-text::-webkit-scrollbar-track { background: #222; } | |
| .legal-text::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; } | |
| .close-btn { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: transparent; | |
| color: #777; | |
| font-size: 24px; | |
| width: auto; | |
| padding: 0; | |
| line-height: 1; | |
| } | |
| .close-btn:hover { color: white; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>MandreIcon Creator</h1> | |
| <div class="container"> | |
| <div class="preview-area"> | |
| <canvas id="mainCanvas" width="512" height="512"></canvas> | |
| <p class="hint">Кликни на стикер для выделения. <br>Delete = удалить.</p> | |
| </div> | |
| <div class="controls"> | |
| <div class="section-title">Фон (Base Icon)</div> | |
| <div class="drop-zone" id="dropZoneBase"> | |
| <p><b>SVG</b> (Градиентный фон)<br>Клик или Drop</p> | |
| <input type="file" id="fileInputBase" class="hidden-input" accept=".svg"> | |
| </div> | |
| <div> | |
| <input type="text" id="textInput" placeholder="Или текст (A)"> | |
| </div> | |
| <div> | |
| <label>Размер хуйни этой: <span id="scaleVal">100%</span></label> | |
| <input type="range" id="scaleRange" min="10" max="200" value="100"> | |
| </div> | |
| <div style="display: flex; gap: 10px;"> | |
| <div style="flex:1"> | |
| <label>X:</label> | |
| <input type="range" id="offsetXRange" min="-256" max="256" value="0"> | |
| </div> | |
| <div style="flex:1"> | |
| <label>Y:</label> | |
| <input type="range" id="offsetYRange" min="-256" max="256" value="0"> | |
| </div> | |
| </div> | |
| <div class="section-title">Стикеры (PNG)</div> | |
| <button class="btn-secondary" id="addStickerBtn">+ Добавить PNG</button> | |
| <input type="file" id="stickerInput" class="hidden-input" accept="image/png, image/jpeg" multiple> | |
| <button class="btn-primary" id="downloadBtn">Скачать PNG</button> | |
| <div class="disclaimer-link"> | |
| <span id="openDisclaimerBtn">Отказ от ответственности</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-overlay" id="modalOverlay"> | |
| <div class="modal-content"> | |
| <button class="close-btn" id="closeModalBtn">×</button> | |
| <h2>Отказ от ответственности (Disclaimer)</h2> | |
| <div class="legal-text"> | |
| <p><strong>1. Общие положения</strong><br> | |
| Данный веб-инструмент ("Swaga Icon Maker") предоставляется на условиях «как есть» (as is). Разработчик и владельцы ресурса не несут ответственности за любые прямые или косвенные убытки, возникшие в результате использования или невозможности использования данного сервиса.</p> | |
| <p><strong>2. Пользовательский контент и Авторские права</strong><br> | |
| Весь функционал сайта выполняется исключительно на стороне клиента (в вашем браузере). Мы не храним, не проверяем и не модерируем изображения, загружаемые пользователем. | |
| Пользователь несет полную и единоличную ответственность за соблюдение законодательства об авторском праве и смежных правах при использовании загружаемых материалов (SVG, PNG, JPEG и других форматов). Загружая изображения, вы подтверждаете, что обладаете необходимыми правами на их использование и модификацию.</p> | |
| <p><strong>3. Созданные материалы</strong><br> | |
| Изображения, созданные с помощью данного инструмента, являются результатом творческой деятельности пользователя. Создатель сайта не претендует на права собственности созданных вами иконок, но также не несет ответственности за их дальнейшее использование, распространение или законность содержания.</p> | |
| <p><strong>4. Технические ограничения</strong><br> | |
| Разработчик не гарантирует бесперебойную работу сайта, отсутствие программных ошибок или полную совместимость с конкретными устройствами и браузерами.</p> | |
| <p><strong>5. Различия в законодательстве стран</strong><br> | |
| Сайт доступен пользователям по всему миру. Пользователь признает, что законы, регулирующие авторское право, использование символики и распространение контента, могут существенно различаться в зависимости от юрисдикции. Пользователь самостоятельно несет ответственность за соблюдение законодательства страны своего проживания, а также законодательства стран, на территории которых планируется использование созданных материалов. Разработчик не несет ответственности за нарушение пользователем локальных законов.</p> | |
| <p>Используя данный сайт, вы выражаете свое полное согласие с вышеуказанными условиями.</p> | |
| </div> | |
| <button class="btn-primary" id="acceptBtn" style="margin-top: 20px;">Всё понятно</button> | |
| </div> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('mainCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const dropZoneBase = document.getElementById('dropZoneBase'); | |
| const fileInputBase = document.getElementById('fileInputBase'); | |
| const textInput = document.getElementById('textInput'); | |
| const scaleRange = document.getElementById('scaleRange'); | |
| const offsetYRange = document.getElementById('offsetYRange'); | |
| const offsetXRange = document.getElementById('offsetXRange'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const addStickerBtn = document.getElementById('addStickerBtn'); | |
| const stickerInput = document.getElementById('stickerInput'); | |
| const BG_COLOR = '#E13839'; | |
| const GRADIENT_START = '#FFFFFF'; | |
| const GRADIENT_END = '#FFACC7'; | |
| let baseLayer = { | |
| type: 'none', | |
| object: null, | |
| scale: 1, | |
| x: 0, | |
| y: 0 | |
| }; | |
| let stickers = []; | |
| let selectedStickerIndex = -1; | |
| let isDragging = false; | |
| let isRotating = false; | |
| let isResizing = false; | |
| let startX, startY; | |
| let initialRotation = 0; | |
| let initialDistance = 0; | |
| let initialScale = 1; | |
| window.onload = () => { | |
| drawAll(); | |
| }; | |
| const modalOverlay = document.getElementById('modalOverlay'); | |
| const openDisclaimerBtn = document.getElementById('openDisclaimerBtn'); | |
| const closeModalBtn = document.getElementById('closeModalBtn'); | |
| const acceptBtn = document.getElementById('acceptBtn'); | |
| function openModal() { | |
| modalOverlay.style.display = 'flex'; | |
| setTimeout(() => modalOverlay.classList.add('active'), 10); | |
| } | |
| function closeModal() { | |
| modalOverlay.classList.remove('active'); | |
| setTimeout(() => modalOverlay.style.display = 'none', 300); | |
| } | |
| openDisclaimerBtn.addEventListener('click', openModal); | |
| closeModalBtn.addEventListener('click', closeModal); | |
| acceptBtn.addEventListener('click', closeModal); | |
| modalOverlay.addEventListener('click', (e) => { | |
| if (e.target === modalOverlay) closeModal(); | |
| }); | |
| function updateBaseLayerParams() { | |
| baseLayer.scale = parseInt(scaleRange.value) / 100; | |
| baseLayer.x = parseInt(offsetXRange.value); | |
| baseLayer.y = parseInt(offsetYRange.value); | |
| document.getElementById('scaleVal').innerText = scaleRange.value + '%'; | |
| drawAll(); | |
| } | |
| scaleRange.addEventListener('input', updateBaseLayerParams); | |
| offsetXRange.addEventListener('input', updateBaseLayerParams); | |
| offsetYRange.addEventListener('input', updateBaseLayerParams); | |
| textInput.addEventListener('input', () => { | |
| if (textInput.value) { | |
| baseLayer.type = 'text'; | |
| baseLayer.object = textInput.value; | |
| drawAll(); | |
| } else { | |
| baseLayer.type = 'none'; | |
| drawAll(); | |
| } | |
| }); | |
| dropZoneBase.addEventListener('click', () => fileInputBase.click()); | |
| dropZoneBase.addEventListener('dragover', (e) => { e.preventDefault(); dropZoneBase.classList.add('dragover'); }); | |
| dropZoneBase.addEventListener('dragleave', () => dropZoneBase.classList.remove('dragover')); | |
| dropZoneBase.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZoneBase.classList.remove('dragover'); | |
| handleBaseFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInputBase.addEventListener('change', (e) => handleBaseFile(e.target.files[0])); | |
| function handleBaseFile(file) { | |
| if (!file) return; | |
| if (!file.type.includes('svg')) { alert('Сюда только SVG!'); return; } | |
| const reader = new FileReader(); | |
| reader.onload = (ev) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| baseLayer.type = 'image'; | |
| baseLayer.object = img; | |
| textInput.value = ''; | |
| scaleRange.value = 100; | |
| offsetXRange.value = 0; | |
| offsetYRange.value = 0; | |
| updateBaseLayerParams(); | |
| }; | |
| img.src = ev.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| addStickerBtn.addEventListener('click', () => stickerInput.click()); | |
| stickerInput.addEventListener('change', (e) => { | |
| Array.from(e.target.files).forEach(file => { | |
| const reader = new FileReader(); | |
| reader.onload = (ev) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| addSticker(img); | |
| }; | |
| img.src = ev.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| stickerInput.value = ''; | |
| }); | |
| function addSticker(img) { | |
| const maxSize = 200; | |
| let w = img.width; | |
| let h = img.height; | |
| if (w > h) { | |
| if (w > maxSize) { h *= maxSize / w; w = maxSize; } | |
| } else { | |
| if (h > maxSize) { w *= maxSize / h; h = maxSize; } | |
| } | |
| stickers.push({ | |
| img: img, | |
| x: canvas.width / 2, | |
| y: canvas.height / 2, | |
| width: w, | |
| height: h, | |
| rotation: 0 | |
| }); | |
| selectedStickerIndex = stickers.length - 1; | |
| drawAll(); | |
| } | |
| window.addEventListener('keydown', (e) => { | |
| if ((e.key === 'Delete' || e.key === 'Backspace') && selectedStickerIndex !== -1) { | |
| if (document.activeElement === textInput) return; | |
| stickers.splice(selectedStickerIndex, 1); | |
| selectedStickerIndex = -1; | |
| drawAll(); | |
| } | |
| }); | |
| function drawAll() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = BG_COLOR; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| if (baseLayer.type !== 'none') { | |
| drawBaseLayer(); | |
| } | |
| stickers.forEach((sticker, index) => { | |
| ctx.save(); | |
| ctx.translate(sticker.x, sticker.y); | |
| ctx.rotate(sticker.rotation); | |
| ctx.drawImage(sticker.img, -sticker.width/2, -sticker.height/2, sticker.width, sticker.height); | |
| ctx.restore(); | |
| }); | |
| if (selectedStickerIndex !== -1) { | |
| drawControls(stickers[selectedStickerIndex]); | |
| } | |
| } | |
| function drawBaseLayer() { | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvas.width; | |
| tempCanvas.height = canvas.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| const cx = tempCanvas.width / 2; | |
| const cy = tempCanvas.height / 2; | |
| tempCtx.translate(cx + baseLayer.x, cy + baseLayer.y); | |
| tempCtx.scale(baseLayer.scale, baseLayer.scale); | |
| if (baseLayer.type === 'image') { | |
| const w = 300; | |
| const h = 300 * (baseLayer.object.height / baseLayer.object.width); | |
| tempCtx.drawImage(baseLayer.object, -w/2, -h/2, w, h); | |
| iile} else if (baseLayer.type === 'text') { | |
| tempCtx.font = "900 300px 'Nunito'"; | |
| tempCtx.textAlign = "center"; | |
| tempCtx.textBaseline = "middle"; | |
| tempCtx.fillStyle = "#000"; | |
| tempCtx.fillText(baseLayer.object, 0, 20); | |
| } | |
| tempCtx.globalCompositeOperation = 'source-in'; | |
| const gradient = tempCtx.createLinearGradient(canvas.width, 0, 0, canvas.height); | |
| gradient.addColorStop(0, GRADIENT_START); | |
| gradient.addColorStop(1, GRADIENT_END); | |
| tempCtx.fillStyle = gradient; | |
| tempCtx.setTransform(1, 0, 0, 1, 0, 0); | |
| tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); | |
| ctx.drawImage(tempCanvas, 0, 0); | |
| } | |
| function drawControls(sticker) { | |
| ctx.save(); | |
| ctx.translate(sticker.x, sticker.y); | |
| ctx.rotate(sticker.rotation); | |
| const w = sticker.width; | |
| const h = sticker.height; | |
| const halfW = w / 2; | |
| const halfH = h / 2; | |
| ctx.strokeStyle = '#FFFFFF'; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([5, 5]); | |
| ctx.strokeRect(-halfW, -halfH, w, h); | |
| ctx.setLineDash([]); | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -halfH); | |
| ctx.lineTo(0, -halfH - 25); | |
| ctx.stroke(); | |
| ctx.fillStyle = '#E13839'; | |
| ctx.beginPath(); | |
| ctx.arc(0, -halfH - 25, 8, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.arc(halfW, halfH, 8, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| function getMousePos(evt) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = canvas.width / rect.width; | |
| const scaleY = canvas.height / rect.height; | |
| return { | |
| x: (evt.clientX - rect.left) * scaleX, | |
| y: (evt.clientY - rect.top) * scaleY | |
| }; | |
| } | |
| function isPointInRotatedRect(x, y, sticker) { | |
| const dx = x - sticker.x; | |
| const dy = y - sticker.y; | |
| const rotatedX = dx * Math.cos(-sticker.rotation) - dy * Math.sin(-sticker.rotation); | |
| const rotatedY = dx * Math.sin(-sticker.rotation) + dy * Math.cos(-sticker.rotation); | |
| const halfW = sticker.width / 2; | |
| const halfH = sticker.height / 2; | |
| return rotatedX >= -halfW && rotatedX <= halfW && | |
| rotatedY >= -halfH && rotatedY <= halfH; | |
| } | |
| function getHandleHit(x, y, sticker) { | |
| const dx = x - sticker.x; | |
| const dy = y - sticker.y; | |
| const rx = dx * Math.cos(-sticker.rotation) - dy * Math.sin(-sticker.rotation); | |
| const ry = dx * Math.sin(-sticker.rotation) + dy * Math.cos(-sticker.rotation); | |
| const halfW = sticker.width / 2; | |
| const halfH = sticker.height / 2; | |
| const handleRadius = 15; | |
| const rotHandleX = 0; | |
| const rotHandleY = -halfH - 25; | |
| if (Math.hypot(rx - rotHandleX, ry - rotHandleY) < handleRadius) return 'rotate'; | |
| if (Math.hypot(rx - halfW, ry - halfH) < handleRadius) return 'resize'; | |
| return null; | |
| } | |
| canvas.addEventListener('mousedown', (e) => { | |
| const {x, y} = getMousePos(e); | |
| if (selectedStickerIndex !== -1) { | |
| const sticker = stickers[selectedStickerIndex]; | |
| const handle = getHandleHit(x, y, sticker); | |
| if (handle === 'rotate') { | |
| isRotating = true; | |
| initialRotation = Math.atan2(y - sticker.y, x - sticker.x) - sticker.rotation; | |
| return; | |
| } | |
| if (handle === 'resize') { | |
| isResizing = true; | |
| initialDistance = Math.hypot(x - sticker.x, y - sticker.y); | |
| initialScale = sticker.width; | |
| return; | |
| } | |
| } | |
| let clickedIndex = -1; | |
| for (let i = stickers.length - 1; i >= 0; i--) { | |
| if (isPointInRotatedRect(x, y, stickers[i])) { | |
| clickedIndex = i; | |
| break; | |
| } | |
| } | |
| if (clickedIndex !== -1) { | |
| selectedStickerIndex = clickedIndex; | |
| if (clickedIndex !== stickers.length - 1) { | |
| const movedSticker = stickers.splice(clickedIndex, 1)[0]; | |
| stickers.push(movedSticker); | |
| selectedStickerIndex = stickers.length - 1; | |
| } | |
| isDragging = true; | |
| startX = x; | |
| startY = y; | |
| drawAll(); | |
| } else { | |
| selectedStickerIndex = -1; | |
| drawAll(); | |
| } | |
| }); | |
| canvas.addEventListener('mousemove', (e) => { | |
| const {x, y} = getMousePos(e); | |
| const sticker = selectedStickerIndex !== -1 ? stickers[selectedStickerIndex] : null; | |
| let cursor = 'default'; | |
| if (isRotating) cursor = 'grabbing'; | |
| else if (isResizing) cursor = 'nwse-resize'; | |
| else if (sticker) { | |
| const handle = getHandleHit(x, y, sticker); | |
| if (handle === 'rotate') cursor = 'grab'; | |
| else if (handle === 'resize') cursor = 'nwse-resize'; | |
| else if (isPointInRotatedRect(x, y, sticker)) cursor = 'move'; | |
| } else { | |
| for (let i = stickers.length - 1; i >= 0; i--) { | |
| if (isPointInRotatedRect(x, y, stickers[i])) { | |
| cursor = 'move'; | |
| break; | |
| } | |
| } | |
| } | |
| canvas.style.cursor = cursor; | |
| if (!sticker) return; | |
| if (isDragging) { | |
| sticker.x += x - startX; | |
| sticker.y += y - startY; | |
| startX = x; | |
| startY = y; | |
| drawAll(); | |
| } else if (isRotating) { | |
| const angle = Math.atan2(y - sticker.y, x - sticker.x); | |
| sticker.rotation = angle - initialRotation; | |
| drawAll(); | |
| } else if (isResizing) { | |
| const currentDist = Math.hypot(x - sticker.x, y - sticker.y); | |
| const ratio = currentDist / initialDistance; | |
| const oldW = sticker.width; | |
| const aspect = sticker.height / sticker.width; | |
| let newW = initialScale * ratio; | |
| if (newW < 20) newW = 20; | |
| sticker.width = newW; | |
| sticker.height = newW * aspect; | |
| drawAll(); | |
| } | |
| }); | |
| window.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| isRotating = false; | |
| isResizing = false; | |
| }); | |
| downloadBtn.addEventListener('click', () => { | |
| const prevSelection = selectedStickerIndex; | |
| selectedStickerIndex = -1; | |
| drawAll(); | |
| const link = document.createElement('a'); | |
| link.download = 'SWAGA_ICON.png'; | |
| link.href = canvas.toDataURL('image/png'); | |
| link.click(); | |
| selectedStickerIndex = prevSelection; | |
| drawAll(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |