Update rendering for small size

And fix rendering flicker bug.
This commit is contained in:
Alexander Wainwright
2025-12-18 20:25:28 +10:00
parent 7db5c18018
commit 693ebf0172
2 changed files with 79 additions and 30 deletions

View File

@@ -23,6 +23,7 @@ document.addEventListener('DOMContentLoaded', () => {
const fmtRadios = document.getElementsByName('opt-fmt');
let debounceTimer;
let renderSeq = 0;
// --- 2. STATE GETTERS ---
@@ -136,6 +137,8 @@ document.addEventListener('DOMContentLoaded', () => {
// --- 4. CORE RENDERER ---
function renderQR() {
const mySeq = ++renderSeq;
// 1. Gather State
const textData = getDataString();
const format = getFormat();
@@ -143,35 +146,38 @@ document.addEventListener('DOMContentLoaded', () => {
const colorDark = optFg.value;
const colorLight = optBg.value;
// 2. Validate Size (Do this EARLY so we can update the label)
// 2. Validate Size & Labels
let size = parseInt(optSize.value) || 256;
if (size < 64) size = 64;
if (size > 4000) size = 4000;
// 3. Update UI Labels (Stateless: Input -> Label)
hexFg.textContent = colorDark;
hexBg.textContent = colorLight;
// Update Resolution Label immediately
const resDisplay = document.getElementById('resolution-display');
if (resDisplay) resDisplay.textContent = `${size} x ${size} px`;
// 4. Reset DOM
qrContainer.innerHTML = '';
// 3. Reset Error State
stickyWrapper.classList.remove('has-error');
// 5. Handle Empty State (The Early Return)
// 4. Handle Empty State
if (!textData || textData.trim() === '') {
downloadBtn.disabled = true;
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
return; // Stop here. Container is clean, labels are updated.
// Only clear if we are the latest render
if (mySeq === renderSeq) {
qrContainer.innerHTML = '';
downloadBtn.disabled = true;
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
}
return;
}
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
// 6. Generate
// 5. Generate (Off-screen)
try {
const instance = new QRCode(qrContainer, {
const tempContainer = document.createElement('div');
const instance = new QRCode(tempContainer, {
text: textData,
width: size,
height: size,
@@ -180,29 +186,61 @@ document.addEventListener('DOMContentLoaded', () => {
correctLevel : ecc
});
// 7. Post-Process
if (format === 'svg') {
const modules = instance._oQRCode.modules;
const nodes = qrContainer.childNodes;
for(let i=0; i<nodes.length; i++) {
if(nodes[i].style) nodes[i].style.display = 'none';
}
const svgString = generateSVGString(modules, size, colorDark, colorLight);
qrContainer.insertAdjacentHTML('beforeend', svgString);
// SVG is instant, but check sequence just in case
if (mySeq === renderSeq) {
qrContainer.innerHTML = svgString;
downloadBtn.disabled = false;
}
}
else {
const img = qrContainer.querySelector('img');
if(img) img.style.display = 'block';
// PNG Mode (Async with Race Condition Protection)
const canvas = tempContainer.querySelector('canvas');
if (canvas) {
const dataUrl = canvas.toDataURL("image/png");
const newImg = new Image();
newImg.style.display = 'block';
newImg.style.width = '100%';
newImg.style.height = '100%';
newImg.style.imageRendering = 'pixelated';
newImg.src = dataUrl;
// Use decode() if available for seamless paint
const ready = newImg.decode
? newImg.decode()
: new Promise((resolve, reject) => {
newImg.onload = resolve;
newImg.onerror = reject;
});
ready.then(() => {
// [CHECK] If a newer render started while we were decoding, ABORT.
if (mySeq !== renderSeq) return;
qrContainer.replaceChildren(newImg);
downloadBtn.disabled = false;
}).catch(() => {
// Even on error, respect the sequence
if (mySeq !== renderSeq) return;
// Fallback: Just shove it in
qrContainer.replaceChildren(newImg);
downloadBtn.disabled = false;
});
}
}
downloadBtn.disabled = false;
} catch (e) {
// 8. Error Handling
// Error Handling (Sequence Check Not Strictly Needed here but good practice)
if (mySeq !== renderSeq) return;
const blob = new Blob([textData]);
const bytes = blob.size;
const maxCapacityMap = { 'L': 2950, 'M': 2328, 'Q': 1660, 'H': 1270 };
const maxBytes = maxCapacityMap[optEcc.value] || 1270;
const maxCapacityMap = { 'L': 2953, 'M': 2331, 'Q': 1663, 'H': 1273 };
const maxBytes = maxCapacityMap[optEcc.value] || 1273;
if (bytes >= maxBytes) {
renderError("CAPACITY EXCEEDED", "REDUCE TEXT OR LOWER ECC LEVEL");
@@ -252,11 +290,11 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
document.getElementById('form-text').classList.add('active');
// 5. Update Output (This will clear the QR code since inputs are empty)
// 5. Update Output
handleUpdate(true);
// 6. Focus the first box
const firstField = document.querySelector('#form-text textarea'); // Textarea for text mode
const firstField = document.querySelector('#form-text textarea');
if (firstField) firstField.focus();
}
@@ -328,7 +366,6 @@ document.addEventListener('DOMContentLoaded', () => {
const initialForm = document.getElementById(`form-${initialMode}`);
if (initialForm) {
initialForm.classList.add('active');
// Auto-focus the restored form
const startField = initialForm.querySelector('input, textarea');
if (startField) startField.focus();
}

View File

@@ -220,10 +220,22 @@ details .control-group:last-child {
background: #fff;
}
#qr-container img, #qr-container canvas {
/* Inside .preview-pane or relevant section */
#qr-container img,
#qr-container canvas,
#qr-container svg {
display: block;
max-width: 100%;
image-rendering: pixelated; /* Crisp edges */
/* CHANGE: Force it to fill the container exactly */
width: 100%;
height: 100%;
/* CRITICAL: This ensures upscaling looks like squares, not blur */
image-rendering: pixelated;
/* Optional: Ensure the SVG scales correctly */
object-fit: contain;
}
/* Buttons */