ypv 2 недель назад
Родитель
Сommit
624a3cbee2
2 измененных файлов с 657 добавлено и 146 удалено
  1. 639 145
      ПМ.03.01/3.1.400/001/index.html
  2. 18 1
      ПМ.03.01/3.1.400/001/specification.md

+ 639 - 145
ПМ.03.01/3.1.400/001/index.html

@@ -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();

+ 18 - 1
ПМ.03.01/3.1.400/001/specification.md

@@ -160,11 +160,28 @@
 
 Заполняется при разработке кода. Ожидаемые ошибки ИИ, которые нужно проверить:
 
-| № | Что попросили | Что выдал ИИ (ожидаемо) | Ошибка | Коммит |
+| № | Сцена | Ошибка| Что попросили | Что выдал ИИ (ожидаемо)  | Коммит |
 |---|---|---|---|---|---|
 | 1 | Связь индуктивной наводки с физическими величинами | «Зависит от напряжения» | Индуктивная наводка зависит от **тока**, а не от напряжения |  | 3.1.400/001_init |
 | 2 | Сцена Актуализация · 3 из 4 | магнит движется по нажатию кнопки. | Надо добавить интерактива, например магнит двигается мышью и показания гальванометра зависят от скорости изменения магнитного поля, которое тоже надо показать, например линиями |  | 3.1.400/001_a3 |
 | 3 | Сцена Актуализация · 3 из 4 | линии магнитного поля возле магнита постоянны. | если их продлить в правой части до катушки, то изменение магнитного поля станет нагляднее |  | 3.1.400/001_a3_1 |
+| 4 | Сцена Актуализация · 4 из 4 | нет интерактива | надо наглядно и интерактивно показать работу конденсатора и катушки индуктивности в разрезе наводок |  | 3.1.400/001_a3_1 |
+| 5 | Сцена Актуализация · 3 из 4 | интенсивность магнитного поля иллюстрируется густотой линий, а не только яркостью | надо добавить увеличение количества линий магнитного поля при приближении к катушке |  | 3.1.400/001_a3_2 |
+
+| 6 | 
+Механизм 1 · индуктивный | 
+мало интерактива и он неинтуитивный | 
+ползунки сила тока и частота оставляем, но при увеличении тока интенсивность магнитного поля должна визуализироваться густотой и насыщенностью линий; надо добавить визуализацию наведенной ЭДС в петле; добавить визуализацию частоты; расстояние изменяется не только бегунком но и перетягиванием проводника с током (и ползунок при этом тоже меняет значение), при перемещении же ползунка меняется и положение проводника; площадь петли тоже меняется и ползунком и перетаскиванием сторон или еще как-то | 
+
+Что изменилось в сцене 8 (Механизм 1 · индуктивный)
+Ток → густота и насыщенность линий. Количество концентрических эллипсов вокруг провода: numLines = I * 1.4 (0 при I=0, ~14 при I=10). Прозрачность линий плавает с частотой — они мерцают, показывая, что поле переменное. При f=0 мерцание прекращается, поле статичное.
+Частота видна через мерцание. Фаза phase += 2π * (f/200) * dt. На 500 Гц — 2.5 Гц визуальных колебаний. Видно, как линии становятся ярче и тусклее, и стрелка ЭДС в петле качается туда-сюда.
+ЭДС видна стрелкой внутри петли. Дуга со стрелкой, длина и толщина зависят от мгновенной ЭДС. Направление (по/против часовой) меняется при смене знака — ровно как переменный ток в контуре.
+Расстояние меняется двумя способами. Провод можно перетаскивать мышью (курсор grab/grabbing). Ползунок Расстояние синхронизирован — при перетаскивании он обновляется, а при движении ползунка двигается провод.
+Площадь меняется двумя способами. У петли четыре угла-ручки, любой можно тянуть — противоположный угол остаётся на месте. Ползунок Площадь синхронизирован с текущей площадью.
+Мгновенная ЭДС показана знаком (+/−) и числом, меняется каждый кадр. При f=0 или I=0 — ровно 0. |
+3.1.400/001_м1_2 |
+
 
 ---