Node Graph Editor
GPT 5.6 terra · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 17,787 bytes · SHA-256 4c909226a647
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logic Circuit Lab</title>
<style>
/* ── css: layout ── */
*{box-sizing:border-box}html,body,#app{margin:0;width:100%;height:100%;overflow:hidden}body{background:#101419;color:#e8edf0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}#app{display:flex}#palette{width:218px;flex:none;background:linear-gradient(180deg,#181e25,#12171d);border-right:1px solid #29323b;padding:18px 12px;position:relative;z-index:2;box-shadow:8px 0 28px #0005}.brand{display:flex;align-items:center;gap:10px;margin:0 8px 26px}.brand-mark{font-size:32px;color:#ffae52;text-shadow:0 0 15px #ff8a3d}.brand b{font-size:15px;letter-spacing:1.8px;display:block}.brand small{font-size:8px;letter-spacing:1.55px;color:#788694}.palette-label{font-size:9px;letter-spacing:1.7px;color:#63717d;margin:19px 8px 8px}#palette button{border:1px solid transparent;background:transparent;color:#cdd6dd;width:100%;height:34px;text-align:left;border-radius:7px;padding:0 10px;cursor:pointer;transition:.15s;font-size:13px}#palette button i{font-size:18px;display:inline-block;width:29px;text-align:center;color:#9db2c2;font-style:normal}#palette button:hover{background:#242d36;border-color:#394653;transform:translateX(2px);color:#fff}#palette button:hover i{color:#ffb15b}.hint{position:absolute;bottom:17px;left:20px;font-size:10px;line-height:1.65;color:#677681}.hint em{font-style:normal;color:#ffad52}#workspace{position:relative;flex:1;overflow:hidden;background:#12171c}#graph{display:block;width:100%;height:100%;cursor:default}#toolbar{position:absolute;right:18px;top:16px;display:flex;align-items:center;gap:5px;background:#1a2129dd;border:1px solid #33404c;border-radius:9px;padding:5px;box-shadow:0 6px 18px #0005}#toolbar span{font:600 11px ui-monospace,monospace;color:#aebbc5;padding:0 6px}#toolbar button{width:27px;height:25px;border:0;border-radius:5px;background:#28333d;color:#dce5eb;font-size:17px;cursor:pointer}#toolbar button:hover{background:#3d4d5a;color:#fff}#status{position:absolute;left:18px;bottom:15px;font:10px ui-monospace,monospace;color:#8695a1;letter-spacing:.3px;background:#181f27cc;border:1px solid #2a3540;border-radius:7px;padding:8px 11px}.status-dot{display:inline-block;width:6px;height:6px;background:#6fe2a1;border-radius:50%;box-shadow:0 0 8px #6fe2a1;margin-right:7px}#toast{position:absolute;left:50%;top:17px;transform:translate(-50%,-12px);opacity:0;pointer-events:none;background:#542c32;border:1px solid #e86767;color:#ffdada;border-radius:7px;padding:8px 12px;font-size:12px;transition:.2s;box-shadow:0 5px 20px #0007}#toast.show{opacity:1;transform:translate(-50%,0)}
</style>
</head>
<body>
<div id="app"><aside id="palette"><div class="brand"><span class="brand-mark">⌁</span><div><b>LOGIC LAB</b><small>COMBINATIONAL CIRCUITS</small></div></div><div class="palette-label">SOURCES</div><button data-type="toggle"><i>●</i> Toggle</button><button data-type="clock"><i>◷</i> Clock</button><div class="palette-label">LOGIC GATES</div><button data-type="and"><i>∧</i> AND</button><button data-type="or"><i>∨</i> OR</button><button data-type="not"><i>¬</i> NOT</button><button data-type="xor"><i>⊕</i> XOR</button><button data-type="nand"><i>⊼</i> NAND</button><button data-type="nor"><i>⊽</i> NOR</button><div class="palette-label">INDICATORS</div><button data-type="lamp"><i>◉</i> Lamp</button><p class="hint">Drag nodes by their body.<br>Wire from <em>amber</em> to <em>blue</em>.</p></aside><main id="workspace"><canvas id="graph"></canvas><div id="toolbar"><span id="zoomLabel">100%</span><button id="zoomOut">−</button><button id="zoomIn">+</button><button id="resetView">⌖</button></div><div id="status"><span class="status-dot"></span><span id="statusText">READY · 0 NODES · 0 WIRES</span></div><div id="toast"></div></main></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// Logic Lab — data model, evaluation, persistence, rendering, and interaction
const C = { minZoom: .5, maxZoom: 2, portR: 7, nodeW: 142, nodeH: 74, grid: 24 };
const canvas = document.querySelector('#graph'), ctx = canvas.getContext('2d');
const ws = document.querySelector('#workspace'), zoomLabel = document.querySelector('#zoomLabel'), status = document.querySelector('#statusText'), toast = document.querySelector('#toast');
let scene = { nodes: [], wires: [] }, view = { x: 110, y: 90, z: 1 };
let selected = null, hover = null;
let drag = null, mouse = { x: 0, y: 0 }, flash = 0;
const inputs = (t) => t === 'not' || t === 'lamp' ? 1 : t === 'toggle' || t === 'clock' ? 0 : 2;
const hasOutput = (t) => t !== 'lamp';
const label = (t) => ({ toggle: 'TOGGLE', clock: 'CLOCK', and: 'AND', or: 'OR', not: 'NOT', xor: 'XOR', nand: 'NAND', nor: 'NOR', lamp: 'LAMP' }[t]);
function id() { return 'n' + Math.random().toString(36).slice(2, 9); }
function nodeBy(i) { return scene.nodes.find(n => n.id === i); }
function save() { localStorage.setItem('logic-lab-v1', JSON.stringify(scene)); updateStatus(); }
function load() { try {
const s = localStorage.getItem('logic-lab-v1');
if (s)
scene = JSON.parse(s);
else
demo();
}
catch {
demo();
} evaluate(); }
function demo() { const a = { id: 'demo-a', type: 'toggle', x: 70, y: 130, state: false, rate: 1 }, b = { id: 'demo-b', type: 'toggle', x: 70, y: 270, state: true, rate: 1 }, g = { id: 'demo-and', type: 'and', x: 330, y: 190, state: false, rate: 1 }, l = { id: 'demo-lamp', type: 'lamp', x: 585, y: 190, state: false, rate: 1 }; scene = { nodes: [a, b, g, l], wires: [{ from: a.id, fromPort: 0, to: g.id, toPort: 0 }, { from: b.id, fromPort: 0, to: g.id, toPort: 1 }, { from: g.id, fromPort: 0, to: l.id, toPort: 0 }] }; }
function updateStatus() { status.textContent = `READY · ${scene.nodes.length} NODES · ${scene.wires.length} WIRES`; }
function port(n, input, index = 0) { const count = inputs(n.type); return { x: n.x + (input ? 0 : C.nodeW), y: n.y + C.nodeH / 2 + (input ? (index - (count - 1) / 2) * 20 : 0) }; }
function evalNode(n) { if (n.type === 'toggle' || n.type === 'clock')
return; const a = scene.wires.find(w => w.to === n.id && w.toPort === 0), b = scene.wires.find(w => w.to === n.id && w.toPort === 1); const A = !!(a && nodeBy(a.from)?.state), B = !!(b && nodeBy(b.from)?.state); n.state = n.type === 'and' ? A && B : n.type === 'or' ? A || B : n.type === 'not' ? !A : n.type === 'xor' ? A !== B : n.type === 'nand' ? !(A && B) : n.type === 'nor' ? !(A || B) : A; }
function evaluate() { const indeg = new Map(); scene.nodes.forEach(n => indeg.set(n.id, 0)); scene.wires.forEach(w => indeg.set(w.to, (indeg.get(w.to) || 0) + 1)); const q = scene.nodes.filter(n => !indeg.get(n.id)); while (q.length) {
const n = q.shift();
evalNode(n);
scene.wires.filter(w => w.from === n.id).forEach(w => { const d = (indeg.get(w.to) || 1) - 1; indeg.set(w.to, d); if (!d) {
const target = nodeBy(w.to);
if (target)
q.push(target);
} });
} updateStatus(); }
function world(x, y) { return { x: (x - view.x) / view.z, y: (y - view.y) / view.z }; }
function screen(x, y) { return { x: x * view.z + view.x, y: y * view.z + view.y }; }
function hitPort(w, wantInput) { for (const n of scene.nodes) {
const count = wantInput ? inputs(n.type) : 0;
for (let i = 0; i < count; i++) {
const p = port(n, wantInput, i);
if (Math.hypot(w.x - p.x, w.y - p.y) < 13 / view.z)
return { n, i };
}
if (!wantInput && hasOutput(n.type)) {
const p = port(n, false);
if (Math.hypot(w.x - p.x, w.y - p.y) < 13 / view.z)
return { n, i: 0 };
}
} return null; }
function hitNode(w) { return [...scene.nodes].reverse().find(n => w.x >= n.x && w.x <= n.x + C.nodeW && w.y >= n.y && w.y <= n.y + C.nodeH); }
function bez(a, b) { ctx.beginPath(); ctx.moveTo(a.x, a.y); const d = Math.max(45, Math.abs(b.x - a.x) * .55); ctx.bezierCurveTo(a.x + d, a.y, b.x - d, b.y, b.x, b.y); }
function draw() {
const r = ws.getBoundingClientRect();
canvas.width = r.width * devicePixelRatio;
canvas.height = r.height * devicePixelRatio;
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
ctx.clearRect(0, 0, r.width, r.height);
ctx.fillStyle = '#12171c';
ctx.fillRect(0, 0, r.width, r.height);
const step = C.grid * view.z;
ctx.fillStyle = '#33404a';
for (let x = ((view.x % step) + step) % step; x < r.width; x += step)
for (let y = ((view.y % step) + step) % step; y < r.height; y += step)
ctx.fillRect(x, y, 1, 1);
// wires
for (const w of scene.wires) {
const a = nodeBy(w.from), b = nodeBy(w.to);
if (!a || !b)
continue;
const A = screen(port(a, false).x, port(a, false).y), B = screen(port(b, true, w.toPort).x, port(b, true, w.toPort).y), on = a.state;
ctx.lineWidth = on ? 4 : 2;
ctx.strokeStyle = on ? '#ffad52' : '#495762';
ctx.shadowBlur = on ? 13 : 0;
ctx.shadowColor = '#ff9d3d';
bez(A, B);
ctx.stroke();
ctx.shadowBlur = 0;
}
if (drag?.kind === 'wire' && drag.from) {
const a = nodeBy(drag.from);
const A = screen(port(a, false).x, port(a, false).y);
ctx.lineWidth = 3;
ctx.strokeStyle = flash ? '#ef5c64' : '#ffba68';
ctx.setLineDash([7, 5]);
bez(A, mouse);
ctx.stroke();
ctx.setLineDash([]);
}
// nodes
for (const n of scene.nodes) {
const P = screen(n.x, n.y), W = C.nodeW * view.z, H = C.nodeH * view.z, sel = selected === n.id;
ctx.save();
ctx.translate(P.x, P.y);
ctx.fillStyle = sel ? '#252f39' : '#1b232b';
ctx.strokeStyle = sel ? '#ffb259' : '#3a4651';
ctx.lineWidth = sel ? 2 : 1;
ctx.shadowColor = '#000';
ctx.shadowBlur = 10;
round(0, 0, W, H, 10);
ctx.fill();
ctx.shadowBlur = 0;
ctx.stroke();
const on = n.state;
ctx.fillStyle = '#aebbc5';
ctx.font = `600 ${11 * view.z}px system-ui`;
ctx.fillText(label(n.type), 14 * view.z, 22 * view.z);
ctx.font = `${8 * view.z}px ui-monospace,monospace`;
ctx.fillStyle = on ? '#79e6ad' : '#71808c';
ctx.fillText(n.type === 'clock' ? `${n.rate} Hz` : (on ? 'HIGH' : 'LOW'), 14 * view.z, 40 * view.z);
if (n.type === 'toggle') {
ctx.fillStyle = on ? '#62d997' : '#37434d';
round(92 * view.z, 22 * view.z, 35 * view.z, 19 * view.z, 11 * view.z);
ctx.fill();
ctx.fillStyle = '#eef5f1';
ctx.beginPath();
ctx.arc((on ? 117 : 101) * view.z, 31.5 * view.z, 7 * view.z, 0, 7);
ctx.fill();
}
else if (n.type === 'lamp') {
ctx.fillStyle = on ? '#ffe099' : '#34414b';
ctx.shadowBlur = on ? 18 : 0;
ctx.shadowColor = '#ffae49';
ctx.beginPath();
ctx.arc(111 * view.z, 35 * view.z, 14 * view.z, 0, 7);
ctx.fill();
ctx.shadowBlur = 0;
}
else {
ctx.fillStyle = '#73818b';
ctx.font = `bold ${18 * view.z}px system-ui`;
ctx.fillText(n.type === 'not' ? '¬' : n.type === 'xor' ? '⊕' : n.type === 'nand' ? '⊼' : n.type === 'nor' ? '⊽' : n.type === 'and' ? '∧' : n.type === 'or' ? '∨' : '◷', 105 * view.z, 42 * view.z);
}
for (let i = 0; i < inputs(n.type); i++) {
const q = port(n, true, i);
circle((q.x - n.x) * view.z, (q.y - n.y) * view.z, C.portR * view.z, '#60bde9');
}
if (hasOutput(n.type)) {
const q = port(n, false);
circle((q.x - n.x) * view.z, (q.y - n.y) * view.z, C.portR * view.z, '#ffad52');
}
ctx.restore();
}
requestAnimationFrame(draw);
}
function round(x, y, w, h, r) { ctx.beginPath(); ctx.roundRect(x, y, w, h, r); }
function circle(x, y, r, c) { ctx.beginPath(); ctx.fillStyle = c; ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); }
function add(type) { const center = world(ws.clientWidth * .52, ws.clientHeight * .45); const n = { id: id(), type, x: center.x - 70 + (Math.random() - .5) * 35, y: center.y - 35 + (Math.random() - .5) * 35, state: type === 'toggle' ? false : type === 'not' || type === 'nand' || type === 'nor' ? true : false, rate: 1 }; scene.nodes.push(n); selected = n.id; evaluate(); save(); }
function cycle(from, to) { const seen = new Set(), walk = (x) => { if (x === from)
return true; if (seen.has(x))
return false; seen.add(x); return scene.wires.filter(w => w.from === x).some(w => walk(w.to)); }; return walk(to); }
canvas.addEventListener('pointerdown', e => { canvas.setPointerCapture(e.pointerId); mouse = { x: e.offsetX, y: e.offsetY }; const w = world(mouse.x, mouse.y), inp = hitPort(w, true), out = hitPort(w, false); if (inp) {
const old = scene.wires.find(x => x.to === inp.n.id && x.toPort === inp.i);
if (old) {
scene.wires = scene.wires.filter(x => x !== old);
drag = { kind: 'wire', x: 0, y: 0, from: old.from, fromPort: 0, old };
}
else
drag = { kind: 'pan', x: e.clientX, y: e.clientY };
return;
} if (out) {
drag = { kind: 'wire', x: 0, y: 0, from: out.n.id, fromPort: 0 };
return;
} const n = hitNode(w); if (n) {
selected = n.id;
drag = { kind: 'node', x: w.x - n.x, y: w.y - n.y, node: n.id };
}
else {
selected = null;
drag = { kind: 'pan', x: e.clientX, y: e.clientY };
} });
canvas.addEventListener('pointermove', e => { mouse = { x: e.offsetX, y: e.offsetY }; if (!drag)
return; if (drag.kind === 'pan') {
view.x += e.clientX - drag.x;
view.y += e.clientY - drag.y;
drag.x = e.clientX;
drag.y = e.clientY;
}
else if (drag.kind === 'node' && drag.node) {
const n = nodeBy(drag.node), w = world(mouse.x, mouse.y);
if (n) {
n.x = w.x - drag.x;
n.y = w.y - drag.y;
}
} });
canvas.addEventListener('pointerup', e => { if (drag?.kind === 'wire' && drag.from) {
const target = hitPort(world(e.offsetX, e.offsetY), true);
if (target) {
if (cycle(drag.from, target.n.id)) {
flash = 1;
setTimeout(() => flash = 0, 400);
toast.textContent = 'Connection refused — feedback loop';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1400);
if (drag.old)
scene.wires.push(drag.old);
}
else {
scene.wires = scene.wires.filter(w => !(w.to === target.n.id && w.toPort === target.i));
scene.wires.push({ from: drag.from, fromPort: 0, to: target.n.id, toPort: target.i });
}
}
else if (drag.old)
scene.wires.push(drag.old);
evaluate();
save();
}
else if (drag?.kind === 'node')
save(); drag = null; });
canvas.addEventListener('click', e => { const n = hitNode(world(e.offsetX, e.offsetY)); if (n?.type === 'toggle') {
n.state = !n.state;
evaluate();
save();
}
else if (n?.type === 'clock') {
n.rate = n.rate >= 4 ? .5 : n.rate * 2;
save();
} });
canvas.addEventListener('wheel', e => { e.preventDefault(); const before = world(e.offsetX, e.offsetY), factor = e.deltaY < 0 ? 1.12 : 1 / 1.12; view.z = Math.max(C.minZoom, Math.min(C.maxZoom, view.z * factor)); view.x = e.offsetX - before.x * view.z; view.y = e.offsetY - before.y * view.z; zoomLabel.textContent = Math.round(view.z * 100) + '%'; }, { passive: false });
document.querySelectorAll('#palette button').forEach(b => b.addEventListener('click', () => add(b.dataset.type)));
document.querySelector('#zoomIn').addEventListener('click', () => { view.z = Math.min(2, view.z * 1.2); zoomLabel.textContent = Math.round(view.z * 100) + '%'; });
document.querySelector('#zoomOut').addEventListener('click', () => { view.z = Math.max(.5, view.z / 1.2); zoomLabel.textContent = Math.round(view.z * 100) + '%'; });
document.querySelector('#resetView').addEventListener('click', () => { view = { x: 110, y: 90, z: 1 }; zoomLabel.textContent = '100%'; });
document.addEventListener('keydown', e => { if (e.key === 'Delete' && selected) {
scene.nodes = scene.nodes.filter(n => n.id !== selected);
scene.wires = scene.wires.filter(w => w.from !== selected && w.to !== selected);
selected = null;
evaluate();
save();
} });
setInterval(() => { let changed = false; scene.nodes.filter(n => n.type === 'clock').forEach(n => { const period = 1000 / Math.max(.25, n.rate); const bucket = Math.floor(Date.now() / period) % 2 === 0; if (n.state !== bucket) {
n.state = bucket;
changed = true;
} }); if (changed)
evaluate(); }, 80);
load();
draw();
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-terra","provider":"openrouter","persona":"typescript","sessionId":"52808db0-aaf4-4004-a827-09505e54918c","tokensIn":194585,"tokensOut":6859,"tokensTotal":201444,"turns":21,"toolCalls":20,"failedToolCalls":1,"timestamp":"2026-07-31T18:01:48.206Z"} -->