从零到一:用JointJS复刻一个简易的“逻辑门”模拟器(含完整源码)

从零到一:用JointJS复刻一个简易的“逻辑门”模拟器(含完整源码) 从零构建逻辑门模拟器JointJS实战与可视化电路设计在数字电路设计与计算机科学教育中逻辑门是最基础的构建模块。传统教学往往依赖抽象符号或静态图示而通过JointJS构建交互式逻辑门模拟器不仅能直观展示与门、或门、非门的工作原理还能让学习者通过拖拽连接亲自搭建电路。本文将完整演示如何利用JointJS的joint.shapes.devs.Model创建带端口的可交互逻辑门元件实现信号传递的动态模拟并最终导出为可持久化的JSON数据。1. 环境准备与JointJS核心概念1.1 技术栈选型分析JointJS作为基于SVG的图形库其核心优势在于元素-连接模型所有图形由Element和Link两类对象构成端口系统通过ports定义输入输出接口事件驱动支持拖拽、连线、点击等交互事件的监听处理序列化能力整个图形可导出为JSON格式与其他可视化库如D3.js或GoJS相比JointJS特别适合需要复杂连接关系的场景。下表对比了常见图形库特性特性JointJSD3.jsGoJS预置图形元素✔✖✔连接线管理✔✖✔端口系统✔✖✔序列化支持✔✖✔学习曲线中等陡峭平缓1.2 项目初始化通过npm安装依赖npm install jointjs jquery backbone lodash基础HTML结构!DOCTYPE html html head link relstylesheet hrefnode_modules/jointjs/dist/joint.min.css /head body div idcanvas/div script srcnode_modules/jquery/dist/jquery.min.js/script script srcnode_modules/lodash/lodash.min.js/script script srcnode_modules/backbone/backbone-min.js/script script srcnode_modules/jointjs/dist/joint.min.js/script script srcapp.js/script /body /html2. 构建逻辑门元件库2.1 定义基础元件模板使用joint.shapes.devs.Model创建可复用的逻辑门模板const gateTemplate new joint.shapes.devs.Model({ size: { width: 60, height: 80 }, ports: { groups: { in: { attrs: { .port-body: { magnet: passive, width: 10, height: 10 } }, position: { name: left } }, out: { attrs: { .port-body: { magnet: active, width: 10, height: 10 } }, position: { name: right } } } }, attrs: { .body: { stroke: #333, fill: #FFF }, .label: { text: GATE, fontSize: 12 } } });2.2 实现具体逻辑门类型基于模板创建三种基本逻辑门与门(AND)实现function createANDGate(x, y) { return gateTemplate.clone().set({ position: { x, y }, inPorts: [A, B], outPorts: [Q], attrs: { .label: { text: AND }, .body: { d: M 0 0 L 60 0 L 60 80 L 30 80 L 0 40 Z } } }); }或门(OR)实现function createORGate(x, y) { return gateTemplate.clone().set({ position: { x, y }, inPorts: [A, B], outPorts: [Q], attrs: { .label: { text: OR }, .body: { d: M 0 20 Q 30 -20 60 20 L 60 60 Q 30 100 0 60 Z } } }); }非门(NOT)实现function createNOTGate(x, y) { return gateTemplate.clone().set({ position: { x, y }, inPorts: [A], outPorts: [Q], attrs: { .label: { text: NOT }, .body: { d: M 0 20 L 40 20 L 60 40 L 40 60 L 0 60 Z } } }); }3. 信号传递与交互逻辑3.1 连接线行为配置设置连线样式与交互规则const paper new joint.dia.Paper({ el: document.getElementById(canvas), model: graph, width: 800, height: 600, defaultLink: new joint.shapes.standard.Link({ attrs: { line: { stroke: #555, strokeWidth: 2, targetMarker: { type: path, d: M 10 -5 0 0 10 5 Z } } }, router: { name: manhattan } }), validateConnection: function(cellViewS, magnetS, cellViewT, magnetT) { // 只允许输出端口连接到输入端口 return magnetS.getAttribute(port-group) out magnetT.getAttribute(port-group) in; } });3.2 实时信号计算实现逻辑门运算核心逻辑function calculateOutput(gate, inputValues) { switch(gate.attr(.label/text)) { case AND: return inputValues.every(v v 1) ? 1 : 0; case OR: return inputValues.some(v v 1) ? 1 : 0; case NOT: return inputValues[0] 1 ? 0 : 1; default: return 0; } } // 更新所有连接元件状态 function updateCircuit() { graph.getElements().forEach(gate { const inputs graph.getConnectedLinks(gate, { inbound: true }); const inputValues inputs.map(link link.get(source).value || 0); const outputValue calculateOutput(gate, inputValues); // 更新输出端口值 gate.set(outValue, outputValue); // 传播到下游元件 graph.getConnectedLinks(gate, { outbound: true }).forEach(link { link.set(target).value outputValue; link.attr(line/stroke, outputValue ? #F00 : #555); }); }); }4. 高级功能实现4.1 状态持久化与恢复导出当前电路为JSONfunction exportCircuit() { return JSON.stringify(graph.toJSON()); }从JSON导入电路function importCircuit(json) { graph.clear(); graph.fromJSON(JSON.parse(json)); }4.2 动态元件创建界面创建可拖拽的元件面板const palette new joint.ui.Palette({ el: document.getElementById(palette), groups: { gates: { label: 逻辑门, index: 1 } }, groupsCollapsible: true }); palette.addGroup(gates, [ { type: AND, template: createANDGate(0, 0), label: 与门(AND) }, { type: OR, template: createORGate(0, 0), label: 或门(OR) }, { type: NOT, template: createNOTGate(0, 0), label: 非门(NOT) } ]); // 启用拖拽创建 palette.on(element:dragstart, function(elementView) { elementView.model.position(0, 0); // 重置位置 });5. 教学应用场景扩展5.1 真值表可视化自动生成当前电路真值表function generateTruthTable() { const inputs []; const outputs []; // 识别所有输入输出端口 graph.getElements().forEach(gate { if (gate.get(inPorts).length 0 graph.getConnectedLinks(gate, { inbound: true }).length 0) { inputs.push(gate); } if (gate.get(outPorts).length 0 graph.getConnectedLinks(gate, { outbound: true }).length 0) { outputs.push(gate); } }); // 生成所有可能的输入组合 const combinations Math.pow(2, inputs.length); const table []; for (let i 0; i combinations; i) { const row {}; // 设置输入值 inputs.forEach((input, idx) { const value (i idx) 1; input.set(inValue, value); row[input.attr(.label/text)] value; }); // 计算输出 updateCircuit(); // 记录输出值 outputs.forEach(output { row[output.attr(.label/text)] output.get(outValue); }); table.push(row); } return table; }5.2 电路验证模式添加自动验证功能function verifyCircuit(expectedLogic) { const truthTable generateTruthTable(); return truthTable.every(row { const inputs Object.keys(row) .filter(k [AND,OR,NOT].includes(k)) .map(k row[k]); const output row[OUTPUT]; return expectedLogic(inputs) output; }); } // 示例验证一个AND-OR组合电路 const isValid verifyCircuit(inputs { return (inputs[0] inputs[1]) || inputs[2]; });6. 性能优化与实践建议6.1 大规模电路处理当元件数量超过100个时可采取以下优化措施批量操作使用graph.startBatch()和graph.stopBatch()包裹多次更新虚拟渲染对不可见区域启用延迟渲染简化样式减少复杂SVG滤镜和渐变的使用// 批量操作示例 graph.startBatch(complex_update); // 执行多个元素修改 elements.forEach(el el.set(position, randomPosition())); graph.stopBatch(complex_update);6.2 调试技巧常见问题排查方法问题现象可能原因解决方案连线无法连接端口magnet属性未正确设置检查port-group和magnet配置元素显示不全画布尺寸不足调用paper.scaleContentToFit()性能明显下降频繁触发全局更新使用增量更新代替全量计算导入后图形错位坐标系统不一致检查JSON中的position单位6.3 扩展方向基于当前项目可进一步开发复合逻辑门将常用电路组合保存为可复用的自定义元件时序电路添加时钟信号支持触发器、计数器等元件教学关卡设计渐进式电路搭建挑战任务多人协作通过WebSocket实现实时协作编辑// 自定义复合元件示例 joint.shapes.logic.CompoundGate joint.shapes.devs.Model.extend({ defaults: joint.util.deepSupplement({ type: logic.CompoundGate, // 自定义属性和端口 }, joint.shapes.devs.Model.prototype.defaults) });在开发过程中特别注意保持代码模块化将图形定义、业务逻辑和交互控制分离。例如可以将所有元件模板集中在components.js中电路计算逻辑放在simulator.js中而界面交互则通过controller.js来管理。