Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ComfyBASIC - Visual BASIC Programming Environment</title> | |
| <style> | |
| :root { | |
| --bg-dark: #1e1e1e; | |
| --bg-darker: #121212; | |
| --bg-panel: #2d2d2d; | |
| --bg-node: #333333; | |
| --border-color: #444; | |
| --accent-primary: #646cff; | |
| --accent-secondary: #ff6b6b; | |
| --text-primary: #ffffff; | |
| --text-secondary: #cccccc; | |
| --node-input: #4caf50; | |
| --node-output: #2196f3; | |
| --node-process: #ff9800; | |
| --node-control: #9c27b0; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-darker); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| } | |
| .header { | |
| background-color: var(--bg-dark); | |
| padding: 12px 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: var(--accent-primary); | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .btn { | |
| background-color: var(--bg-panel); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border-color); | |
| padding: 8px 16px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .btn:hover { | |
| background-color: var(--accent-primary); | |
| border-color: var(--accent-primary); | |
| } | |
| .btn-primary { | |
| background-color: var(--accent-primary); | |
| border-color: var(--accent-primary); | |
| } | |
| .btn-primary:hover { | |
| background-color: #535bf2; | |
| } | |
| .btn-success { | |
| background-color: var(--node-input); | |
| border-color: var(--node-input); | |
| } | |
| .btn-success:hover { | |
| background-color: #3d8b40; | |
| } | |
| .main-content { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| .sidebar { | |
| width: 250px; | |
| background-color: var(--bg-dark); | |
| border-right: 1px solid var(--border-color); | |
| padding: 20px 0; | |
| overflow-y: auto; | |
| } | |
| .sidebar-title { | |
| padding: 0 20px 10px; | |
| font-size: 1.1rem; | |
| color: var(--text-secondary); | |
| border-bottom: 1px solid var(--border-color); | |
| margin-bottom: 15px; | |
| } | |
| .node-category { | |
| margin-bottom: 20px; | |
| } | |
| .category-title { | |
| padding: 5px 20px; | |
| font-weight: bold; | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| } | |
| .node-item { | |
| padding: 10px 20px; | |
| cursor: grab; | |
| transition: background 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .node-item:hover { | |
| background-color: var(--bg-panel); | |
| } | |
| .node-icon { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 4px; | |
| } | |
| .workspace-container { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| .workspace { | |
| flex: 1; | |
| position: relative; | |
| background-color: var(--bg-darker); | |
| overflow: hidden; | |
| } | |
| .workspace-grid { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| linear-gradient(rgba(68, 68, 68, 0.2) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(68, 68, 68, 0.2) 1px, transparent 1px); | |
| background-size: 20px 20px; | |
| } | |
| .node { | |
| position: absolute; | |
| background-color: var(--bg-node); | |
| border: 1px solid var(--border-color); | |
| border-radius: 6px; | |
| min-width: 180px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| cursor: move; | |
| z-index: 10; | |
| } | |
| .node-header { | |
| padding: 10px; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| cursor: move; | |
| } | |
| .node-title { | |
| font-weight: 500; | |
| font-size: 0.9rem; | |
| } | |
| .node-close { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 1.2rem; | |
| } | |
| .node-close:hover { | |
| color: var(--accent-secondary); | |
| } | |
| .node-content { | |
| padding: 15px; | |
| } | |
| .node-inputs, .node-outputs { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .io-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .io-connector { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| cursor: crosshair; | |
| } | |
| .input-connector { | |
| background-color: var(--node-input); | |
| } | |
| .output-connector { | |
| background-color: var(--node-output); | |
| } | |
| .io-label { | |
| font-size: 0.85rem; | |
| } | |
| .node-body { | |
| padding: 10px 0; | |
| } | |
| .node-param { | |
| margin-bottom: 10px; | |
| } | |
| .param-label { | |
| display: block; | |
| font-size: 0.8rem; | |
| margin-bottom: 4px; | |
| color: var(--text-secondary); | |
| } | |
| .param-input { | |
| width: 100%; | |
| padding: 6px; | |
| background-color: var(--bg-dark); | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| color: var(--text-primary); | |
| } | |
| .connections-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| .connection-line { | |
| stroke: var(--accent-primary); | |
| stroke-width: 2; | |
| fill: none; | |
| } | |
| .output-panel { | |
| width: 300px; | |
| background-color: var(--bg-dark); | |
| border-left: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .output-header { | |
| padding: 12px; | |
| border-bottom: 1px solid var(--border-color); | |
| font-weight: bold; | |
| } | |
| .output-content { | |
| flex: 1; | |
| padding: 15px; | |
| overflow-y: auto; | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| background-color: var(--bg-darker); | |
| } | |
| .status-bar { | |
| background-color: var(--bg-dark); | |
| padding: 8px 20px; | |
| border-top: 1px solid var(--border-color); | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| } | |
| .node-process { border-left: 3px solid var(--node-process); } | |
| .node-control { border-left: 3px solid var(--node-control); } | |
| .node-io { border-left: 3px solid var(--node-output); } | |
| .console-output { | |
| color: #00ff00; | |
| font-size: 14px; | |
| line-height: 1.4; | |
| } | |
| .console-input { | |
| color: #ffff00; | |
| } | |
| .console-error { | |
| color: #ff5555; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <div class="header"> | |
| <div class="logo"> | |
| <span>⚙️</span> | |
| <span>ComfyBASIC</span> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn" id="clearBtn"> | |
| <span>🗑️</span> | |
| Clear | |
| </button> | |
| <button class="btn" id="downloadBtn"> | |
| <span>💾</span> | |
| Download | |
| </button> | |
| <button class="btn btn-success" id="runBtn"> | |
| <span>▶️</span> | |
| Run Program | |
| </button> | |
| </div> | |
| </div> | |
| <div class="main-content"> | |
| <div class="sidebar"> | |
| <div class="sidebar-title">NODE LIBRARY</div> | |
| <div class="node-category"> | |
| <div class="category-title">Input/Output</div> | |
| <div class="node-item" data-node="print"> | |
| <div class="node-icon" style="background-color: var(--node-output);"></div> | |
| <span>Print Statement</span> | |
| </div> | |
| <div class="node-item" data-node="input"> | |
| <div class="node-icon" style="background-color: var(--node-input);"></div> | |
| <span>Input Statement</span> | |
| </div> | |
| </div> | |
| <div class="node-category"> | |
| <div class="category-title">Variables</div> | |
| <div class="node-item" data-node="variable"> | |
| <div class="node-icon" style="background-color: var(--node-process);"></div> | |
| <span>Variable Assignment</span> | |
| </div> | |
| <div class="node-item" data-node="getvar"> | |
| <div class="node-icon" style="background-color: var(--node-process);"></div> | |
| <span>Get Variable</span> | |
| </div> | |
| </div> | |
| <div class="node-category"> | |
| <div class="category-title">Control Flow</div> | |
| <div class="node-item" data-node="if"> | |
| <div class="node-icon" style="background-color: var(--node-control);"></div> | |
| <span>If Statement</span> | |
| </div> | |
| <div class="node-item" data-node="for"> | |
| <div class="node-icon" style="background-color: var(--node-control);"></div> | |
| <span>For Loop</span> | |
| </div> | |
| </div> | |
| <div class="node-category"> | |
| <div class="category-title">Math Operations</div> | |
| <div class="node-item" data-node="add"> | |
| <div class="node-icon" style="background-color: var(--node-process);"></div> | |
| <span>Addition</span> | |
| </div> | |
| <div class="node-item" data-node="multiply"> | |
| <div class="node-icon" style="background-color: var(--node-process);"></div> | |
| <span>Multiplication</span> | |
| </div> | |
| </div> | |
| <div class="node-category"> | |
| <div class="category-title">Values</div> | |
| <div class="node-item" data-node="number"> | |
| <div class="node-icon" style="background-color: var(--node-input);"></div> | |
| <span>Number</span> | |
| </div> | |
| <div class="node-item" data-node="string"> | |
| <div class="node-icon" style="background-color: var(--node-input);"></div> | |
| <span>String</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="workspace-container"> | |
| <div class="workspace"> | |
| <div class="workspace-grid" id="workspaceGrid"></div> | |
| <svg class="connections-container" id="connectionsContainer"></svg> | |
| <!-- Nodes will be added here dynamically --> | |
| </div> | |
| <div class="output-panel"> | |
| <div class="output-header">PROGRAM OUTPUT</div> | |
| <div class="output-content" id="outputContent"> | |
| <!-- Output will appear here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="status-bar"> | |
| <div class="status-left">Ready</div> | |
| <div class="status-right">ComfyBASIC v1.0</div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const workspace = document.querySelector('.workspace'); | |
| const nodeItems = document.querySelectorAll('.node-item'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const runBtn = document.getElementById('runBtn'); | |
| const outputContent = document.getElementById('outputContent'); | |
| const connectionsContainer = document.getElementById('connectionsContainer'); | |
| let nodeId = 0; | |
| let connections = []; | |
| let isConnecting = false; | |
| let startConnector = null; | |
| let tempLine = null; | |
| let nodeData = {}; | |
| // Node templates | |
| const nodeTemplates = { | |
| print: { | |
| title: "Print Statement", | |
| inputs: [{id: "value", label: "Value"}], | |
| outputs: [], | |
| params: [], | |
| color: "var(--node-output)" | |
| }, | |
| input: { | |
| title: "Input Statement", | |
| inputs: [], | |
| outputs: [{id: "value", label: "Input Value"}], | |
| params: [{id: "prompt", label: "Prompt", type: "text", value: "Enter value:"}], | |
| color: "var(--node-input)" | |
| }, | |
| variable: { | |
| title: "Variable Assignment", | |
| inputs: [{id: "value", label: "Value"}], | |
| outputs: [], | |
| params: [{id: "name", label: "Variable Name", type: "text", value: "X"}], | |
| color: "var(--node-process)" | |
| }, | |
| getvar: { | |
| title: "Get Variable", | |
| inputs: [], | |
| outputs: [{id: "value", label: "Variable Value"}], | |
| params: [{id: "name", label: "Variable Name", type: "text", value: "X"}], | |
| color: "var(--node-process)" | |
| }, | |
| if: { | |
| title: "If Statement", | |
| inputs: [{id: "condition", label: "Condition"}], | |
| outputs: [ | |
| {id: "true", label: "True Branch"}, | |
| {id: "false", label: "False Branch"} | |
| ], | |
| params: [{id: "expression", label: "Expression", type: "text", value: "X > 0"}], | |
| color: "var(--node-control)" | |
| }, | |
| for: { | |
| title: "For Loop", | |
| inputs: [], | |
| outputs: [{id: "body", label: "Loop Body"}], | |
| params: [ | |
| {id: "variable", label: "Counter", type: "text", value: "I"}, | |
| {id: "start", label: "Start", type: "number", value: "1"}, | |
| {id: "end", label: "End", type: "number", value: "10"} | |
| ], | |
| color: "var(--node-control)" | |
| }, | |
| add: { | |
| title: "Addition", | |
| inputs: [ | |
| {id: "a", label: "Value A"}, | |
| {id: "b", label: "Value B"} | |
| ], | |
| outputs: [{id: "result", label: "Result"}], | |
| params: [], | |
| color: "var(--node-process)" | |
| }, | |
| multiply: { | |
| title: "Multiplication", | |
| inputs: [ | |
| {id: "a", label: "Value A"}, | |
| {id: "b", label: "Value B"} | |
| ], | |
| outputs: [{id: "result", label: "Result"}], | |
| params: [], | |
| color: "var(--node-process)" | |
| }, | |
| number: { | |
| title: "Number", | |
| inputs: [], | |
| outputs: [{id: "value", label: "Number Value"}], | |
| params: [{id: "value", label: "Number", type: "number", value: "0"}], | |
| color: "var(--node-input)" | |
| }, | |
| string: { | |
| title: "String", | |
| inputs: [], | |
| outputs: [{id: "value", label: "String Value"}], | |
| params: [{id: "value", label: "Text", type: "text", value: "Hello"}], | |
| color: "var(--node-input)" | |
| } | |
| }; | |
| // Make sidebar items draggable | |
| nodeItems.forEach(item => { | |
| item.addEventListener('dragstart', function(e) { | |
| e.dataTransfer.setData('text/plain', this.dataset.node); | |
| }); | |
| }); | |
| // Enable dropping in workspace | |
| workspace.addEventListener('dragover', function(e) { | |
| e.preventDefault(); | |
| }); | |
| workspace.addEventListener('drop', function(e) { | |
| e.preventDefault(); | |
| const nodeType = e.dataTransfer.getData('text/plain'); | |
| if (nodeType) { | |
| createNode(nodeType, e.clientX - workspace.getBoundingClientRect().left, e.clientY - workspace.getBoundingClientRect().top); | |
| } | |
| }); | |
| // Create a new node | |
| function createNode(type, x, y) { | |
| const template = nodeTemplates[type]; | |
| if (!template) return; | |
| nodeId++; | |
| const nodeIdStr = `node-${nodeId}`; | |
| nodeData[nodeIdStr] = { | |
| type: type, | |
| params: {} | |
| }; | |
| const node = document.createElement('div'); | |
| node.className = 'node'; | |
| node.id = nodeIdStr; | |
| node.style.left = `${x}px`; | |
| node.style.top = `${y}px`; | |
| node.innerHTML = ` | |
| <div class="node-header" style="border-left: 3px solid ${template.color}"> | |
| <div class="node-title">${template.title}</div> | |
| <button class="node-close">×</button> | |
| </div> | |
| <div class="node-content"> | |
| ${template.inputs.length > 0 ? ` | |
| <div class="node-inputs"> | |
| ${template.inputs.map(input => ` | |
| <div class="io-item"> | |
| <div class="io-connector input-connector" data-node="${nodeIdStr}" data-io="${input.id}"></div> | |
| <div class="io-label">${input.label}</div> | |
| </div> | |
| `).join('')} | |
| </div>` : ''} | |
| <div class="node-body"> | |
| ${template.params.map(param => ` | |
| <div class="node-param"> | |
| <label class="param-label">${param.label}</label> | |
| <input type="${param.type}" class="param-input" data-param="${param.id}" value="${param.value}"> | |
| </div> | |
| `).join('')} | |
| </div> | |
| ${template.outputs.length > 0 ? ` | |
| <div class="node-outputs"> | |
| ${template.outputs.map(output => ` | |
| <div class="io-item"> | |
| <div class="io-label">${output.label}</div> | |
| <div class="io-connector output-connector" data-node="${nodeIdStr}" data-io="${output.id}"></div> | |
| </div> | |
| `).join('')} | |
| </div>` : ''} | |
| </div> | |
| `; | |
| workspace.appendChild(node); | |
| // Store initial parameter values | |
| template.params.forEach(param => { | |
| nodeData[nodeIdStr].params[param.id] = param.value; | |
| }); | |
| // Add event listeners for the new node | |
| const closeBtn = node.querySelector('.node-close'); | |
| closeBtn.addEventListener('click', function() { | |
| deleteNode(nodeIdStr); | |
| }); | |
| // Add parameter change listeners | |
| const paramInputs = node.querySelectorAll('.param-input'); | |
| paramInputs.forEach(input => { | |
| input.addEventListener('change', function() { | |
| const paramId = this.dataset.param; | |
| nodeData[nodeIdStr].params[paramId] = this.value; | |
| }); | |
| }); | |
| // Make node draggable | |
| makeNodeDraggable(node); | |
| // Add connection handlers | |
| const connectors = node.querySelectorAll('.io-connector'); | |
| connectors.forEach(connector => { | |
| connector.addEventListener('mousedown', startConnection); | |
| }); | |
| } | |
| // Delete a node and its connections | |
| function deleteNode(nodeId) { | |
| // Remove connections associated with this node | |
| connections = connections.filter(conn => { | |
| if (conn.from.nodeId === nodeId || conn.to.nodeId === nodeId) { | |
| // Remove SVG line | |
| const line = document.getElementById(conn.id); | |
| if (line) line.remove(); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| // Remove node data | |
| delete nodeData[nodeId]; | |
| // Remove node element | |
| const node = document.getElementById(nodeId); | |
| if (node) node.remove(); | |
| } | |
| // Make nodes draggable | |
| function makeNodeDraggable(node) { | |
| let isDragging = false; | |
| let offsetX, offsetY; | |
| const header = node.querySelector('.node-header'); | |
| header.addEventListener('mousedown', function(e) { | |
| if (e.target.classList.contains('node-close')) return; | |
| isDragging = true; | |
| const rect = node.getBoundingClientRect(); | |
| offsetX = e.clientX - rect.left; | |
| offsetY = e.clientY - rect.top; | |
| node.style.zIndex = 1000; | |
| }); | |
| document.addEventListener('mousemove', function(e) { | |
| if (!isDragging) return; | |
| const workspaceRect = workspace.getBoundingClientRect(); | |
| const x = e.clientX - workspaceRect.left - offsetX; | |
| const y = e.clientY - workspaceRect.top - offsetY; | |
| node.style.left = `${x}px`; | |
| node.style.top = `${y}px`; | |
| // Update connections | |
| updateConnections(); | |
| }); | |
| document.addEventListener('mouseup', function() { | |
| isDragging = false; | |
| node.style.zIndex = 10; | |
| }); | |
| } | |
| // Connection handling | |
| function startConnection(e) { | |
| isConnecting = true; | |
| startConnector = e.target; | |
| // Create temporary line | |
| tempLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); | |
| tempLine.classList.add('connection-line'); | |
| tempLine.setAttribute('id', 'temp-line'); | |
| connectionsContainer.appendChild(tempLine); | |
| updateTempLine(e); | |
| document.addEventListener('mousemove', updateTempLine); | |
| document.addEventListener('mouseup', finishConnection); | |
| } | |
| function updateTempLine(e) { | |
| if (!isConnecting || !startConnector) return; | |
| const startRect = startConnector.getBoundingClientRect(); | |
| const workspaceRect = workspace.getBoundingClientRect(); | |
| const startX = startRect.left + startRect.width/2 - workspaceRect.left; | |
| const startY = startRect.top + startRect.height/2 - workspaceRect.top; | |
| tempLine.setAttribute('x1', startX); | |
| tempLine.setAttribute('y1', startY); | |
| tempLine.setAttribute('x2', e.clientX - workspaceRect.left); | |
| tempLine.setAttribute('y2', e.clientY - workspaceRect.top); | |
| } | |
| function finishConnection(e) { | |
| if (!isConnecting) return; | |
| isConnecting = false; | |
| document.removeEventListener('mousemove', updateTempLine); | |
| document.removeEventListener('mouseup', finishConnection); | |
| // Remove temporary line | |
| if (tempLine) { | |
| tempLine.remove(); | |
| } | |
| // Check if we're over a connector | |
| const element = document.elementFromPoint(e.clientX, e.clientY); | |
| if (element && element.classList.contains('io-connector') && element !== startConnector) { | |
| // Create permanent connection | |
| createConnection(startConnector, element); | |
| } | |
| startConnector = null; | |
| tempLine = null; | |
| } | |
| function createConnection(from, to) { | |
| // Prevent connecting input to input or output to output | |
| const fromIsOutput = from.classList.contains('output-connector'); | |
| const toIsInput = to.classList.contains('input-connector'); | |
| if (!(fromIsOutput && toIsInput)) return; | |
| // Check if connection already exists | |
| const existingConnection = connections.find(conn => | |
| conn.from.nodeId === from.dataset.node && | |
| conn.from.ioId === from.dataset.io && | |
| conn.to.nodeId === to.dataset.node && | |
| conn.to.ioId === to.dataset.io | |
| ); | |
| if (existingConnection) return; | |
| const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| connections.push({ | |
| id: connectionId, | |
| from: { | |
| nodeId: from.dataset.node, | |
| ioId: from.dataset.io | |
| }, | |
| to: { | |
| nodeId: to.dataset.node, | |
| ioId: to.dataset.io | |
| } | |
| }); | |
| drawConnection(from, to, connectionId); | |
| } | |
| function drawConnection(from, to, id) { | |
| const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); | |
| line.classList.add('connection-line'); | |
| line.setAttribute('id', id); | |
| connectionsContainer.appendChild(line); | |
| updateConnectionLine(from, to, line); | |
| } | |
| function updateConnectionLine(from, to, line) { | |
| const fromRect = from.getBoundingClientRect(); | |
| const toRect = to.getBoundingClientRect(); | |
| const workspaceRect = workspace.getBoundingClientRect(); | |
| const fromX = fromRect.left + fromRect.width/2 - workspaceRect.left; | |
| const fromY = fromRect.top + fromRect.height/2 - workspaceRect.top; | |
| const toX = toRect.left + toRect.width/2 - workspaceRect.left; | |
| const toY = toRect.top + toRect.height/2 - workspaceRect.top; | |
| line.setAttribute('x1', fromX); | |
| line.setAttribute('y1', fromY); | |
| line.setAttribute('x2', toX); | |
| line.setAttribute('y2', toY); | |
| } | |
| function updateConnections() { | |
| connections.forEach(connection => { | |
| const fromNode = document.querySelector(`.io-connector[data-node="${connection.from.nodeId}"][data-io="${connection.from.ioId}"]`); | |
| const toNode = document.querySelector(`.io-connector[data-node="${connection.to.nodeId}"][data-io="${connection.to.ioId}"]`); | |
| if (fromNode && toNode) { | |
| const line = document.getElementById(connection.id); | |
| if (line) { | |
| updateConnectionLine(fromNode, toNode, line); | |
| } | |
| } | |
| }); | |
| } | |
| // Clear workspace | |
| clearBtn.addEventListener('click', function() { | |
| document.querySelectorAll('.node').forEach(node => node.remove()); | |
| document.querySelectorAll('.connection-line').forEach(line => line.remove()); | |
| connections = []; | |
| nodeData = {}; | |
| nodeId = 0; | |
| outputContent.innerHTML = ''; | |
| }); | |
| // Download program | |
| downloadBtn.addEventListener('click', function() { | |
| const program = generateBASICProgram(); | |
| const blob = new Blob([program], {type: 'text/plain'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'program.bas'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| // Run program | |
| runBtn.addEventListener('click', function() { | |
| runProgram(); | |
| }); | |
| // Generate BASIC program from nodes | |
| function generateBASICProgram() { | |
| let program = []; | |
| program.push("10 REM Generated by ComfyBASIC"); | |
| program.push("20 REM Program Start"); | |
| let lineNum = 30; | |
| const processedNodes = new Set(); | |
| // Find all print nodes and trace back their inputs | |
| const printNodes = Array.from(document.querySelectorAll('.node')).filter(node => { | |
| const nodeId = node.id; | |
| const nodeInfo = nodeData[nodeId]; | |
| return nodeInfo && nodeInfo.type === 'print'; | |
| }); | |
| // Process each print node | |
| printNodes.forEach(printNode => { | |
| const nodeId = printNode.id; | |
| // Find input connection to print node | |
| const inputConn = connections.find(conn => | |
| conn.to.nodeId === nodeId && conn.to.ioId === 'value' | |
| ); | |
| if (inputConn) { | |
| const valueExpr = getNodeExpression(inputConn.from.nodeId); | |
| if (valueExpr) { | |
| program.push(`${lineNum} PRINT ${valueExpr}`); | |
| lineNum += 10; | |
| } | |
| } else { | |
| program.push(`${lineNum} PRINT "Hello from ComfyBASIC"`); | |
| lineNum += 10; | |
| } | |
| }); | |
| // Handle input nodes that aren't connected to anything | |
| const inputNodes = Array.from(document.querySelectorAll('.node')).filter(node => { | |
| const nodeId = node.id; | |
| const nodeInfo = nodeData[nodeId]; | |
| return nodeInfo && nodeInfo.type === 'input'; | |
| }); | |
| inputNodes.forEach(inputNode => { | |
| const nodeId = inputNode.id; | |
| // Check if this input node is connected to anything | |
| const hasOutputConnections = connections.some(conn => | |
| conn.from.nodeId === nodeId | |
| ); | |
| if (!hasOutputConnections) { | |
| const nodeInfo = nodeData[nodeId]; | |
| const prompt = nodeInfo.params.prompt || 'Enter value:'; | |
| program.push(`${lineNum} INPUT "${prompt}", TEMP${nodeId.split('-')[1]}`); | |
| lineNum += 10; | |
| } | |
| }); | |
| program.push(`${lineNum} END`); | |
| return program.join('\n') + '\n'; | |
| } | |
| // Get expression for a node | |
| function getNodeExpression(nodeId) { | |
| const nodeInfo = nodeData[nodeId]; | |
| if (!nodeInfo) return null; | |
| switch (nodeInfo.type) { | |
| case 'getvar': | |
| return nodeInfo.params.name || 'X'; | |
| case 'number': | |
| return nodeInfo.params.value || '0'; | |
| case 'string': | |
| return `"${nodeInfo.params.value || 'Hello'}"`; | |
| case 'add': | |
| const aConn = connections.find(conn => | |
| conn.to.nodeId === nodeId && conn.to.ioId === 'a' | |
| ); | |
| const bConn = connections.find(conn => | |
| conn.to.nodeId === nodeId && conn.to.ioId === 'b' | |
| ); | |
| const aExpr = aConn ? getNodeExpression(aConn.from.nodeId) : '0'; | |
| const bExpr = bConn ? getNodeExpression(bConn.from.nodeId) : '0'; | |
| return `${aExpr} + ${bExpr}`; | |
| case 'multiply': | |
| const mulAConn = connections.find(conn => | |
| conn.to.nodeId === nodeId && conn.to.ioId === 'a' | |
| ); | |
| const mulBConn = connections.find(conn => | |
| conn.to.nodeId === nodeId && conn.to.ioId === 'b' | |
| ); | |
| const mulAExpr = mulAConn ? getNodeExpression(mulAConn.from.nodeId) : '1'; | |
| const mulBExpr = mulBConn ? getNodeExpression(mulBConn.from.nodeId) : '1'; | |
| return `${mulAExpr} * ${mulBExpr}`; | |
| default: | |
| return null; | |
| } | |
| } | |
| // Run the program in the browser | |
| function runProgram() { | |
| outputContent.innerHTML = ''; | |
| const program = generateBASICProgram(); | |
| const lines = program.split('\n'); | |
| // Display program header | |
| appendToOutput("ComfyBASIC Interpreter v1.0\n", "console-output"); | |
| appendToOutput("Running program...\n", "console-output"); | |
| appendToOutput("--------------------\n", "console-output"); | |
| // Simple BASIC interpreter | |
| let variables = {}; | |
| let lineIndex = 0; | |
| function executeLine(line) { | |
| const trimmedLine = line.trim(); | |
| if (!trimmedLine || trimmedLine.startsWith('REM')) return true; | |
| // Parse line number and command | |
| const match = trimmedLine.match(/^(\d+)\s+(.*)$/); | |
| if (!match) return true; | |
| const command = match[2]; | |
| if (command.startsWith('PRINT')) { | |
| const expr = command.substring(6).trim(); | |
| if (expr.startsWith('"') && expr.endsWith('"')) { | |
| // String literal | |
| appendToOutput(expr.substring(1, expr.length - 1) + '\n', "console-output"); | |
| } else { | |
| // Variable or expression | |
| try { | |
| const result = evaluateExpression(expr, variables); | |
| appendToOutput(result + '\n', "console-output"); | |
| } catch (e) { | |
| appendToOutput(`Error: ${e.message}\n`, "console-error"); | |
| } | |
| } | |
| } else if (command.startsWith('INPUT')) { | |
| const parts = command.substring(6).split(','); | |
| const prompt = parts[0].trim().replace(/"/g, ''); | |
| const varName = parts[1].trim(); | |
| appendToOutput(prompt + ' ', "console-input"); | |
| // In a real implementation, we'd wait for input | |
| // For demo, we'll just assign a default value | |
| variables[varName] = Math.floor(Math.random() * 100); | |
| appendToOutput(variables[varName] + '\n', "console-output"); | |
| } else if (command.startsWith('LET')) { | |
| const expr = command.substring(4).trim(); | |
| const eqIndex = expr.indexOf('='); | |
| if (eqIndex > 0) { | |
| const varName = expr.substring(0, eqIndex).trim(); | |
| const valueExpr = expr.substring(eqIndex + 1).trim(); | |
| variables[varName] = evaluateExpression(valueExpr, variables); | |
| } | |
| } else if (command === 'END') { | |
| return false; | |
| } | |
| return true; | |
| } | |
| function evaluateExpression(expr, vars) { | |
| // Simple expression evaluator (supports +, -, *, /, variables, and numbers) | |
| expr = expr.replace(/\s+/g, ''); | |
| // Replace variables with their values | |
| for (const [varName, value] of Object.entries(vars)) { | |
| const regex = new RegExp(`\\b${varName}\\b`, 'g'); | |
| expr = expr.replace(regex, value); | |
| } | |
| try { | |
| // This is a very simple evaluator - in reality you'd want a proper parser | |
| return Function('"use strict"; return (' + expr + ')')(); | |
| } catch (e) { | |
| throw new Error(`Cannot evaluate expression: ${expr}`); | |
| } | |
| } | |
| function appendToOutput(text, className) { | |
| const span = document.createElement('span'); | |
| span.className = className; | |
| span.textContent = text; | |
| outputContent.appendChild(span); | |
| outputContent.scrollTop = outputContent.scrollHeight; | |
| } | |
| // Execute program line by line | |
| for (const line of lines) { | |
| if (!executeLine(line)) break; | |
| } | |
| appendToOutput("--------------------\n", "console-output"); | |
| appendToOutput("Program finished.\n", "console-output"); | |
| } | |
| // Create some initial nodes for demo | |
| setTimeout(() => { | |
| createNode('number', 150, 100); | |
| createNode('number', 150, 200); | |
| createNode('add', 350, 150); | |
| createNode('print', 550, 150); | |
| // Set parameters | |
| document.querySelector('#node-1 .param-input[data-param="value"]').value = '5'; | |
| nodeData['node-1'].params.value = '5'; | |
| document.querySelector('#node-2 .param-input[data-param="value"]').value = '3'; | |
| nodeData['node-2'].params.value = '3'; | |
| // Auto-connect nodes | |
| setTimeout(() => { | |
| const num1Output = document.querySelector('.io-connector.output-connector[data-node="node-1"]'); | |
| const addInputA = document.querySelector('.io-connector.input-connector[data-node="node-3"][data-io="a"]'); | |
| const num2Output = document.querySelector('.io-connector.output-connector[data-node="node-2"]'); | |
| const addInputB = document.querySelector('.io-connector.input-connector[data-node="node-3"][data-io="b"]'); | |
| const addOutput = document.querySelector('.io-connector.output-connector[data-node="node-3"]'); | |
| const printInput = document.querySelector('.io-connector.input-connector[data-node="node-4"][data-io="value"]'); | |
| if (num1Output && addInputA) createConnection(num1Output, addInputA); | |
| if (num2Output && addInputB) createConnection(num2Output, addInputB); | |
| if (addOutput && printInput) createConnection(addOutput, printInput); | |
| }, 100); | |
| }, 500); | |
| }); | |
| </script> | |
| </body> | |
| </html> |