2 Choose Background Color
3 Choose Sizes
4 Generate & Download
Generate Wallpapers
Select an image and at least one size to continue
`;
} else {
pageImages.forEach((img) => {
const thumb = document.createElement('div');
thumb.className = 'gallery-thumb' + (selectedImage && selectedImage.src === img.src ? ' selected' : '');
thumb.innerHTML = `
${img.name}
`;
thumb.addEventListener('click', () => onThumbClick(thumb, img));
grid.appendChild(thumb);
});
}
// Meta
const showingStart = totalFiltered === 0 ? 0 : start + 1;
const showingEnd = Math.min(start + perPage, totalFiltered);
document.getElementById('metaShowing').textContent = totalFiltered === 0 ? '0' : `${showingStart}–${showingEnd}`;
document.getElementById('metaTotal').textContent = totalFiltered.toLocaleString();
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const wrap = document.getElementById('paginationWrap');
const btnsDiv = document.getElementById('pageButtons');
wrap.style.display = totalPages <= 1 ? 'none' : 'flex';
if (totalPages <= 1) return;
document.getElementById('prevBtn').disabled = currentPage === 1;
document.getElementById('nextBtn').disabled = currentPage === totalPages;
// Build page numbers with ellipsis
btnsDiv.innerHTML = '';
const pages = getPageNumbers(currentPage, totalPages);
pages.forEach(p => {
const btn = document.createElement('button');
if (p === '…') {
btn.className = 'page-btn ellipsis'; btn.textContent = '…';
} else {
btn.className = 'page-btn' + (p === currentPage ? ' active' : '');
btn.textContent = p;
btn.addEventListener('click', () => changePage(p));
}
btnsDiv.appendChild(btn);
});
}
function getPageNumbers(current, total) {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages = [];
pages.push(1);
if (current > 3) pages.push('…');
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) pages.push(i);
if (current < total - 2) pages.push('…');
pages.push(total);
return pages;
}
function clearSelection() {
if (selectedImage && selectedImage.blobURL) URL.revokeObjectURL(selectedImage.blobURL);
selectedImage = null;
document.getElementById('previewImg').classList.remove('visible');
document.getElementById('previewImg').src = '';
document.getElementById('previewPlaceholder').style.display = '';
document.getElementById('previewMeta').classList.remove('visible');
updateBtn();
}
function changePage(p) {
const totalPages = Math.ceil(filteredImages.length / perPage);
if (p < 1 || p > totalPages) return;
clearSelection();
currentPage = p;
renderGallery();
document.querySelector('.gallery-section').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function changePerPage(val) {
clearSelection();
perPage = parseInt(val);
currentPage = 1;
renderGallery();
}
// ── Search ──
const searchInput = document.getElementById('searchInput');
const clearBtn = document.getElementById('clearSearch');
let searchTimer;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
const q = searchInput.value.trim().toLowerCase();
clearBtn.classList.toggle('visible', q.length > 0);
filteredImages = q ? IMAGES.filter(img => img.name.toLowerCase().includes(q)) : [...IMAGES];
currentPage = 1;
renderGallery();
}, 200);
});
clearBtn.addEventListener('click', () => {
searchInput.value = '';
clearBtn.classList.remove('visible');
filteredImages = [...IMAGES];
currentPage = 1;
renderGallery();
searchInput.focus();
});
// ── Thumb click ──
async function onThumbClick(el, img) {
// If already selected, deselect
if (selectedImage && selectedImage.src === img.src) {
// Clicking the already-selected image deselects it
selectedImage = null;
el.classList.remove('selected');
document.getElementById('previewImg').classList.remove('visible');
document.getElementById('previewPlaceholder').style.display = '';
document.getElementById('previewMeta').classList.remove('visible');
updateBtn();
return;
}
// Deselect any currently visible selected thumb on this page
// (cross-page deselection is handled by renderGallery which reads selectedImage state)
document.querySelectorAll('#galleryGrid .gallery-thumb.selected').forEach(t => t.classList.remove('selected'));
el.classList.add('selected');
if (selectedImage && selectedImage.blobURL) URL.revokeObjectURL(selectedImage.blobURL);
try {
const response = await fetch(img.src);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const blobURL = URL.createObjectURL(blob);
const imgEl = new Image();
imgEl.onload = () => {
selectedImage = { name: img.name, src: img.src, imgEl, blobURL };
const pi = document.getElementById('previewImg');
pi.src = blobURL; pi.classList.add('visible');
document.getElementById('previewPlaceholder').style.display = 'none';
const meta = document.getElementById('previewMeta');
meta.classList.add('visible');
document.getElementById('previewName').textContent = img.name;
document.getElementById('previewSize').textContent = `${imgEl.naturalWidth} × ${imgEl.naturalHeight}px`;
updateBtn();
};
imgEl.src = blobURL;
} catch (e) {
// Revert the optimistic selected class since load failed
el.classList.remove('selected');
alert(`Could not load "${img.name}".\n\nFull-res URL: ${img.src}\nThumb URL: ${img.thumb || img.src}\n\nCheck that the files exist and you're running from a web server (not file://).\n\nDetails: ${e.message}`);
}
}
function updateBtn() {
const ready = selectedImage && selectedPresets.size > 0;
document.getElementById('generateBtn').disabled = !ready;
const hint = document.getElementById('generateHint');
if (!selectedImage && selectedPresets.size === 0)
hint.innerHTML = 'Select an image and at least one size to continue';
else if (!selectedImage)
hint.innerHTML = 'Now
pick an image above';
else if (selectedPresets.size === 0)
hint.innerHTML = 'Now
choose at least one size ';
else
hint.innerHTML = `
${selectedPresets.size} size${selectedPresets.size > 1 ? 's' : ''} selected — ready to generate`;
}
// ── Generate ──
async function generate() {
if (!selectedImage || selectedPresets.size === 0) return;
generatedResults = [];
const resultsGrid = document.getElementById('resultsGrid');
const resultsArea = document.getElementById('resultsArea');
resultsGrid.innerHTML = ''; resultsArea.style.display = 'none';
const pb = document.getElementById('progressBar');
const pf = document.getElementById('progressFill');
pb.classList.add('active'); pf.style.width = '0%';
const btn = document.getElementById('generateBtn');
btn.disabled = true; btn.textContent = 'Generating…';
const selected = [...selectedPresets];
try {
for (let i = 0; i < selected.length; i++) {
const p = ALL_PRESETS[selected[i]];
const dataURL = cropCentered(selectedImage.imgEl, p.w, p.h);
generatedResults.push({ preset: p, dataURL });
pf.style.width = `${((i + 1) / selected.length) * 100}%`;
await new Promise(r => setTimeout(r, 30));
}
} catch (e) {
pb.classList.remove('active');
btn.disabled = false; btn.textContent = 'Generate Wallpapers';
alert(e.message); return;
}
pb.classList.remove('active');
document.getElementById('resultsTitle').textContent =
`${generatedResults.length} Wallpaper${generatedResults.length !== 1 ? 's' : ''} — ${selectedImage.name}`;
resultsArea.style.display = 'block';
generatedResults.forEach((r, i) => {
const ar = r.preset.w / r.preset.h;
const card = document.createElement('div');
card.className = 'result-card';
card.style.animationDelay = `${i * 0.05}s`;
card.innerHTML = `
`;
resultsGrid.appendChild(card);
});
resultsArea.scrollIntoView({ behavior: 'smooth', block: 'start' });
btn.disabled = false; btn.textContent = 'Generate Wallpapers';
showToast(`${generatedResults.length} wallpapers ready`);
}
function cropCentered(img, targetW, targetH) {
const canvas = document.createElement('canvas');
canvas.width = targetW; canvas.height = targetH;
const ctx = canvas.getContext('2d');
// Fill background color first (shows through any transparent areas in the PNG)
ctx.fillStyle = selectedBgColor;
ctx.fillRect(0, 0, targetW, targetH);
const tAR = targetW / targetH, sAR = img.width / img.height;
let sx, sy, sw, sh;
if (sAR > tAR) { sh = img.height; sw = sh * tAR; sx = (img.width - sw) / 2; sy = 0; }
else { sw = img.width; sh = sw / tAR; sx = 0; sy = (img.height - sh) / 2; }
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetW, targetH);
try { return canvas.toDataURL('image/png'); }
catch (e) { throw new Error('Could not read image data. Make sure this page is served from a web server, not opened as a local file.'); }
}
function downloadOne(i) {
const r = generatedResults[i];
const safeName = selectedImage.name.replace(/\s+/g, '-').toLowerCase();
const a = document.createElement('a');
a.href = r.dataURL;
a.download = `${safeName}-${r.preset.w}x${r.preset.h}.png`;
a.click();
}
async function downloadAll() {
for (let i = 0; i < generatedResults.length; i++) {
downloadOne(i); await new Promise(r => setTimeout(r, 150));
}
showToast('All wallpapers downloaded');
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2500);
}
// ─────────────────────────────────────────────────────────────
// BACKGROUND COLOR
// ─────────────────────────────────────────────────────────────
const SWATCHES = [
{ name: 'Yellow', hex: '#ffdd00' },
{ name: 'Leaf Green', hex: '#83c341' },
{ name: 'Fire Red', hex: '#ef3a2d' },
{ name: 'Water Blue', hex: '#3ec4e1' },
{ name: 'Orange', hex: '#ee8100'},
{ name: 'Pokémon Red', hex: '#e40114'},
{ name: 'Charcoal', hex: '#3D3D3D' },
{ name: 'White', hex: '#ffffff', light: true },
{ name: 'Silver', hex: '#bdbdbd', light: true },
{ name: 'Cream', hex: '#fff8e1', light: true },
{ name: 'Transparent', hex: 'transparent' },
];
function setColor(hex, name, skipPickerSync) {
selectedBgColor = hex === 'transparent' ? 'transparent' : hex;
// Update swatch active states
document.querySelectorAll('.swatch').forEach(s => {
s.classList.toggle('active', s.dataset.hex === hex);
});
// Sync color picker (can't set 'transparent' on input[type=color])
if (!skipPickerSync && hex !== 'transparent') {
document.getElementById('colorPicker').value = hex;
}
// Sync hex input
document.getElementById('hexInput').value = hex === 'transparent' ? 'transparent' : hex.toUpperCase();
// Update preview chip
const swatch = document.getElementById('colorPreviewSwatch');
const name_el = document.getElementById('colorPreviewName');
if (hex === 'transparent') {
swatch.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0 / 10px 10px';
} else {
swatch.style.background = hex;
}
name_el.textContent = name || hex.toUpperCase();
// Update preview image background
const previewWrap = document.getElementById('previewImg').parentElement;
previewWrap.style.background = hex === 'transparent'
? 'repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0 / 16px 16px'
: hex;
}
// Build swatch grid
const swatchGrid = document.getElementById('swatchGrid');
SWATCHES.forEach(s => {
const el = document.createElement('div');
el.className = 'swatch' + (s.light ? ' light-swatch' : '');
el.dataset.hex = s.hex;
el.title = s.name;
if (s.hex === 'transparent') {
el.style.background = 'repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0 0 / 10px 10px';
el.style.border = '2px solid #ccc';
} else {
el.style.background = s.hex;
}
el.addEventListener('click', () => setColor(s.hex, s.name));
swatchGrid.appendChild(el);
});
// Color picker sync
const colorPicker = document.getElementById('colorPicker');
const hexInput = document.getElementById('hexInput');
colorPicker.addEventListener('input', () => {
setColor(colorPicker.value, '', true);
});
hexInput.addEventListener('input', () => {
const val = hexInput.value.trim();
if (val.toLowerCase() === 'transparent') {
setColor('transparent', 'Transparent', false);
return;
}
const hex = val.startsWith('#') ? val : '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(hex)) {
setColor(hex, '', false);
}
});
hexInput.addEventListener('blur', () => {
// Reformat on blur
const val = hexInput.value.trim().toLowerCase();
if (val !== 'transparent') {
const hex = val.startsWith('#') ? val : '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(hex)) {
hexInput.value = hex.toUpperCase();
} else {
// Invalid — reset to current
hexInput.value = selectedBgColor === 'transparent' ? 'transparent' : selectedBgColor.toUpperCase();
}
}
});
// Default to white on load
setColor('#ffffff', 'White');
// ── Init ──
renderGallery();