Interim working solution
This commit is contained in:
14
index.html
14
index.html
@@ -127,9 +127,21 @@
|
||||
|
||||
<section class="pane preview-pane">
|
||||
<div class="sticky-wrapper">
|
||||
|
||||
<div id="qr-container">
|
||||
</div>
|
||||
<button id="btn-download" disabled>Download PNG</button>
|
||||
|
||||
<button id="btn-download" disabled>DOWNLOAD PNG</button>
|
||||
|
||||
<div id="capacity-meter" style="display:none; margin-top:0.75rem; width:100%;">
|
||||
<div style="display:flex; justify-content:space-between; font-size:0.7rem; font-family:var(--font-mono); margin-bottom:0.2rem;">
|
||||
<span>CAPACITY</span>
|
||||
<span id="capacity-text">0%</span>
|
||||
</div>
|
||||
<div style="width:100%; height:4px; background:#eee; border:1px solid #000;">
|
||||
<div id="capacity-bar" style="width:0%; height:100%; background:#000;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
281
script.js
281
script.js
@@ -2,7 +2,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- GLOBALS ---
|
||||
const qrContainer = document.getElementById('qr-container');
|
||||
const downloadBtn = document.getElementById('btn-download');
|
||||
|
||||
const stickyWrapper = document.querySelector('.sticky-wrapper');
|
||||
|
||||
// Gauge Elements
|
||||
const capMeter = document.getElementById('capacity-meter');
|
||||
const capBar = document.getElementById('capacity-bar');
|
||||
const capText = document.getElementById('capacity-text');
|
||||
|
||||
// Inputs
|
||||
const modeSelector = document.getElementById('mode-selector');
|
||||
const optEcc = document.getElementById('opt-ecc');
|
||||
@@ -11,14 +17,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const optBg = document.getElementById('opt-bg');
|
||||
const hexFg = document.getElementById('hex-fg');
|
||||
const hexBg = document.getElementById('hex-bg');
|
||||
|
||||
|
||||
// Format Radios
|
||||
const fmtRadios = document.getElementsByName('opt-fmt');
|
||||
|
||||
let qrcodeObj = null;
|
||||
let debounceTimer;
|
||||
|
||||
// --- LOGIC ---
|
||||
// --- 1. DATA GATHERING ---
|
||||
|
||||
function getFormat() {
|
||||
for (const r of fmtRadios) {
|
||||
@@ -27,36 +33,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return 'png';
|
||||
}
|
||||
|
||||
function createQRInstance() {
|
||||
// We clean the container but keep the instance variable if we can
|
||||
qrContainer.innerHTML = '';
|
||||
|
||||
let size = parseInt(optSize.value);
|
||||
if (isNaN(size) || size < 64) size = 64;
|
||||
if (size > 4000) size = 4000;
|
||||
|
||||
const ecc = QRCode.CorrectLevel[optEcc.value];
|
||||
const colorDark = optFg.value;
|
||||
const colorLight = optBg.value;
|
||||
|
||||
hexFg.textContent = colorDark;
|
||||
hexBg.textContent = colorLight;
|
||||
|
||||
// Initialize the library
|
||||
// Note: The library always creates a canvas internally.
|
||||
// We will just hide it if we are in SVG mode.
|
||||
qrcodeObj = new QRCode(qrContainer, {
|
||||
width: size,
|
||||
height: size,
|
||||
colorDark : colorDark,
|
||||
colorLight : colorLight,
|
||||
correctLevel : ecc
|
||||
});
|
||||
|
||||
// Force update to draw
|
||||
updateQR();
|
||||
}
|
||||
|
||||
function getDataString() {
|
||||
const mode = modeSelector.value;
|
||||
if (mode === 'text') {
|
||||
@@ -65,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const ssid = document.getElementById('inp-wifi-ssid').value;
|
||||
const pass = document.getElementById('inp-wifi-pass').value;
|
||||
const type = document.getElementById('inp-wifi-type').value;
|
||||
if (!ssid) return '';
|
||||
// return `WIFI:S:${ssid};T:${type};P:${pass};;`;
|
||||
if (!ssid) return '';
|
||||
const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1');
|
||||
const cleanPass = pass.replace(/([\\;,:])/g, '\\$1');
|
||||
return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`;
|
||||
@@ -79,133 +56,230 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return '';
|
||||
}
|
||||
|
||||
// The Custom SVG Renderer
|
||||
// We read the grid data from the library and build a vector string
|
||||
// --- 2. INSTANCE MANAGEMENT ---
|
||||
|
||||
function createQRInstance() {
|
||||
qrContainer.innerHTML = '';
|
||||
|
||||
let size = parseInt(optSize.value);
|
||||
if (isNaN(size) || size < 64) size = 64;
|
||||
if (size > 4000) size = 4000;
|
||||
|
||||
const ecc = QRCode.CorrectLevel[optEcc.value];
|
||||
const colorDark = optFg.value;
|
||||
const colorLight = optBg.value;
|
||||
|
||||
// Update Labels (UI)
|
||||
hexFg.textContent = colorDark;
|
||||
hexBg.textContent = colorLight;
|
||||
|
||||
try {
|
||||
return new QRCode(qrContainer, {
|
||||
width: size,
|
||||
height: size,
|
||||
colorDark : colorDark,
|
||||
colorLight : colorLight,
|
||||
correctLevel : ecc
|
||||
});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. RENDERING HELPERS ---
|
||||
|
||||
function renderSVG(modules) {
|
||||
const size = parseInt(optSize.value) || 256;
|
||||
const fg = optFg.value;
|
||||
const bg = optBg.value;
|
||||
const count = modules.length;
|
||||
|
||||
// Calculate module (pixel) size
|
||||
const modSize = size / count;
|
||||
|
||||
let pathData = '';
|
||||
|
||||
// Loop through the matrix
|
||||
for (let r = 0; r < count; r++) {
|
||||
for (let c = 0; c < count; c++) {
|
||||
if (modules[r][c]) {
|
||||
// It's a black dot. Draw a rect.
|
||||
// To prevent "hairline cracks" between blocks in some viewers,
|
||||
// we can slightly overlap or just use standard math.
|
||||
// Standard math is usually fine for SVGs.
|
||||
const x = c * modSize;
|
||||
const y = r * modSize;
|
||||
// Using 'h' and 'v' in path is more efficient than rects
|
||||
pathData += `M${x},${y}h${modSize}v${modSize}h-${modSize}z`;
|
||||
pathData += `M${x},${y}h${modSize}v${modSize}h-${modSize}z`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const svg = `
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
|
||||
<rect width="100%" height="100%" fill="${bg}"/>
|
||||
<path d="${pathData}" fill="${fg}" shape-rendering="crispEdges"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
function updateQR() {
|
||||
if (!qrcodeObj) return;
|
||||
const data = getDataString();
|
||||
// --- 4. CORE FUNCTIONS ---
|
||||
|
||||
// A. The Fast One: Updates the bar immediately
|
||||
function updateCapacityGauge(textData) {
|
||||
if (!textData || !capMeter) {
|
||||
if (capMeter) capMeter.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([textData]);
|
||||
const bytes = blob.size;
|
||||
|
||||
const maxCapacityMap = { 'L': 2953, 'M': 2331, 'Q': 1663, 'H': 1273 };
|
||||
const maxBytes = maxCapacityMap[optEcc.value] || 1273;
|
||||
|
||||
const usage = Math.min(100, Math.round((bytes / maxBytes) * 100));
|
||||
|
||||
if (usage > 5) {
|
||||
capMeter.style.display = 'block';
|
||||
capBar.style.width = `${usage}%`;
|
||||
capText.textContent = `${usage}% (${bytes}B)`;
|
||||
|
||||
if (usage > 90) {
|
||||
capBar.style.backgroundColor = 'red';
|
||||
capText.style.color = 'red';
|
||||
} else {
|
||||
capBar.style.backgroundColor = '#000';
|
||||
capText.style.color = '#000';
|
||||
}
|
||||
} else {
|
||||
capMeter.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// B. The Heavy One: Generates the image (Debounced)
|
||||
function generateQR(textData) {
|
||||
// Reset Error State
|
||||
stickyWrapper.classList.remove('has-error');
|
||||
const format = getFormat();
|
||||
|
||||
if (!data || data.trim() === '') {
|
||||
qrcodeObj.clear();
|
||||
qrContainer.innerHTML = ''; // Clear SVG leftovers
|
||||
// Handle Empty
|
||||
if (!textData || textData.trim() === '') {
|
||||
if (qrcodeObj) qrcodeObj.clear();
|
||||
qrcodeObj = null;
|
||||
qrContainer.innerHTML = '';
|
||||
downloadBtn.disabled = true;
|
||||
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
// Ensure Instance
|
||||
if (!qrcodeObj) qrcodeObj = createQRInstance();
|
||||
if (!qrcodeObj) return;
|
||||
|
||||
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
|
||||
|
||||
// 1. Let the library calculate the math
|
||||
qrcodeObj.makeCode(data);
|
||||
try {
|
||||
// Generate
|
||||
// qrcodeObj.clear()
|
||||
qrcodeObj.makeCode(textData);
|
||||
|
||||
// 2. Handle Display based on format
|
||||
if (format === 'svg') {
|
||||
// Hide the canvas image generated by lib
|
||||
// check if anything royally fucked up
|
||||
const imgs = qrContainer.querySelectorAll('img');
|
||||
imgs.forEach(i => i.style.display = 'none');
|
||||
const canvas = qrContainer.querySelectorAll('canvas');
|
||||
canvas.forEach(c => c.style.display = 'none');
|
||||
// if (imgs.length == 0) {
|
||||
// console.log('==================== YIKES =====================')
|
||||
// qrcodeObj = createQRInstance();
|
||||
// qrcodeObj.makeCode(textData)
|
||||
// }
|
||||
|
||||
// Access internal data to build SVG
|
||||
// Safety check: _oQRCode is the internal object of qrcode.js
|
||||
if (qrcodeObj._oQRCode && qrcodeObj._oQRCode.modules) {
|
||||
const svgString = renderSVG(qrcodeObj._oQRCode.modules);
|
||||
// Render SVG vs PNG
|
||||
if (format === 'svg') {
|
||||
const imgs = qrContainer.querySelectorAll('img');
|
||||
imgs.forEach(i => i.style.display = 'none');
|
||||
const canvas = qrContainer.querySelectorAll('canvas');
|
||||
canvas.forEach(c => c.style.display = 'none');
|
||||
|
||||
// Remove old SVG if exists
|
||||
const oldSvg = qrContainer.querySelector('svg');
|
||||
if (oldSvg) oldSvg.remove();
|
||||
|
||||
// Inject new SVG
|
||||
qrContainer.insertAdjacentHTML('beforeend', svgString);
|
||||
if (qrcodeObj._oQRCode && qrcodeObj._oQRCode.modules) {
|
||||
const svgString = renderSVG(qrcodeObj._oQRCode.modules);
|
||||
// Clear old SVGs/Errors
|
||||
qrContainer.querySelectorAll('svg, .qr-error-msg').forEach(e => e.remove());
|
||||
qrContainer.insertAdjacentHTML('beforeend', svgString);
|
||||
}
|
||||
} else {
|
||||
// PNG Mode
|
||||
const imgs = qrContainer.querySelectorAll('img');
|
||||
imgs.forEach(i => i.style.display = 'block');
|
||||
qrContainer.querySelectorAll('svg, .qr-error-msg').forEach(e => e.remove());
|
||||
}
|
||||
} else {
|
||||
// PNG Mode: Ensure canvas/img is visible
|
||||
// The library handles the rest
|
||||
const imgs = qrContainer.querySelectorAll('img');
|
||||
imgs.forEach(i => i.style.display = 'block');
|
||||
|
||||
// Remove SVG if exists
|
||||
const oldSvg = qrContainer.querySelector('svg');
|
||||
if (oldSvg) oldSvg.remove();
|
||||
downloadBtn.disabled = false;
|
||||
|
||||
} catch (e) {
|
||||
// Error Handling
|
||||
// console.log(e)
|
||||
qrcodeObj = null;
|
||||
qrContainer.innerHTML = `
|
||||
<div class="qr-error-msg">
|
||||
ERROR: CAPACITY EXCEEDED<br>
|
||||
<span style="font-size:0.7em; opacity:0.8; display:block; margin-top:0.5rem;">
|
||||
REDUCE TEXT OR<br>LOWER ECC LEVEL
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
downloadBtn.disabled = true;
|
||||
downloadBtn.textContent = "GENERATION FAILED";
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. THE COORDINATOR ---
|
||||
|
||||
function handleInput() {
|
||||
console.log("Input detected. Text length:", getDataString().length);
|
||||
const text = getDataString();
|
||||
console.log(text)
|
||||
|
||||
// 1. Instant Feedback
|
||||
updateCapacityGauge(text);
|
||||
|
||||
// 2. Delayed Heavy Lifting
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
generateQR(text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Force Immediate Update (for Config changes)
|
||||
function forceUpdate() {
|
||||
const text = getDataString();
|
||||
updateCapacityGauge(text);
|
||||
generateQR(text);
|
||||
}
|
||||
|
||||
// --- EVENT LISTENERS ---
|
||||
|
||||
// 1. Inputs triggering update
|
||||
// Config Changes (Require Rebuild)
|
||||
[optEcc, optSize, optFg, optBg].forEach(el => {
|
||||
el.addEventListener('input', () => {
|
||||
// Debounce the rebuild slightly
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(createQRInstance, 100);
|
||||
debounceTimer = setTimeout(() => {
|
||||
qrcodeObj = null;
|
||||
forceUpdate();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// Format Change
|
||||
fmtRadios.forEach(r => {
|
||||
r.addEventListener('change', updateQR);
|
||||
});
|
||||
// Format Change (Instant)
|
||||
fmtRadios.forEach(r => r.addEventListener('change', forceUpdate));
|
||||
|
||||
// Mode Switch
|
||||
// Mode Switch (Instant)
|
||||
modeSelector.addEventListener('change', (e) => {
|
||||
const newMode = e.target.value;
|
||||
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
|
||||
document.getElementById(`form-${newMode}`).classList.add('active');
|
||||
updateQR();
|
||||
forceUpdate();
|
||||
});
|
||||
|
||||
// Data Entry
|
||||
// DATA ENTRY (The optimized path)
|
||||
document.querySelectorAll('.input-form').forEach(form => {
|
||||
form.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(updateQR, 300);
|
||||
});
|
||||
form.addEventListener('input', handleInput);
|
||||
});
|
||||
|
||||
// 2. Download Logic
|
||||
// Download
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
const format = getFormat();
|
||||
|
||||
if (format === 'png') {
|
||||
// PNG Download
|
||||
const img = qrContainer.querySelector('img');
|
||||
if (img && img.src) {
|
||||
const link = document.createElement('a');
|
||||
@@ -213,28 +287,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
link.download = `qr-manifesto-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
link.remove();
|
||||
}
|
||||
} else {
|
||||
// SVG Download
|
||||
const svgEl = qrContainer.querySelector('svg');
|
||||
if (svgEl) {
|
||||
// Serialize the SVG DOM to a string
|
||||
const serializer = new XMLSerializer();
|
||||
const svgString = serializer.serializeToString(svgEl);
|
||||
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `qr-manifesto-${Date.now()}.svg`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
createQRInstance();
|
||||
// Initial Start
|
||||
forceUpdate();
|
||||
});
|
||||
|
||||
23
style.css
23
style.css
@@ -445,3 +445,26 @@ input[type="radio"]:checked {
|
||||
input[type="radio"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* In-Box Error Message */
|
||||
.qr-error-msg {
|
||||
color: red;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
|
||||
/* Brutalist Box inside the Box */
|
||||
border: 2px solid red;
|
||||
padding: 1rem;
|
||||
background-color: #fff;
|
||||
width: 80%; /* Don't touch the edges */
|
||||
}
|
||||
|
||||
/* Ensure the container keeps its shape even when showing error */
|
||||
#qr-container {
|
||||
/* Existing styles... */
|
||||
position: relative; /* Just in case */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user