Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# jsplayground
# JS Playground

This is a small browser-only logic gate simulator. Open `index.html` in a modern
web browser and drag gates from the sidebar onto the workspace. Connect gate
pins to create wires, toggle switches by double-clicking them and watch lights
update live.

Use the **Save** button to download the circuit as a JSON file and **Load** to
restore a saved design.

Select gates or wires with the mouse and press `Delete` to remove them.
285 changes: 285 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
const workspace = document.getElementById('workspace');
const wiresSvg = document.getElementById('wires');
const gateOptions = document.querySelectorAll('.gate-option');
const saveBtn = document.getElementById('save');
const loadInput = document.getElementById('load');
const loadBtn = document.getElementById('loadBtn');
const sidebar = document.getElementById('sidebar');

let gates = [];
let wires = [];
let dragData = null;
let connectData = null;
let gateId = 0;
let selectedGate = null;
let selectedWire = null;

function selectGate(gate) {
if (selectedGate && selectedGate.element) {
selectedGate.element.classList.remove('selected');
}
selectedGate = gate;
if (gate && gate.element) {
gate.element.classList.add('selected');
}
}

function selectWire(wire) {
if (selectedWire && selectedWire.el) {
selectedWire.el.classList.remove('selected-wire');
}
selectedWire = wire;
if (wire && wire.el) {
wire.el.classList.add('selected-wire');
}
}

function createGate(type, x, y) {
const gate = {
id: gateId++,
type,
x,
y,
inputs: [],
outputs: [],
element: null,
state: false,
};
const el = document.createElement('div');
el.classList.add('gate');
el.style.left = x + 'px';
el.style.top = y + 'px';
el.setAttribute('data-id', gate.id);
el.innerText = type;
if (type === 'SWITCH') el.classList.add('switch');
if (type === 'LIGHT') el.classList.add('light');
el.addEventListener('mousedown', startDrag);
el.addEventListener('click', () => selectGate(gate));
workspace.appendChild(el);
gate.element = el;

if (type === 'NOT') {
addPin(gate, 'input');
addPin(gate, 'output');
} else if (type === 'AND' || type === 'OR') {
addPin(gate, 'input');
addPin(gate, 'input');
addPin(gate, 'output');
} else if (type === 'SWITCH') {
addPin(gate, 'output');
el.addEventListener('dblclick', () => {
gate.state = !gate.state;
el.style.background = gate.state ? '#ff8080' : '#ffe0e0';
});
} else if (type === 'LIGHT') {
addPin(gate, 'input');
}
gates.push(gate);
return gate;
}

function addPin(gate, kind) {
const pin = document.createElement('div');
pin.classList.add('pin');
pin.classList.add(kind === 'input' ? 'input-pin' : 'output-pin');
pin.setAttribute('data-gate', gate.id);
pin.setAttribute('data-kind', kind);
pin.setAttribute('data-index', kind === 'input' ? gate.inputs.length : gate.outputs.length);
pin.addEventListener('mousedown', startConnect);
gate.element.appendChild(pin);
if (kind === 'input') gate.inputs.push({ state: false, el: pin });
else gate.outputs.push({ state: false, el: pin });
layoutPins(gate);
}

function layoutPins(gate) {
gate.inputs.forEach((p, i) => { p.el.style.top = (20 * i + 10) + 'px'; });
gate.outputs.forEach((p, i) => { p.el.style.top = (20 * i + 10) + 'px'; });
}

function startDrag(e) {
const el = e.currentTarget;
dragData = { el, offsetX: e.offsetX, offsetY: e.offsetY };
const gid = parseInt(el.getAttribute('data-id'));
const gate = gates.find(g => g.id === gid);
selectGate(gate);
document.addEventListener('mousemove', dragMove);
document.addEventListener('mouseup', endDrag);
}

function dragMove(e) {
if (!dragData) return;
const x = e.pageX - dragData.offsetX - sidebar.offsetWidth;
const y = e.pageY - dragData.offsetY;
dragData.el.style.left = x + 'px';
dragData.el.style.top = y + 'px';
const gate = gates.find(g => g.id == dragData.el.getAttribute('data-id'));
gate.x = x;
gate.y = y;
updateWires();
}

function endDrag() {
dragData = null;
document.removeEventListener('mousemove', dragMove);
document.removeEventListener('mouseup', endDrag);
}

function startConnect(e) {
e.stopPropagation();
const gid = parseInt(e.target.getAttribute('data-gate'));
const kind = e.target.getAttribute('data-kind');
const idx = parseInt(e.target.getAttribute('data-index'));
if (!connectData && kind === 'output') {
connectData = { fromGate: gid, fromIndex: idx };
selectWire(null);
} else if (connectData && kind === 'input') {
const wire = { fromGate: connectData.fromGate, fromIndex: connectData.fromIndex, toGate: gid, toIndex: idx, el: null };
wires.push(wire);
connectData = null;
selectWire(wire);
updateWires();
}
}

