Initial commit: logic gate simulator
This commit is contained in:
580
index.html
Normal file
580
index.html
Normal file
@@ -0,0 +1,580 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Logic Gates — MontLab</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: #0a0a0f;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
#toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
background: #12121a;
|
||||
border-bottom: 1px solid #2a2a3a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
z-index: 100;
|
||||
}
|
||||
#toolbar .logo {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: #00e599;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.gate-btn {
|
||||
padding: 6px 14px;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a3a;
|
||||
border-radius: 6px;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.gate-btn:hover { background: #252540; border-color: #00e599; color: #fff; }
|
||||
.gate-btn.input-btn { border-color: #3388ff; }
|
||||
.gate-btn.input-btn:hover { border-color: #55aaff; }
|
||||
.gate-btn.output-btn { border-color: #ff8833; }
|
||||
.gate-btn.output-btn:hover { border-color: #ffaa55; }
|
||||
.separator { width: 1px; height: 28px; background: #2a2a3a; margin: 0 8px; }
|
||||
.toolbar-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.action-btn:hover { border-color: #ff4444; color: #ff4444; }
|
||||
.action-btn.help-btn:hover { border-color: #00e599; color: #00e599; }
|
||||
|
||||
/* Canvas */
|
||||
#canvas {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Help modal */
|
||||
#help-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 200;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#help-modal.visible { display: flex; }
|
||||
#help-content {
|
||||
background: #12121a;
|
||||
border: 1px solid #2a2a3a;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
max-width: 520px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#help-content h2 { color: #00e599; margin-bottom: 16px; }
|
||||
#help-content p, #help-content li {
|
||||
color: #aaa; font-size: 14px; line-height: 1.6; margin-bottom: 8px;
|
||||
}
|
||||
#help-content ul { padding-left: 20px; }
|
||||
#help-content kbd {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
}
|
||||
#help-close {
|
||||
margin-top: 16px;
|
||||
padding: 8px 20px;
|
||||
background: #00e599;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="toolbar">
|
||||
<span class="logo">⚡ Logic Lab</span>
|
||||
<button class="gate-btn input-btn" data-gate="INPUT">INPUT</button>
|
||||
<button class="gate-btn output-btn" data-gate="OUTPUT">OUTPUT</button>
|
||||
<div class="separator"></div>
|
||||
<button class="gate-btn" data-gate="AND">AND</button>
|
||||
<button class="gate-btn" data-gate="OR">OR</button>
|
||||
<button class="gate-btn" data-gate="NOT">NOT</button>
|
||||
<button class="gate-btn" data-gate="NAND">NAND</button>
|
||||
<button class="gate-btn" data-gate="NOR">NOR</button>
|
||||
<button class="gate-btn" data-gate="XOR">XOR</button>
|
||||
<div class="toolbar-right">
|
||||
<button class="action-btn help-btn" id="help-btn">? Help</button>
|
||||
<button class="action-btn" id="clear-btn">Clear All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div id="help-modal">
|
||||
<div id="help-content">
|
||||
<h2>Logic Gate Simulator</h2>
|
||||
<ul>
|
||||
<li>Click a gate button in the toolbar, then click on the canvas to place it</li>
|
||||
<li>Drag gates to move them around</li>
|
||||
<li>Click on an <strong>output port</strong> (right side), then click an <strong>input port</strong> (left side) to connect them</li>
|
||||
<li>Click on an <kbd>INPUT</kbd> gate to toggle its value (0/1)</li>
|
||||
<li>Press <kbd>Delete</kbd> or <kbd>Backspace</kbd> while hovering a gate to delete it</li>
|
||||
<li>Right-click a connection to delete it</li>
|
||||
<li>The circuit evaluates in real-time</li>
|
||||
</ul>
|
||||
<p style="margin-top: 16px; color: #666;">Built with ❤️ at MontLab</p>
|
||||
<button id="help-close">Got it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// State
|
||||
let gates = [];
|
||||
let connections = [];
|
||||
let placingGate = null;
|
||||
let dragging = null;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
let connecting = null; // { gate, portIndex, portType }
|
||||
let hoveredGate = null;
|
||||
let hoveredPort = null;
|
||||
let mouseX = 0, mouseY = 0;
|
||||
let nextId = 1;
|
||||
|
||||
// Gate definitions
|
||||
const GATE_W = 100, GATE_H = 60;
|
||||
const PORT_R = 7;
|
||||
const GATE_COLORS = {
|
||||
AND: '#00e599', OR: '#3388ff', NOT: '#ff6644',
|
||||
NAND: '#e5cc00', NOR: '#cc44ff', XOR: '#ff44aa',
|
||||
INPUT: '#3388ff', OUTPUT: '#ff8833'
|
||||
};
|
||||
|
||||
function gateInputCount(type) {
|
||||
if (type === 'NOT' || type === 'INPUT' || type === 'OUTPUT') return type === 'INPUT' ? 0 : 1;
|
||||
return 2;
|
||||
}
|
||||
function gateOutputCount(type) {
|
||||
return type === 'OUTPUT' ? 0 : 1;
|
||||
}
|
||||
|
||||
function evaluate(gate, visited = new Set()) {
|
||||
if (visited.has(gate.id)) return gate.value || 0;
|
||||
visited.add(gate.id);
|
||||
|
||||
if (gate.type === 'INPUT') return gate.value;
|
||||
|
||||
// Get input values
|
||||
const inputCount = gateInputCount(gate.type);
|
||||
const inputs = [];
|
||||
for (let i = 0; i < inputCount; i++) {
|
||||
const conn = connections.find(c => c.to === gate.id && c.toPort === i);
|
||||
if (conn) {
|
||||
const srcGate = gates.find(g => g.id === conn.from);
|
||||
inputs.push(srcGate ? evaluate(srcGate, visited) : 0);
|
||||
} else {
|
||||
inputs.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
switch (gate.type) {
|
||||
case 'AND': result = (inputs[0] && inputs[1]) ? 1 : 0; break;
|
||||
case 'OR': result = (inputs[0] || inputs[1]) ? 1 : 0; break;
|
||||
case 'NOT': result = inputs[0] ? 0 : 1; break;
|
||||
case 'NAND': result = (inputs[0] && inputs[1]) ? 0 : 1; break;
|
||||
case 'NOR': result = (inputs[0] || inputs[1]) ? 0 : 1; break;
|
||||
case 'XOR': result = (inputs[0] !== inputs[1]) ? 1 : 0; break;
|
||||
case 'OUTPUT': result = inputs[0] || 0; break;
|
||||
}
|
||||
gate.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateAll() {
|
||||
gates.forEach(g => { if (g.type !== 'INPUT') g.value = 0; });
|
||||
gates.forEach(g => evaluate(g));
|
||||
}
|
||||
|
||||
function getInputPorts(gate) {
|
||||
const count = gateInputCount(gate.type);
|
||||
const ports = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const spacing = GATE_H / (count + 1);
|
||||
ports.push({ x: gate.x, y: gate.y + spacing * (i + 1), index: i, type: 'input' });
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
function getOutputPorts(gate) {
|
||||
const count = gateOutputCount(gate.type);
|
||||
const ports = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
ports.push({ x: gate.x + GATE_W, y: gate.y + GATE_H / 2, index: i, type: 'output' });
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight - 56;
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
function drawGate(gate) {
|
||||
const color = GATE_COLORS[gate.type];
|
||||
const isHovered = hoveredGate === gate;
|
||||
const isActive = gate.value === 1;
|
||||
|
||||
// Shadow for active gates
|
||||
if (isActive) {
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 20;
|
||||
}
|
||||
|
||||
// Body
|
||||
ctx.fillStyle = isActive ? color + '22' : '#14141e';
|
||||
ctx.strokeStyle = isHovered ? '#fff' : color;
|
||||
ctx.lineWidth = isHovered ? 2.5 : 1.5;
|
||||
|
||||
const r = 8;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(gate.x, gate.y, GATE_W, GATE_H, r);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = isActive ? '#fff' : color;
|
||||
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(gate.type, gate.x + GATE_W / 2, gate.y + GATE_H / 2 - (gate.type === 'INPUT' || gate.type === 'OUTPUT' ? 8 : 0));
|
||||
|
||||
// Value for INPUT/OUTPUT
|
||||
if (gate.type === 'INPUT' || gate.type === 'OUTPUT') {
|
||||
ctx.font = 'bold 18px monospace';
|
||||
ctx.fillStyle = gate.value ? '#00ff88' : '#ff4444';
|
||||
ctx.fillText(gate.value ? '1' : '0', gate.x + GATE_W / 2, gate.y + GATE_H / 2 + 12);
|
||||
}
|
||||
|
||||
// Input ports
|
||||
getInputPorts(gate).forEach(p => {
|
||||
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'input';
|
||||
const conn = connections.find(c => c.to === gate.id && c.toPort === p.index);
|
||||
const portActive = conn ? gates.find(g => g.id === conn.from)?.value : 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isPortHovered ? '#fff' : (portActive ? '#00ff88' : '#1a1a2e');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// Output ports
|
||||
getOutputPorts(gate).forEach(p => {
|
||||
const isPortHovered = hoveredPort && hoveredPort.gate === gate && hoveredPort.index === p.index && hoveredPort.type === 'output';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
|
||||
ctx.fillStyle = isPortHovered ? '#fff' : (gate.value ? '#00ff88' : '#1a1a2e');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isPortHovered ? '#fff' : '#555';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
function drawConnection(conn) {
|
||||
const fromGate = gates.find(g => g.id === conn.from);
|
||||
const toGate = gates.find(g => g.id === conn.to);
|
||||
if (!fromGate || !toGate) return;
|
||||
|
||||
const fromPort = getOutputPorts(fromGate)[conn.fromPort];
|
||||
const toPort = getInputPorts(toGate)[conn.toPort];
|
||||
if (!fromPort || !toPort) return;
|
||||
|
||||
const active = fromGate.value === 1;
|
||||
const midX = (fromPort.x + toPort.x) / 2;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(fromPort.x, fromPort.y);
|
||||
ctx.bezierCurveTo(midX, fromPort.y, midX, toPort.y, toPort.x, toPort.y);
|
||||
ctx.strokeStyle = active ? '#00ff88' : '#333';
|
||||
ctx.lineWidth = active ? 2.5 : 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Glow effect for active wires
|
||||
if (active) {
|
||||
ctx.strokeStyle = '#00ff8844';
|
||||
ctx.lineWidth = 6;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Grid
|
||||
ctx.strokeStyle = '#111118';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < canvas.width; x += 40) {
|
||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < canvas.height; y += 40) {
|
||||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
||||
}
|
||||
|
||||
connections.forEach(drawConnection);
|
||||
gates.forEach(drawGate);
|
||||
|
||||
// Drawing connection wire
|
||||
if (connecting) {
|
||||
const gate = connecting.gate;
|
||||
const port = connecting.portType === 'output'
|
||||
? getOutputPorts(gate)[connecting.portIndex]
|
||||
: getInputPorts(gate)[connecting.portIndex];
|
||||
if (port) {
|
||||
const midX = (port.x + mouseX) / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(port.x, port.y);
|
||||
ctx.bezierCurveTo(midX, port.y, midX, mouseY, mouseX, mouseY);
|
||||
ctx.strokeStyle = '#00e59988';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([6, 4]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Placing ghost
|
||||
if (placingGate) {
|
||||
ctx.globalAlpha = 0.5;
|
||||
const ghost = { x: mouseX - GATE_W/2, y: mouseY - GATE_H/2, type: placingGate, value: 0, id: -1 };
|
||||
drawGate(ghost);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
function findGateAt(x, y) {
|
||||
return gates.find(g => x >= g.x && x <= g.x + GATE_W && y >= g.y && y <= g.y + GATE_H);
|
||||
}
|
||||
|
||||
function findPortAt(x, y) {
|
||||
for (const gate of gates) {
|
||||
for (const p of getInputPorts(gate)) {
|
||||
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) {
|
||||
return { gate, index: p.index, type: 'input' };
|
||||
}
|
||||
}
|
||||
for (const p of getOutputPorts(gate)) {
|
||||
if (Math.hypot(x - p.x, y - p.y) < PORT_R + 4) {
|
||||
return { gate, index: p.index, type: 'output' };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousemove', e => {
|
||||
mouseX = e.offsetX;
|
||||
mouseY = e.offsetY;
|
||||
hoveredPort = findPortAt(mouseX, mouseY);
|
||||
hoveredGate = hoveredPort ? hoveredPort.gate : findGateAt(mouseX, mouseY);
|
||||
|
||||
if (dragging) {
|
||||
dragging.x = mouseX - dragOffset.x;
|
||||
dragging.y = mouseY - dragOffset.y;
|
||||
evaluateAll();
|
||||
}
|
||||
|
||||
canvas.style.cursor = placingGate ? 'crosshair'
|
||||
: hoveredPort ? 'pointer'
|
||||
: hoveredGate ? 'grab'
|
||||
: 'default';
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
if (e.button !== 0) return;
|
||||
const x = e.offsetX, y = e.offsetY;
|
||||
|
||||
// Placing a gate
|
||||
if (placingGate) {
|
||||
gates.push({
|
||||
id: nextId++,
|
||||
type: placingGate,
|
||||
x: x - GATE_W / 2,
|
||||
y: y - GATE_H / 2,
|
||||
value: 0
|
||||
});
|
||||
evaluateAll();
|
||||
placingGate = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check port click
|
||||
const port = findPortAt(x, y);
|
||||
if (port) {
|
||||
if (connecting) {
|
||||
// Complete connection
|
||||
if (connecting.portType === 'output' && port.type === 'input') {
|
||||
// Remove existing connection to this input
|
||||
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
||||
connections.push({
|
||||
from: connecting.gate.id,
|
||||
fromPort: connecting.portIndex,
|
||||
to: port.gate.id,
|
||||
toPort: port.index
|
||||
});
|
||||
evaluateAll();
|
||||
} else if (connecting.portType === 'input' && port.type === 'output') {
|
||||
connections = connections.filter(c => !(c.to === connecting.gate.id && c.toPort === connecting.portIndex));
|
||||
connections.push({
|
||||
from: port.gate.id,
|
||||
fromPort: port.index,
|
||||
to: connecting.gate.id,
|
||||
toPort: connecting.portIndex
|
||||
});
|
||||
evaluateAll();
|
||||
}
|
||||
connecting = null;
|
||||
} else {
|
||||
connecting = { gate: port.gate, portIndex: port.index, portType: port.type };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel connecting
|
||||
if (connecting) {
|
||||
connecting = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle input
|
||||
const gate = findGateAt(x, y);
|
||||
if (gate && gate.type === 'INPUT') {
|
||||
gate.value = gate.value ? 0 : 1;
|
||||
evaluateAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start drag
|
||||
if (gate) {
|
||||
dragging = gate;
|
||||
dragOffset = { x: x - gate.x, y: y - gate.y };
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
dragging = null;
|
||||
});
|
||||
|
||||
// Right-click to delete connections
|
||||
canvas.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
const x = e.offsetX, y = e.offsetY;
|
||||
|
||||
// Check if clicking near a connection
|
||||
const port = findPortAt(x, y);
|
||||
if (port && port.type === 'input') {
|
||||
connections = connections.filter(c => !(c.to === port.gate.id && c.toPort === port.index));
|
||||
evaluateAll();
|
||||
return;
|
||||
}
|
||||
if (port && port.type === 'output') {
|
||||
connections = connections.filter(c => !(c.from === port.gate.id && c.fromPort === port.index));
|
||||
evaluateAll();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete key
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
if (hoveredGate && document.activeElement === document.body) {
|
||||
e.preventDefault();
|
||||
connections = connections.filter(c => c.from !== hoveredGate.id && c.to !== hoveredGate.id);
|
||||
gates = gates.filter(g => g !== hoveredGate);
|
||||
hoveredGate = null;
|
||||
evaluateAll();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
placingGate = null;
|
||||
connecting = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Toolbar buttons
|
||||
document.querySelectorAll('.gate-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
placingGate = btn.dataset.gate;
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('clear-btn').addEventListener('click', () => {
|
||||
if (gates.length === 0 || confirm('Clear all gates and connections?')) {
|
||||
gates = [];
|
||||
connections = [];
|
||||
connecting = null;
|
||||
placingGate = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('help-btn').addEventListener('click', () => {
|
||||
document.getElementById('help-modal').classList.add('visible');
|
||||
});
|
||||
document.getElementById('help-close').addEventListener('click', () => {
|
||||
document.getElementById('help-modal').classList.remove('visible');
|
||||
});
|
||||
document.getElementById('help-modal').addEventListener('click', e => {
|
||||
if (e.target === e.currentTarget) {
|
||||
document.getElementById('help-modal').classList.remove('visible');
|
||||
}
|
||||
});
|
||||
|
||||
// Start
|
||||
draw();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user