'use strict'; /* ══════════════════════════════════════════════════════════ LogicSim — Логические схемы Canvas-based digital logic circuit builder. Exports: LogicSim class, logicTool(), logicPreset(), logicClearAll(), _openLogic() ══════════════════════════════════════════════════════════ */ /* ── Gate definitions ── */ const GATE_DEFS = { INPUT: { ins: 0, outs: 1, label: 'IN', w: 56, h: 36 }, CLOCK: { ins: 0, outs: 1, label: 'CLK', w: 56, h: 36 }, OUTPUT: { ins: 1, outs: 0, label: 'OUT', w: 56, h: 36 }, AND: { ins: 2, outs: 1, label: 'AND', w: 64, h: 44 }, OR: { ins: 2, outs: 1, label: 'OR', w: 64, h: 44 }, NOT: { ins: 1, outs: 1, label: 'NOT', w: 56, h: 36 }, XOR: { ins: 2, outs: 1, label: 'XOR', w: 64, h: 44 }, NAND: { ins: 2, outs: 1, label: 'NAND', w: 64, h: 44 }, NOR: { ins: 2, outs: 1, label: 'NOR', w: 64, h: 44 }, XNOR: { ins: 2, outs: 1, label: 'XNOR', w: 64, h: 44 }, BUFFER: { ins: 1, outs: 1, label: 'BUF', w: 56, h: 36 }, }; const PORT_R = 5; // port dot radius const GRID = 20; // snap grid size /* ── evaluate a single gate ── */ function evalGate(type, inputs) { const a = inputs[0] || 0; const b = inputs[1] || 0; switch (type) { case 'AND': return a & b; case 'OR': return a | b; case 'NOT': return a ? 0 : 1; case 'XOR': return a ^ b; case 'NAND': return (a & b) ? 0 : 1; case 'NOR': return (a | b) ? 0 : 1; case 'XNOR': return (a ^ b) ? 0 : 1; case 'BUFFER': return a; case 'INPUT': return a; // value from state.value case 'CLOCK': return a; case 'OUTPUT': return a; default: return 0; } } /* ═══════════════════════════════════════════════════════════ LogicSim ═══════════════════════════════════════════════════════════ */ class LogicSim { constructor(canvas, exprEl, tableEl) { this._canvas = canvas; this._ctx = canvas.getContext('2d'); this._exprEl = exprEl; // element for boolean expression display this._tableEl = tableEl; // element for truth table this._gates = []; // { id, type, x, y, value, label, freq, _phase } this._wires = []; // { from: {gateId, port:'out'|'in0'|'in1'}, to: {gateId, port} } this._nextId = 1; this._tool = 'select'; // 'select' | type key this._drag = null; this._wireStart = null; // { gateId, side:'out', px, py } this._history = []; this._histIdx = -1; this._raf = null; this._clockRaf = null; this._fxLastT = 0; this._bindEvents(); this._startClock(); this._startDrawLoop(); } /* ── port pixel positions ── */ _portPx(gate, port) { const def = GATE_DEFS[gate.type]; const hw = def.w / 2, hh = def.h / 2; const cx = gate.x, cy = gate.y; if (port === 'out') return { x: cx + hw, y: cy }; if (port === 'in0') { if (def.ins === 1) return { x: cx - hw, y: cy }; return { x: cx - hw, y: cy - hh / 2 }; } if (port === 'in1') return { x: cx - hw, y: cy + hh / 2 }; return { x: cx, y: cy }; } /* ── snap to grid ── */ _snap(v) { return Math.round(v / GRID) * GRID; } /* ── find gate near point ── */ _hitGate(px, py) { for (let i = this._gates.length - 1; i >= 0; i--) { const g = this._gates[i]; const def = GATE_DEFS[g.type]; if (Math.abs(px - g.x) <= def.w / 2 + 2 && Math.abs(py - g.y) <= def.h / 2 + 2) return g; } return null; } /* ── find port near point; returns { gateId, port, px, py } or null ── */ _hitPort(px, py) { for (const g of this._gates) { const def = GATE_DEFS[g.type]; const ports = []; if (def.outs > 0) ports.push('out'); if (def.ins >= 1) ports.push('in0'); if (def.ins >= 2) ports.push('in1'); for (const port of ports) { const p = this._portPx(g, port); if (Math.hypot(px - p.x, py - p.y) <= 10) { return { gateId: g.id, port, px: p.x, py: p.y }; } } } return null; } /* ── canvas coordinates from event ── */ _pos(e) { const r = this._canvas.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; } /* ══ Event binding ══ */ _bindEvents() { const c = this._canvas; c.addEventListener('mousedown', this._onDown.bind(this)); c.addEventListener('mousemove', this._onMove.bind(this)); c.addEventListener('mouseup', this._onUp.bind(this)); c.addEventListener('dblclick', this._onDbl.bind(this)); c.addEventListener('contextmenu', e => { e.preventDefault(); this._onRightClick(e); }); window.addEventListener('keydown', e => { if (e.ctrlKey && e.key === 'z') { e.preventDefault(); this.undo(); } if (e.ctrlKey && e.key === 'y') { e.preventDefault(); this.redo(); } }); } _onDown(e) { if (e.button !== 0) return; const { x, y } = this._pos(e); if (this._tool !== 'select') { // place a new gate const type = this._tool; const def = GATE_DEFS[type]; if (!def) return; this._pushHistory(); const g = this._addGate(type, this._snap(x), this._snap(y)); if (window.LabFX) LabFX.sound.play('tick', { pitch: 1.4, volume: 0.3 }); this._propagate(); this._updatePanels(); this.draw(); return; } // select tool: check port first (wire drawing) const hitP = this._hitPort(x, y); if (hitP && (hitP.port === 'out')) { this._wireStart = hitP; this._mouseX = x; this._mouseY = y; return; } // check gate drag const g = this._hitGate(x, y); if (g) { this._drag = { gate: g, ox: x - g.x, oy: y - g.y }; } } _onMove(e) { const { x, y } = this._pos(e); this._mouseX = x; this._mouseY = y; if (this._drag) { this._drag.gate.x = this._snap(x - this._drag.ox); this._drag.gate.y = this._snap(y - this._drag.oy); this._propagate(); this._updatePanels(); this.draw(); return; } if (this._wireStart) { this.draw(); return; } // hover cursor const hp = this._hitPort(x, y); this._canvas.style.cursor = hp ? 'crosshair' : (this._hitGate(x, y) ? 'grab' : 'default'); } _onUp(e) { if (this._drag) { this._pushHistory(); this._drag = null; return; } if (this._wireStart) { const { x, y } = this._pos(e); const hitP = this._hitPort(x, y); if (hitP && (hitP.port === 'in0' || hitP.port === 'in1') && hitP.gateId !== this._wireStart.gateId) { // check not already wired const exists = this._wires.some(w => w.to.gateId === hitP.gateId && w.to.port === hitP.port); if (!exists) { this._pushHistory(); this._wires.push({ from: { gateId: this._wireStart.gateId, port: this._wireStart.port }, to: { gateId: hitP.gateId, port: hitP.port } }); if (window.LabFX) LabFX.sound.play('click', { pitch: 1.3 }); this._propagate(); this._updatePanels(); } } this._wireStart = null; this.draw(); } } _onDbl(e) { const { x, y } = this._pos(e); const g = this._hitGate(x, y); if (g && (g.type === 'INPUT')) { this._pushHistory(); g.value = g.value ? 0 : 1; if (window.LabFX) { LabFX.sound.play('bounce', { pitch: 1.2 }); LabFX.particles.emit({ ctx: this._ctx, x: g.x, y: g.y, count: 5, color: '#00ff88', speed: 20, spread: Math.PI * 2, angle: 0, gravity: 0, life: 400, fade: true, glow: true, shape: 'spark', size: 3, sizeFade: true }); } this._propagate(); this._updatePanels(); this.draw(); } if (g && g.type === 'OUTPUT') { // rename label cycle: OUT → OUT₁ → OUT₂ → OUT (no-op, just show) } } _onRightClick(e) { const { x, y } = this._pos(e); // delete wire near click for (let i = this._wires.length - 1; i >= 0; i--) { const w = this._wires[i]; const g1 = this._gateById(w.from.gateId); if (!g1) continue; const g2 = this._gateById(w.to.gateId); if (!g2) continue; const p1 = this._portPx(g1, w.from.port); const p2 = this._portPx(g2, w.to.port); if (this._distToSeg(x, y, p1.x, p1.y, p2.x, p2.y) < 8) { this._pushHistory(); this._wires.splice(i, 1); this._propagate(); this._updatePanels(); this.draw(); return; } } // delete gate const g = this._hitGate(x, y); if (g) { this._pushHistory(); this._wires = this._wires.filter(w => w.from.gateId !== g.id && w.to.gateId !== g.id); this._gates = this._gates.filter(gg => gg.id !== g.id); if (window.LabFX) LabFX.sound.play('fizz', { volume: 0.2 }); this._propagate(); this._updatePanels(); this.draw(); } } _distToSeg(px, py, ax, ay, bx, by) { const dx = bx - ax, dy = by - ay; const len2 = dx * dx + dy * dy; if (len2 === 0) return Math.hypot(px - ax, py - ay); const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2)); return Math.hypot(px - (ax + t * dx), py - (ay + t * dy)); } /* ══ Gate management ══ */ _addGate(type, x, y) { const id = this._nextId++; const def = GATE_DEFS[type]; const g = { id, type, x, y, value: 0, label: def.label }; if (type === 'INPUT') { g.label = String.fromCharCode(64 + this._gates.filter(gg => gg.type === 'INPUT').length + 1); } if (type === 'OUTPUT') { g.label = 'OUT' + (this._gates.filter(gg => gg.type === 'OUTPUT').length + 1 > 1 ? (this._gates.filter(gg => gg.type === 'OUTPUT').length + 1) : ''); } if (type === 'CLOCK') { g.freq = 1; g._phase = 0; } this._gates.push(g); return g; } _gateById(id) { return this._gates.find(g => g.id === id) || null; } /* ══ Logic propagation (topological sort + eval) ══ */ _propagate() { // Build in-degree map const inDeg = {}; this._gates.forEach(g => { inDeg[g.id] = 0; }); const deps = {}; // id -> [id of gates this gate depends on] this._gates.forEach(g => { deps[g.id] = []; }); this._wires.forEach(w => { inDeg[w.to.gateId]++; deps[w.to.gateId].push(w.from.gateId); }); // Kahn's algorithm const queue = this._gates.filter(g => inDeg[g.id] === 0).map(g => g.id); const sorted = []; const visited = new Set(); while (queue.length) { const id = queue.shift(); if (visited.has(id)) continue; visited.add(id); sorted.push(id); // find wires going FROM this gate this._wires.forEach(w => { if (w.from.gateId === id) { inDeg[w.to.gateId]--; if (inDeg[w.to.gateId] === 0) queue.push(w.to.gateId); } }); } // any unvisited (loops): add them to sorted anyway this._gates.forEach(g => { if (!visited.has(g.id)) sorted.push(g.id); }); for (const id of sorted) { const g = this._gateById(id); if (!g) continue; if (g.type === 'INPUT' || g.type === 'CLOCK') continue; // value set externally const ins = this._getInputValues(g); g.value = evalGate(g.type, ins); } } _getInputValues(gate) { const def = GATE_DEFS[gate.type]; const vals = new Array(def.ins).fill(0); this._wires.forEach(w => { if (w.to.gateId !== gate.id) return; const src = this._gateById(w.from.gateId); if (!src) return; const idx = w.to.port === 'in0' ? 0 : 1; vals[idx] = src.value; }); return vals; } /* ══ Clock ══ */ _startClock() { let last = 0; const tick = (now) => { this._clockRaf = requestAnimationFrame(tick); const dt = (now - last) / 1000; const dtMs = now - (last || now); last = now; let changed = false; this._gates.forEach(g => { if (g.type !== 'CLOCK') return; g._phase = (g._phase || 0) + dt * (g.freq || 1); const newVal = g._phase % 1 < 0.5 ? 1 : 0; if (newVal !== g.value) { g.value = newVal; changed = true; } }); if (window.LabFX && dtMs > 0) LabFX.particles.update(dtMs); if (changed) { this._propagate(); this._updatePanels(); this.draw(); } }; this._clockRaf = requestAnimationFrame(tick); } /* ══ Undo / Redo ══ */ _pushHistory() { const snap = JSON.stringify({ gates: this._gates, wires: this._wires, nextId: this._nextId }); this._history = this._history.slice(0, this._histIdx + 1); this._history.push(snap); if (this._history.length > 50) this._history.shift(); this._histIdx = this._history.length - 1; } undo() { if (this._histIdx <= 0) return; this._histIdx--; this._restoreHistory(this._history[this._histIdx]); } redo() { if (this._histIdx >= this._history.length - 1) return; this._histIdx++; this._restoreHistory(this._history[this._histIdx]); } _restoreHistory(snap) { const s = JSON.parse(snap); this._gates = s.gates; this._wires = s.wires; this._nextId = s.nextId; this._propagate(); this._updatePanels(); this.draw(); } /* ══ Fit canvas to element ══ */ fit() { const el = this._canvas.parentElement || this._canvas; const dpr = window.devicePixelRatio || 1; const w = el.clientWidth || 800; const h = el.clientHeight || 500; this._canvas.width = w * dpr; this._canvas.height = h * dpr; this._canvas.style.width = w + 'px'; this._canvas.style.height = h + 'px'; this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0); this.draw(); } /* ══ Drawing ══ */ draw() { const ctx = this._ctx; const W = this._canvas.width / (window.devicePixelRatio || 1); const H = this._canvas.height / (window.devicePixelRatio || 1); ctx.clearRect(0, 0, W, H); // grid ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1; for (let x = 0; x < W; x += GRID) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); } for (let y = 0; y < H; y += GRID) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } // wires this._wires.forEach(w => this._drawWire(ctx, w)); // ghost wire while dragging if (this._wireStart) { const p = this._wireStart; ctx.beginPath(); ctx.moveTo(p.px, p.py); ctx.lineTo(this._mouseX || p.px, this._mouseY || p.py); ctx.strokeStyle = 'rgba(255,255,100,0.7)'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]); ctx.stroke(); ctx.setLineDash([]); } // gates this._gates.forEach(g => this._drawGate(ctx, g)); if (window.LabFX) LabFX.particles.draw(ctx); } _drawWire(ctx, w) { const g1 = this._gateById(w.from.gateId); const g2 = this._gateById(w.to.gateId); if (!g1 || !g2) return; const p1 = this._portPx(g1, w.from.port); const p2 = this._portPx(g2, w.to.port); const val = g1.value; ctx.beginPath(); // L-route const mx = (p1.x + p2.x) / 2; ctx.moveTo(p1.x, p1.y); ctx.lineTo(mx, p1.y); ctx.lineTo(mx, p2.y); ctx.lineTo(p2.x, p2.y); ctx.strokeStyle = val ? '#00ff88' : 'rgba(255,255,255,0.25)'; ctx.lineWidth = val ? 2.2 : 1.5; ctx.stroke(); // Wire HIGH: animated dot flowing along path if (val && window.LabFX) { const frac = ((performance.now() * 0.001) % 1); // interpolate along L-route: seg1 p1→(mx,p1.y), seg2 (mx,p1.y)→(mx,p2.y), seg3 (mx,p2.y)→p2 const seg1 = Math.abs(mx - p1.x); const seg2 = Math.abs(p2.y - p1.y); const seg3 = Math.abs(p2.x - mx); const total = seg1 + seg2 + seg3 || 1; const dist = frac * total; let dx, dy; if (dist <= seg1) { dx = p1.x + (mx - p1.x) * (dist / (seg1 || 1)); dy = p1.y; } else if (dist <= seg1 + seg2) { dx = mx; dy = p1.y + (p2.y - p1.y) * ((dist - seg1) / (seg2 || 1)); } else { dx = mx + (p2.x - mx) * ((dist - seg1 - seg2) / (seg3 || 1)); dy = p2.y; } ctx.save(); ctx.beginPath(); ctx.arc(dx, dy, 3.5, 0, Math.PI * 2); ctx.fillStyle = '#06D6E0'; ctx.shadowColor = '#06D6E0'; ctx.shadowBlur = 8; ctx.fill(); ctx.restore(); } } _drawGate(ctx, g) { const def = GATE_DEFS[g.type]; const hw = def.w / 2, hh = def.h / 2; const x = g.x, y = g.y; // OUTPUT LED glow via LabFX const isHigh = g.value === 1; if (g.type === 'OUTPUT' && isHigh && window.LabFX) { LabFX.glow.drawGlow(ctx, () => { ctx.beginPath(); ctx.roundRect(x - hw, y - hh, def.w, def.h, 6); ctx.fill(); }, { color: '#00FF80', intensity: 18, layers: 2 }); } // gate body ctx.beginPath(); ctx.roundRect(x - hw, y - hh, def.w, def.h, 6); let fill = 'rgba(30,30,60,0.9)'; if (g.type === 'INPUT') fill = isHigh ? 'rgba(0,220,100,0.35)' : 'rgba(60,60,100,0.8)'; if (g.type === 'CLOCK') fill = isHigh ? 'rgba(0,180,255,0.35)' : 'rgba(40,40,100,0.8)'; if (g.type === 'OUTPUT') fill = isHigh ? 'rgba(255,80,80,0.55)' : 'rgba(60,30,30,0.8)'; ctx.fillStyle = fill; ctx.fill(); const borderCol = g.type === 'OUTPUT' ? (isHigh ? '#ff6060' : 'rgba(255,255,255,0.2)') : g.type === 'INPUT' ? (isHigh ? '#00cc66' : 'rgba(255,255,255,0.2)') : g.type === 'CLOCK' ? (isHigh ? '#00aaff' : 'rgba(255,255,255,0.2)') : 'rgba(155,93,229,0.6)'; ctx.strokeStyle = borderCol; ctx.lineWidth = 1.5; ctx.stroke(); // label ctx.fillStyle = isHigh ? '#fff' : 'rgba(255,255,255,0.75)'; ctx.font = `bold ${def.ins <= 1 ? 10 : 9}px Manrope,sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const lbl = (g.type === 'INPUT' || g.type === 'OUTPUT') ? g.label : def.label; ctx.fillText(lbl, x, y); // value badge (INPUT / OUTPUT / CLOCK) if (g.type === 'INPUT' || g.type === 'OUTPUT' || g.type === 'CLOCK') { ctx.fillStyle = isHigh ? '#00ff88' : 'rgba(255,255,255,0.3)'; ctx.font = 'bold 9px Manrope,sans-serif'; ctx.fillText(isHigh ? '1' : '0', x + hw - 10, y - hh + 9); } // ports this._drawPorts(ctx, g); } _drawPorts(ctx, g) { const def = GATE_DEFS[g.type]; const ports = []; if (def.outs > 0) ports.push('out'); if (def.ins >= 1) ports.push('in0'); if (def.ins >= 2) ports.push('in1'); for (const port of ports) { const p = this._portPx(g, port); const isOut = port === 'out'; const srcGate = isOut ? g : null; const val = isOut ? g.value : this._getInputValues(g)[port === 'in0' ? 0 : 1]; ctx.beginPath(); ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2); ctx.fillStyle = val ? '#00ff88' : 'rgba(255,255,255,0.3)'; ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1; ctx.stroke(); } } /* ══ Boolean expression panel ══ */ _updatePanels() { this._updateExprPanel(); this._updateTruthTable(); } _buildExpr(gateId, depth) { if (depth > 20) return '…'; const g = this._gateById(gateId); if (!g) return '?'; if (g.type === 'INPUT') return g.label; if (g.type === 'CLOCK') return g.label || 'CLK'; const srcOf = (port) => { const w = this._wires.find(ww => ww.to.gateId === gateId && ww.to.port === port); return w ? this._buildExpr(w.from.gateId, depth + 1) : '0'; }; const a = g.type !== 'NOT' && g.type !== 'BUFFER' ? srcOf('in0') : srcOf('in0'); const b = srcOf('in1'); switch (g.type) { case 'AND': return `(${a} ∧ ${b})`; case 'OR': return `(${a} ∨ ${b})`; case 'NOT': return `¬${a}`; case 'XOR': return `(${a} ⊕ ${b})`; case 'NAND': return `¬(${a} ∧ ${b})`; case 'NOR': return `¬(${a} ∨ ${b})`; case 'XNOR': return `¬(${a} ⊕ ${b})`; case 'BUFFER': return a; default: return '?'; } } _updateExprPanel() { if (!this._exprEl) return; const outputs = this._gates.filter(g => g.type === 'OUTPUT'); if (outputs.length === 0) { this._exprEl.textContent = 'Добавьте OUTPUT для вывода выражения'; return; } const lines = outputs.map(out => { const w = this._wires.find(ww => ww.to.gateId === out.id); if (!w) return `${out.label} = ?`; const expr = this._buildExpr(w.from.gateId, 0); return `${out.label} = ${expr}`; }); this._exprEl.textContent = lines.join(' | '); } _updateTruthTable() { if (!this._tableEl) return; const inputs = this._gates.filter(g => g.type === 'INPUT'); const outputs = this._gates.filter(g => g.type === 'OUTPUT'); if (inputs.length === 0 || outputs.length === 0) { this._tableEl.innerHTML = 'Добавьте INPUT и OUTPUT'; return; } const n = inputs.length; if (n > 6) { this._tableEl.innerHTML = 'Слишком много входов (макс 6)'; return; } const rows = 1 << n; // save current values const savedVals = inputs.map(g => g.value); let html = '
| ${g.label} | `; }); outputs.forEach(g => { html += `${g.label} | `; }); html += '
|---|---|
| ${(r >> (n - 1 - i)) & 1} | `; }); outputs.forEach(g => { html += `${g.value} | `; }); html += '