326 lines
18 KiB
HTML
326 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Генератор Favicon — WA Dev Tools</title>
|
|
<link href="/vendor/tailwind.min.css" rel="stylesheet">
|
|
<script src="/shared.js?v=3"></script>
|
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
|
<link href="/shared.css" rel="stylesheet">
|
|
<script>window.WA_TOOL_ID = 'favicon';</script>
|
|
<style>
|
|
/* ── Dropzone ── */
|
|
.dropzone-wrap { position: relative; border: 2px dashed var(--surface-600); border-radius: 14px; padding: 48px 20px; text-align: center; cursor: pointer; transition: border-color .25s, background .25s; }
|
|
.dropzone-wrap:hover, .dropzone-wrap.active { border-color: var(--accent); background: var(--accent-bg); }
|
|
.dropzone-wrap .drop-icon { transition: transform .3s cubic-bezier(.34,1.56,.64,1); }
|
|
.dropzone-wrap.active .drop-icon { transform: scale(1.12) translateY(-4px); }
|
|
|
|
/* ── Size grid ── */
|
|
.sizes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 12px; margin-top: 20px; }
|
|
.size-card { background: var(--surface-800); border: 1px solid var(--surface-600); border-radius: 10px; padding: 10px 8px; text-align: center; }
|
|
.size-card img { display: block; margin: 0 auto 6px; border-radius: 4px; border: 1px solid var(--surface-600); image-rendering: pixelated; background: repeating-conic-gradient(var(--surface-700) 0% 25%, transparent 0% 50%) 0 0 / 10px 10px; }
|
|
.size-label { font-size: 10px; font-family: 'JetBrains Mono', monospace; color: var(--text-muted); font-weight: 600; }
|
|
|
|
/* ── Code blocks ── */
|
|
.code-block-wrap { position: relative; margin-top: 16px; }
|
|
.code-block { background: var(--surface-900); border: 1px solid var(--surface-600); border-radius: 10px; padding: 14px 44px 14px 16px; font-size: 12px; font-family: 'JetBrains Mono', monospace; color: var(--text-primary); white-space: pre; overflow-x: auto; line-height: 1.7; }
|
|
.copy-btn { position: absolute; top: 8px; right: 8px; padding: 4px 10px; border-radius: 6px; font-size: 10px; font-weight: 700; font-family: 'JetBrains Mono', monospace; letter-spacing: .04em; background: var(--surface-700); color: var(--text-muted); border: 1px solid var(--surface-600); cursor: pointer; transition: background .2s, color .2s; white-space: nowrap; }
|
|
.copy-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
.copy-btn.copied { background: #4caf50; color: #fff; border-color: #4caf50; }
|
|
|
|
/* ── Section header ── */
|
|
.section-title { font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; font-family: 'JetBrains Mono', monospace; color: var(--text-muted); margin-bottom: 8px; margin-top: 24px; }
|
|
|
|
/* ── Download button ── */
|
|
.dl-all-btn { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 14px; background: var(--accent); color: #fff; border: none; border-radius: 12px; font-size: 14px; font-weight: 700; cursor: pointer; transition: background .2s, box-shadow .2s; margin-top: 20px; text-decoration: none; }
|
|
.dl-all-btn:hover { background: var(--accent-dim); box-shadow: 0 6px 20px rgba(0,84,230,.25); }
|
|
|
|
/* ── Spinner ── */
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.spinner { width: 22px; height: 22px; border: 3px solid var(--surface-600); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; margin: 0 auto; }
|
|
|
|
/* ── Error / hidden ── */
|
|
.error-block { background: rgba(244,67,54,.08); border: 1px solid rgba(244,67,54,.2); border-radius: 10px; padding: 12px 16px; font-size: 13px; color: #f44336; margin-top: 14px; }
|
|
.hidden { display: none !important; }
|
|
|
|
/* ── Animations ── */
|
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.fade-up { animation: fadeUp .35s ease-out forwards; }
|
|
.fade-up-delay { animation: fadeUp .35s ease-out .1s forwards; opacity: 0; }
|
|
|
|
/* ── Uploading overlay ── */
|
|
.upload-state { padding: 32px 0; text-align: center; }
|
|
.upload-state p { font-size: 13px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; margin-top: 14px; }
|
|
|
|
@media (max-width: 500px) {
|
|
.sizes-grid { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
|
|
|
<div class="main-content">
|
|
<div class="tool-container relative z-10 max-w-2xl mx-auto py-8 sm:py-12">
|
|
|
|
<!-- Header -->
|
|
<div class="text-center mb-8 fade-up">
|
|
<div class="page-header" style="text-align:center;">
|
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
|
FAVICON
|
|
</div>
|
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
|
Генератор <span class="text-accent">Favicon</span>
|
|
</h1>
|
|
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">PNG · Apple Touch · Android · Manifest · HTML теги</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main card -->
|
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
|
|
|
<!-- Drop zone -->
|
|
<div id="dropzone" class="dropzone-wrap" role="button" tabindex="0" aria-label="Зона загрузки изображения">
|
|
<div class="drop-icon mb-3">
|
|
<svg class="mx-auto w-10 h-10" style="color:var(--text-muted)" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z" />
|
|
</svg>
|
|
</div>
|
|
<p style="font-size:14px;color:var(--text-muted)">
|
|
Перетащите изображение или <span style="color:var(--accent);font-weight:600">выберите файл</span>
|
|
</p>
|
|
<p style="font-size:11px;color:var(--text-muted);margin-top:5px;font-family:'JetBrains Mono',monospace">PNG или SVG рекомендуется · любой формат · до 5 МБ</p>
|
|
<input type="file" accept="image/*" id="fileInput" style="position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;" tabindex="-1">
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Error block -->
|
|
<div id="errorBlock" class="error-block hidden"></div>
|
|
|
|
<!-- Upload progress -->
|
|
<div id="uploadState" class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none mt-4 hidden">
|
|
<div class="upload-state">
|
|
<div class="spinner"></div>
|
|
<p>Генерация иконок...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
<div id="results" class="hidden">
|
|
|
|
<!-- Preview grid -->
|
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none mt-4">
|
|
<div class="section-title">Сгенерированные иконки</div>
|
|
<div id="sizesGrid" class="sizes-grid"></div>
|
|
|
|
<!-- HTML tags -->
|
|
<div class="section-title">HTML теги</div>
|
|
<div class="code-block-wrap">
|
|
<pre id="htmlTagsBlock" class="code-block"></pre>
|
|
<button class="copy-btn" onclick="copyCode('htmlTagsBlock', this)">Копировать</button>
|
|
</div>
|
|
|
|
<!-- Manifest -->
|
|
<div class="section-title">site.webmanifest</div>
|
|
<div class="code-block-wrap">
|
|
<pre id="manifestBlock" class="code-block"></pre>
|
|
<button class="copy-btn" onclick="copyCode('manifestBlock', this)">Копировать</button>
|
|
</div>
|
|
|
|
<!-- Download ZIP -->
|
|
<a id="dlBtn" href="#" download class="dl-all-btn">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
|
</svg>
|
|
Скачать все иконки (ZIP)
|
|
</a>
|
|
|
|
<!-- Reset -->
|
|
<button id="resetBtn" type="button" onclick="resetTool()" style="display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:10px;background:transparent;color:var(--text-muted);border:1px solid var(--surface-600);border-radius:12px;font-size:12px;font-weight:600;font-family:'JetBrains Mono',monospace;cursor:pointer;margin-top:10px;transition:border-color .2s,color .2s;">
|
|
Загрузить другое изображение
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div><!-- /main-content -->
|
|
|
|
<script>
|
|
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
|
const dropzone = document.getElementById('dropzone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const errorBlock = document.getElementById('errorBlock');
|
|
const uploadState = document.getElementById('uploadState');
|
|
const results = document.getElementById('results');
|
|
const sizesGrid = document.getElementById('sizesGrid');
|
|
const htmlTagsBlock = document.getElementById('htmlTagsBlock');
|
|
const manifestBlock = document.getElementById('manifestBlock');
|
|
const dlBtn = document.getElementById('dlBtn');
|
|
|
|
// ── Dropzone events ───────────────────────────────────────────────────────────
|
|
dropzone.addEventListener('click', () => fileInput.click());
|
|
dropzone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') fileInput.click(); });
|
|
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.closest('.dark\\:bg-surface-800\\/80, .bg-white\\/85') || dropzone; dropzone.classList.add('active'); });
|
|
dropzone.addEventListener('dragleave', e => { if (!dropzone.contains(e.relatedTarget)) dropzone.classList.remove('active'); });
|
|
dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('active'); handleFile(e.dataTransfer.files[0]); });
|
|
fileInput.addEventListener('change', e => { handleFile(e.target.files[0]); e.target.value = ''; });
|
|
|
|
// ── Error helper ──────────────────────────────────────────────────────────────
|
|
function showError(msg) {
|
|
errorBlock.textContent = msg;
|
|
errorBlock.classList.remove('hidden');
|
|
setTimeout(() => errorBlock.classList.add('hidden'), 7000);
|
|
}
|
|
|
|
// ── File handling ─────────────────────────────────────────────────────────────
|
|
function handleFile(file) {
|
|
if (!file) return;
|
|
if (!file.type.startsWith('image/')) {
|
|
showError('Допустимы только изображения (PNG, SVG, JPEG, WebP, ...)');
|
|
return;
|
|
}
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
showError('Файл слишком большой. Максимум 5 МБ.');
|
|
return;
|
|
}
|
|
uploadFile(file);
|
|
}
|
|
|
|
// ── Upload & generate ─────────────────────────────────────────────────────────
|
|
async function uploadFile(file) {
|
|
errorBlock.classList.add('hidden');
|
|
results.classList.add('hidden');
|
|
uploadState.classList.remove('hidden');
|
|
dropzone.closest('.dark\\:bg-surface-800\\/80') && (dropzone.parentElement.style.pointerEvents = 'none');
|
|
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
try {
|
|
const resp = await fetch('/favicon/generate', { method: 'POST', body: formData });
|
|
const data = await resp.json();
|
|
|
|
if (!resp.ok) {
|
|
throw new Error(data.error || 'Ошибка ' + resp.status);
|
|
}
|
|
|
|
renderResults(file, data);
|
|
} catch (err) {
|
|
showError('Ошибка: ' + err.message);
|
|
} finally {
|
|
uploadState.classList.add('hidden');
|
|
if (dropzone.parentElement) dropzone.parentElement.style.pointerEvents = '';
|
|
}
|
|
}
|
|
|
|
// ── Render results ────────────────────────────────────────────────────────────
|
|
async function renderResults(originalFile, data) {
|
|
// Build size preview grid
|
|
sizesGrid.innerHTML = '';
|
|
|
|
// Read original file as data URL for client-side preview rendering
|
|
const origDataUrl = await fileToDataUrl(originalFile);
|
|
|
|
for (const spec of data.sizes) {
|
|
const card = document.createElement('div');
|
|
card.className = 'size-card';
|
|
|
|
// Render preview using canvas (avoids round-trip for thumbnails)
|
|
const canvas = document.createElement('canvas');
|
|
const displaySize = Math.min(spec.size, 64); // cap display at 64px
|
|
canvas.width = displaySize;
|
|
canvas.height = displaySize;
|
|
const ctx = canvas.getContext('2d');
|
|
const img = new Image();
|
|
img.src = origDataUrl;
|
|
await new Promise(resolve => { img.onload = resolve; img.onerror = resolve; });
|
|
ctx.drawImage(img, 0, 0, displaySize, displaySize);
|
|
|
|
const previewImg = document.createElement('img');
|
|
previewImg.src = canvas.toDataURL('image/png');
|
|
previewImg.width = displaySize;
|
|
previewImg.height = displaySize;
|
|
previewImg.alt = spec.filename;
|
|
|
|
const label = document.createElement('div');
|
|
label.className = 'size-label';
|
|
label.textContent = spec.size + '\u00d7' + spec.size;
|
|
|
|
card.appendChild(previewImg);
|
|
card.appendChild(label);
|
|
sizesGrid.appendChild(card);
|
|
}
|
|
|
|
// Code blocks
|
|
htmlTagsBlock.textContent = data.htmlTags;
|
|
manifestBlock.textContent = data.manifestJson;
|
|
|
|
// Download link
|
|
dlBtn.href = data.downloadUrl;
|
|
|
|
results.classList.remove('hidden');
|
|
}
|
|
|
|
function fileToDataUrl(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = e => resolve(e.target.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
// ── Copy to clipboard ─────────────────────────────────────────────────────────
|
|
function copyCode(blockId, btn) {
|
|
const text = document.getElementById(blockId).textContent;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const orig = btn.textContent;
|
|
btn.textContent = 'Скопировано!';
|
|
btn.classList.add('copied');
|
|
setTimeout(() => {
|
|
btn.textContent = orig;
|
|
btn.classList.remove('copied');
|
|
}, 2000);
|
|
}).catch(() => {
|
|
// Fallback for older browsers
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
ta.remove();
|
|
});
|
|
}
|
|
|
|
// ── Reset ─────────────────────────────────────────────────────────────────────
|
|
function resetTool() {
|
|
results.classList.add('hidden');
|
|
errorBlock.classList.add('hidden');
|
|
sizesGrid.innerHTML = '';
|
|
htmlTagsBlock.textContent = '';
|
|
manifestBlock.textContent = '';
|
|
dlBtn.href = '#';
|
|
fileInput.value = '';
|
|
}
|
|
</script>
|
|
|
|
<!-- Yandex.Metrika counter -->
|
|
<script type="text/javascript">
|
|
(function(m,e,t,r,i,k,a){
|
|
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
|
m[i].l=1*new Date();
|
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
|
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
|
|
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
|
</script>
|
|
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
|
<!-- /Yandex.Metrika counter -->
|
|
</body>
|
|
</html>
|