feat: add Examples dropdown with pre-built circuits
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -42,6 +42,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Examples Dropdown -->
|
||||
<div class="toolbar-dropdown" id="examples-section">
|
||||
<button class="dropdown-toggle">Examples <span class="dropdown-arrow">▾</span></button>
|
||||
<div class="dropdown-menu" id="examples-menu"></div>
|
||||
</div>
|
||||
|
||||
<button class="action-btn sim-btn" id="sim-btn">Waveform</button>
|
||||
<div class="toolbar-right">
|
||||
<button class="action-btn export-btn" id="export-btn" title="Export circuit">↓ Export</button>
|
||||
|
||||
29
js/events.js
29
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 = `<span class="example-name">${ex.name}</span><span class="example-desc">${ex.description}</span>`;
|
||||
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 = [];
|
||||
|
||||
300
js/examples.js
Normal file
300
js/examples.js
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user