diff --git a/index.html b/index.html index 31eb552..242124b 100644 --- a/index.html +++ b/index.html @@ -72,7 +72,7 @@ ADVANCED CONFIGURATION - + Error Correction @@ -99,7 +99,7 @@ #000000 - + Background @@ -108,6 +108,20 @@ + + + Output Format + + + + PNG + + + + SVG + + + diff --git a/script.js b/script.js index afea26f..f191aaf 100644 --- a/script.js +++ b/script.js @@ -2,39 +2,49 @@ document.addEventListener('DOMContentLoaded', () => { // --- GLOBALS --- const qrContainer = document.getElementById('qr-container'); const downloadBtn = document.getElementById('btn-download'); - + // Inputs const modeSelector = document.getElementById('mode-selector'); const optEcc = document.getElementById('opt-ecc'); const optSize = document.getElementById('opt-size'); const optFg = document.getElementById('opt-fg'); const optBg = document.getElementById('opt-bg'); - - // Hex Labels (for display) 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 --- + function getFormat() { + for (const r of fmtRadios) { + if (r.checked) return r.value; + } + return 'png'; + } + function createQRInstance() { + // We clean the container but keep the instance variable if we can qrContainer.innerHTML = ''; - - // Validation: Clamp size to prevent crashing browser + 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 Hex Labels 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, @@ -42,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => { colorLight : colorLight, correctLevel : ecc }); - + + // Force update to draw updateQR(); } @@ -54,7 +65,7 @@ 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 ''; + if (!ssid) return ''; const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1'); const cleanPass = pass.replace(/([\\;,:])/g, '\\$1'); return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`; @@ -68,35 +79,112 @@ document.addEventListener('DOMContentLoaded', () => { return ''; } + // The Custom SVG Renderer + // We read the grid data from the library and build a vector string + 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`; + } + } + } + + const svg = ` + + + + + `; + + return svg; + } + function updateQR() { if (!qrcodeObj) return; const data = getDataString(); - + const format = getFormat(); + if (!data || data.trim() === '') { qrcodeObj.clear(); + qrContainer.innerHTML = ''; // Clear SVG leftovers downloadBtn.disabled = true; + downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`; return; } downloadBtn.disabled = false; + downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`; + + // 1. Let the library calculate the math qrcodeObj.makeCode(data); + + // 2. Handle Display based on format + if (format === 'svg') { + // Hide the canvas image generated by lib + const imgs = qrContainer.querySelectorAll('img'); + imgs.forEach(i => i.style.display = 'none'); + const canvas = qrContainer.querySelectorAll('canvas'); + canvas.forEach(c => c.style.display = 'none'); + + // 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); + + // Remove old SVG if exists + const oldSvg = qrContainer.querySelector('svg'); + if (oldSvg) oldSvg.remove(); + + // Inject new SVG + qrContainer.insertAdjacentHTML('beforeend', svgString); + } + } 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(); + } } // --- EVENT LISTENERS --- - // 1. Config Changes (Re-create instance) - // We listen to 'change' for colors (happens when picker closes) - // and 'input' (happens while dragging) depending on preference. - // 'input' is smoother but more CPU intensive. Let's use 'input'. + // 1. Inputs triggering update [optEcc, optSize, optFg, optBg].forEach(el => { el.addEventListener('input', () => { - // Debounce the recreation slightly to prevent lag during color dragging clearTimeout(debounceTimer); debounceTimer = setTimeout(createQRInstance, 100); }); }); - // 2. Mode Switching + // Format Change + fmtRadios.forEach(r => { + r.addEventListener('change', updateQR); + }); + + // Mode Switch modeSelector.addEventListener('change', (e) => { const newMode = e.target.value; document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active')); @@ -104,7 +192,7 @@ document.addEventListener('DOMContentLoaded', () => { updateQR(); }); - // 3. Data Entry (Text, WiFi, Email) + // Data Entry document.querySelectorAll('.input-form').forEach(form => { form.addEventListener('input', () => { clearTimeout(debounceTimer); @@ -112,16 +200,38 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // 4. Download + // 2. Download Logic downloadBtn.addEventListener('click', () => { - const img = qrContainer.querySelector('img'); - if (img && img.src) { - const link = document.createElement('a'); - link.href = img.src; - link.download = `qrcode-${Date.now()}.png`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const format = getFormat(); + + if (format === 'png') { + // PNG Download + const img = qrContainer.querySelector('img'); + if (img && img.src) { + const link = document.createElement('a'); + link.href = img.src; + link.download = `qr-manifesto-${Date.now()}.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } 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); + } } }); diff --git a/style.css b/style.css index b07c706..4ce8b91 100644 --- a/style.css +++ b/style.css @@ -4,7 +4,7 @@ --text-color: #000000; --border-color: #000000; --pane-bg: #ffffff; - + /* Typography */ /* Serif for the Interface/Authority */ /* --font-serif: "Georgia", "Times New Roman", Times, serif; */ @@ -28,13 +28,13 @@ body { -webkit-font-smoothing: antialiased; } -/* LAYOUT: CSS GRID +/* LAYOUT: CSS GRID Rows: Content (1fr) -> Footer (auto) */ .app-container { display: grid; grid-template-columns: 1fr; - grid-template-rows: 1fr auto; + grid-template-rows: 1fr auto; min-height: 100vh; border-top: 5px solid var(--border-color); /* Masthead line */ } @@ -86,12 +86,12 @@ input, textarea, select { background-color: transparent; border: 1px solid var(--border-color); border-radius: 0; /* Crucial: No rounded corners */ - + /* Data font */ font-family: var(--font-mono); font-size: 1rem; color: var(--text-color); - + /* Reset native styles */ appearance: none; -webkit-appearance: none; @@ -140,7 +140,7 @@ summary { transition: background 0.1s; } -/* Custom indicator logic if you want complete control, +/* Custom indicator logic if you want complete control, but standard text [+]/[-] is very brutalist. */ details[open] summary { border-bottom: 1px solid var(--border-color); @@ -187,10 +187,10 @@ details .control-group:last-child { width: 256px; height: 256px; margin: 0 auto 2rem auto; - + /* Frame */ border: 1px solid var(--border-color); - padding: 10px; + padding: 10px; display: flex; align-items: center; justify-content: center; @@ -211,7 +211,7 @@ button { color: var(--bg-color); /* White */ border: 1px solid var(--text-color); border-radius: 0; - + font-family: var(--font-mono); font-size: 0.9rem; font-weight: 700; @@ -273,7 +273,7 @@ button:disabled { @media (min-width: 768px) { .app-container { /* Sidebar fixed 400px, Content fluid */ - grid-template-columns: 400px 1fr; + grid-template-columns: 400px 1fr; max-width: 100%; margin: 0; border-top: none; @@ -282,20 +282,20 @@ button:disabled { .input-pane { border-right: 1px solid var(--border-color); /* Allow natural height so page scrolls to footer */ - height: auto; + height: auto; min-height: 100vh; } .preview-pane { min-height: 100vh; justify-content: center; /* Center Vertically */ - + /* Engineering Paper Texture */ background-color: #fdfdfd; background-image: radial-gradient(#000 0.5px, transparent 0.5px); background-size: 20px 20px; } - + .sticky-wrapper { position: sticky; top: 2rem; @@ -371,10 +371,10 @@ input[type="number"] { -moz-appearance: textfield; /* Remove Firefox spinner */ } /* Remove Webkit spinners if you prefer a cleaner look */ -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } /* Color Group Layout */ @@ -422,3 +422,26 @@ input[type="color"]::-webkit-color-swatch { font-size: 0.9rem; text-transform: uppercase; } + +/* Radio Button Logic inside the bordered box */ +input[type="radio"] { + /* Reset the width 100% from the general input rule */ + width: 1.2rem; + height: 1.2rem; + border: 1px solid var(--border-color); + border-radius: 50%; /* Radios can stay round, or make them 0 for squares */ + appearance: none; + -webkit-appearance: none; + background: transparent; + position: relative; + margin-right: 0.5rem; +} + +input[type="radio"]:checked { + background: var(--text-color); /* Fill black when checked */ + box-shadow: inset 0 0 0 3px #fff; /* Create a 'donut' look */ +} + +input[type="radio"]:focus { + outline: none; +}