Skip to content

Commit 9cf9127

Browse files
goatcounter
1 parent 8d02ab8 commit 9cf9127

8 files changed

Lines changed: 394 additions & 194 deletions

File tree

build/app.js

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ document.addEventListener('DOMContentLoaded', () => {
104104
}
105105

106106
// Build Google-Docs-friendly HTML from a table element.
107-
function buildCopyableHTML(tableEl) {
107+
// If rawOnly is true, returns just the styled table HTML (no <html> wrapper).
108+
function buildCopyableHTML(tableEl, rawOnly) {
108109
const clone = tableEl.cloneNode(true);
109110
clone.setAttribute('border', '1');
110111
clone.style.borderCollapse = 'collapse';
@@ -117,6 +118,7 @@ document.addEventListener('DOMContentLoaded', () => {
117118
th.style.backgroundColor = '#f5f5f5';
118119
th.style.fontWeight = 'bold';
119120
});
121+
if (rawOnly) return clone.outerHTML;
120122
return '<html><body><!--StartFragment-->' + clone.outerHTML + '<!--EndFragment--></body></html>';
121123
}
122124

@@ -160,26 +162,49 @@ document.addEventListener('DOMContentLoaded', () => {
160162

161163
// Copy K-map as CSV
162164
copyKMapCSVBtn.addEventListener('click', () => {
163-
const tableEl = kmapContainer.querySelector('table');
164-
if (!tableEl) return;
165-
const rows = [];
166-
tableEl.querySelectorAll('tr').forEach(tr => {
167-
const cells = [];
168-
tr.querySelectorAll('th, td').forEach(cell => cells.push(cell.textContent));
169-
rows.push(cells.join(','));
165+
const tables = kmapContainer.querySelectorAll('table');
166+
if (!tables.length) return;
167+
const parts = [];
168+
tables.forEach((tableEl, idx) => {
169+
// Include sub-map label if present
170+
const label = tableEl.closest('.kmap-submap');
171+
if (label) {
172+
const labelEl = label.querySelector('.kmap-submap-label');
173+
if (labelEl) parts.push(labelEl.textContent);
174+
}
175+
const rows = [];
176+
tableEl.querySelectorAll('tr').forEach(tr => {
177+
const cells = [];
178+
tr.querySelectorAll('th, td').forEach(cell => cells.push(cell.textContent));
179+
rows.push(cells.join(','));
180+
});
181+
parts.push(rows.join('\n'));
170182
});
171-
const csv = rows.join('\n');
172-
navigator.clipboard.writeText(csv);
183+
navigator.clipboard.writeText(parts.join('\n\n'));
173184
});
174185

175186
// Copy K-map as HTML table
176187
copyKMapHTMLBtn.addEventListener('click', () => {
177-
const tableEl = kmapContainer.querySelector('table');
178-
if (!tableEl) return;
179-
const htmlStr = buildCopyableHTML(tableEl);
180-
const plainText = getPlainText(tableEl);
181-
const blob = new Blob([htmlStr], { type: 'text/html' });
182-
const textBlob = new Blob([plainText], { type: 'text/plain' });
188+
const tables = kmapContainer.querySelectorAll('table');
189+
if (!tables.length) return;
190+
const htmlParts = [];
191+
const textParts = [];
192+
tables.forEach(tableEl => {
193+
const label = tableEl.closest('.kmap-submap');
194+
if (label) {
195+
const labelEl = label.querySelector('.kmap-submap-label');
196+
if (labelEl) {
197+
htmlParts.push('<p><b>' + labelEl.textContent + '</b></p>');
198+
textParts.push(labelEl.textContent);
199+
}
200+
}
201+
htmlParts.push(buildCopyableHTML(tableEl, true));
202+
textParts.push(getPlainText(tableEl));
203+
});
204+
const fullHtml = '<html><body><!--StartFragment-->' + htmlParts.join('') + '<!--EndFragment--></body></html>';
205+
const fullText = textParts.join('\n\n');
206+
const blob = new Blob([fullHtml], { type: 'text/html' });
207+
const textBlob = new Blob([fullText], { type: 'text/plain' });
183208
navigator.clipboard.write([
184209
new ClipboardItem({
185210
'text/html': blob,

build/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,7 @@ <h3>XNOR</h3>
9494
<script src="parser.js"></script>
9595
<script src="visualizer.js"></script>
9696
<script src="app.js"></script>
97+
<script data-goatcounter="https://deankuruzovich.goatcounter.com/count"
98+
async src="//gc.zgo.at/count.js"></script>
9799
</body>
98100
</html>

build/styles.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,27 @@ td.low {
152152
color: #666;
153153
}
154154

155+
/* K-map sub-map grid (5+ variables) */
156+
.kmap-grid {
157+
display: grid;
158+
gap: 20px;
159+
margin-top: 10px;
160+
justify-content: start;
161+
}
162+
163+
.kmap-submap {
164+
display: flex;
165+
flex-direction: column;
166+
align-items: flex-start;
167+
}
168+
169+
.kmap-submap-label {
170+
font-weight: bold;
171+
font-size: 13px;
172+
margin-bottom: 4px;
173+
color: #555;
174+
}
175+
155176
/* Help button */
156177
.help-btn {
157178
width: 36px;

build/visualizer.js

Lines changed: 133 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,115 +1252,167 @@ class Visualizer {
12521252
}
12531253

12541254
/**
1255-
* Render Karnaugh Map (K-map)
1256-
* Supports 2, 3, and 4 variable expressions
1255+
* Generate n-bit Gray code sequence
1256+
* e.g. grayCode(2) → ['00','01','11','10']
12571257
*/
1258-
renderKMap(containerElement) {
1259-
containerElement.innerHTML = '';
1260-
1261-
const numVars = this.variables.length;
1258+
grayCode(n) {
1259+
if (n === 0) return [''];
1260+
if (n === 1) return ['0', '1'];
1261+
const prev = this.grayCode(n - 1);
1262+
return [
1263+
...prev.map(code => '0' + code),
1264+
...[...prev].reverse().map(code => '1' + code)
1265+
];
1266+
}
12621267

1263-
if (numVars < 2 || numVars > 4) {
1264-
containerElement.innerHTML = `<p>K-maps are only supported for 2–4 variables (you have ${numVars})</p>`;
1265-
return;
1266-
}
1268+
/**
1269+
* Build a single K-map table for given row/col variables and a prefix key
1270+
* rowVars: array of variable names for the row axis
1271+
* colVars: array of variable names for the column axis
1272+
* prefix: fixed bit string prepended to each lookup key
1273+
* valueMap: full truth table lookup { binaryKey → 0|1 }
1274+
*/
1275+
buildKMapTable(rowVars, colVars, prefix, valueMap) {
1276+
const rowGray = this.grayCode(rowVars.length);
1277+
const colGray = this.grayCode(colVars.length);
12671278

1268-
// Create a lookup: binary-key → output value
1269-
const valueMap = {};
1270-
this.truthTable.forEach(row => {
1271-
const key = this.variables.map(v => row[v]).join('');
1272-
valueMap[key] = row.output;
1273-
});
1279+
const rowLabel = rowVars.map(v => v.toUpperCase()).join('');
1280+
const colLabel = colVars.map(v => v.toUpperCase()).join('');
12741281

12751282
const table = document.createElement('table');
12761283
table.className = 'kmap-table';
12771284

1278-
if (numVars === 2) {
1279-
this.renderKMap2Var(table, valueMap);
1280-
} else if (numVars === 3) {
1281-
this.renderKMap3Var(table, valueMap);
1282-
} else if (numVars === 4) {
1283-
this.renderKMap4Var(table, valueMap);
1284-
}
1285-
1286-
containerElement.appendChild(table);
1287-
}
1288-
1289-
renderKMap2Var(table, valueMap) {
1290-
const [v0, v1] = this.variables.map(v => v.toUpperCase());
1291-
1285+
// Header row
12921286
const headerRow = document.createElement('tr');
1293-
headerRow.innerHTML = `<th class="kmap-corner">${v0} \\ ${v1}</th><th>0</th><th>1</th>`;
1287+
const corner = document.createElement('th');
1288+
corner.className = 'kmap-corner';
1289+
corner.textContent = rowLabel + ' \\ ' + colLabel;
1290+
headerRow.appendChild(corner);
1291+
colGray.forEach(c => {
1292+
const th = document.createElement('th');
1293+
th.textContent = c;
1294+
headerRow.appendChild(th);
1295+
});
12941296
table.appendChild(headerRow);
12951297

1296-
['0', '1'].forEach(r => {
1298+
// Data rows
1299+
rowGray.forEach(r => {
12971300
const row = document.createElement('tr');
1298-
const rowLabel = document.createElement('th');
1299-
rowLabel.textContent = r;
1300-
row.appendChild(rowLabel);
1301-
1302-
['0', '1'].forEach(c => {
1301+
const rowTh = document.createElement('th');
1302+
rowTh.textContent = r;
1303+
row.appendChild(rowTh);
1304+
colGray.forEach(c => {
13031305
const td = document.createElement('td');
1304-
const key = r + c;
1306+
const key = prefix + r + c;
13051307
td.textContent = valueMap[key];
13061308
td.className = valueMap[key] === 1 ? 'high' : 'low';
13071309
row.appendChild(td);
13081310
});
1309-
13101311
table.appendChild(row);
13111312
});
1312-
}
13131313

1314-
renderKMap3Var(table, valueMap) {
1315-
const [v0, v1, v2] = this.variables.map(v => v.toUpperCase());
1316-
const colOrder = ['00', '01', '11', '10'];
1314+
return table;
1315+
}
13171316

1318-
const headerRow = document.createElement('tr');
1319-
headerRow.innerHTML = `<th class="kmap-corner">${v0} \\ ${v1}${v2}</th><th>00</th><th>01</th><th>11</th><th>10</th>`;
1320-
table.appendChild(headerRow);
1317+
/**
1318+
* Render Karnaugh Map (K-map)
1319+
* 2-4 vars: single table
1320+
* 5+ vars: grid of labeled 4×4 sub-maps (standard textbook layout)
1321+
*/
1322+
renderKMap(containerElement) {
1323+
containerElement.innerHTML = '';
13211324

1322-
['0', '1'].forEach(r => {
1323-
const row = document.createElement('tr');
1324-
const rowLabel = document.createElement('th');
1325-
rowLabel.textContent = r;
1326-
row.appendChild(rowLabel);
1325+
const numVars = this.variables.length;
1326+
const vars = this.variables; // already sorted
13271327

1328-
colOrder.forEach(c => {
1329-
const td = document.createElement('td');
1330-
const key = r + c;
1331-
td.textContent = valueMap[key];
1332-
td.className = valueMap[key] === 1 ? 'high' : 'low';
1333-
row.appendChild(td);
1334-
});
1328+
if (numVars < 2) {
1329+
containerElement.innerHTML = '<p>K-maps require at least 2 variables.</p>';
1330+
return;
1331+
}
13351332

1336-
table.appendChild(row);
1333+
// Create a lookup: binary-key → output value
1334+
const valueMap = {};
1335+
this.truthTable.forEach(row => {
1336+
const key = vars.map(v => row[v]).join('');
1337+
valueMap[key] = row.output;
13371338
});
1338-
}
1339-
1340-
renderKMap4Var(table, valueMap) {
1341-
const [v0, v1, v2, v3] = this.variables.map(v => v.toUpperCase());
1342-
const grayOrder = ['00', '01', '11', '10'];
1343-
1344-
const headerRow = document.createElement('tr');
1345-
headerRow.innerHTML = `<th class="kmap-corner">${v0}${v1} \\ ${v2}${v3}</th><th>00</th><th>01</th><th>11</th><th>10</th>`;
1346-
table.appendChild(headerRow);
13471339

1348-
grayOrder.forEach(r => {
1349-
const row = document.createElement('tr');
1350-
const rowLabel = document.createElement('th');
1351-
rowLabel.textContent = r;
1352-
row.appendChild(rowLabel);
1340+
// Decide variable split:
1341+
// Row vars: first floor(innerVars/2) bits (max 2 for ≤4 total inner)
1342+
// Col vars: remaining inner bits (max 2 for ≤4 total inner)
1343+
// Outer vars (5+): extra variables that select which sub-map
1344+
//
1345+
// Standard textbook: inner map is always 4 vars (2 row + 2 col)
1346+
// except for 2 vars (1+1) and 3 vars (1+2)
1347+
let rowVars, colVars, outerVars;
1348+
1349+
if (numVars <= 4) {
1350+
// Single table, no outer vars
1351+
outerVars = [];
1352+
const innerCount = numVars;
1353+
const rowCount = Math.floor(innerCount / 2);
1354+
const colCount = innerCount - rowCount;
1355+
rowVars = vars.slice(0, rowCount);
1356+
colVars = vars.slice(rowCount, rowCount + colCount);
1357+
} else {
1358+
// 5+ vars: first 2 → rows, next 2 → cols, rest → outer (sub-map selectors)
1359+
rowVars = vars.slice(0, 2);
1360+
colVars = vars.slice(2, 4);
1361+
outerVars = vars.slice(4);
1362+
}
13531363

1354-
grayOrder.forEach(c => {
1355-
const td = document.createElement('td');
1356-
const key = r + c;
1357-
td.textContent = valueMap[key];
1358-
td.className = valueMap[key] === 1 ? 'high' : 'low';
1359-
row.appendChild(td);
1364+
if (outerVars.length === 0) {
1365+
// Simple single table (2-4 vars)
1366+
const table = this.buildKMapTable(rowVars, colVars, '', valueMap);
1367+
containerElement.appendChild(table);
1368+
} else {
1369+
// Multiple sub-maps in a grid
1370+
const outerGray = this.grayCode(outerVars.length);
1371+
const outerLabel = outerVars.map(v => v.toUpperCase()).join('');
1372+
1373+
// Determine grid layout (standard textbook):
1374+
// outerVars=1 → 1 row × 2 cols
1375+
// outerVars=2 → 2 rows × 2 cols (Gray-coded)
1376+
// outerVars=3 → 2 rows × 4 cols
1377+
// outerVars=4 → 4 rows × 4 cols
1378+
// General: gridRows = 2^floor(outerVars/2), gridCols = 2^ceil(outerVars/2)
1379+
const gridRowBits = Math.floor(outerVars.length / 2);
1380+
const gridColBits = outerVars.length - gridRowBits;
1381+
const gridRowGray = this.grayCode(gridRowBits);
1382+
const gridColGray = this.grayCode(gridColBits);
1383+
1384+
const gridRowVars = outerVars.slice(0, gridRowBits);
1385+
const gridColVars = outerVars.slice(gridRowBits);
1386+
1387+
const wrapper = document.createElement('div');
1388+
wrapper.className = 'kmap-grid';
1389+
// CSS grid dimensions
1390+
wrapper.style.gridTemplateColumns = `repeat(${gridColGray.length}, auto)`;
1391+
1392+
gridRowGray.forEach(gr => {
1393+
gridColGray.forEach(gc => {
1394+
const prefix = gr + gc;
1395+
const subDiv = document.createElement('div');
1396+
subDiv.className = 'kmap-submap';
1397+
1398+
// Build label for this sub-map
1399+
const label = document.createElement('div');
1400+
label.className = 'kmap-submap-label';
1401+
const parts = [];
1402+
for (let i = 0; i < outerVars.length; i++) {
1403+
parts.push(outerVars[i].toUpperCase() + '=' + prefix[i]);
1404+
}
1405+
label.textContent = parts.join(', ');
1406+
subDiv.appendChild(label);
1407+
1408+
const table = this.buildKMapTable(rowVars, colVars, prefix, valueMap);
1409+
subDiv.appendChild(table);
1410+
wrapper.appendChild(subDiv);
1411+
});
13601412
});
13611413

1362-
table.appendChild(row);
1363-
});
1414+
containerElement.appendChild(wrapper);
1415+
}
13641416
}
13651417
}
13661418

0 commit comments

Comments
 (0)