anycoder-cc45b938 / index.html
minhvtt's picture
Upload folder using huggingface_hub
ac2e2a9 verified
Raw
History Blame Contribute Delete
46.4 kB
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sơ Đồ Dòng Điện - Trình Mô Phỏng Tương Tác</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #0f172a;
color: #e2e8f0;
overflow-x: hidden;
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Glassmorphism */
.glass {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Neon Glows */
.glow-text {
text-shadow: 0 0 10px rgba(56, 189, 248, 0.5);
}
.glow-border {
box-shadow: 0 0 15px rgba(56, 189, 248, 0.2);
}
/* Canvas Container */
#canvas-container {
background-image:
radial-gradient(circle at 1px 1px, rgba(255,255,255,0.05) 1px, transparent 0);
background-size: 20px 20px;
cursor: crosshair;
}
/* Range Slider Styling */
input[type=range] {
-webkit-appearance: none;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #38bdf8;
cursor: pointer;
margin-top: -6px;
box-shadow: 0 0 10px #38bdf8;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #334155;
border-radius: 2px;
}
/* Animations */
@keyframes pulse-ring {
0% { transform: scale(0.8); opacity: 0.5; }
100% { transform: scale(1.2); opacity: 0; }
}
.animate-pulse-ring::before {
content: '';
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
border-radius: 50%;
border: 2px solid #38bdf8;
animation: pulse-ring 2s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
.component-btn {
transition: all 0.2s ease;
}
.component-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
border-color: #38bdf8;
}
.component-btn.active {
background: #38bdf8;
color: #0f172a;
border-color: #38bdf8;
}
</style>
</head>
<body class="h-screen flex flex-col">
<!-- Header -->
<header class="h-16 border-b border-slate-700 bg-slate-900/90 flex items-center justify-between px-6 z-20 relative">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-blue-500 rounded flex items-center justify-center shadow-[0_0_15px_rgba(59,130,246,0.5)]">
<i class="fa-solid fa-bolt text-white"></i>
</div>
<h1 class="text-xl font-bold tracking-tight text-white glow-text">Circuit<span class="text-blue-400">Sim</span> <span class="text-xs font-normal text-slate-400 ml-2 border border-slate-700 px-2 py-0.5 rounded">v2.0</span></h1>
</div>
<div class="hidden md:flex items-center gap-6 text-sm text-slate-400">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
<span>Hệ thống sẵn sàng</span>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="hover:text-blue-400 transition-colors text-xs border-b border-dashed border-slate-600 pb-0.5">
Built with anycoder
</a>
</div>
<div class="flex gap-3">
<button onclick="app.clearCanvas()" class="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-800 rounded transition-colors">
<i class="fa-solid fa-trash-can mr-2"></i>Xóa
</button>
<button onclick="app.toggleSimulation()" id="sim-btn" class="px-4 py-2 text-sm font-bold bg-blue-600 hover:bg-blue-500 text-white rounded shadow-[0_0_15px_rgba(37,99,235,0.4)] transition-all flex items-center gap-2">
<i class="fa-solid fa-play"></i> <span id="sim-text">Chạy Mô Phỏng</span>
</button>
</div>
</header>
<!-- Main Workspace -->
<div class="flex-1 flex overflow-hidden relative">
<!-- Left Sidebar: Components -->
<aside class="w-64 bg-slate-900 border-r border-slate-700 flex flex-col z-10">
<div class="p-4 border-b border-slate-800">
<h2 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Thư viện linh kiện</h2>
<p class="text-[10px] text-slate-600">Kéo thả hoặc click để thêm</p>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-3">
<!-- Component Items -->
<div class="component-btn group cursor-pointer bg-slate-800 p-3 rounded border border-slate-700 flex items-center gap-3" onclick="app.setTool('wire')">
<div class="w-8 h-8 rounded bg-slate-700 flex items-center justify-center group-hover:bg-slate-600">
<i class="fa-solid fa-minus text-slate-300"></i>
</div>
<div>
<div class="text-sm font-medium text-slate-200">Dây dẫn</div>
<div class="text-[10px] text-slate-500">Nối các linh kiện</div>
</div>
</div>
<div class="component-btn group cursor-pointer bg-slate-800 p-3 rounded border border-slate-700 flex items-center gap-3" onclick="app.setTool('resistor')">
<div class="w-8 h-8 rounded bg-slate-700 flex items-center justify-center group-hover:bg-slate-600">
<span class="font-serif font-bold text-slate-300">R</span>
</div>
<div>
<div class="text-sm font-medium text-slate-200">Điện trở</div>
<div class="text-[10px] text-slate-500">Cản trở dòng điện</div>
</div>
</div>
<div class="component-btn group cursor-pointer bg-slate-800 p-3 rounded border border-slate-700 flex items-center gap-3" onclick="app.setTool('battery')">
<div class="w-8 h-8 rounded bg-slate-700 flex items-center justify-center group-hover:bg-slate-600 text-yellow-400">
<i class="fa-solid fa-car-battery"></i>
</div>
<div>
<div class="text-sm font-medium text-slate-200">Nguồn điện</div>
<div class="text-[10px] text-slate-500">Cung cấp hiệu điện thế</div>
</div>
</div>
<div class="component-btn group cursor-pointer bg-slate-800 p-3 rounded border border-slate-700 flex items-center gap-3" onclick="app.setTool('bulb')">
<div class="w-8 h-8 rounded bg-slate-700 flex items-center justify-center group-hover:bg-slate-600 text-yellow-200">
<i class="fa-regular fa-lightbulb"></i>
</div>
<div>
<div class="text-sm font-medium text-slate-200">Bóng đèn</div>
<div class="text-[10px] text-slate-500">Tải tiêu thụ điện</div>
</div>
</div>
<div class="component-btn group cursor-pointer bg-slate-800 p-3 rounded border border-slate-700 flex items-center gap-3" onclick="app.setTool('switch')">
<div class="w-8 h-8 rounded bg-slate-700 flex items-center justify-center group-hover:bg-slate-600 text-green-400">
<i class="fa-solid fa-toggle-off"></i>
</div>
<div>
<div class="text-sm font-medium text-slate-200">Công tắc</div>
<div class="text-[10px] text-slate-500">Đóng/ngắt mạch</div>
</div>
</div>
</div>
<div class="p-4 border-t border-slate-800 text-[10px] text-slate-600 text-center">
Click để chọn công cụ<br>Double click linh kiện để xóa
</div>
</aside>
<!-- Center: Canvas -->
<main class="flex-1 relative bg-slate-950" id="canvas-wrapper">
<div id="canvas-container" class="w-full h-full relative">
<canvas id="circuitCanvas"></canvas>
<!-- Floating Info Overlay -->
<div class="absolute top-4 left-4 glass px-4 py-2 rounded text-xs text-slate-300 pointer-events-none select-none">
<div class="flex items-center gap-2 mb-1">
<div class="w-2 h-2 rounded-full bg-yellow-400"></div>
<span>Dòng điện chạy: + -> -</span>
</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
<span>Electron chạy: - -> +</span>
</div>
</div>
</div>
</main>
<!-- Right Sidebar: Properties -->
<aside class="w-72 bg-slate-900 border-l border-slate-700 flex flex-col z-10 transition-transform duration-300" id="properties-panel">
<div class="p-4 border-b border-slate-800">
<h2 class="text-xs font-bold text-slate-500 uppercase tracking-wider">Thông số kỹ thuật</h2>
</div>
<div id="empty-state" class="flex-1 flex flex-col items-center justify-center text-slate-600 p-6 text-center">
<i class="fa-solid fa-arrow-pointer text-3xl mb-3 opacity-50"></i>
<p class="text-sm">Chọn một linh kiện trên sơ đồ để chỉnh sửa thông số</p>
</div>
<div id="properties-content" class="hidden flex-1 overflow-y-auto p-4 space-y-6">
<!-- Dynamic Content Injected Here -->
<div>
<label class="block text-xs font-medium text-slate-400 mb-2">Loại linh kiện</label>
<div id="prop-type" class="text-lg font-bold text-white capitalize">Resistor</div>
</div>
<div id="prop-control-resistance" class="space-y-2">
<div class="flex justify-between text-xs text-slate-400">
<label>Điện trở (R)</label>
<span id="val-resistance" class="mono text-blue-400">100 Ω</span>
</div>
<input type="range" min="10" max="1000" step="10" id="input-resistance" class="w-full">
</div>
<div id="prop-control-voltage" class="space-y-2">
<div class="flex justify-between text-xs text-slate-400">
<label>Hiệu điện thế (V)</label>
<span id="val-voltage" class="mono text-yellow-400">9 V</span>
</div>
<input type="range" min="1" max="24" step="1" id="input-voltage" class="w-full">
</div>
<div class="p-3 bg-slate-800/50 rounded border border-slate-700/50">
<h3 class="text-[10px] uppercase text-slate-500 font-bold mb-2">Phân tích mạch</h3>
<div class="space-y-2">
<div class="flex justify-between text-xs">
<span class="text-slate-400">Cường độ dòng điện (I):</span>
<span id="analysis-current" class="mono text-white">0.00 A</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-slate-400">Công suất (P):</span>
<span id="analysis-power" class="mono text-white">0.00 W</span>
</div>
</div>
</div>
<div class="pt-4 border-t border-slate-800">
<button onclick="app.deleteSelected()" class="w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded text-xs transition-colors">
Xóa linh kiện này
</button>
</div>
</div>
</aside>
</div>
<script>
/**
* Circuit Simulator Logic
* Implements a simple grid-based circuit editor with visual simulation of current flow.
*/
class CircuitApp {
constructor() {
this.canvas = document.getElementById('circuitCanvas');
this.ctx = this.canvas.getContext('2d');
this.container = document.getElementById('canvas-container');
// State
this.components = [];
this.wires = []; // Connections between components
this.selectedTool = 'wire'; // wire, resistor, battery, bulb, switch
this.isSimulating = false;
this.selectedComponent = null;
this.draggedComponent = null;
this.hoveredComponent = null;
// Interaction State
this.isDrawingWire = false;
this.wireStartNode = null;
this.mousePos = { x: 0, y: 0 };
// Animation
this.electrons = []; // Array of electron particles
this.animationFrame = null;
// Constants
this.gridSize = 20;
this.componentSize = 40;
this.init();
}
init() {
this.resize();
window.addEventListener('resize', () => this.resize());
// Mouse Events
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.canvas.addEventListener('mouseup', (e) => this.handleMouseUp(e));
this.canvas.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
// UI Bindings
this.bindPropertiesPanel();
// Start Loop
this.loop();
}
resize() {
this.canvas.width = this.container.clientWidth;
this.canvas.height = this.container.clientHeight;
}
// --- Core Logic: Components & Wires ---
addComponent(type, x, y) {
// Snap to grid
const snapX = Math.round(x / this.gridSize) * this.gridSize;
const snapY = Math.round(y / this.gridSize) * this.gridSize;
const comp = {
id: Date.now(),
type: type,
x: snapX,
y: snapY,
rotation: 0, // 0, 1, 2, 3 (x90 deg)
value: type === 'resistor' ? 100 : (type === 'battery' ? 9 : 0),
state: type === 'switch' ? false : true, // false = open, true = closed
nodes: this.calculateNodes(snapX, snapY, 0)
};
this.components.push(comp);
this.selectComponent(comp);
}
calculateNodes(x, y, rotation) {
// Define connection points relative to center
// 0 deg: Left (-1,0) and Right (1,0)
const offset = this.componentSize / 2;
return [
{ x: x - offset, y: y, id: `n-${x}-${y}-l` }, // Left
{ x: x + offset, y: y, id: `n-${x}-${y}-r` } // Right
];
}
getComponentAt(x, y) {
// Check wires first (line segment check)
for (let w of this.wires) {
const dist = this.pointToLineDistance(x, y, w.x1, w.y1, w.x2, w.y2);
if (dist < 5) return { type: 'wire', obj: w };
}
// Check components
const size = this.componentSize / 2 + 5; // tolerance
return this.components.find(c =>
x >= c.x - size && x <= c.x + size &&
y >= c.y - size && y <= c.y + size
);
}
pointToLineDistance(x, y, x1, y1, x2, y2) {
const A = x - x1;
const B = y - y1;
const C = x2 - x1;
const D = y2 - y1;
const dot = A * C + B * D;
const len_sq = C * C + D * D;
let param = -1;
if (len_sq !== 0) param = dot / len_sq;
let xx, yy;
if (param < 0) {
xx = x1; yy = y1;
} else if (param > 1) {
xx = x2; yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
const dx = x - xx;
const dy = y - yy;
return Math.sqrt(dx * dx + dy * dy);
}
// --- Interaction Handlers ---
handleMouseDown(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const clicked = this.getComponentAt(x, y);
if (this.selectedTool === 'wire') {
if (clicked && clicked.type !== 'wire') {
// Start wire from component node
const node = this.getClosestNode(clicked.obj, x, y);
this.isDrawingWire = true;
this.wireStartNode = { x: node.x, y: node.y, comp: clicked.obj, nodeIndex: clicked.obj.nodes.indexOf(node) };
} else if (!clicked) {
// Start wire from empty space (less common but supported)
this.isDrawingWire = true;
this.wireStartNode = { x: Math.round(x/10)*10, y: Math.round(y/10)*10, comp: null };
}
} else {
// Placing components or selecting
if (!clicked) {
if (this.selectedTool !== 'wire') {
this.addComponent(this.selectedTool, x, y);
} else {
this.selectComponent(null);
}
} else if (clicked.type !== 'wire') {
this.draggedComponent = clicked.obj;
this.selectComponent(clicked.obj);
}
}
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
this.mousePos.x = e.clientX - rect.left;
this.mousePos.y = e.clientY - rect.top;
if (this.draggedComponent) {
const snapX = Math.round(this.mousePos.x / this.gridSize) * this.gridSize;
const snapY = Math.round(this.mousePos.y / this.gridSize) * this.gridSize;
this.draggedComponent.x = snapX;
this.draggedComponent.y = snapY;
this.draggedComponent.nodes = this.calculateNodes(snapX, snapY, 0);
// Update connected wires
this.wires.forEach(w => {
if (w.from.comp === this.draggedComponent) {
const node = this.draggedComponent.nodes[w.from.nodeIndex];
w.x1 = node.x; w.y1 = node.y;
}
if (w.to.comp === this.draggedComponent) {
const node = this.draggedComponent.nodes[w.to.nodeIndex];
w.x2 = node.x; w.y2 = node.y;
}
});
}
// Hover effect
const hovered = this.getComponentAt(this.mousePos.x, this.mousePos.y);
this.hoveredComponent = hovered ? hovered.obj : null;
}
handleMouseUp(e) {
if (this.isDrawingWire && this.wireStartNode) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const target = this.getComponentAt(x, y);
if (target && target.type !== 'wire' && target.obj !== this.wireStartNode.comp) {
// Connect to component
const node = this.getClosestNode(target.obj, x, y);
this.wires.push({
x1: this.wireStartNode.x, y1: this.wireStartNode.y,
x2: node.x, y2: node.y,
from: this.wireStartNode,
to: { x: node.x, y: node.y, comp: target.obj, nodeIndex: target.obj.nodes.indexOf(node) }
});
} else if (!target) {
// Connect to empty space (junction)
this.wires.push({
x1: this.wireStartNode.x, y1: this.wireStartNode.y,
x2: Math.round(x/10)*10, y2: Math.round(y/10)*10,
from: this.wireStartNode,
to: null // Floating end? No, let's just snap to grid for now
});
}
this.isDrawingWire = false;
this.wireStartNode = null;
}
this.draggedComponent = null;
}
handleDoubleClick(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const clicked = this.getComponentAt(x, y);
if (clicked) {
if (clicked.type === 'wire') {
this.wires = this.wires.filter(w => w !== clicked.obj);
} else {
// Toggle switch
if (clicked.obj.type === 'switch') {
clicked.obj.state = !clicked.obj.state;
}
// Delete others
else {
this.components = this.components.filter(c => c !== clicked.obj);
// Remove attached wires
this.wires = this.wires.filter(w => w.from.comp !== clicked.obj && w.to.comp !== clicked.obj);
if (this.selectedComponent === clicked.obj) this.selectComponent(null);
}
}
}
}
getClosestNode(comp, x, y) {
// Simple distance check to nodes
let closest = comp.nodes[0];
let minDist = Infinity;
comp.nodes.forEach(n => {
const d = Math.hypot(n.x - x, n.y - y);
if (d < minDist) {
minDist = d;
closest = n;
}
});
return closest;
}
// --- Simulation Logic ---
toggleSimulation() {
this.isSimulating = !this.isSimulating;
const btn = document.getElementById('sim-btn');
const txt = document.getElementById('sim-text');
const icon = btn.querySelector('i');
if (this.isSimulating) {
btn.classList.replace('bg-blue-600', 'bg-red-600');
btn.classList.replace('hover:bg-blue-500', 'hover:bg-red-500');
txt.innerText = "Dừng Mô Phỏng";
icon.classList.replace('fa-play', 'fa-stop');
this.startSimulation();
} else {
btn.classList.replace('bg-red-600', 'bg-blue-600');
btn.classList.replace('hover:bg-red-500', 'hover:bg-blue-500');
txt.innerText = "Chạy Mô Phỏng";
icon.classList.replace('fa-stop', 'fa-play');
this.electrons = [];
}
}
startSimulation() {
// 1. Find Battery
const battery = this.components.find(c => c.type === 'battery');
if (!battery) return;
// 2. Trace Path (Simplified: assume single loop for this demo)
// In a real app, we'd use MNA or Nodal Analysis.
// Here we find paths from battery + to battery -.
const path = this.findPath(battery, 0); // Start at node 0 (left)
if (path) {
this.currentPath = path;
// Calculate Total Resistance
let totalR = 0;
path.forEach(item => {
if (item.comp.type === 'resistor') totalR += item.comp.value;
if (item.comp.type === 'bulb') totalR += 50; // Fixed bulb resistance
if (item.comp.type === 'switch' && !item.comp.state) totalR += 999999; // Open switch
});
const current = battery.value / totalR;
this.circuitCurrent = current; // Amps
// Spawn electrons based on current
// We visualize electrons moving along the path
this.spawnElectrons(path, current);
} else {
this.circuitCurrent = 0;
}
}
findPath(startComp, startNodeIndex) {
// DFS to find loop back to battery
// This is a very basic pathfinder for visual demo purposes
// It assumes a simple series circuit layout.
let path = [];
let visited = new Set();
const traverse = (comp, entryNodeIdx) => {
if (visited.has(comp.id)) return false; // Loop detected or dead end
visited.add(comp.id);
// Determine exit node (the other node)
const exitNodeIdx = entryNodeIdx === 0 ? 1 : 0;
path.push({ comp: comp, entry: entryNodeIdx, exit: exitNodeIdx });
if (comp.type === 'battery' && path.length > 1) {
return true; // Completed loop
}
// Find wire connected to exit node
const exitNode = comp.nodes[exitNodeIdx];
const wire = this.wires.find(w =>
(w.x1 === exitNode.x && w.y1 === exitNode.y) ||
(w.x2 === exitNode.x && w.y2 === exitNode.y)
);
if (!wire) return false;
// Find next component connected to other end of wire
const otherEnd = (wire.x1 === exitNode.x && wire.y1 === exitNode.y) ?
{ x: wire.x2, y: wire.y2 } : { x: wire.x1, y: wire.y1 };
// Find component at other end
// Check wires connected to this point
const nextWire = this.wires.find(w =>
(w.x1 === otherEnd.x && w.y1 === otherEnd.y && w !== wire) ||
(w.x2 === otherEnd.x && w.y2 === otherEnd.y && w !== wire)
);
// Find component connected to this wire
let nextComp = null;
let nextNodeIdx = -1;
// Check all components
for (let c of this.components) {
if (c === comp) continue;
c.nodes.forEach((n, idx) => {
if (n.x === otherEnd.x && n.y === otherEnd.y) {
nextComp = c;
nextNodeIdx = idx;
}
});
}
if (nextComp) {
return traverse(nextComp, nextNodeIdx);
}
return false;
};
const found = traverse(startComp, startNodeIndex);
return found ? path : null;
}
spawnElectrons(path, current) {
this.electrons = [];
// Create segments from path
this.pathSegments = [];
for (let i = 0; i < path.length; i++) {
const item = path[i];
const nextItem = path[(i + 1) % path.length];
// Segment 1: Inside component (Entry to Exit)
// Segment 2: Wire (Exit to Next Entry)
// For visual simplicity, we treat the whole path as a continuous line
// We need coordinates.
const startNode = item.comp.nodes[item.entry];
const endNode = item.comp.nodes[item.exit];
// Add wire to next component
const wire = this.wires.find(w =>
(w.x1 === endNode.x && w.y1 === endNode.y) ||
(w.x2 === endNode.x && w.y2 === endNode.y)
);
if (wire) {
const wireStart = (wire.x1 === endNode.x && wire.y1 === endNode.y) ?
{x: wire.x1, y: wire.y1} : {x: wire.x2, y: wire.y2};
const wireEnd = (wire.x1 === endNode.x && wire.y1 === endNode.y) ?
{x: wire.x2, y: wire.y2} : {x: wire.x1, y: wire.y1};
this.pathSegments.push({
from: {x: startNode.x, y: startNode.y},
to: {x: endNode.x, y: endNode.y},
type: 'comp',
comp: item.comp
});
this.pathSegments.push({
from: {x: wireStart.x, y: wireStart.y},
to: {x: wireEnd.x, y: wireEnd.y},
type: 'wire'
});
}
}
// Spawn initial electrons
const speed = Math.min(Math.max(current * 2, 0.5), 5); // Visual speed cap
for(let i=0; i<20; i++) {
this.electrons.push({
segmentIndex: 0,
progress: i / 20, // 0 to 1 along segment
speed: speed
});
}
}
updateElectrons() {
if (!this.isSimulating || !this.pathSegments) return;
// Check if circuit is broken (switch open)
const broken = this.currentPath && this.currentPath.some(p => p.comp.type === 'switch' && !p.comp.state);
if (broken) {
this.electrons = []; // Stop electrons
return;
}
this.electrons.forEach(e => {
e.progress += 0.005 * e.speed;
if (e.progress >= 1) {
e.progress = 0;
e.segmentIndex = (e.segmentIndex + 1) % this.pathSegments.length;
}
});
}
// --- Rendering ---
loop() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawGrid();
this.drawWires();
this.drawComponents();
this.drawUIOverlay();
if (this.isSimulating) {
this.updateElectrons();
this.drawElectrons();
}
requestAnimationFrame(() => this.loop());
}
drawGrid() {
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
this.ctx.lineWidth = 1;
this.ctx.beginPath();
for (let x = 0; x < this.canvas.width; x += this.gridSize) {
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, this.canvas.height);
}
for (let y = 0; y < this.canvas.height; y += this.gridSize) {
this.ctx.moveTo(0, y);
this.ctx.lineTo(this.canvas.width, y);
}
this.ctx.stroke();
}
drawWires() {
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.wires.forEach(w => {
this.ctx.beginPath();
this.ctx.moveTo(w.x1, w.y1);
this.ctx.lineTo(w.x2, w.y2);
// Style
if (this.hoveredComponent === w) {
this.ctx.strokeStyle = '#38bdf8';
this.ctx.lineWidth = 4;
this.ctx.shadowBlur = 10;
this.ctx.shadowColor = '#38bdf8';
} else {
this.ctx.strokeStyle = '#64748b';
this.ctx.lineWidth = 3;
this.ctx.shadowBlur = 0;
}
this.ctx.stroke();
this.ctx.shadowBlur = 0; // Reset
// Draw junction dots
this.drawJunction(w.x1, w.y1);
this.drawJunction(w.x2, w.y2);
});
// Drawing line while dragging
if (this.isDrawingWire && this.wireStartNode) {
this.ctx.beginPath();
this.ctx.moveTo(this.wireStartNode.x, this.wireStartNode.y);
// Orthogonal routing hint
const midX = (this.wireStartNode.x + this.mousePos.x) / 2;
this.ctx.lineTo(midX, this.wireStartNode.y);
this.ctx.lineTo(midX, this.mousePos.y);
this.ctx.lineTo(this.mousePos.x, this.mousePos.y);
this.ctx.strokeStyle = '#38bdf8';
this.ctx.lineWidth = 2;
this.ctx.setLineDash([5, 5]);
this.ctx.stroke();
this.ctx.setLineDash([]);
}
}
drawJunction(x, y) {
this.ctx.fillStyle = '#94a3b8';
this.ctx.beginPath();
this.ctx.arc(x, y, 3, 0, Math.PI * 2);
this.ctx.fill();
}
drawComponents() {
this.components.forEach(c => {
this.ctx.save();
this.ctx.translate(c.x, c.y);
// Highlight selected
if (this.selectedComponent === c) {
this.ctx.shadowColor = '#38bdf8';
this.ctx.shadowBlur = 15;
} else if (this.hoveredComponent === c) {
this.ctx.shadowColor = 'rgba(255,255,255,0.3)';
this.ctx.shadowBlur = 10;
}
// Draw Component Body
this.ctx.fillStyle = '#1e293b';
this.ctx.strokeStyle = this.selectedComponent === c ? '#38bdf8' : '#94a3b8';
this.ctx.lineWidth = 2;
// Rotation
// this.ctx.rotate(c.rotation * Math.PI / 2);
// Draw based on type
const s = this.componentSize / 2; // half size
// Connection points (visual only, logic handled by nodes)
this.ctx.fillStyle = '#94a3b8';
this.ctx.beginPath(); this.ctx.arc(-s, 0, 4, 0, Math.PI*2); this.ctx.fill();
this.ctx.beginPath(); this.ctx.arc(s, 0, 4, 0, Math.PI*2); this.ctx.fill();
this.ctx.beginPath();
if (c.type === 'resistor') {
// Zigzag
this.ctx.moveTo(-s, 0);
this.ctx.lineTo(-s + 5, 0);
this.ctx.lineTo(-s + 10, -8);
this.ctx.lineTo(-s + 20, 8);
this.ctx.lineTo(-s + 30, -8);
this.ctx.lineTo(-s + 40, 8);
this.ctx.lineTo(s - 5, 0);
this.ctx.lineTo(s, 0);
this.ctx.stroke();
// Text
this.ctx.fillStyle = '#fff';
this.ctx.font = '10px Inter';
this.ctx.textAlign = 'center';
this.ctx.fillText(`${c.value}Ω`, 0, -12);
} else if (c.type === 'battery') {
// Long and short line
// Left (Neg)
this.ctx.moveTo(-s, 0); this.ctx.lineTo(-10, 0);
this.ctx.moveTo(-10, -15); this.ctx.lineTo(-10, 15); // Long
// Right (Pos)
this.ctx.moveTo(10, -8); this.ctx.lineTo(10, 8); // Short
this.ctx.moveTo(10, 0); this.ctx.lineTo(s, 0);
this.ctx.stroke();
// Signs
this.ctx.fillStyle = '#38bdf8';
this.ctx.font = 'bold 14px sans-serif';
this.ctx.fillText('-', -18, 4);
this.ctx.fillStyle = '#ef4444';
this.ctx.fillText('+', 18, 4);
this.ctx.fillStyle = '#fbbf24';
this.ctx.font = '10px Inter';
this.ctx.fillText(`${c.value}V`, 0, 25);
} else if (c.type === 'bulb') {
// Circle
this.ctx.beginPath();
this.ctx.arc(0, 0, 15, 0, Math.PI * 2);
this.ctx.fillStyle = '#334155';
this.ctx.fill();
this.ctx.stroke();
// Cross
this.ctx.beginPath();
this.ctx.moveTo(-8, -8); this.ctx.lineTo(8, 8);
this.ctx.moveTo(8, -8); this.ctx.lineTo(-8, 8);
this.ctx.stroke();
// Glow if simulating and current flowing
if (this.isSimulating && this.circuitCurrent > 0) {
const alpha = Math.min(this.circuitCurrent / 0.5, 1);
this.ctx.shadowColor = `rgba(250, 204, 21, ${alpha})`;
this.ctx.shadowBlur = 20 + (alpha * 20);
this.ctx.fillStyle = `rgba(250, 204, 21, ${alpha * 0.5})`;
this.ctx.beginPath();
this.ctx.arc(0, 0, 12, 0, Math.PI*2);
this.ctx.fill();
}
} else if (c.type === 'switch') {
// Line with break
this.ctx.moveTo(-s, 0); this.ctx.lineTo(-10, 0);
this.ctx.moveTo(10, 0); this.ctx.lineTo(s, 0);
this.ctx.stroke();
// Lever
this.ctx.beginPath();
if (c.state) { // Closed
this.ctx.moveTo(-10, 0);
this.ctx.lineTo(10, 0);
this.ctx.strokeStyle = '#4ade80';
} else { // Open
this.ctx.moveTo(-10, 0);
this.ctx.lineTo(8, -15);
}
this.ctx.stroke();
// Dots
this.ctx.fillStyle = c.state ? '#4ade80' : '#94a3b8';
this.ctx.beginPath(); this.ctx.arc(-10, 0, 3, 0, Math.PI*2); this.ctx.fill();
this.ctx.beginPath(); this.ctx.arc(10, 0, 3, 0, Math.PI*2); this.ctx.fill();
}
this.ctx.restore();
});
}
drawElectrons() {
if (!this.pathSegments) return;
this.ctx.fillStyle = '#facc15'; // Electron yellow
this.ctx.shadowColor = '#facc15';
this.ctx.shadowBlur = 5;
this.electrons.forEach(e => {
const seg = this.pathSegments[e.segmentIndex];
if (!seg) return;
const x = seg.from.x + (seg.to.x - seg.from.x) * e.progress;
const y = seg.from.y + (seg.to.y - seg.from.y) * e.progress;
this.ctx.beginPath();
this.ctx.arc(x, y, 3, 0, Math.PI * 2);
this.ctx.fill();
});
this.ctx.shadowBlur = 0;
}
drawUIOverlay() {
// Tooltip for hover
if (this.hoveredComponent && typeof this.hoveredComponent === 'object') {
// Handled by cursor mostly, but could add tooltips here
}
}
// --- UI Logic ---
setTool(tool) {
this.selectedTool = tool;
// Update UI classes
document.querySelectorAll('.component-btn').forEach(btn => btn.classList.remove('active'));
// Find button with onclick containing tool name (simple hack)
const btns = document.querySelectorAll('.component-btn');
if(tool === 'wire') btns[0].classList.add('active');
if(tool === 'resistor') btns[1].classList.add('active');
if(tool === 'battery') btns[2].classList.add('active');
if(tool === 'bulb') btns[3].classList.add('active');
if(tool === 'switch') btns[4].classList.add('active');
}
selectComponent(comp) {
this.selectedComponent = comp;
const panel = document.getElementById('properties-panel');
const empty = document.getElementById('empty-state');
const content = document.getElementById('properties-content');
if (!comp) {
empty.classList.remove('hidden');
content.classList.add('hidden');
return;
}
empty.classList.add('hidden');
content.classList.remove('hidden');
// Populate fields
document.getElementById('prop-type').innerText = comp.type;
// Show/Hide relevant controls
const rControl = document.getElementById('prop-control-resistance');
const vControl = document.getElementById('prop-control-voltage');
if (comp.type === 'resistor') {
rControl.classList.remove('hidden');
vControl.classList.add('hidden');
document.getElementById('input-resistance').value = comp.value;
document.getElementById('val-resistance').innerText = comp.value + ' Ω';
} else if (comp.type === 'battery') {
rControl.classList.add('hidden');
vControl.classList.remove('hidden');
document.getElementById('input-voltage').value = comp.value;
document.getElementById('val-voltage').innerText = comp.value + ' V';
} else {