function updateWires() {
wiresSvg.innerHTML = '';
wires.forEach(w => {
const fromGate = gates.find(g => g.id === w.fromGate);
const toGate = gates.find(g => g.id === w.toGate);
if (!fromGate || !toGate) return;
const fromPin = fromGate.outputs[w.fromIndex].el;
const toPin = toGate.inputs[w.toIndex].el;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
const fx = fromGate.x + fromPin.offsetLeft + 5;
const fy = fromGate.y + fromPin.offsetTop + 5;
const tx = toGate.x + toPin.offsetLeft + 5;
const ty = toGate.y + toPin.offsetTop + 5;
line.setAttribute('x1', fx);
line.setAttribute('y1', fy);
line.setAttribute('x2', tx);
line.setAttribute('y2', ty);
line.setAttribute('stroke', '#000');
line.setAttribute('stroke-width', '2');
line.style.pointerEvents = 'auto';
line.addEventListener('click', (evt) => {
evt.stopPropagation();
selectWire(w);
});
wiresSvg.appendChild(line);
w.el = line;
if (selectedWire === w) {
line.classList.add('selected-wire');
}
});
}

function evaluate() {
gates.forEach(g => g.inputs.forEach(i => i.state = false));
wires.forEach(w => {
const fromGate = gates.find(g => g.id === w.fromGate);
const toGate = gates.find(g => g.id === w.toGate);
const value = fromGate.outputs[w.fromIndex].state;
toGate.inputs[w.toIndex].state = value;
});
gates.forEach(g => {
if (g.type === 'SWITCH') {
g.outputs[0].state = g.state;
} else if (g.type === 'AND') {
g.outputs[0].state = g.inputs.every(i => i.state);
} else if (g.type === 'OR') {
g.outputs[0].state = g.inputs.some(i => i.state);
} else if (g.type === 'NOT') {
g.outputs[0].state = !g.inputs[0].state;
} else if (g.type === 'LIGHT') {
g.element.style.background = g.inputs[0].state ? '#80ff80' : '#e0ffe0';
}
});
}

setInterval(evaluate, 100);

// Drag from sidebar
gateOptions.forEach(opt => {
opt.setAttribute('draggable', 'true');
opt.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', opt.getAttribute('data-type'));
});
});

workspace.addEventListener('dragover', e => e.preventDefault());
workspace.addEventListener('drop', e => {
const type = e.dataTransfer.getData('text/plain');
const x = e.offsetX;
const y = e.offsetY;
createGate(type, x, y);
});
workspace.addEventListener('mousedown', () => {
selectGate(null);
selectWire(null);
});

document.addEventListener('keydown', e => {
if (e.key === 'Delete') {
if (selectedWire) {
removeWire(selectedWire);
} else if (selectedGate) {
removeGate(selectedGate.id);
}
}
});

function removeGate(id) {
const gate = gates.find(g => g.id === id);
if (!gate) return;
gate.element.remove();
wires = wires.filter(w => w.fromGate !== id && w.toGate !== id);
gates = gates.filter(g => g.id !== id);
updateWires();
}

function removeWire(wire) {
if (!wire) return;
wires = wires.filter(w => w !== wire);
if (wire.el) wire.el.remove();
selectWire(null);
updateWires();
}

saveBtn.addEventListener('click', () => {
const data = {
gates: gates.map(g => ({ id: g.id, type: g.type, x: g.x, y: g.y, state: g.state })),
wires: wires.map(w => ({ fromGate: w.fromGate, fromIndex: w.fromIndex, toGate: w.toGate, toIndex: w.toIndex }))
};
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'circuit.json';
a.click();
URL.revokeObjectURL(url);
});

loadBtn.addEventListener('click', () => loadInput.click());
loadInput.addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = evt => {
const data = JSON.parse(evt.target.result);
loadCircuit(data);
};
reader.readAsText(file);
});

function loadCircuit(data) {
gates.forEach(g => g.element.remove());
gates = [];
wires = [];
data.gates.forEach(g => {
const gate = createGate(g.type, g.x, g.y);
if (g.type === 'SWITCH') gate.state = g.state;
});
wires = data.wires.map(w => ({ ...w, el: null }));
updateWires();
}
26 changes: 26 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Logic Gate Simulator</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="sidebar">
<h3>Gates</h3>
<div class="gate-option" data-type="AND">AND</div>
<div class="gate-option" data-type="OR">OR</div>
<div class="gate-option" data-type="NOT">NOT</div>
<div class="gate-option" data-type="SWITCH">Switch</div>
<div class="gate-option" data-type="LIGHT">Light</div>
<button id="save">Save</button>
<input type="file" id="load" style="display:none" />
<button id="loadBtn">Load</button>
</div>
<div id="workspace-container">
<svg id="wires"></svg>
<div id="workspace"></div>
</div>
<script src="app.js"></script>
</body>
</html>
72 changes: 72 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
body {
margin: 0;
font-family: Arial, sans-serif;
}
#sidebar {
width: 150px;
background: #f0f0f0;
padding: 10px;
box-sizing: border-box;
position: fixed;
top: 0;
bottom: 0;
overflow-y: auto;
}
#workspace-container {
margin-left: 150px;
position: relative;
height: 100vh;
}
#workspace {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
overflow: hidden;
}
#gates svg {
pointer-events: none;
}
.gate {
position: absolute;
border: 1px solid #444;
padding: 5px;
background: #ddd;
cursor: move;
user-select: none;
}
.gate.selected {
border-color: red;
}
.pin {
width: 10px;
height: 10px;
background: #666;
border-radius: 50%;
position: absolute;
}
.input-pin {
left: -5px;
}
.output-pin {
right: -5px;
}
.switch {
background: #ffe0e0;
}
.light {
background: #e0ffe0;
}
svg#wires {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
line.selected-wire {
stroke: red;
}