Add SVG option
This commit is contained in:
18
index.html
18
index.html
@@ -72,7 +72,7 @@
|
||||
|
||||
<details class="advanced-options">
|
||||
<summary>ADVANCED CONFIGURATION</summary>
|
||||
|
||||
|
||||
<div class="control-group">
|
||||
<label for="opt-ecc">Error Correction</label>
|
||||
<select id="opt-ecc">
|
||||
@@ -99,7 +99,7 @@
|
||||
<span id="hex-fg">#000000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="color-input">
|
||||
<label for="opt-bg">Background</label>
|
||||
<div class="input-wrapper">
|
||||
@@ -108,6 +108,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Output Format</label>
|
||||
<div style="display: flex; gap: 2rem; align-items: center; border: 1px solid black; padding: 1rem; background: #fff;">
|
||||
<label style="margin:0; display:flex; align-items:center; cursor:pointer;">
|
||||
<input type="radio" name="opt-fmt" value="png" checked style="width:auto; margin-right:0.5rem;">
|
||||
PNG
|
||||
</label>
|
||||
<label style="margin:0; display:flex; align-items:center; cursor:pointer;">
|
||||
<input type="radio" name="opt-fmt" value="svg" style="width:auto; margin-right:0.5rem;">
|
||||
SVG
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
|
||||
162
script.js
162
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 = `
|
||||
<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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
57
style.css
57
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user