|
|
@@ -352,6 +352,7 @@
|
|
|
#inductionCanvas { cursor: grab; touch-action: none; }
|
|
|
#inductionCanvas.dragging { cursor: grabbing; }
|
|
|
#inductiveViz { cursor: default; touch-action: none; }
|
|
|
+ #capacitiveViz { cursor: default; touch-action: none; }
|
|
|
.hint { font-size: 12px; color: var(--ink-light); font-style: italic; margin-top: 8px; }
|
|
|
@media (max-width: 640px) {
|
|
|
body { padding: 10px; }
|
|
|
@@ -579,15 +580,15 @@
|
|
|
<div class="scene" data-scene="9">
|
|
|
<div class="scene-num">Механизм 2 · ёмкостный</div>
|
|
|
<h2 class="scene-title">Ёмкостная наводка</h2>
|
|
|
- <p>Источник — провод с переменным напряжением. Среда — электрическое поле. Приёмник — провод или плата, на которой наводится заряд. Два провода рядом образуют паразитный конденсатор.</p>
|
|
|
+ <p>Источник — провод с переменным напряжением. Среда — электрическое поле. Приёмник — провод или плата. Меняйте напряжение, частоту, расстояние и площадь. Провод можно перетаскивать мышью, углы приёмника — тянуть и менять площадь.</p>
|
|
|
<div class="viz">
|
|
|
- <canvas id="capacitiveViz" width="800" height="240"></canvas>
|
|
|
- <div class="viz-caption">Электрическое поле между проводами</div>
|
|
|
+ <canvas id="capacitiveViz" width="800" height="440"></canvas>
|
|
|
+ <div class="viz-caption">Плотность линий = напряжённость поля E · мерцание = частота · стрелка на приёмнике = наведённый заряд</div>
|
|
|
</div>
|
|
|
<div class="two-col">
|
|
|
<div>
|
|
|
<div class="control-group">
|
|
|
- <div class="control-label"><span>Напряжение, В</span><span class="val" id="capVoltVal">500</span></div>
|
|
|
+ <div class="control-label"><span>Напряжение источника, В</span><span class="val" id="capVoltVal">500</span></div>
|
|
|
<input type="range" id="capVolt" min="0" max="1000" step="50" value="500">
|
|
|
</div>
|
|
|
<div class="control-group">
|
|
|
@@ -601,19 +602,19 @@
|
|
|
<input type="range" id="capDist" min="1" max="20" step="1" value="10">
|
|
|
</div>
|
|
|
<div class="control-group">
|
|
|
- <div class="control-label"><span>Площадь, см²</span><span class="val" id="capAreaVal">50</span></div>
|
|
|
+ <div class="control-label"><span>Площадь приёмника, см²</span><span class="val" id="capAreaVal">50</span></div>
|
|
|
<input type="range" id="capArea" min="0" max="100" step="5" value="50">
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="indicator">
|
|
|
- <small>Наведённое напряжение</small>
|
|
|
+ <small>Мгновенное наведённое напряжение</small>
|
|
|
<span><span id="capResult">0.0</span> мВ</span>
|
|
|
</div>
|
|
|
- <div class="feedback" id="capFeedback">Двигайте ползунки, чтобы увидеть, как меняется наводка.</div>
|
|
|
+ <div class="feedback" id="capFeedback">Двигайте ползунки или перетаскивайте провод и углы приёмника.</div>
|
|
|
<div class="notebook">
|
|
|
<p><strong>Ёмкостная наводка</strong> — наведение напряжения в проводнике за счёт электрического поля соседнего проводника с переменным напряжением.</p>
|
|
|
- <p>Зависит от напряжения, частоты, площади и расстояния. При постоянном напряжении (частота = 0) наводки нет — нет изменения заряда.</p>
|
|
|
+ <p>Зависит от напряжения, частоты, площади и расстояния. При постоянном напряжении (частота = 0) наводки нет — заряд не меняется.</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -755,7 +756,7 @@
|
|
|
if (index === 3) initInductionScene();
|
|
|
if (index === 4) { drawCapacitor(); drawInductor(); }
|
|
|
if (index === 7) initInductive();
|
|
|
- if (index === 8) drawCapacitive();
|
|
|
+ if (index === 8) initCapacitive8();
|
|
|
if (index === 9) drawGalvanic();
|
|
|
if (index === 11) initQuiz();
|
|
|
|
|
|
@@ -847,7 +848,7 @@
|
|
|
drawWireB();
|
|
|
});
|
|
|
|
|
|
- // === СЦЕНА 4: Индукция ===
|
|
|
+ // === СЦЕНА 4: Индукция (магнит и катушка) ===
|
|
|
const induction = {
|
|
|
canvas: null, ctx: null,
|
|
|
magnet: { x: 100, y: 130, w: 70, h: 60, dragging: false, offsetX: 0 },
|
|
|
@@ -1131,7 +1132,7 @@
|
|
|
ctx.fillText('+', gx + gr + 12, gy - 5);
|
|
|
}
|
|
|
|
|
|
- // === СЦЕНА 5: Конденсатор и индуктивность ===
|
|
|
+ // === СЦЕНА 5: Конденсатор и индуктивность (демо) ===
|
|
|
function drawCapacitor() {
|
|
|
const c = document.getElementById('capacitorViz');
|
|
|
if (!c) return;
|
|
|
@@ -1262,7 +1263,7 @@
|
|
|
drawInductor();
|
|
|
});
|
|
|
|
|
|
- // === СЦЕНА 8: Индуктивная наводка (расширенная) ===
|
|
|
+ // === СЦЕНА 8: Индуктивная наводка ===
|
|
|
const ind8 = {
|
|
|
canvas: null,
|
|
|
ctx: null,
|
|
|
@@ -1436,7 +1437,6 @@
|
|
|
if (x2 > 780) x2 = 780;
|
|
|
if (y2 > 420) y2 = 420;
|
|
|
|
|
|
- // Ограничение по расстоянию
|
|
|
const wireRightEdge = ind8.wire.x + ind8.wire.w / 2;
|
|
|
if (x1 < wireRightEdge + 1 * ind8.pxPerCm) {
|
|
|
const shift = (wireRightEdge + 1 * ind8.pxPerCm) - x1;
|
|
|
@@ -1451,7 +1451,6 @@
|
|
|
l.w = x2 - x1;
|
|
|
l.h = y2 - y1;
|
|
|
|
|
|
- // Синхронизация слайдеров
|
|
|
const areaCm2 = Math.round((l.w * l.h) / 250);
|
|
|
document.getElementById('indArea').value = Math.max(0, Math.min(100, areaCm2));
|
|
|
document.getElementById('indAreaVal').textContent = Math.max(0, Math.min(100, areaCm2));
|
|
|
@@ -1776,89 +1775,514 @@
|
|
|
ctx.fillText(sign + Math.abs(emf).toFixed(1) + ' мВ', cx, cy + r + 18);
|
|
|
}
|
|
|
|
|
|
- function drawCapacitive() {
|
|
|
- const canvas = document.getElementById('capacitiveViz');
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
- function render() {
|
|
|
- const w = canvas.width, h = canvas.height;
|
|
|
- ctx.fillStyle = '#f2e9d6';
|
|
|
- ctx.fillRect(0, 0, w, h);
|
|
|
- ctx.fillStyle = '#1a4a6a';
|
|
|
- ctx.fillRect(80, 30, 14, h - 60);
|
|
|
- ctx.fillStyle = '#1a4a6a';
|
|
|
- ctx.font = 'bold 12px monospace';
|
|
|
- ctx.textAlign = 'center';
|
|
|
- ctx.fillText('U', 87, 20);
|
|
|
- const U = parseFloat(document.getElementById('capVolt').value);
|
|
|
- const lineCount = Math.max(1, Math.floor(U / 100));
|
|
|
- ctx.strokeStyle = 'rgba(26,74,106,0.35)';
|
|
|
- ctx.lineWidth = 1.5;
|
|
|
- for (let i = 0; i < lineCount; i++) {
|
|
|
- const y = 40 + i * (h - 80) / Math.max(1, lineCount - 1);
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(94, y);
|
|
|
- ctx.lineTo(520, y);
|
|
|
- ctx.stroke();
|
|
|
+ // === СЦЕНА 9: Ёмкостная наводка ===
|
|
|
+ const cap8 = {
|
|
|
+ canvas: null,
|
|
|
+ ctx: null,
|
|
|
+ src: { x: 280, w: 26, h: 280, cy: 220 },
|
|
|
+ recv: { x1: 560, y1: 140, w: 40, h: 160 },
|
|
|
+ pxPerCm: 18,
|
|
|
+ phase: 0,
|
|
|
+ lastT: 0,
|
|
|
+ dragMode: null,
|
|
|
+ dragOffset: { x: 0, y: 0 },
|
|
|
+ initialized: false
|
|
|
+ };
|
|
|
+
|
|
|
+ function initCapacitive8() {
|
|
|
+ const c = document.getElementById('capacitiveViz');
|
|
|
+ if (!c) return;
|
|
|
+ cap8.canvas = c;
|
|
|
+ cap8.ctx = c.getContext('2d');
|
|
|
+
|
|
|
+ if (!cap8.initialized) {
|
|
|
+ cap8.initialized = true;
|
|
|
+
|
|
|
+ c.addEventListener('mousedown', onCap8Down);
|
|
|
+ c.addEventListener('mousemove', onCap8Move);
|
|
|
+ c.addEventListener('mouseup', onCap8Up);
|
|
|
+ c.addEventListener('mouseleave', onCap8Up);
|
|
|
+ c.addEventListener('touchstart', onCap8Down, { passive: false });
|
|
|
+ c.addEventListener('touchmove', onCap8Move, { passive: false });
|
|
|
+ c.addEventListener('touchend', onCap8Up);
|
|
|
+ c.addEventListener('touchcancel', onCap8Up);
|
|
|
+
|
|
|
+ ['capVolt', 'capFreq'].forEach(id => {
|
|
|
+ document.getElementById(id).addEventListener('input', updateCapacitive8);
|
|
|
+ });
|
|
|
+ document.getElementById('capDist').addEventListener('input', function() {
|
|
|
+ applyCap8DistSlider();
|
|
|
+ updateCapacitive8();
|
|
|
+ });
|
|
|
+ document.getElementById('capArea').addEventListener('input', function() {
|
|
|
+ applyCap8AreaSlider();
|
|
|
+ updateCapacitive8();
|
|
|
+ });
|
|
|
+
|
|
|
+ applyCap8AreaSlider();
|
|
|
+ applyCap8DistSlider();
|
|
|
+
|
|
|
+ cap8.lastT = performance.now();
|
|
|
+ requestAnimationFrame(drawCap8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function getCap8Coords(e) {
|
|
|
+ const c = cap8.canvas;
|
|
|
+ const rect = c.getBoundingClientRect();
|
|
|
+ const sx = c.width / rect.width;
|
|
|
+ const sy = c.height / rect.height;
|
|
|
+ let cx, cy;
|
|
|
+ if (e.touches && e.touches.length) {
|
|
|
+ cx = e.touches[0].clientX;
|
|
|
+ cy = e.touches[0].clientY;
|
|
|
+ } else {
|
|
|
+ cx = e.clientX;
|
|
|
+ cy = e.clientY;
|
|
|
+ }
|
|
|
+ return { x: (cx - rect.left) * sx, y: (cy - rect.top) * sy };
|
|
|
+ }
|
|
|
+
|
|
|
+ function onCap8Down(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const p = getCap8Coords(e);
|
|
|
+ const r = cap8.recv;
|
|
|
+
|
|
|
+ const corners = [
|
|
|
+ { name: 'tl', x: r.x1, y: r.y1 },
|
|
|
+ { name: 'tr', x: r.x1 + r.w, y: r.y1 },
|
|
|
+ { name: 'bl', x: r.x1, y: r.y1 + r.h },
|
|
|
+ { name: 'br', x: r.x1 + r.w, y: r.y1 + r.h }
|
|
|
+ ];
|
|
|
+ for (const corner of corners) {
|
|
|
+ if (Math.hypot(p.x - corner.x, p.y - corner.y) < 18) {
|
|
|
+ cap8.dragMode = corner.name;
|
|
|
+ return;
|
|
|
}
|
|
|
- ctx.fillStyle = '#2a5a2a';
|
|
|
- ctx.fillRect(520, 40, 14, h - 80);
|
|
|
- ctx.fillStyle = '#2a5a2a';
|
|
|
- ctx.fillText('приёмник', 527, h - 20);
|
|
|
- const cap = calcCapacitive();
|
|
|
- if (cap > 0.1) {
|
|
|
- const charges = Math.min(8, Math.floor(cap / 10));
|
|
|
- for (let i = 0; i < charges; i++) {
|
|
|
- ctx.fillStyle = '#8b1a1a';
|
|
|
- ctx.beginPath();
|
|
|
- ctx.arc(545, 50 + i * 20, 3, 0, Math.PI*2);
|
|
|
- ctx.fill();
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ const s = cap8.src;
|
|
|
+ const srcLeft = s.x - s.w/2 - 8;
|
|
|
+ const srcRight = s.x + s.w/2 + 8;
|
|
|
+ const srcTop = s.cy - s.h/2;
|
|
|
+ const srcBottom = s.cy + s.h/2;
|
|
|
+ if (p.x >= srcLeft && p.x <= srcRight && p.y >= srcTop && p.y <= srcBottom) {
|
|
|
+ cap8.dragMode = 'src';
|
|
|
+ cap8.dragOffset = { x: p.x - s.x, y: 0 };
|
|
|
+ cap8.canvas.style.cursor = 'grabbing';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onCap8Move(e) {
|
|
|
+ const p = getCap8Coords(e);
|
|
|
+
|
|
|
+ if (!cap8.dragMode) {
|
|
|
+ const r = cap8.recv;
|
|
|
+ const corners = [
|
|
|
+ [r.x1, r.y1], [r.x1 + r.w, r.y1],
|
|
|
+ [r.x1, r.y1 + r.h], [r.x1 + r.w, r.y1 + r.h]
|
|
|
+ ];
|
|
|
+ let overCorner = false;
|
|
|
+ for (const [cx, cy] of corners) {
|
|
|
+ if (Math.hypot(p.x - cx, p.y - cy) < 18) { overCorner = true; break; }
|
|
|
}
|
|
|
- requestAnimationFrame(render);
|
|
|
+ if (overCorner) {
|
|
|
+ cap8.canvas.style.cursor = 'nwse-resize';
|
|
|
+ } else if (Math.abs(p.x - cap8.src.x) < 20 && p.y > 60 && p.y < 380) {
|
|
|
+ cap8.canvas.style.cursor = 'grab';
|
|
|
+ } else {
|
|
|
+ cap8.canvas.style.cursor = 'default';
|
|
|
+ }
|
|
|
+ return;
|
|
|
}
|
|
|
- render();
|
|
|
+
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ if (cap8.dragMode === 'src') {
|
|
|
+ let newX = p.x - cap8.dragOffset.x;
|
|
|
+ const recvLeft = cap8.recv.x1;
|
|
|
+ const minDistPx = 1 * cap8.pxPerCm;
|
|
|
+ const maxDistPx = 20 * cap8.pxPerCm;
|
|
|
+ const srcRightEdge = newX + cap8.src.w / 2;
|
|
|
+ let distPx = recvLeft - srcRightEdge;
|
|
|
+ if (distPx < minDistPx) newX = recvLeft - minDistPx - cap8.src.w / 2;
|
|
|
+ if (distPx > maxDistPx) newX = recvLeft - maxDistPx - cap8.src.w / 2;
|
|
|
+ if (newX < 30) newX = 30;
|
|
|
+ cap8.src.x = newX;
|
|
|
+
|
|
|
+ const d = Math.max(1, Math.min(20, Math.round((recvLeft - (newX + cap8.src.w / 2)) / cap8.pxPerCm)));
|
|
|
+ const slider = document.getElementById('capDist');
|
|
|
+ slider.value = d;
|
|
|
+ document.getElementById('capDistVal').textContent = d;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const r = cap8.recv;
|
|
|
+ let x1 = r.x1, y1 = r.y1, x2 = r.x1 + r.w, y2 = r.y1 + r.h;
|
|
|
+ if (cap8.dragMode === 'tl') { x1 = p.x; y1 = p.y; }
|
|
|
+ if (cap8.dragMode === 'tr') { x2 = p.x; y1 = p.y; }
|
|
|
+ if (cap8.dragMode === 'bl') { x1 = p.x; y2 = p.y; }
|
|
|
+ if (cap8.dragMode === 'br') { x2 = p.x; y2 = p.y; }
|
|
|
+
|
|
|
+ const minW = 24, minH = 60, maxW = 80, maxH = 300;
|
|
|
+
|
|
|
+ if (x2 - x1 < minW) {
|
|
|
+ if (cap8.dragMode === 'tl' || cap8.dragMode === 'bl') x1 = x2 - minW;
|
|
|
+ else x2 = x1 + minW;
|
|
|
+ }
|
|
|
+ if (x2 - x1 > maxW) {
|
|
|
+ if (cap8.dragMode === 'tl' || cap8.dragMode === 'bl') x1 = x2 - maxW;
|
|
|
+ else x2 = x1 + maxW;
|
|
|
+ }
|
|
|
+ if (y2 - y1 < minH) {
|
|
|
+ if (cap8.dragMode === 'tl' || cap8.dragMode === 'tr') y1 = y2 - minH;
|
|
|
+ else y2 = y1 + minH;
|
|
|
+ }
|
|
|
+ if (y2 - y1 > maxH) {
|
|
|
+ if (cap8.dragMode === 'tl' || cap8.dragMode === 'tr') y1 = y2 - maxH;
|
|
|
+ else y2 = y1 + maxH;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (x1 < 20) x1 = 20;
|
|
|
+ if (y1 < 40) y1 = 40;
|
|
|
+ if (x2 > 780) x2 = 780;
|
|
|
+ if (y2 > 400) y2 = 400;
|
|
|
+
|
|
|
+ const srcRightEdge = cap8.src.x + cap8.src.w / 2;
|
|
|
+ if (x1 < srcRightEdge + 1 * cap8.pxPerCm) {
|
|
|
+ x1 = srcRightEdge + 1 * cap8.pxPerCm;
|
|
|
+ if (x2 - x1 < minW) x2 = x1 + minW;
|
|
|
+ }
|
|
|
+
|
|
|
+ r.x1 = x1;
|
|
|
+ r.y1 = y1;
|
|
|
+ r.w = x2 - x1;
|
|
|
+ r.h = y2 - y1;
|
|
|
+
|
|
|
+ const areaCm2 = Math.round((r.w * r.h) / 200);
|
|
|
+ document.getElementById('capArea').value = Math.max(0, Math.min(100, areaCm2));
|
|
|
+ document.getElementById('capAreaVal').textContent = Math.max(0, Math.min(100, areaCm2));
|
|
|
+
|
|
|
+ const distPx = r.x1 - srcRightEdge;
|
|
|
+ const distCm = Math.round(distPx / cap8.pxPerCm);
|
|
|
+ document.getElementById('capDist').value = Math.max(1, Math.min(20, distCm));
|
|
|
+ document.getElementById('capDistVal').textContent = Math.max(1, Math.min(20, distCm));
|
|
|
}
|
|
|
|
|
|
- function calcCapacitive() {
|
|
|
- const U = parseFloat(document.getElementById('capVolt').value);
|
|
|
- const f = parseFloat(document.getElementById('capFreq').value);
|
|
|
+ function onCap8Up() {
|
|
|
+ if (cap8.dragMode) {
|
|
|
+ cap8.dragMode = null;
|
|
|
+ cap8.canvas.style.cursor = 'default';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyCap8DistSlider() {
|
|
|
const d = parseFloat(document.getElementById('capDist').value);
|
|
|
+ cap8.src.x = cap8.recv.x1 - d * cap8.pxPerCm - cap8.src.w / 2;
|
|
|
+ if (cap8.src.x < 30) cap8.src.x = 30;
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyCap8AreaSlider() {
|
|
|
const S = parseFloat(document.getElementById('capArea').value);
|
|
|
- if (U === 0 || f === 0 || S === 0 || d === 0) return 0;
|
|
|
- const base = (U / 1000) * (f / 1000) * (S / 100) * (1 / d) * 100;
|
|
|
- return Math.min(base, 99.9);
|
|
|
+ const targetArea = Math.max(1800, S * 200);
|
|
|
+ const aspect = cap8.recv.w / cap8.recv.h || 0.3;
|
|
|
+ let newH = Math.sqrt(targetArea / aspect);
|
|
|
+ let newW = newH * aspect;
|
|
|
+ if (newW < 24) { newW = 24; newH = targetArea / 24; }
|
|
|
+ if (newH < 60) { newH = 60; newW = targetArea / 60; }
|
|
|
+ if (newW > 80) { newW = 80; newH = targetArea / 80; }
|
|
|
+ if (newH > 300) { newH = 300; newW = targetArea / 300; }
|
|
|
+ cap8.recv.w = newW;
|
|
|
+ cap8.recv.h = newH;
|
|
|
}
|
|
|
|
|
|
- function updateCapacitive() {
|
|
|
+ function updateCapacitive8() {
|
|
|
document.getElementById('capVoltVal').textContent = document.getElementById('capVolt').value;
|
|
|
document.getElementById('capFreqVal').textContent = document.getElementById('capFreq').value;
|
|
|
document.getElementById('capDistVal').textContent = document.getElementById('capDist').value;
|
|
|
document.getElementById('capAreaVal').textContent = document.getElementById('capArea').value;
|
|
|
- const val = calcCapacitive();
|
|
|
- document.getElementById('capResult').textContent = val.toFixed(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawCap8() {
|
|
|
+ const sceneEl = document.querySelector('.scene[data-scene="9"]');
|
|
|
+ if (!sceneEl || !sceneEl.classList.contains('active')) {
|
|
|
+ requestAnimationFrame(drawCap8);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!cap8.ctx) { requestAnimationFrame(drawCap8); return; }
|
|
|
+
|
|
|
+ const now = performance.now();
|
|
|
+ let dt = (now - cap8.lastT) / 1000;
|
|
|
+ cap8.lastT = now;
|
|
|
+ if (dt > 0.15) dt = 0.15;
|
|
|
+ if (dt < 0) dt = 0;
|
|
|
+
|
|
|
const U = parseFloat(document.getElementById('capVolt').value);
|
|
|
const f = parseFloat(document.getElementById('capFreq').value);
|
|
|
- const d = parseFloat(document.getElementById('capDist').value);
|
|
|
- const S = parseFloat(document.getElementById('capArea').value);
|
|
|
- let msg = '';
|
|
|
- if (U === 0) msg = 'Напряжение = 0 → электрического поля нет → наводки нет.';
|
|
|
- else if (f === 0) msg = 'Частота = 0 (постоянное напряжение) → заряд не меняется → наводки нет.';
|
|
|
- else if (S === 0) msg = 'Площадь = 0 → ёмкость связи нулевая → наводки нет.';
|
|
|
- else {
|
|
|
- const parts = [];
|
|
|
- if (U >= 700) parts.push('напряжение большое → заряд на пластинах велик');
|
|
|
- if (f >= 700) parts.push('частота высокая → ток смещения больше');
|
|
|
- if (S >= 70) parts.push('площадь большая → ёмкость больше');
|
|
|
- if (d <= 5) parts.push('расстояние маленькое → ёмкость не ослабла');
|
|
|
- if (parts.length === 0) parts.push('значения в среднем диапазоне');
|
|
|
- msg = 'Наводка: ' + parts.join(', ') + '.';
|
|
|
+ const apparentF = f / 200;
|
|
|
+
|
|
|
+ if (f > 1) {
|
|
|
+ cap8.phase += 2 * Math.PI * apparentF * dt;
|
|
|
}
|
|
|
- document.getElementById('capFeedback').textContent = msg;
|
|
|
+
|
|
|
+ const Uinst = U * Math.cos(cap8.phase);
|
|
|
+ const Uabs = Math.min(1, Math.abs(Uinst) / 1000);
|
|
|
+
|
|
|
+ const ctx = cap8.ctx;
|
|
|
+ const cw = cap8.canvas.width;
|
|
|
+ const ch = cap8.canvas.height;
|
|
|
+
|
|
|
+ ctx.fillStyle = '#f2e9d6';
|
|
|
+ ctx.fillRect(0, 0, cw, ch);
|
|
|
+
|
|
|
+ const s = cap8.src;
|
|
|
+ const r = cap8.recv;
|
|
|
+
|
|
|
+ const srcRight = s.x + s.w / 2;
|
|
|
+ const recvLeft = r.x1;
|
|
|
+ const distPx = Math.max(1, recvLeft - srcRight);
|
|
|
+ const distCm = distPx / cap8.pxPerCm;
|
|
|
+ const areaCm2 = (r.w * r.h) / 200;
|
|
|
+
|
|
|
+ const overlapTop = Math.max(s.cy - s.h/2, r.y1);
|
|
|
+ const overlapBot = Math.min(s.cy + s.h/2, r.y1 + r.h);
|
|
|
+ const overlapH = Math.max(0, overlapBot - overlapTop);
|
|
|
+
|
|
|
+ const polarity = Uinst >= 0 ? 1 : -1;
|
|
|
+ drawCap8Field(ctx, srcRight, recvLeft, overlapTop, overlapBot, U, Uabs, polarity);
|
|
|
+ drawCap8Source(ctx, s, Uinst);
|
|
|
+ drawCap8Receiver(ctx, r, Uinst, Uabs, areaCm2);
|
|
|
+ drawCap8Distance(ctx, srcRight, recvLeft, (overlapTop + overlapBot) / 2, distCm);
|
|
|
+ drawCap8CurrentArrow(ctx, srcRight, recvLeft, overlapTop, overlapBot, Uinst, f);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#4a5a7a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('S = ' + areaCm2.toFixed(0) + ' см²', r.x1 - 4, r.y1 - 24);
|
|
|
+
|
|
|
+ let vInst = 0;
|
|
|
+ if (f > 1 && U > 0 && areaCm2 > 0) {
|
|
|
+ vInst = 0.02 * U * areaCm2 * apparentF * Math.sin(cap8.phase) / Math.max(1, distCm * distCm);
|
|
|
+ if (vInst > 99.9) vInst = 99.9;
|
|
|
+ if (vInst < -99.9) vInst = -99.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ const resEl = document.getElementById('capResult');
|
|
|
+ if (resEl) resEl.textContent = vInst.toFixed(1);
|
|
|
+
|
|
|
+ const fbEl = document.getElementById('capFeedback');
|
|
|
+ if (fbEl) {
|
|
|
+ let msg = '';
|
|
|
+ if (U === 0) {
|
|
|
+ msg = 'Напряжение = 0 → электрического поля нет → наведённый заряд = 0.';
|
|
|
+ } else if (f <= 1) {
|
|
|
+ msg = 'Частота = 0 (постоянное напряжение) → поле не меняется → наводки нет. Постоянное напряжение само по себе не даёт наводки в приёмнике, нужен ток смещения (dU/dt).';
|
|
|
+ } else if (areaCm2 < 15) {
|
|
|
+ msg = 'Площадь перекрытия мала → ёмкость связи слабая.';
|
|
|
+ } else {
|
|
|
+ const parts = [];
|
|
|
+ if (U >= 700) parts.push('напряжение большое → поле сильное');
|
|
|
+ if (f >= 700) parts.push('частота высокая → поле меняется быстро');
|
|
|
+ if (areaCm2 >= 70) parts.push('площадь большая → ёмкость больше');
|
|
|
+ if (distCm <= 5) parts.push('расстояние маленькое → поле не ослабло');
|
|
|
+ if (parts.length === 0) parts.push('значения в среднем диапазоне');
|
|
|
+ msg = 'Амплитуда наводки: ' + parts.join(', ') + '.';
|
|
|
+ }
|
|
|
+ fbEl.textContent = msg;
|
|
|
+ }
|
|
|
+
|
|
|
+ requestAnimationFrame(drawCap8);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawCap8Field(ctx, x1, x2, top, bot, U, Uabs, polarity) {
|
|
|
+ if (U <= 0 || bot <= top) return;
|
|
|
+
|
|
|
+ const height = bot - top;
|
|
|
+ const numLines = Math.max(1, Math.round(U / 100));
|
|
|
+ const alpha = 0.08 + Uabs * 0.85;
|
|
|
+ const lw = 0.8 + Uabs * 2.3;
|
|
|
+
|
|
|
+ const rCol = polarity > 0 ? 26 : 139;
|
|
|
+ const gCol = polarity > 0 ? 74 : 26;
|
|
|
+ const bCol = polarity > 0 ? 106 : 26;
|
|
|
+
|
|
|
+ for (let i = 0; i < numLines; i++) {
|
|
|
+ const t = numLines === 1 ? 0.5 : i / (numLines - 1);
|
|
|
+ const y = top + t * height;
|
|
|
+
|
|
|
+ if (Uabs > 0.4) {
|
|
|
+ ctx.shadowColor = `rgba(255,140,50,${Uabs * 0.5})`;
|
|
|
+ ctx.shadowBlur = 6 + Uabs * 8;
|
|
|
+ }
|
|
|
+ ctx.strokeStyle = `rgba(${rCol},${gCol},${bCol},${alpha})`;
|
|
|
+ ctx.lineWidth = lw;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x1, y);
|
|
|
+ ctx.lineTo(x2, y);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.shadowBlur = 0;
|
|
|
+ ctx.shadowColor = 'transparent';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawCap8Source(ctx, s, Uinst) {
|
|
|
+ const x = s.x - s.w / 2;
|
|
|
+ const y = s.cy - s.h / 2;
|
|
|
+
|
|
|
+ ctx.fillStyle = 'rgba(0,0,0,0.15)';
|
|
|
+ ctx.fillRect(x + 3, y + 3, s.w, s.h);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a4a6a';
|
|
|
+ ctx.fillRect(x, y, s.w, s.h);
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.strokeRect(x, y, s.w, s.h);
|
|
|
+
|
|
|
+ const polarity = Uinst >= 0 ? '+' : '−';
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.92)';
|
|
|
+ ctx.font = 'bold 16px "PT Serif", serif';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
+ const num = 7;
|
|
|
+ for (let i = 0; i < num; i++) {
|
|
|
+ const py = y + 30 + i * (s.h - 60) / (num - 1);
|
|
|
+ ctx.fillText(polarity, s.x, py);
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a4a6a';
|
|
|
+ ctx.font = 'bold 14px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('U', s.x, y - 12);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#4a5a7a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('источник', s.x, y + s.h + 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawCap8Receiver(ctx, r, Uinst, Uabs, areaCm2) {
|
|
|
+ ctx.fillStyle = 'rgba(42,90,42,0.06)';
|
|
|
+ ctx.fillRect(r.x1, r.y1, r.w, r.h);
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#2a5a2a';
|
|
|
+ ctx.lineWidth = 4;
|
|
|
+ ctx.strokeRect(r.x1, r.y1, r.w, r.h);
|
|
|
+
|
|
|
+ const corners = [
|
|
|
+ [r.x1, r.y1],
|
|
|
+ [r.x1 + r.w, r.y1],
|
|
|
+ [r.x1, r.y1 + r.h],
|
|
|
+ [r.x1 + r.w, r.y1 + r.h]
|
|
|
+ ];
|
|
|
+ corners.forEach(([cx, cy]) => {
|
|
|
+ ctx.fillStyle = '#2a5a2a';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(cx, cy, 7, 0, Math.PI * 2);
|
|
|
+ ctx.fill();
|
|
|
+ ctx.strokeStyle = '#fffef8';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.stroke();
|
|
|
+ });
|
|
|
+
|
|
|
+ const polarity = Uinst >= 0 ? '−' : '+';
|
|
|
+ const numCharges = Math.floor(areaCm2 / 15);
|
|
|
+ if (numCharges > 0) {
|
|
|
+ const maxN = Math.min(numCharges, 10);
|
|
|
+ const step = (r.h - 40) / Math.max(1, maxN - 1);
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.font = 'bold 14px "PT Serif", serif';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
+ for (let i = 0; i < maxN; i++) {
|
|
|
+ ctx.fillText(polarity, r.x1 + r.w / 2, r.y1 + 20 + i * step);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#2a5a2a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('приёмник', r.x1 + r.w / 2, r.y1 + r.h + 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawCap8Distance(ctx, x1, x2, y, distCm) {
|
|
|
+ if (y <= 0 || y >= 440) y = 220;
|
|
|
+
|
|
|
+ ctx.strokeStyle = 'rgba(74,90,122,0.55)';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.setLineDash([4, 4]);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x1 + 4, y);
|
|
|
+ ctx.lineTo(x2 - 4, y);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.setLineDash([]);
|
|
|
+
|
|
|
+ ctx.fillStyle = 'rgba(74,90,122,0.85)';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x1 + 4, y);
|
|
|
+ ctx.lineTo(x1 + 14, y - 5);
|
|
|
+ ctx.lineTo(x1 + 14, y + 5);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x2 - 4, y);
|
|
|
+ ctx.lineTo(x2 - 14, y - 5);
|
|
|
+ ctx.lineTo(x2 - 14, y + 5);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+
|
|
|
+ ctx.fillStyle = '#1a2a4a';
|
|
|
+ ctx.font = 'bold 12px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'bottom';
|
|
|
+ ctx.fillText('d = ' + distCm.toFixed(1) + ' см', (x1 + x2) / 2, y - 6);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawCap8CurrentArrow(ctx, x1, x2, top, bot, Uinst, f) {
|
|
|
+ if (f <= 1) return;
|
|
|
+ const midX = (x1 + x2) / 2;
|
|
|
+ const midY = (top + bot) / 2;
|
|
|
+ if (!isFinite(midY)) return;
|
|
|
+
|
|
|
+ const Iinst = -Math.sin(cap8.phase);
|
|
|
+ const ratio = Math.abs(Iinst);
|
|
|
+ const alpha = 0.4 + ratio * 0.55;
|
|
|
+ const lw = 2 + ratio * 2;
|
|
|
+
|
|
|
+ const arrowLen = Math.min(40, (x2 - x1) * 0.3);
|
|
|
+ if (arrowLen < 10) return;
|
|
|
+
|
|
|
+ const dir = Iinst >= 0 ? 1 : -1;
|
|
|
+ const tipX = midX + dir * arrowLen;
|
|
|
+ const tailX = midX - dir * arrowLen * 0.4;
|
|
|
+
|
|
|
+ ctx.strokeStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.lineWidth = lw;
|
|
|
+ ctx.lineCap = 'round';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(tailX, midY);
|
|
|
+ ctx.lineTo(tipX, midY);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ const size = 6 + ratio * 5;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(tipX, midY);
|
|
|
+ ctx.lineTo(tipX - dir * size, midY - size * 0.6);
|
|
|
+ ctx.lineTo(tipX - dir * size, midY + size * 0.6);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fillStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.fill();
|
|
|
+ ctx.lineCap = 'butt';
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.font = 'bold 10px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'top';
|
|
|
+ ctx.fillText('ток смещения', midX, midY + 12);
|
|
|
}
|
|
|
- ['capVolt', 'capFreq', 'capDist', 'capArea'].forEach(id => {
|
|
|
- document.getElementById(id).addEventListener('input', updateCapacitive);
|
|
|
- });
|
|
|
|
|
|
+ // === СЦЕНА 10: Гальваническая наводка ===
|
|
|
function drawGalvanic() {
|
|
|
const canvas = document.getElementById('galvanicViz');
|
|
|
const ctx = canvas.getContext('2d');
|