/
reitz
/
Minecraft-Skin-Editor
Обзор
Документация
Войти
/
reitz
/
Minecraft-Skin-Editor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
target/classes/static/app.js
591 строка
21 KB
reitz
upload files
15 янв 2026, 07:54
15 янв 2026, 07:54
a0879c4
Код
Авторство
О чём код?
const SIZE = 64; const SCALE = 8; const editorCanvas = document.getElementById('editorCanvas'); const ctx = editorCanvas.getContext('2d'); ctx.imageSmoothingEnabled = false; const fileInput = document.getElementById('fileInput'); const downloadBtn = document.getElementById('downloadBtn'); const colorInput = document.getElementById('colorInput'); const undoBtn = document.getElementById('undoBtn'); const clearBtn = document.getElementById('clearBtn'); const statusLabel = document.getElementById('statusLabel'); const saveBtn = document.getElementById('saveBtn'); const skinNameInput = document.getElementById('skinNameInput'); const toolButtons = Array.from(document.querySelectorAll('.tool-btn')); const authStatusLabel = document.getElementById('authStatus'); const registerForm = document.getElementById('registerForm'); const regUsernameInput = document.getElementById('regUsername'); const regPasswordInput = document.getElementById('regPassword'); const loginForm = document.getElementById('loginForm'); const loginUsernameInput = document.getElementById('loginUsername'); const loginPasswordInput = document.getElementById('loginPassword'); const logoutBtn = document.getElementById('logoutBtn'); const skinsListEl = document.getElementById('skinsList'); const changePasswordForm = document.getElementById('changePasswordForm'); const oldPasswordInput = document.getElementById('oldPassword'); const newPasswordInput = document.getElementById('newPassword'); const showChangePasswordBtn = document.getElementById('showChangePasswordBtn'); const previewCanvases = Array.from(document.querySelectorAll('.preview-item canvas')); let currentTool = 'pencil'; let isDrawing = false; let lastX = 0; let lastY = 0; let undoStack = []; const MAX_UNDO = 20; let currentUser = null; let currentSkinImage = null; function setStatus(text) { statusLabel.textContent = text; } function setAuthStatus(text) { if (authStatusLabel) { authStatusLabel.textContent = text; } } function pushUndoState() { if (!currentSkinImage) return; const copy = new Uint8Array(currentSkinImage); undoStack.push(copy); if (undoStack.length > MAX_UNDO) { undoStack.shift(); } } async function restoreFromUndo() { const last = undoStack.pop(); if (last) { currentSkinImage = last; loadImageToCanvas(currentSkinImage); await updateAllPreviews(); setStatus('Отмена шага'); } } function loadImageToCanvas(imageData) { if (!imageData) return; const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, editorCanvas.width, editorCanvas.height); ctx.drawImage(img, 0, 0, SIZE, SIZE, 0, 0, editorCanvas.width, editorCanvas.height); currentSkinImage = imageData; }; img.src = URL.createObjectURL(new Blob([imageData], { type: 'image/png' })); } function loadDefaultSkin() { const img = new Image(); img.src = 'assets/default-skin.png'; img.onload = () => { ctx.clearRect(0, 0, editorCanvas.width, editorCanvas.height); ctx.drawImage(img, 0, 0, SIZE, SIZE, 0, 0, editorCanvas.width, editorCanvas.height); undoStack = []; imgToBlob(img).then(blob => { blob.arrayBuffer().then(buf => { currentSkinImage = new Uint8Array(buf); updateAllPreviews(); }); }); setStatus('Загружен скин по умолчанию'); downloadBtn.disabled = false; }; img.onerror = () => { ctx.clearRect(0, 0, editorCanvas.width, editorCanvas.height); drawCheckerboard(); setStatus('Не найден default-skin.png'); downloadBtn.disabled = false; }; } function imgToBlob(img) { return new Promise((resolve) => { const canvas = document.createElement('canvas'); canvas.width = SIZE; canvas.height = SIZE; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); canvas.toBlob(resolve, 'image/png'); }); } function drawCheckerboard() { const size = 16; for (let y = 0; y < editorCanvas.height; y += size) { for (let x = 0; x < editorCanvas.width; x += size) { const isDark = ((x / size) + (y / size)) % 2 === 0; ctx.fillStyle = isDark ? '#2b2f3a' : '#20232c'; ctx.fillRect(x, y, size, size); } } } function getCanvasCoords(evt) { const rect = editorCanvas.getBoundingClientRect(); const x = Math.floor((evt.clientX - rect.left) / (rect.width / SIZE)); const y = Math.floor((evt.clientY - rect.top) / (rect.height / SIZE)); return { x: Math.min(Math.max(x, 0), SIZE - 1), y: Math.min(Math.max(y, 0), SIZE - 1) }; } async function processImageOnServer(operation, x, y) { if (!currentSkinImage) return; const blob = new Blob([currentSkinImage], { type: 'image/png' }); const formData = new FormData(); formData.append('file', blob, 'skin.png'); let url = `/api/skins/process/${operation}`; if (operation === 'pencil' || operation === 'fill') { url += `?x=${x}&y=${y}&color=${encodeURIComponent(colorInput.value)}`; } else if (operation === 'picker') { url += `?x=${x}&y=${y}`; } try { const res = await fetch(url, { method: 'POST', body: formData, credentials: 'include' }); if (operation === 'picker') { const data = await res.json(); if (data.color) { colorInput.value = data.color; setStatus(`Выбран цвет ${data.color}`); } return; } if (res.ok) { const blob = await res.blob(); const arrayBuffer = await blob.arrayBuffer(); currentSkinImage = new Uint8Array(arrayBuffer); loadImageToCanvas(currentSkinImage); await updateAllPreviews(); } else { const data = await res.json().catch(() => null); alert(data?.error || 'Ошибка при обработке'); } } catch (e) { alert('Не удалось обработать изображение'); } } async function updateAllPreviews() { if (!currentSkinImage) return; const blob = new Blob([currentSkinImage], { type: 'image/png' }); const formData = new FormData(); formData.append('file', blob, 'skin.png'); for (const canvas of previewCanvases) { const side = canvas.dataset.side; try { const res = await fetch(`/api/skins/preview/${side}`, { method: 'POST', body: formData, credentials: 'include' }); if (res.ok) { const previewBlob = await res.blob(); const img = new Image(); img.onload = () => { const c = canvas.getContext('2d'); c.clearRect(0, 0, canvas.width, canvas.height); c.imageSmoothingEnabled = false; c.drawImage(img, 0, 0, canvas.width, canvas.height); }; img.src = URL.createObjectURL(previewBlob); } } catch (e) { console.error('Ошибка загрузки предпросмотра', e); } } } async function apiJson(path, options = {}) { const res = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, ...options }); const ct = res.headers.get('Content-Type') || ''; const hasJson = ct.includes('application/json'); const body = hasJson ? await res.json() : null; return { res, body }; } async function apiGet(path) { const res = await fetch(path, { credentials: 'include' }); const ct = res.headers.get('Content-Type') || ''; const body = ct.includes('application/json') ? await res.json() : null; return { res, body }; } function updateAuthUI() { if (!authStatusLabel) return; if (currentUser) { setAuthStatus(`Вы вошли как ${currentUser.username}`); if (logoutBtn) logoutBtn.style.display = 'block'; if (showChangePasswordBtn) showChangePasswordBtn.style.display = 'block'; if (registerForm) registerForm.classList.add('hidden'); if (loginForm) loginForm.classList.add('hidden'); if (changePasswordForm) changePasswordForm.classList.add('hidden'); } else { setAuthStatus('Не авторизован'); if (logoutBtn) logoutBtn.style.display = 'none'; if (showChangePasswordBtn) showChangePasswordBtn.style.display = 'none'; if (registerForm) registerForm.classList.remove('hidden'); if (loginForm) loginForm.classList.remove('hidden'); if (changePasswordForm) changePasswordForm.classList.add('hidden'); if (skinsListEl) { skinsListEl.innerHTML = '<p class="muted">Войдите в аккаунт, чтобы сохранять скины.</p>'; } } } function renderSkinsList(list) { if (!skinsListEl) return; if (!list || list.length === 0) { skinsListEl.innerHTML = '<p class="muted">У вас пока нет сохранённых скинов.</p>'; return; } skinsListEl.innerHTML = ''; list.forEach(skin => { const div = document.createElement('div'); div.className = 'skin-item'; const nameSpan = document.createElement('span'); nameSpan.textContent = skin.name; const dateSpan = document.createElement('span'); dateSpan.textContent = new Date(skin.updatedAt).toLocaleString(); const buttonsWrap = document.createElement('div'); const openBtn = document.createElement('button'); openBtn.textContent = 'Открыть'; openBtn.addEventListener('click', async () => { try { const res = await fetch(`/api/skins/${skin.id}/data`, { credentials: 'include' }); if (res.ok) { const blob = await res.blob(); const arrayBuffer = await blob.arrayBuffer(); currentSkinImage = new Uint8Array(arrayBuffer); pushUndoState(); loadImageToCanvas(currentSkinImage); updateAllPreviews(); setStatus(`Открыт скин ${skin.name}`); } } catch (e) { alert('Не удалось загрузить скин'); } }); const downloadBtnEl = document.createElement('button'); downloadBtnEl.textContent = 'Скачать'; downloadBtnEl.addEventListener('click', () => { const a = document.createElement('a'); a.href = `/api/skins/${skin.id}/data`; a.download = `${skin.name || 'skin'}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); const deleteBtn = document.createElement('button'); deleteBtn.textContent = 'Удалить'; deleteBtn.className = 'danger'; deleteBtn.addEventListener('click', async () => { if (!confirm(`Удалить скин "${skin.name}"?`)) return; try { const res = await fetch(`/api/skins/${skin.id}`, { method: 'DELETE', credentials: 'include' }); if (res.ok) { setStatus('Скин удалён'); await loadMySkins(); } else { const data = await res.json().catch(() => null); alert(data?.error || 'Ошибка при удалении скина'); } } catch (e) { alert('Не удалось удалить скин'); } }); buttonsWrap.appendChild(openBtn); buttonsWrap.appendChild(downloadBtnEl); buttonsWrap.appendChild(deleteBtn); div.appendChild(nameSpan); div.appendChild(dateSpan); div.appendChild(buttonsWrap); skinsListEl.appendChild(div); }); } async function loadMySkins() { const { res, body } = await apiGet('/api/skins'); if (res.ok) { renderSkinsList(body); } else if (res.status === 401) { currentUser = null; updateAuthUI(); } } async function initAuth() { if (!authStatusLabel) return; try { const { res, body } = await apiGet('/api/auth/me'); if (res.ok && body) { currentUser = body; updateAuthUI(); await loadMySkins(); } else { currentUser = null; updateAuthUI(); } } catch (e) { currentUser = null; updateAuthUI(); } } if (registerForm) { registerForm.addEventListener('submit', async (evt) => { evt.preventDefault(); const username = regUsernameInput.value.trim(); const password = regPasswordInput.value; if (!username || !password) { alert('Введите имя и пароль'); return; } const { res, body } = await apiJson('/api/auth/register', { method: 'POST', body: JSON.stringify({ username, password }) }); if (res.ok && body) { currentUser = body; updateAuthUI(); await loadMySkins(); setStatus('Регистрация прошла успешно'); } else { alert(body?.error || 'Ошибка регистрации'); } }); } if (loginForm) { loginForm.addEventListener('submit', async (evt) => { evt.preventDefault(); const username = loginUsernameInput.value.trim(); const password = loginPasswordInput.value; if (!username || !password) { alert('Введите имя и пароль'); return; } const { res, body } = await apiJson('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); if (res.ok && body) { currentUser = body; updateAuthUI(); await loadMySkins(); setStatus('Вход выполнен'); } else { alert(body?.error || 'Ошибка входа'); } }); } if (logoutBtn) { logoutBtn.addEventListener('click', async () => { await apiJson('/api/auth/logout', { method: 'POST' }); currentUser = null; updateAuthUI(); setStatus('Вы вышли из аккаунта'); }); } if (showChangePasswordBtn) { showChangePasswordBtn.addEventListener('click', () => { if (changePasswordForm) { changePasswordForm.classList.toggle('hidden'); } }); } if (changePasswordForm) { changePasswordForm.addEventListener('submit', async (evt) => { evt.preventDefault(); const oldPassword = oldPasswordInput.value; const newPassword = newPasswordInput.value; if (!oldPassword || !newPassword) { alert('Заполните все поля'); return; } const { res, body } = await apiJson('/api/auth/change-password', { method: 'POST', body: JSON.stringify({ oldPassword, newPassword }) }); if (res.ok) { alert('Пароль успешно изменён'); oldPasswordInput.value = ''; newPasswordInput.value = ''; if (changePasswordForm) changePasswordForm.classList.add('hidden'); } else { alert(body?.error || 'Ошибка при изменении пароля'); } }); } editorCanvas.addEventListener('mousedown', async (evt) => { const { x, y } = getCanvasCoords(evt); if (currentTool === 'pencil') { isDrawing = true; lastX = x; lastY = y; pushUndoState(); await processImageOnServer('pencil', x, y); } else if (currentTool === 'fill' || currentTool === 'picker') { pushUndoState(); await processImageOnServer(currentTool, x, y); } }); editorCanvas.addEventListener('mousemove', async (evt) => { if (!isDrawing || currentTool !== 'pencil') return; const { x, y } = getCanvasCoords(evt); if (x === lastX && y === lastY) return; await processImageOnServer('pencil', x, y); lastX = x; lastY = y; }); window.addEventListener('mouseup', () => { isDrawing = false; }); fileInput.addEventListener('change', async (evt) => { const file = evt.target.files?.[0]; if (!file) return; if (file.type !== 'image/png') { alert('Поддерживаются только PNG-файлы'); return; } const img = new Image(); img.onload = async () => { if (img.width !== SIZE || img.height !== SIZE) { alert('Размер скина должен быть 64×64 пикселя'); return; } pushUndoState(); const blob = await imgToBlob(img); const arrayBuffer = await blob.arrayBuffer(); currentSkinImage = new Uint8Array(arrayBuffer); loadImageToCanvas(currentSkinImage); updateAllPreviews(); setStatus(`Загружен файл: ${file.name}`); downloadBtn.disabled = false; }; img.src = URL.createObjectURL(file); }); downloadBtn.addEventListener('click', async () => { if (!currentSkinImage) return; const blob = new Blob([currentSkinImage], { type: 'image/png' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'skin.png'; a.click(); URL.revokeObjectURL(a.href); setStatus('PNG скачан'); }); if (saveBtn) { saveBtn.addEventListener('click', async () => { if (!currentUser) { alert('Войдите в аккаунт, чтобы сохранять скины.'); return; } if (!currentSkinImage) { alert('Нет изображения для сохранения'); return; } const formData = new FormData(); const name = skinNameInput?.value?.trim() || 'skin'; const blob = new Blob([currentSkinImage], { type: 'image/png' }); formData.append('file', blob, 'skin.png'); formData.append('name', name); try { const res = await fetch('/api/skins', { method: 'POST', body: formData, credentials: 'include' }); if (res.ok) { setStatus('Скин сохранён в аккаунт'); await loadMySkins(); } else { const data = await res.json().catch(() => null); alert(data?.error || 'Ошибка при сохранении скина'); } } catch (e) { alert('Не удалось сохранить скин'); } }); } undoBtn.addEventListener('click', async () => { await restoreFromUndo(); }); clearBtn.addEventListener('click', async () => { if (!currentSkinImage) return; pushUndoState(); const blob = new Blob([currentSkinImage], { type: 'image/png' }); const formData = new FormData(); formData.append('file', blob, 'skin.png'); try { const res = await fetch('/api/skins/process/clear', { method: 'POST', body: formData, credentials: 'include' }); if (res.ok) { const clearedBlob = await res.blob(); const arrayBuffer = await clearedBlob.arrayBuffer(); currentSkinImage = new Uint8Array(arrayBuffer); loadImageToCanvas(currentSkinImage); updateAllPreviews(); setStatus('Холст очищен'); } } catch (e) { alert('Не удалось очистить холст'); } }); toolButtons.forEach(btn => { btn.addEventListener('click', () => { toolButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentTool = btn.dataset.tool; setStatus(`Инструмент: ${btn.textContent}`); }); }); loadDefaultSkin(); initAuth();