From 2384c489b9b5d96dcba855458bf9ff83bf596182 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Fri, 20 Mar 2026 03:54:04 +0100 Subject: [PATCH] feat: add Examples dropdown with pre-built circuits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 4 example circuits accessible from a new toolbar dropdown: - SR Flip-Flop (NOR) — basic set-reset latch - SR Flip-Flop (NAND) — active-low variant - D Latch (1-bit Memory) — gated latch with enable - D Flip-Flop (Master-Slave) — edge-triggered with CLK Each example shows name + description in the dropdown and loads the full circuit with proper gate placement and wiring. Co-Authored-By: Claude Opus 4.6 --- css/style.css | 41 +++++++ index.html | 6 + js/events.js | 29 +++++ js/examples.js | 300 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 js/examples.js diff --git a/css/style.css b/css/style.css index d1e0087..5a30f1d 100644 --- a/css/style.css +++ b/css/style.css @@ -217,6 +217,47 @@ body { background: #252540; } +/* Example buttons */ +.example-btn { + width: 100%; + text-align: left; + padding: 8px 12px; + background: transparent; + border: 1px solid transparent; + border-radius: 5px; + color: #ccc; + cursor: pointer; + transition: all 0.15s; + display: flex; + flex-direction: column; + gap: 2px; +} + +.example-btn:hover { + background: #1a1a3a; + border-color: #00e599; +} + +.example-name { + font-size: 12px; + font-weight: 600; + color: #00e599; +} + +.example-btn:hover .example-name { + color: #00ff99; +} + +.example-desc { + font-size: 10px; + color: #666; + line-height: 1.3; +} + +#examples-menu { + min-width: 260px; +} + /* ==================== Canvas ==================== */ #canvas { position: fixed; diff --git a/index.html b/index.html index 178b10a..f4a3e00 100644 --- a/index.html +++ b/index.html @@ -42,6 +42,12 @@ + +
+ + +
+
diff --git a/js/events.js b/js/events.js index a4ca266..ca2eef1 100644 --- a/js/events.js +++ b/js/events.js @@ -9,6 +9,7 @@ import { puzzleMode, currentLevel, showLevelPanel } from './puzzleUI.js'; import { getLevel } from './levels.js'; import { saveState, loadState, exportAsFile, importFromFile } from './saveLoad.js'; import { enterComponentEditor, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js'; +import { getExampleList, loadExample } from './examples.js'; const PAN_SPEED = 40; @@ -255,6 +256,34 @@ export function initEvents() { }); }); + // ==================== EXAMPLES ==================== + const examplesMenu = document.getElementById('examples-menu'); + getExampleList().forEach((ex, i) => { + const btn = document.createElement('button'); + btn.className = 'example-btn'; + btn.innerHTML = `${ex.name}${ex.description}`; + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const hasGates = state.gates.length > 0; + if (hasGates && !confirm('Load example? This will replace your current circuit.')) return; + const data = loadExample(i); + if (data) { + state.gates = data.circuit.gates; + state.connections = data.circuit.connections; + state.nextId = data.circuit.nextId; + if (data.camera) { + state.camX = data.camera.camX; + state.camY = data.camera.camY; + state.zoom = data.camera.zoom; + } + clearWaveData(); + evaluateAll(); + } + closeAllDropdowns(); + }); + examplesMenu.appendChild(btn); + }); + document.getElementById('clear-btn').addEventListener('click', () => { if (state.gates.length === 0 || confirm('Clear all gates and connections?')) { state.gates = []; diff --git a/js/examples.js b/js/examples.js new file mode 100644 index 0000000..f2e5dec --- /dev/null +++ b/js/examples.js @@ -0,0 +1,300 @@ +// Pre-built example circuits +// Each example is a { gates, connections, nextId, camera } object + +const W = 100; // GATE_W +const H = 60; // GATE_H +const GAP_X = 160; +const GAP_Y = 100; + +/** + * SR Flip-Flop using cross-coupled NOR gates + * + * S ──┐ + * NOR─── Q + * ┌──┘ │ + * │ │ + * └──┐ │ + * NOR─── Q̅ + * R ──┘ + */ +function srFlipFlop() { + const gates = [ + // Inputs + { id: 1, type: 'INPUT', x: 50, y: 80, value: 0 }, // S (Set) + { id: 2, type: 'INPUT', x: 50, y: 280, value: 0 }, // R (Reset) + + // Cross-coupled NOR gates + { id: 3, type: 'NOR', x: 300, y: 80, value: 0 }, // Top NOR → Q + { id: 4, type: 'NOR', x: 300, y: 280, value: 0 }, // Bottom NOR → Q̅ + + // Outputs + { id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0 }, // Q + { id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0 } // Q̅ + ]; + + const connections = [ + // S → Top NOR input 0 + { from: 1, fromPort: 0, to: 3, toPort: 0 }, + // R → Bottom NOR input 1 + { from: 2, fromPort: 0, to: 4, toPort: 1 }, + // Top NOR (Q) → Bottom NOR input 0 (cross-couple) + { from: 3, fromPort: 0, to: 4, toPort: 0 }, + // Bottom NOR (Q̅) → Top NOR input 1 (cross-couple) + { from: 4, fromPort: 0, to: 3, toPort: 1 }, + // NOR outputs → visible outputs + { from: 3, fromPort: 0, to: 5, toPort: 0 }, + { from: 4, fromPort: 0, to: 6, toPort: 0 } + ]; + + return { + name: 'SR Flip-Flop (NOR)', + description: 'Set-Reset latch using cross-coupled NOR gates. Toggle S to set Q=1, toggle R to reset Q=0.', + gates, + connections, + nextId: 7, + camera: { camX: 0, camY: 0, zoom: 1 } + }; +} + +/** + * SR Flip-Flop using cross-coupled NAND gates + * + * S̅ ──┐ + * NAND── Q + * ┌───┘ │ + * │ │ + * └───┐ │ + * NAND── Q̅ + * R̅ ──┘ + */ +function srFlipFlopNand() { + const gates = [ + // Inputs (active low for NAND SR) + { id: 1, type: 'INPUT', x: 50, y: 80, value: 1 }, // S̅ + { id: 2, type: 'INPUT', x: 50, y: 280, value: 1 }, // R̅ + + // Cross-coupled NAND gates + { id: 3, type: 'NAND', x: 300, y: 80, value: 0 }, // Top NAND → Q + { id: 4, type: 'NAND', x: 300, y: 280, value: 0 }, // Bottom NAND → Q̅ + + // Outputs + { id: 5, type: 'OUTPUT', x: 550, y: 80, value: 0 }, // Q + { id: 6, type: 'OUTPUT', x: 550, y: 280, value: 0 } // Q̅ + ]; + + const connections = [ + // S̅ → Top NAND input 0 + { from: 1, fromPort: 0, to: 3, toPort: 0 }, + // R̅ → Bottom NAND input 1 + { from: 2, fromPort: 0, to: 4, toPort: 1 }, + // Top NAND (Q) → Bottom NAND input 0 (cross-couple) + { from: 3, fromPort: 0, to: 4, toPort: 0 }, + // Bottom NAND (Q̅) → Top NAND input 1 (cross-couple) + { from: 4, fromPort: 0, to: 3, toPort: 1 }, + // Outputs + { from: 3, fromPort: 0, to: 5, toPort: 0 }, + { from: 4, fromPort: 0, to: 6, toPort: 0 } + ]; + + return { + name: 'SR Flip-Flop (NAND)', + description: 'Set-Reset latch using cross-coupled NAND gates. Inputs are active-low: set S̅=0 to set, R̅=0 to reset.', + gates, + connections, + nextId: 7, + camera: { camX: 0, camY: 0, zoom: 1 } + }; +} + +/** + * Gated D Latch (1-bit memory) + * + * Uses an SR flip-flop with gating logic: + * D ──AND──┐ + * E ──┤ NOR── Q + * │ │ ┌──┘│ + * │ │ │ │ + * │ │ └──┐│ + * E ──┤ NOR── Q̅ + * D─NOT─AND┘ + * + * When Enable=1, Q follows D. + * When Enable=0, Q holds its value. + */ +function dLatch() { + const gates = [ + // Inputs + { id: 1, type: 'INPUT', x: 50, y: 100, value: 0 }, // D (Data) + { id: 2, type: 'INPUT', x: 50, y: 280, value: 0 }, // E (Enable) + + // NOT gate to invert D + { id: 3, type: 'NOT', x: 200, y: 340, value: 0 }, + + // AND gates for gating + { id: 4, type: 'AND', x: 350, y: 60, value: 0 }, // D AND E → S + { id: 5, type: 'AND', x: 350, y: 340, value: 0 }, // NOT(D) AND E → R + + // Cross-coupled NOR gates (SR latch core) + { id: 6, type: 'NOR', x: 550, y: 60, value: 0 }, // → Q + { id: 7, type: 'NOR', x: 550, y: 340, value: 0 }, // → Q̅ + + // Outputs + { id: 8, type: 'OUTPUT', x: 750, y: 60, value: 0 }, // Q + { id: 9, type: 'OUTPUT', x: 750, y: 340, value: 0 } // Q̅ + ]; + + const connections = [ + // D → AND top input, and D → NOT + { from: 1, fromPort: 0, to: 4, toPort: 0 }, + { from: 1, fromPort: 0, to: 3, toPort: 0 }, + + // E → both AND gates + { from: 2, fromPort: 0, to: 4, toPort: 1 }, + { from: 2, fromPort: 0, to: 5, toPort: 1 }, + + // NOT(D) → bottom AND + { from: 3, fromPort: 0, to: 5, toPort: 0 }, + + // AND outputs → NOR inputs (S and R) + { from: 4, fromPort: 0, to: 6, toPort: 0 }, + { from: 5, fromPort: 0, to: 7, toPort: 1 }, + + // Cross-coupling + { from: 6, fromPort: 0, to: 7, toPort: 0 }, + { from: 7, fromPort: 0, to: 6, toPort: 1 }, + + // Outputs + { from: 6, fromPort: 0, to: 8, toPort: 0 }, + { from: 7, fromPort: 0, to: 9, toPort: 0 } + ]; + + return { + name: 'D Latch (1-bit Memory)', + description: 'Gated D latch — a 1-bit memory cell. When Enable=1, output Q follows input D. When Enable=0, Q holds its last value.', + gates, + connections, + nextId: 10, + camera: { camX: 0, camY: 0, zoom: 1 } + }; +} + +/** + * D Flip-Flop (edge-triggered, master-slave) + * + * Two D latches in series with inverted enable: + * - Master latch captures D when CLK=0 + * - Slave latch outputs when CLK=1 + * This creates rising-edge triggered behavior. + */ +function dFlipFlop() { + const gates = [ + // Inputs + { id: 1, type: 'INPUT', x: 30, y: 100, value: 0 }, // D + { id: 2, type: 'CLOCK', x: 30, y: 350, value: 0 }, // CLK + + // CLK inverter (for master latch) + { id: 3, type: 'NOT', x: 170, y: 350, value: 0 }, + + // === MASTER LATCH (enabled when CLK=0, i.e. NOT CLK=1) === + { id: 4, type: 'NOT', x: 170, y: 200, value: 0 }, // NOT D for master + { id: 5, type: 'AND', x: 300, y: 60, value: 0 }, // D AND !CLK + { id: 6, type: 'AND', x: 300, y: 240, value: 0 }, // !D AND !CLK + { id: 7, type: 'NOR', x: 450, y: 60, value: 0 }, // Master Q + { id: 8, type: 'NOR', x: 450, y: 240, value: 0 }, // Master Q̅ + + // === SLAVE LATCH (enabled when CLK=1) === + { id: 9, type: 'NOT', x: 570, y: 200, value: 0 }, // NOT master Q for slave + { id: 10, type: 'AND', x: 680, y: 60, value: 0 }, // masterQ AND CLK + { id: 11, type: 'AND', x: 680, y: 240, value: 0 }, // !masterQ AND CLK + { id: 12, type: 'NOR', x: 830, y: 60, value: 0 }, // Slave Q + { id: 13, type: 'NOR', x: 830, y: 240, value: 0 }, // Slave Q̅ + + // Outputs + { id: 14, type: 'OUTPUT', x: 1010, y: 60, value: 0 }, // Q + { id: 15, type: 'OUTPUT', x: 1010, y: 240, value: 0 } // Q̅ + ]; + + const connections = [ + // D → master AND, D → master NOT + { from: 1, fromPort: 0, to: 5, toPort: 0 }, + { from: 1, fromPort: 0, to: 4, toPort: 0 }, + + // CLK → NOT (invert for master) + { from: 2, fromPort: 0, to: 3, toPort: 0 }, + + // !CLK → master AND gates (enable) + { from: 3, fromPort: 0, to: 5, toPort: 1 }, + { from: 3, fromPort: 0, to: 6, toPort: 1 }, + + // !D → master bottom AND + { from: 4, fromPort: 0, to: 6, toPort: 0 }, + + // Master AND outputs → Master NOR (SR latch) + { from: 5, fromPort: 0, to: 7, toPort: 0 }, + { from: 6, fromPort: 0, to: 8, toPort: 1 }, + + // Master cross-coupling + { from: 7, fromPort: 0, to: 8, toPort: 0 }, + { from: 8, fromPort: 0, to: 7, toPort: 1 }, + + // Master Q → slave AND, Master Q → slave NOT + { from: 7, fromPort: 0, to: 10, toPort: 0 }, + { from: 7, fromPort: 0, to: 9, toPort: 0 }, + + // CLK → slave AND gates (enable, direct CLK) + { from: 2, fromPort: 0, to: 10, toPort: 1 }, + { from: 2, fromPort: 0, to: 11, toPort: 1 }, + + // !masterQ → slave bottom AND + { from: 9, fromPort: 0, to: 11, toPort: 0 }, + + // Slave AND outputs → Slave NOR (SR latch) + { from: 10, fromPort: 0, to: 12, toPort: 0 }, + { from: 11, fromPort: 0, to: 13, toPort: 1 }, + + // Slave cross-coupling + { from: 12, fromPort: 0, to: 13, toPort: 0 }, + { from: 13, fromPort: 0, to: 12, toPort: 1 }, + + // Outputs + { from: 12, fromPort: 0, to: 14, toPort: 0 }, + { from: 13, fromPort: 0, to: 15, toPort: 0 } + ]; + + return { + name: 'D Flip-Flop (Master-Slave)', + description: 'Edge-triggered D flip-flop built from two D latches. Captures D on the rising edge of CLK. Use with the clock simulation to see it in action.', + gates, + connections, + nextId: 16, + camera: { camX: 0, camY: -50, zoom: 0.9 } + }; +} + +// Export all examples as a list +export const examples = [ + srFlipFlop, + srFlipFlopNand, + dLatch, + dFlipFlop +]; + +export function getExampleList() { + return examples.map((fn, i) => { + const ex = fn(); + return { id: i, name: ex.name, description: ex.description }; + }); +} + +export function loadExample(index) { + if (index < 0 || index >= examples.length) return null; + const ex = examples[index](); + return { + circuit: { + gates: JSON.parse(JSON.stringify(ex.gates)), + connections: JSON.parse(JSON.stringify(ex.connections)), + nextId: ex.nextId + }, + camera: ex.camera + }; +}