|
|
@@ -351,6 +351,7 @@
|
|
|
.final-score .verdict { font-family: 'Oswald', sans-serif; font-size: 18px; text-transform: uppercase; margin-top: 12px; letter-spacing: 1px; }
|
|
|
#inductionCanvas { cursor: grab; touch-action: none; }
|
|
|
#inductionCanvas.dragging { cursor: grabbing; }
|
|
|
+ #inductiveViz { 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; }
|
|
|
@@ -436,10 +437,10 @@
|
|
|
<div class="scene" data-scene="4">
|
|
|
<div class="scene-num">Актуализация · 3 из 4</div>
|
|
|
<h2 class="scene-title">Возникновение тока при изменении магнитного поля</h2>
|
|
|
- <p>Зажмите магнит и тяните его к катушке или от неё. Линии поля с обеих сторон магнита тянутся за ним и пронизывают катушку — стрелка гальванометра отклоняется. Чем быстрее движение, тем сильнее отклонение. Остановите магнит — линии перестают меняться, стрелка возвращается в нуль.</p>
|
|
|
+ <p>Зажмите магнит и тяните его к катушке или от неё. Чем ближе магнит — тем больше линий поля пронизывают катушку и тем они ярче. Стрелка гальванометра отклоняется, пока магнит движется. Остановите магнит — линии перестают меняться, стрелка возвращается в нуль.</p>
|
|
|
<div class="viz">
|
|
|
<canvas id="inductionCanvas" width="800" height="320"></canvas>
|
|
|
- <div class="viz-caption">Линии магнитного поля · гальванометр показывает ЭДС</div>
|
|
|
+ <div class="viz-caption">Линии магнитного поля: чем ближе магнит, тем гуще и ярче · гальванометр показывает ЭДС</div>
|
|
|
</div>
|
|
|
<div class="indicator">
|
|
|
<small>ЭДС = −dФ/dt</small>
|
|
|
@@ -455,22 +456,30 @@
|
|
|
<div class="scene" data-scene="5">
|
|
|
<div class="scene-num">Актуализация · 4 из 4</div>
|
|
|
<h2 class="scene-title">Конденсатор и индуктивность</h2>
|
|
|
- <p>Два элемента, которые накапливают энергию поля. Они понадобятся, чтобы понять паразитные связи.</p>
|
|
|
+ <p>Два элемента, которые накапливают энергию поля. Подвигайте ползунки — увидите, как меняется поле внутри и вокруг элемента.</p>
|
|
|
<div class="two-col">
|
|
|
<div class="viz">
|
|
|
- <canvas id="capacitorViz" width="400" height="180"></canvas>
|
|
|
- <div class="viz-caption">Конденсатор: две пластины, электрическое поле</div>
|
|
|
+ <canvas id="capacitorViz" width="400" height="200"></canvas>
|
|
|
+ <div class="viz-caption">Конденсатор в разрезе: заряд и поле E между пластинами</div>
|
|
|
+ <div class="control-group">
|
|
|
+ <div class="control-label"><span>Заряд на пластинах</span><span class="val" id="capChargeVal">50</span>%</div>
|
|
|
+ <input type="range" id="capCharge" min="0" max="100" step="5" value="50">
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="viz">
|
|
|
- <canvas id="inductorViz" width="400" height="180"></canvas>
|
|
|
- <div class="viz-caption">Индуктивность: катушка, магнитное поле</div>
|
|
|
+ <canvas id="inductorViz" width="400" height="200"></canvas>
|
|
|
+ <div class="viz-caption">Катушка индуктивности: ток и поле B вокруг витков</div>
|
|
|
+ <div class="control-group">
|
|
|
+ <div class="control-label"><span>Ток через катушку</span><span class="val" id="indCurrentVal3">50</span>%</div>
|
|
|
+ <input type="range" id="indCurrent3" min="0" max="100" step="5" value="50">
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="notebook">
|
|
|
- <p><strong>Конденсатор</strong> — две проводящие пластины, разделённые диэлектриком. Накапливает заряд, создаёт электрическое поле между пластинами.</p>
|
|
|
- <p><strong>Индуктивность</strong> — свойство проводника накапливать энергию магнитного поля при протекании тока.</p>
|
|
|
+ <p><strong>Конденсатор</strong> — две проводящие пластины, разделённые диэлектриком. Накапливает заряд, создаёт электрическое поле между пластинами. Чем больше заряд — тем сильнее поле.</p>
|
|
|
+ <p><strong>Индуктивность</strong> — свойство проводника накапливать энергию магнитного поля при протекании тока. Чем больше ток — тем сильнее поле.</p>
|
|
|
</div>
|
|
|
- <p class="hint">Подводка: два провода рядом — это паразитный конденсатор (ёмкость). Провод с током рядом с другим — паразитная индуктивность. Отсюда — два механизма наводок.</p>
|
|
|
+ <p class="hint">Связь с наводками: два провода рядом образуют паразитный конденсатор (ёмкостная связь). Провод с током рядом с другим создаёт магнитное поле (индуктивная связь).</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="scene" data-scene="6">
|
|
|
@@ -529,10 +538,10 @@
|
|
|
<div class="scene" data-scene="8">
|
|
|
<div class="scene-num">Механизм 1 · индуктивный</div>
|
|
|
<h2 class="scene-title">Индуктивная наводка</h2>
|
|
|
- <p>Источник — провод с переменным током. Среда — магнитное поле. Приёмник — петля сигнального провода. Изменение магнитного потока через петлю наводит в ней ЭДС.</p>
|
|
|
+ <p>Источник — провод с переменным током. Среда — магнитное поле. Приёмник — петля сигнального провода. Меняйте ток, частоту, расстояние и площадь петли. Провод можно перетаскивать мышью, углы петли — тянуть и менять площадь.</p>
|
|
|
<div class="viz">
|
|
|
- <canvas id="inductiveViz" width="800" height="240"></canvas>
|
|
|
- <div class="viz-caption">Магнитное поле от силового провода прошивает петлю сигнального</div>
|
|
|
+ <canvas id="inductiveViz" width="800" height="440"></canvas>
|
|
|
+ <div class="viz-caption">Плотность линий = интенсивность поля · мерцание = частота · стрелка в петле = наведённая ЭДС</div>
|
|
|
</div>
|
|
|
<div class="two-col">
|
|
|
<div>
|
|
|
@@ -557,10 +566,10 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="indicator">
|
|
|
- <small>Наведённая ЭДС</small>
|
|
|
+ <small>Мгновенная ЭДС = −dФ/dt</small>
|
|
|
<span><span id="indResult">0.0</span> мВ</span>
|
|
|
</div>
|
|
|
- <div class="feedback" id="indFeedback">Двигайте ползунки, чтобы увидеть, как меняется ЭДС.</div>
|
|
|
+ <div class="feedback" id="indFeedback">Двигайте ползунки или перетаскивайте провод и углы петли.</div>
|
|
|
<div class="notebook">
|
|
|
<p><strong>Индуктивная наводка</strong> — наведение ЭДС в контуре за счёт изменения магнитного потока от соседнего проводника с током.</p>
|
|
|
<p>Зависит от тока, частоты, площади петли и расстояния. При постоянном токе (частота = 0) наводки нет — поток не меняется.</p>
|
|
|
@@ -745,7 +754,7 @@
|
|
|
if (index === 2) drawWireB();
|
|
|
if (index === 3) initInductionScene();
|
|
|
if (index === 4) { drawCapacitor(); drawInductor(); }
|
|
|
- if (index === 7) drawInductive();
|
|
|
+ if (index === 7) initInductive();
|
|
|
if (index === 8) drawCapacitive();
|
|
|
if (index === 9) drawGalvanic();
|
|
|
if (index === 11) initQuiz();
|
|
|
@@ -983,9 +992,7 @@
|
|
|
const dist = Math.abs(magnetCenterX - coilCenterX);
|
|
|
const flux = 1000 / (dist * dist + 5000);
|
|
|
const reach = Math.max(0, Math.min(1, 1 - (dist - 60) / 400));
|
|
|
- const intensity = Math.min(1, flux * 8);
|
|
|
|
|
|
- // Катушка
|
|
|
ctx.strokeStyle = '#1a2a4a';
|
|
|
ctx.lineWidth = 3;
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
@@ -1000,7 +1007,6 @@
|
|
|
ctx.textAlign = 'center';
|
|
|
ctx.fillText('катушка', coil.x + coil.w/2, coil.y + coil.h + 20);
|
|
|
|
|
|
- // Свечение вокруг катушки при сильном потоке
|
|
|
if (reach > 0.4) {
|
|
|
const glow = (reach - 0.4) / 0.6;
|
|
|
const grad = ctx.createRadialGradient(
|
|
|
@@ -1013,28 +1019,24 @@
|
|
|
ctx.fillRect(coil.x - 80, coil.y - 60, coil.w + 160, coil.h + 120);
|
|
|
}
|
|
|
|
|
|
- // === Линии магнитного поля: симметрично слева и справа ===
|
|
|
- const nX = m.x + m.w; // правый полюс N
|
|
|
- const sX = m.x; // левый полюс S
|
|
|
+ const nX = m.x + m.w;
|
|
|
+ const sX = m.x;
|
|
|
const myCenter = m.y + m.h / 2;
|
|
|
- const numFlux = 5;
|
|
|
+ const numFlux = Math.max(3, Math.round(3 + reach * 10));
|
|
|
+ const spread = m.h * (0.35 + reach * 0.35);
|
|
|
|
|
|
- // Правая сторона: от N до левого края катушки (всегда доходит)
|
|
|
const rightEndX = coil.x + 15;
|
|
|
- // Левая сторона: симметричная длина, но не выходит за левый край canvas
|
|
|
const rightLen = rightEndX - nX;
|
|
|
const leftEndX = Math.max(10, sX - rightLen);
|
|
|
|
|
|
- // Параметры визуализации — зависят от reach
|
|
|
const alpha = 0.1 + reach * 0.9;
|
|
|
- const lineWidth = 1 + reach * 2.5;
|
|
|
+ const lineWidth = 0.8 + reach * 2.2;
|
|
|
const rCol = Math.floor(139 + reach * 80);
|
|
|
const gCol = Math.floor(20 + reach * 130);
|
|
|
const bCol = Math.floor(20 + reach * 30);
|
|
|
const shadowCol = `rgba(255,140,50,${reach * 0.7})`;
|
|
|
const shadowBlur = reach * 12;
|
|
|
|
|
|
- // Функция рисования линии
|
|
|
function drawFluxLine(x1, startY, x2, endY, curvature) {
|
|
|
ctx.strokeStyle = `rgba(${rCol},${gCol},${bCol},${alpha})`;
|
|
|
ctx.lineWidth = lineWidth;
|
|
|
@@ -1054,20 +1056,14 @@
|
|
|
|
|
|
for (let i = 0; i < numFlux; i++) {
|
|
|
const t = numFlux === 1 ? 0.5 : i / (numFlux - 1);
|
|
|
- const rel = (t - 0.5) * 2; // -1..+1
|
|
|
-
|
|
|
- const startY = myCenter + rel * m.h * 0.35;
|
|
|
-
|
|
|
- // Правая сторона (от N): конечная точка разбросана, изгиб наружу
|
|
|
- const rightEndY = myCenter + rel * (m.h * 0.35 + coil.h * 0.3 * reach);
|
|
|
+ const rel = (t - 0.5) * 2;
|
|
|
+ const startY = myCenter + rel * spread;
|
|
|
+ const rightEndY = myCenter + rel * (spread + coil.h * 0.3 * reach);
|
|
|
drawFluxLine(nX, startY, rightEndX, rightEndY, rel * 15);
|
|
|
-
|
|
|
- // Левая сторона (от S): симметричный изгиб, конечная точка разбросана
|
|
|
- const leftEndY = myCenter + rel * m.h * 0.35;
|
|
|
+ const leftEndY = myCenter + rel * spread;
|
|
|
drawFluxLine(sX, startY, leftEndX, leftEndY, rel * 15);
|
|
|
}
|
|
|
|
|
|
- // Магнит
|
|
|
ctx.fillStyle = '#1a4a6a';
|
|
|
ctx.fillRect(m.x, m.y, m.w / 2, m.h);
|
|
|
ctx.fillStyle = '#8b1a1a';
|
|
|
@@ -1083,7 +1079,6 @@
|
|
|
ctx.fillText('N', m.x + 3 * m.w / 4, m.y + m.h / 2);
|
|
|
ctx.textBaseline = 'alphabetic';
|
|
|
|
|
|
- // Гальванометр
|
|
|
const gx = 700, gy = 200, gr = 50;
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(gx, gy, gr, 0, Math.PI * 2);
|
|
|
@@ -1136,119 +1131,362 @@
|
|
|
ctx.fillText('+', gx + gr + 12, gy - 5);
|
|
|
}
|
|
|
|
|
|
+ // === СЦЕНА 5: Конденсатор и индуктивность ===
|
|
|
function drawCapacitor() {
|
|
|
const c = document.getElementById('capacitorViz');
|
|
|
+ if (!c) return;
|
|
|
const ctx = c.getContext('2d');
|
|
|
+ const charge = parseFloat(document.getElementById('capCharge').value);
|
|
|
+ const w = c.width, h = c.height;
|
|
|
+
|
|
|
ctx.fillStyle = '#f2e9d6';
|
|
|
- ctx.fillRect(0, 0, 400, 180);
|
|
|
+ ctx.fillRect(0, 0, w, h);
|
|
|
+
|
|
|
+ const plateW = 16;
|
|
|
+ const plateH = 120;
|
|
|
+ const leftX = 80;
|
|
|
+ const rightX = w - 80 - plateW;
|
|
|
+ const topY = (h - plateH) / 2;
|
|
|
+
|
|
|
ctx.fillStyle = '#8b1a1a';
|
|
|
- ctx.fillRect(120, 50, 12, 80);
|
|
|
+ ctx.fillRect(leftX, topY, plateW, plateH);
|
|
|
ctx.fillStyle = '#1a4a6a';
|
|
|
- ctx.fillRect(268, 50, 12, 80);
|
|
|
- ctx.strokeStyle = 'rgba(139,26,26,0.4)';
|
|
|
- ctx.lineWidth = 1.5;
|
|
|
- for (let y = 60; y <= 120; y += 10) {
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(132, y);
|
|
|
- ctx.lineTo(268, y);
|
|
|
- ctx.stroke();
|
|
|
+ ctx.fillRect(rightX, topY, plateW, plateH);
|
|
|
+
|
|
|
+ const numCharges = Math.floor(charge / 10);
|
|
|
+ if (numCharges > 0) {
|
|
|
+ const signY0 = topY + 30;
|
|
|
+ const signYStep = (plateH - 60) / Math.max(1, numCharges - 1);
|
|
|
+ ctx.fillStyle = '#fff';
|
|
|
+ ctx.font = 'bold 14px serif';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
+ for (let i = 0; i < numCharges; i++) {
|
|
|
+ const y = signY0 + i * signYStep;
|
|
|
+ ctx.fillText('+', leftX + plateW/2, y);
|
|
|
+ ctx.fillText('−', rightX + plateW/2, y);
|
|
|
+ }
|
|
|
}
|
|
|
- ctx.fillStyle = '#1a2a4a';
|
|
|
- ctx.font = '13px serif';
|
|
|
+
|
|
|
+ if (charge > 0) {
|
|
|
+ const lines = Math.max(1, Math.floor(charge / 12));
|
|
|
+ const alpha = 0.15 + (charge / 100) * 0.75;
|
|
|
+ const lw = 1 + (charge / 100) * 2;
|
|
|
+ ctx.strokeStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.lineWidth = lw;
|
|
|
+ for (let i = 0; i < lines; i++) {
|
|
|
+ const y = topY + 20 + i * (plateH - 40) / Math.max(1, lines - 1);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(leftX + plateW, y);
|
|
|
+ ctx.lineTo(rightX, y);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.font = 'bold 13px "Roboto Mono", monospace';
|
|
|
ctx.textAlign = 'center';
|
|
|
- ctx.fillText('+', 126, 45);
|
|
|
- ctx.fillText('−', 274, 45);
|
|
|
- ctx.font = '12px monospace';
|
|
|
- ctx.fillText('E', 200, 145);
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('E', w/2, topY - 10);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#4a5a7a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('диэлектрик', w/2, h - 12);
|
|
|
+
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('+', leftX - 18, topY + plateH/2);
|
|
|
+ ctx.textAlign = 'right';
|
|
|
+ ctx.fillText('−', rightX + plateW + 18, topY + plateH/2);
|
|
|
}
|
|
|
|
|
|
function drawInductor() {
|
|
|
const c = document.getElementById('inductorViz');
|
|
|
+ if (!c) return;
|
|
|
const ctx = c.getContext('2d');
|
|
|
+ const current = parseFloat(document.getElementById('indCurrent3').value);
|
|
|
+ const w = c.width, h = c.height;
|
|
|
+
|
|
|
ctx.fillStyle = '#f2e9d6';
|
|
|
- ctx.fillRect(0, 0, 400, 180);
|
|
|
- ctx.strokeStyle = '#1a2a4a';
|
|
|
- ctx.lineWidth = 3;
|
|
|
- ctx.beginPath();
|
|
|
- for (let i = 0; i < 5; i++) {
|
|
|
- const x = 140 + i * 24;
|
|
|
- ctx.arc(x, 90, 14, -Math.PI/2, Math.PI/2);
|
|
|
+ ctx.fillRect(0, 0, w, h);
|
|
|
+
|
|
|
+ const coilCX = w/2;
|
|
|
+ const coilCY = h/2 + 10;
|
|
|
+ const coilW = 180;
|
|
|
+ const coilH = 100;
|
|
|
+ const turns = 5;
|
|
|
+
|
|
|
+ if (current > 0) {
|
|
|
+ const lineCount = Math.max(1, Math.floor(current / 12));
|
|
|
+ const alpha = 0.15 + (current / 100) * 0.75;
|
|
|
+ const lw = 1 + (current / 100) * 2;
|
|
|
+ const maxR = 55;
|
|
|
+ ctx.strokeStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.lineWidth = lw;
|
|
|
+ for (let i = 0; i < lineCount; i++) {
|
|
|
+ const r = 12 + i * (maxR - 12) / Math.max(1, lineCount - 1);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.ellipse(coilCX, coilCY, coilW/2 + r, coilH/2 + r*0.7, 0, 0, Math.PI*2);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
}
|
|
|
- ctx.stroke();
|
|
|
- ctx.strokeStyle = 'rgba(26,74,106,0.4)';
|
|
|
- ctx.lineWidth = 1.5;
|
|
|
- for (let r = 20; r <= 60; r += 12) {
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.lineWidth = 4;
|
|
|
+ for (let i = 0; i < turns; i++) {
|
|
|
+ const x = coilCX - coilW/2 + (i + 0.5) * (coilW / turns);
|
|
|
+ const rx = (coilW / turns) * 0.55;
|
|
|
ctx.beginPath();
|
|
|
- ctx.ellipse(200, 90, r * 1.4, r, 0, 0, Math.PI*2);
|
|
|
+ ctx.ellipse(x, coilCY, rx, coilH/2, 0, 0, Math.PI*2);
|
|
|
ctx.stroke();
|
|
|
}
|
|
|
- ctx.fillStyle = '#1a2a4a';
|
|
|
- ctx.font = '12px monospace';
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.font = 'bold 13px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('B', 20, 24);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#4a5a7a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
ctx.textAlign = 'center';
|
|
|
- ctx.fillText('L', 200, 145);
|
|
|
+ ctx.fillText('витки', coilCX, h - 12);
|
|
|
}
|
|
|
|
|
|
- function drawInductive() {
|
|
|
- const canvas = document.getElementById('inductiveViz');
|
|
|
- const ctx = canvas.getContext('2d');
|
|
|
- function render() {
|
|
|
- const w = canvas.width, h = canvas.height;
|
|
|
- ctx.fillStyle = '#f2e9d6';
|
|
|
- ctx.fillRect(0, 0, w, h);
|
|
|
- const I = parseFloat(document.getElementById('indCurrent').value);
|
|
|
- ctx.fillStyle = '#8b1a1a';
|
|
|
- ctx.fillRect(80, 30, 14, h - 60);
|
|
|
- ctx.fillStyle = '#8b1a1a';
|
|
|
- ctx.font = 'bold 12px monospace';
|
|
|
- ctx.textAlign = 'center';
|
|
|
- ctx.fillText('I', 87, 20);
|
|
|
- const lineCount = Math.max(1, Math.floor(I));
|
|
|
- ctx.strokeStyle = 'rgba(139,26,26,0.3)';
|
|
|
- ctx.lineWidth = 1.5;
|
|
|
- for (let i = 1; i <= lineCount; i++) {
|
|
|
- const r = (i / lineCount) * 180;
|
|
|
- ctx.beginPath();
|
|
|
- ctx.ellipse(87, h/2, r * 0.7, r, 0, 0, Math.PI*2);
|
|
|
- ctx.stroke();
|
|
|
+ document.getElementById('capCharge').addEventListener('input', function() {
|
|
|
+ document.getElementById('capChargeVal').textContent = this.value;
|
|
|
+ drawCapacitor();
|
|
|
+ });
|
|
|
+ document.getElementById('indCurrent3').addEventListener('input', function() {
|
|
|
+ document.getElementById('indCurrentVal3').textContent = this.value;
|
|
|
+ drawInductor();
|
|
|
+ });
|
|
|
+
|
|
|
+ // === СЦЕНА 8: Индуктивная наводка (расширенная) ===
|
|
|
+ const ind8 = {
|
|
|
+ canvas: null,
|
|
|
+ ctx: null,
|
|
|
+ wire: { x: 370, w: 22, h: 260, cy: 220 },
|
|
|
+ loop: { x1: 560, y1: 150, w: 120, h: 105 },
|
|
|
+ pxPerCm: 18,
|
|
|
+ phase: 0,
|
|
|
+ lastT: 0,
|
|
|
+ dragMode: null,
|
|
|
+ dragOffset: { x: 0, y: 0 },
|
|
|
+ initialized: false
|
|
|
+ };
|
|
|
+
|
|
|
+ function initInductive() {
|
|
|
+ const c = document.getElementById('inductiveViz');
|
|
|
+ if (!c) return;
|
|
|
+ ind8.canvas = c;
|
|
|
+ ind8.ctx = c.getContext('2d');
|
|
|
+
|
|
|
+ if (!ind8.initialized) {
|
|
|
+ ind8.initialized = true;
|
|
|
+
|
|
|
+ c.addEventListener('mousedown', onInd8Down);
|
|
|
+ c.addEventListener('mousemove', onInd8Move);
|
|
|
+ c.addEventListener('mouseup', onInd8Up);
|
|
|
+ c.addEventListener('mouseleave', onInd8Up);
|
|
|
+ c.addEventListener('touchstart', onInd8Down, { passive: false });
|
|
|
+ c.addEventListener('touchmove', onInd8Move, { passive: false });
|
|
|
+ c.addEventListener('touchend', onInd8Up);
|
|
|
+ c.addEventListener('touchcancel', onInd8Up);
|
|
|
+
|
|
|
+ ['indCurrent', 'indFreq'].forEach(id => {
|
|
|
+ document.getElementById(id).addEventListener('input', updateInductive);
|
|
|
+ });
|
|
|
+ document.getElementById('indDist').addEventListener('input', function() {
|
|
|
+ applyInd8DistSlider();
|
|
|
+ updateInductive();
|
|
|
+ });
|
|
|
+ document.getElementById('indArea').addEventListener('input', function() {
|
|
|
+ applyInd8AreaSlider();
|
|
|
+ updateInductive();
|
|
|
+ });
|
|
|
+
|
|
|
+ applyInd8AreaSlider();
|
|
|
+ applyInd8DistSlider();
|
|
|
+
|
|
|
+ ind8.lastT = performance.now();
|
|
|
+ requestAnimationFrame(drawInd8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function getInd8Coords(e) {
|
|
|
+ const c = ind8.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 onInd8Down(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const p = getInd8Coords(e);
|
|
|
+ const l = ind8.loop;
|
|
|
+
|
|
|
+ const corners = [
|
|
|
+ { name: 'tl', x: l.x1, y: l.y1 },
|
|
|
+ { name: 'tr', x: l.x1 + l.w, y: l.y1 },
|
|
|
+ { name: 'bl', x: l.x1, y: l.y1 + l.h },
|
|
|
+ { name: 'br', x: l.x1 + l.w, y: l.y1 + l.h }
|
|
|
+ ];
|
|
|
+ for (const corner of corners) {
|
|
|
+ if (Math.hypot(p.x - corner.x, p.y - corner.y) < 18) {
|
|
|
+ ind8.dragMode = corner.name;
|
|
|
+ return;
|
|
|
}
|
|
|
- ctx.strokeStyle = '#2a5a2a';
|
|
|
- ctx.lineWidth = 3;
|
|
|
- ctx.beginPath();
|
|
|
- ctx.ellipse(550, h/2, 120, 70, 0, 0, Math.PI*2);
|
|
|
- ctx.stroke();
|
|
|
- ctx.fillStyle = '#2a5a2a';
|
|
|
- ctx.font = '12px monospace';
|
|
|
- ctx.fillText('петля', 550, h - 20);
|
|
|
- const emf = calcInductive();
|
|
|
- if (emf > 0.1) {
|
|
|
- ctx.strokeStyle = '#8b1a1a';
|
|
|
- ctx.lineWidth = 2;
|
|
|
- const arrowX = 550 + 100;
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(arrowX, h/2);
|
|
|
- ctx.lineTo(arrowX + 30, h/2);
|
|
|
- ctx.stroke();
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(arrowX + 30, h/2);
|
|
|
- ctx.lineTo(arrowX + 22, h/2 - 6);
|
|
|
- ctx.lineTo(arrowX + 22, h/2 + 6);
|
|
|
- ctx.closePath();
|
|
|
- ctx.fillStyle = '#8b1a1a';
|
|
|
- ctx.fill();
|
|
|
+ }
|
|
|
+
|
|
|
+ const wire = ind8.wire;
|
|
|
+ const wireLeft = wire.x - wire.w/2 - 8;
|
|
|
+ const wireRight = wire.x + wire.w/2 + 8;
|
|
|
+ const wireTop = wire.cy - wire.h/2;
|
|
|
+ const wireBottom = wire.cy + wire.h/2;
|
|
|
+ if (p.x >= wireLeft && p.x <= wireRight && p.y >= wireTop && p.y <= wireBottom) {
|
|
|
+ ind8.dragMode = 'wire';
|
|
|
+ ind8.dragOffset = { x: p.x - wire.x, y: 0 };
|
|
|
+ ind8.canvas.style.cursor = 'grabbing';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onInd8Move(e) {
|
|
|
+ const p = getInd8Coords(e);
|
|
|
+
|
|
|
+ if (!ind8.dragMode) {
|
|
|
+ const l = ind8.loop;
|
|
|
+ const corners = [
|
|
|
+ [l.x1, l.y1], [l.x1 + l.w, l.y1],
|
|
|
+ [l.x1, l.y1 + l.h], [l.x1 + l.w, l.y1 + l.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) {
|
|
|
+ ind8.canvas.style.cursor = 'nwse-resize';
|
|
|
+ } else if (Math.abs(p.x - ind8.wire.x) < 20 && p.y > 60 && p.y < 380) {
|
|
|
+ ind8.canvas.style.cursor = 'grab';
|
|
|
+ } else {
|
|
|
+ ind8.canvas.style.cursor = 'default';
|
|
|
+ }
|
|
|
+ return;
|
|
|
}
|
|
|
- render();
|
|
|
+
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ if (ind8.dragMode === 'wire') {
|
|
|
+ let newX = p.x - ind8.dragOffset.x;
|
|
|
+ const loopLeft = ind8.loop.x1;
|
|
|
+ const minDistPx = 1 * ind8.pxPerCm;
|
|
|
+ const maxDistPx = 20 * ind8.pxPerCm;
|
|
|
+ const wireRightEdge = newX + ind8.wire.w / 2;
|
|
|
+ let distPx = loopLeft - wireRightEdge;
|
|
|
+ if (distPx < minDistPx) newX = loopLeft - minDistPx - ind8.wire.w / 2;
|
|
|
+ if (distPx > maxDistPx) newX = loopLeft - maxDistPx - ind8.wire.w / 2;
|
|
|
+ if (newX < 30) newX = 30;
|
|
|
+ ind8.wire.x = newX;
|
|
|
+
|
|
|
+ const d = Math.max(1, Math.min(20, Math.round((loopLeft - (newX + ind8.wire.w / 2)) / ind8.pxPerCm)));
|
|
|
+ const slider = document.getElementById('indDist');
|
|
|
+ slider.value = d;
|
|
|
+ document.getElementById('indDistVal').textContent = d;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const l = ind8.loop;
|
|
|
+ let x1 = l.x1, y1 = l.y1, x2 = l.x1 + l.w, y2 = l.y1 + l.h;
|
|
|
+ if (ind8.dragMode === 'tl') { x1 = p.x; y1 = p.y; }
|
|
|
+ if (ind8.dragMode === 'tr') { x2 = p.x; y1 = p.y; }
|
|
|
+ if (ind8.dragMode === 'bl') { x1 = p.x; y2 = p.y; }
|
|
|
+ if (ind8.dragMode === 'br') { x2 = p.x; y2 = p.y; }
|
|
|
+
|
|
|
+ const minW = 50, minH = 50, maxW = 220, maxH = 260;
|
|
|
+
|
|
|
+ if (x2 - x1 < minW) {
|
|
|
+ if (ind8.dragMode === 'tl' || ind8.dragMode === 'bl') x1 = x2 - minW;
|
|
|
+ else x2 = x1 + minW;
|
|
|
+ }
|
|
|
+ if (x2 - x1 > maxW) {
|
|
|
+ if (ind8.dragMode === 'tl' || ind8.dragMode === 'bl') x1 = x2 - maxW;
|
|
|
+ else x2 = x1 + maxW;
|
|
|
+ }
|
|
|
+ if (y2 - y1 < minH) {
|
|
|
+ if (ind8.dragMode === 'tl' || ind8.dragMode === 'tr') y1 = y2 - minH;
|
|
|
+ else y2 = y1 + minH;
|
|
|
+ }
|
|
|
+ if (y2 - y1 > maxH) {
|
|
|
+ if (ind8.dragMode === 'tl' || ind8.dragMode === 'tr') y1 = y2 - maxH;
|
|
|
+ else y2 = y1 + maxH;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (x1 < 20) x1 = 20;
|
|
|
+ if (y1 < 20) y1 = 20;
|
|
|
+ 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;
|
|
|
+ x1 += shift; x2 += shift;
|
|
|
+ if (ind8.dragMode === 'tl' || ind8.dragMode === 'bl') {
|
|
|
+ x1 = wireRightEdge + 1 * ind8.pxPerCm;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ l.x1 = x1;
|
|
|
+ l.y1 = y1;
|
|
|
+ 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));
|
|
|
+
|
|
|
+ const distPx = l.x1 - wireRightEdge;
|
|
|
+ const distCm = Math.round(distPx / ind8.pxPerCm);
|
|
|
+ document.getElementById('indDist').value = Math.max(1, Math.min(20, distCm));
|
|
|
+ document.getElementById('indDistVal').textContent = Math.max(1, Math.min(20, distCm));
|
|
|
}
|
|
|
|
|
|
- function calcInductive() {
|
|
|
- const I = parseFloat(document.getElementById('indCurrent').value);
|
|
|
- const f = parseFloat(document.getElementById('indFreq').value);
|
|
|
+ function onInd8Up() {
|
|
|
+ if (ind8.dragMode) {
|
|
|
+ ind8.dragMode = null;
|
|
|
+ ind8.canvas.style.cursor = 'default';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyInd8DistSlider() {
|
|
|
const d = parseFloat(document.getElementById('indDist').value);
|
|
|
+ ind8.wire.x = ind8.loop.x1 - d * ind8.pxPerCm - ind8.wire.w / 2;
|
|
|
+ if (ind8.wire.x < 30) ind8.wire.x = 30;
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyInd8AreaSlider() {
|
|
|
const S = parseFloat(document.getElementById('indArea').value);
|
|
|
- if (I === 0 || f === 0 || S === 0 || d === 0) return 0;
|
|
|
- const base = (I / 10) * (f / 1000) * (S / 100) * (1 / d) * 100;
|
|
|
- return Math.min(base, 99.9);
|
|
|
+ const targetArea = Math.max(2500, S * 250);
|
|
|
+ const aspect = ind8.loop.w / ind8.loop.h || 1;
|
|
|
+ let newH = Math.sqrt(targetArea / aspect);
|
|
|
+ let newW = newH * aspect;
|
|
|
+ if (newW < 50) { newW = 50; newH = targetArea / 50; }
|
|
|
+ if (newH < 50) { newH = 50; newW = targetArea / 50; }
|
|
|
+ if (newW > 220) { newW = 220; newH = targetArea / 220; }
|
|
|
+ if (newH > 260) { newH = 260; newW = targetArea / 260; }
|
|
|
+ ind8.loop.w = newW;
|
|
|
+ ind8.loop.h = newH;
|
|
|
}
|
|
|
|
|
|
function updateInductive() {
|
|
|
@@ -1256,30 +1494,287 @@
|
|
|
document.getElementById('indFreqVal').textContent = document.getElementById('indFreq').value;
|
|
|
document.getElementById('indDistVal').textContent = document.getElementById('indDist').value;
|
|
|
document.getElementById('indAreaVal').textContent = document.getElementById('indArea').value;
|
|
|
- const val = calcInductive();
|
|
|
- document.getElementById('indResult').textContent = val.toFixed(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawInd8() {
|
|
|
+ const sceneEl = document.querySelector('.scene[data-scene="8"]');
|
|
|
+ if (!sceneEl || !sceneEl.classList.contains('active')) {
|
|
|
+ requestAnimationFrame(drawInd8);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!ind8.ctx) { requestAnimationFrame(drawInd8); return; }
|
|
|
+
|
|
|
+ const now = performance.now();
|
|
|
+ let dt = (now - ind8.lastT) / 1000;
|
|
|
+ ind8.lastT = now;
|
|
|
+ if (dt > 0.15) dt = 0.15;
|
|
|
+ if (dt < 0) dt = 0;
|
|
|
+
|
|
|
const I = parseFloat(document.getElementById('indCurrent').value);
|
|
|
const f = parseFloat(document.getElementById('indFreq').value);
|
|
|
- const d = parseFloat(document.getElementById('indDist').value);
|
|
|
- const S = parseFloat(document.getElementById('indArea').value);
|
|
|
- let msg = '';
|
|
|
- if (I === 0) msg = 'Ток = 0 → магнитного поля нет → ЭДС = 0.';
|
|
|
- else if (f === 0) msg = 'Частота = 0 (постоянный ток) → магнитный поток не меняется → ЭДС = 0. Это ключевое отличие: постоянный ток не даёт индуктивной наводки.';
|
|
|
- else if (S === 0) msg = 'Площадь петли = 0 → поток не прошивает контур → ЭДС = 0.';
|
|
|
- else {
|
|
|
- const parts = [];
|
|
|
- if (I >= 7) 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) {
|
|
|
+ ind8.phase += 2 * Math.PI * apparentF * dt;
|
|
|
+ }
|
|
|
+
|
|
|
+ const B = I * Math.cos(ind8.phase);
|
|
|
+ const Babs = Math.min(1, Math.abs(B) / 10);
|
|
|
+
|
|
|
+ const ctx = ind8.ctx;
|
|
|
+ const cw = ind8.canvas.width;
|
|
|
+ const ch = ind8.canvas.height;
|
|
|
+
|
|
|
+ ctx.fillStyle = '#f2e9d6';
|
|
|
+ ctx.fillRect(0, 0, cw, ch);
|
|
|
+
|
|
|
+ const l = ind8.loop;
|
|
|
+ const lx1 = l.x1;
|
|
|
+ const ly1 = l.y1;
|
|
|
+ const lx2 = l.x1 + l.w;
|
|
|
+ const ly2 = l.y1 + l.h;
|
|
|
+
|
|
|
+ const wireRight = ind8.wire.x + ind8.wire.w / 2;
|
|
|
+ const distPx = Math.max(1, lx1 - wireRight);
|
|
|
+ const distCm = distPx / ind8.pxPerCm;
|
|
|
+ const areaCm2 = (l.w * l.h) / 250;
|
|
|
+
|
|
|
+ drawInd8Field(ctx, I, Babs, ind8.wire.x, ind8.wire.cy);
|
|
|
+ drawInd8Wire(ctx, ind8.wire, B);
|
|
|
+ drawInd8Loop(ctx, lx1, ly1, lx2, ly2);
|
|
|
+ drawInd8Distance(ctx, wireRight, lx1, (ly1 + ly2) / 2, distCm);
|
|
|
+
|
|
|
+ let emfInst = 0;
|
|
|
+ if (f > 1 && I > 0 && areaCm2 > 0) {
|
|
|
+ emfInst = 0.02 * I * areaCm2 * apparentF * Math.sin(ind8.phase) / Math.max(1, distCm);
|
|
|
+ }
|
|
|
+ drawInd8EmfArrow(ctx, lx1, ly1, lx2, ly2, emfInst);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#4a5a7a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('S = ' + areaCm2.toFixed(0) + ' см²', lx1 + 4, ly1 - 24);
|
|
|
+
|
|
|
+ const emfEl = document.getElementById('indResult');
|
|
|
+ if (emfEl) emfEl.textContent = emfInst.toFixed(1);
|
|
|
+
|
|
|
+ const fbEl = document.getElementById('indFeedback');
|
|
|
+ if (fbEl) {
|
|
|
+ let msg = '';
|
|
|
+ if (I === 0) {
|
|
|
+ msg = 'Ток = 0 → магнитного поля нет → ЭДС = 0.';
|
|
|
+ } else if (f <= 1) {
|
|
|
+ msg = 'Частота = 0 (постоянный ток) → поле не меняется → ЭДС = 0. Ключевое отличие от гальванической наводки: постоянный ток здесь не работает.';
|
|
|
+ } else if (areaCm2 < 15) {
|
|
|
+ msg = 'Площадь петли маленькая → поток захватывается слабо.';
|
|
|
+ } else {
|
|
|
+ const parts = [];
|
|
|
+ if (I >= 7) 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;
|
|
|
}
|
|
|
- document.getElementById('indFeedback').textContent = msg;
|
|
|
+
|
|
|
+ requestAnimationFrame(drawInd8);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawInd8Field(ctx, I, Babs, cx, cy) {
|
|
|
+ if (I <= 0) return;
|
|
|
+ const numLines = Math.max(1, Math.round(I * 1.4));
|
|
|
+ const alpha = 0.08 + Babs * 0.85;
|
|
|
+ const lw = 0.8 + Babs * 2.5;
|
|
|
+
|
|
|
+ for (let i = 1; i <= numLines; i++) {
|
|
|
+ const r = 30 + (i / numLines) * 260;
|
|
|
+ const rx = r * 0.7;
|
|
|
+ const ry = r;
|
|
|
+
|
|
|
+ if (Babs > 0.4) {
|
|
|
+ ctx.shadowColor = `rgba(255,140,50,${Babs * 0.55})`;
|
|
|
+ ctx.shadowBlur = 6 + Babs * 10;
|
|
|
+ }
|
|
|
+ ctx.strokeStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.lineWidth = lw;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
|
+ ctx.stroke();
|
|
|
+ ctx.shadowBlur = 0;
|
|
|
+ ctx.shadowColor = 'transparent';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawInd8Wire(ctx, wire, B) {
|
|
|
+ const x = wire.x;
|
|
|
+ const w = wire.w;
|
|
|
+ const y = wire.cy - wire.h / 2;
|
|
|
+ const h = wire.h;
|
|
|
+
|
|
|
+ ctx.fillStyle = 'rgba(0,0,0,0.15)';
|
|
|
+ ctx.fillRect(x - w/2 + 3, y + 3, w, h);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.fillRect(x - w/2, y, w, h);
|
|
|
+ ctx.strokeStyle = '#1a2a4a';
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.strokeRect(x - w/2, y, w, h);
|
|
|
+
|
|
|
+ const dir = B >= 0 ? -1 : 1;
|
|
|
+ const numArrows = 5;
|
|
|
+ const arrowY0 = y + 40;
|
|
|
+ const arrowStep = (h - 80) / (numArrows - 1);
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
|
|
+ for (let i = 0; i < numArrows; i++) {
|
|
|
+ const ay = arrowY0 + i * arrowStep;
|
|
|
+ const tip = ay + 8 * dir;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x, tip);
|
|
|
+ ctx.lineTo(x - 6, ay);
|
|
|
+ ctx.lineTo(x + 6, ay);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.font = 'bold 14px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('I', x, y - 12);
|
|
|
+
|
|
|
+ ctx.fillStyle = '#4a5a7a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.fillText('источник', x, y + h + 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawInd8Loop(ctx, x1, y1, x2, y2) {
|
|
|
+ ctx.fillStyle = 'rgba(42,90,42,0.04)';
|
|
|
+ ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
|
|
|
+
|
|
|
+ ctx.strokeStyle = '#2a5a2a';
|
|
|
+ ctx.lineWidth = 4;
|
|
|
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
|
|
+
|
|
|
+ const corners = [[x1,y1],[x2,y1],[x1,y2],[x2,y2]];
|
|
|
+ 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();
|
|
|
+ });
|
|
|
+
|
|
|
+ ctx.fillStyle = '#2a5a2a';
|
|
|
+ ctx.font = '11px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'alphabetic';
|
|
|
+ ctx.fillText('петля', (x1+x2)/2, y2 + 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ function drawInd8Distance(ctx, x1, x2, y, distCm) {
|
|
|
+ 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 drawInd8EmfArrow(ctx, x1, y1, x2, y2, emf) {
|
|
|
+ const maxEmf = 30;
|
|
|
+ const ratio = Math.min(1, Math.abs(emf) / maxEmf);
|
|
|
+ if (ratio < 0.03) return;
|
|
|
+
|
|
|
+ const cx = (x1 + x2) / 2;
|
|
|
+ const cy = (y1 + y2) / 2;
|
|
|
+ const r = Math.min(x2 - x1, y2 - y1) * 0.28;
|
|
|
+ if (r < 8) return;
|
|
|
+
|
|
|
+ const alpha = 0.35 + ratio * 0.6;
|
|
|
+ const clockwise = emf >= 0;
|
|
|
+ const lw = 2 + ratio * 3;
|
|
|
+
|
|
|
+ const A = -Math.PI * 0.85;
|
|
|
+ const B = Math.PI * 0.55;
|
|
|
+
|
|
|
+ ctx.save();
|
|
|
+ ctx.translate(cx, cy);
|
|
|
+ ctx.strokeStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.lineWidth = lw;
|
|
|
+ ctx.lineCap = 'round';
|
|
|
+
|
|
|
+ let hx, hy, tang;
|
|
|
+ if (clockwise) {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(0, 0, r, A, B, false);
|
|
|
+ ctx.stroke();
|
|
|
+ hx = Math.cos(B) * r;
|
|
|
+ hy = Math.sin(B) * r;
|
|
|
+ tang = B + Math.PI / 2;
|
|
|
+ } else {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.arc(0, 0, r, B, A, true);
|
|
|
+ ctx.stroke();
|
|
|
+ hx = Math.cos(A) * r;
|
|
|
+ hy = Math.sin(A) * r;
|
|
|
+ tang = A - Math.PI / 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ const size = 6 + ratio * 6;
|
|
|
+ const tipX = hx + Math.cos(tang) * size;
|
|
|
+ const tipY = hy + Math.sin(tang) * size;
|
|
|
+ const perpX = Math.cos(tang + Math.PI / 2);
|
|
|
+ const perpY = Math.sin(tang + Math.PI / 2);
|
|
|
+ const baseX1 = hx + perpX * size * 0.6;
|
|
|
+ const baseY1 = hy + perpY * size * 0.6;
|
|
|
+ const baseX2 = hx - perpX * size * 0.6;
|
|
|
+ const baseY2 = hy - perpY * size * 0.6;
|
|
|
+
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(tipX, tipY);
|
|
|
+ ctx.lineTo(baseX1, baseY1);
|
|
|
+ ctx.lineTo(baseX2, baseY2);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fillStyle = `rgba(139,26,26,${alpha})`;
|
|
|
+ ctx.fill();
|
|
|
+
|
|
|
+ ctx.restore();
|
|
|
+ ctx.lineCap = 'butt';
|
|
|
+
|
|
|
+ ctx.fillStyle = '#8b1a1a';
|
|
|
+ ctx.font = 'bold 12px "Roboto Mono", monospace';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
+ const sign = emf >= 0 ? '+' : '−';
|
|
|
+ ctx.fillText(sign + Math.abs(emf).toFixed(1) + ' мВ', cx, cy + r + 18);
|
|
|
}
|
|
|
- ['indCurrent', 'indFreq', 'indDist', 'indArea'].forEach(id => {
|
|
|
- document.getElementById(id).addEventListener('input', updateInductive);
|
|
|
- });
|
|
|
|
|
|
function drawCapacitive() {
|
|
|
const canvas = document.getElementById('capacitiveViz');
|
|
|
@@ -1373,7 +1868,6 @@
|
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
const I = parseFloat(document.getElementById('galCurrent').value);
|
|
|
const R = parseFloat(document.getElementById('galRes').value);
|
|
|
- const G = parseFloat(document.getElementById('galGround').value);
|
|
|
ctx.strokeStyle = '#1a2a4a';
|
|
|
ctx.lineWidth = 4;
|
|
|
ctx.beginPath();
|