Update rendering for small size
And fix rendering flicker bug.
This commit is contained in:
91
script.js
91
script.js
@@ -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();
|
||||
}
|
||||
|
||||
18
style.css
18
style.css
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user