Files
Learn_System/frontend/js/phys.js
T
Maxim Dolgolyov 33a91900a8 feat(phys8): Phase 0 — skeleton hub + 3 chapters + lab + phys.js/optics.js
Полная инфраструктура курса «Физика 8» (Исаченкова, 2018):
- physics_8_hub.html: палитра violet/indigo, 3 главы + ЛР + финал курса
  с 10 интегрированными боссами и ачивкой «Магистр физики 8» (+150 XP)
- physics_8_ch1.html (Тепловые, §§1–11): красный акцент
- physics_8_ch2.html (Электромагнитные, §§12–31): янтарный акцент
- physics_8_ch3.html (Световые, §§32–40): голубой акцент
- physics_8_lab.html (7 ЛР): зелёный акцент
- Расширение phys.js: tempColor, thermometer, calorimeter, createHeatBar,
  phaseGraphTT, Rseries, Rparallel
- Новый модуль optics.js: ray, refractRay, reflectRay, mirrorPlane,
  mirrorSpherical, thinLens, buildLensImage, goldenRays, eyeDiagram,
  lightObject, shadowTriangle
- Миграция 037: replace legacy children (thermal/electro/optics) на
  physics-8-ch1/ch2/ch3 + physics-8-lab; обновлён hub до 47 пунктов

BUILDERS всех § рендерят stub с указанием Phase/Wave из PLAN_PHYSICS_8.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:41:37 +03:00

403 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// phys.js — модуль физических хелперов для учебника Физика 10
// Экспорт в window.PHYS = { ... }
(function(){
'use strict';
// === Стрелка вектора (2D) ===
function drawArrow(x1, y1, x2, y2, color, width, headSize) {
width = width || 2;
headSize = headSize || 10;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy);
if (len < 1e-6) return '';
const ux = dx / len, uy = dy / len;
const px = -uy, py = ux;
const h = headSize, w = headSize * 0.6;
const bx = x2 - ux*h, by = y2 - uy*h;
const lx = bx + px*w, ly = by + py*w;
const rx = bx - px*w, ry = by - py*w;
return `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${bx.toFixed(1)}" y2="${by.toFixed(1)}" stroke="${color}" stroke-width="${width}" stroke-linecap="round"/>`
+ `<polygon points="${x2.toFixed(1)},${y2.toFixed(1)} ${lx.toFixed(1)},${ly.toFixed(1)} ${rx.toFixed(1)},${ry.toFixed(1)}" fill="${color}"/>`;
}
// === Линии электрического поля от точечного заряда ===
function fieldLinesPointCharge(cx, cy, sign, scale, numLines) {
numLines = numLines || 16;
scale = scale || 80;
let s = '';
const color = sign > 0 ? '#dc2626' : '#2563eb';
for (let i = 0; i < numLines; i++) {
const a = 2 * Math.PI * i / numLines;
const r1 = 18, r2 = scale;
const x1 = cx + r1*Math.cos(a), y1 = cy + r1*Math.sin(a);
const x2 = cx + r2*Math.cos(a), y2 = cy + r2*Math.sin(a);
if (sign > 0) s += drawArrow(x1, y1, x2, y2, color, 1.4, 7);
else s += drawArrow(x2, y2, x1, y1, color, 1.4, 7);
}
return s;
}
// === Обозначение заряда (кружок с +/-) ===
function chargeMark(cx, cy, sign, r, label) {
r = r || 14;
const color = sign > 0 ? '#dc2626' : '#2563eb';
const fill = sign > 0 ? '#fecaca' : '#bfdbfe';
let s = '';
s += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" stroke="${color}" stroke-width="2"/>`;
if (sign > 0) {
s += `<line x1="${cx-r*0.5}" y1="${cy}" x2="${cx+r*0.5}" y2="${cy}" stroke="${color}" stroke-width="2.5"/>`;
s += `<line x1="${cx}" y1="${cy-r*0.5}" x2="${cx}" y2="${cy+r*0.5}" stroke="${color}" stroke-width="2.5"/>`;
} else {
s += `<line x1="${cx-r*0.5}" y1="${cy}" x2="${cx+r*0.5}" y2="${cy}" stroke="${color}" stroke-width="2.5"/>`;
}
if (label) {
s += `<text x="${cx+r+4}" y="${cy+4}" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="${color}">${label}</text>`;
}
return s;
}
// === Магнитное поле сквозь экран (сетка крестиков или точек) ===
function magneticFieldGrid(x0, y0, w, h, cols, rows, direction) {
// direction: 'in' = крест (× — вошло в плоскость), 'out' = точка (• — вышло)
let s = '';
const dx = w / (cols - 1), dy = h / (rows - 1);
const color = '#7c3aed';
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const cx = x0 + i * dx, cy = y0 + j * dy;
s += `<circle cx="${cx}" cy="${cy}" r="6" fill="white" stroke="${color}" stroke-width="1.3"/>`;
if (direction === 'in') {
s += `<line x1="${cx-4}" y1="${cy-4}" x2="${cx+4}" y2="${cy+4}" stroke="${color}" stroke-width="1.5"/>`;
s += `<line x1="${cx-4}" y1="${cy+4}" x2="${cx+4}" y2="${cy-4}" stroke="${color}" stroke-width="1.5"/>`;
} else {
s += `<circle cx="${cx}" cy="${cy}" r="2.2" fill="${color}"/>`;
}
}
}
return s;
}
// === Молекула газа (частица) ===
function molecule(x, y, r, color) {
r = r || 4;
color = color || '#2563eb';
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${r}" fill="${color}" stroke="#0f172a" stroke-width="0.6"/>`;
}
// === Симуляция газа (упругое столкновение со стенками) ===
function createGasSim(opts) {
opts = opts || {};
const N = opts.N || 30;
const W = opts.W || 320;
const H = opts.H || 220;
const baseSpeed = opts.speed || 60; // px/s
const r = opts.r || 4;
const particles = [];
for (let i = 0; i < N; i++) {
particles.push({
x: r + Math.random() * (W - 2*r),
y: r + Math.random() * (H - 2*r),
vx: (Math.random() - 0.5) * 2 * baseSpeed,
vy: (Math.random() - 0.5) * 2 * baseSpeed
});
}
return {
W: W, H: H, r: r, particles: particles,
step(dt) {
for (const p of particles) {
p.x += p.vx * dt; p.y += p.vy * dt;
if (p.x < r) { p.x = r; p.vx = -p.vx; }
if (p.x > W - r) { p.x = W - r; p.vx = -p.vx; }
if (p.y < r) { p.y = r; p.vy = -p.vy; }
if (p.y > H - r) { p.y = H - r; p.vy = -p.vy; }
}
},
render(color) {
color = color || '#2563eb';
return particles.map(p => molecule(p.x, p.y, r, color)).join('');
},
setSpeed(scale) {
for (const p of particles) { p.vx *= scale; p.vy *= scale; }
}
};
}
// === Электрические схемы: компоненты ===
// orientation: 'h' (горизонтально, по умолчанию) или 'v' (вертикально)
function batteryEMF(x, y, EMF, orientation) {
orientation = orientation || 'h';
let s = '';
if (orientation === 'h') {
s += `<line x1="${x-3}" y1="${y-18}" x2="${x-3}" y2="${y+18}" stroke="#0f172a" stroke-width="2.5"/>`;
s += `<line x1="${x+3}" y1="${y-9}" x2="${x+3}" y2="${y+9}" stroke="#0f172a" stroke-width="2.5"/>`;
s += `<text x="${x-3}" y="${y-24}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#0f172a">+</text>`;
s += `<text x="${x+3}" y="${y-24}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#0f172a">&#8722;</text>`;
if (EMF !== undefined) s += `<text x="${x}" y="${y+34}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">&#949; = ${EMF}</text>`;
} else {
s += `<line x1="${x-18}" y1="${y-3}" x2="${x+18}" y2="${y-3}" stroke="#0f172a" stroke-width="2.5"/>`;
s += `<line x1="${x-9}" y1="${y+3}" x2="${x+9}" y2="${y+3}" stroke="#0f172a" stroke-width="2.5"/>`;
if (EMF !== undefined) s += `<text x="${x+24}" y="${y+4}" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">&#949; = ${EMF}</text>`;
}
return s;
}
function resistor(x, y, R, orientation) {
orientation = orientation || 'h';
let s = '';
if (orientation === 'h') {
s += `<rect x="${x-20}" y="${y-7}" width="40" height="14" fill="white" stroke="#0f172a" stroke-width="1.6"/>`;
if (R !== undefined) s += `<text x="${x}" y="${y+25}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">R = ${R}</text>`;
} else {
s += `<rect x="${x-7}" y="${y-20}" width="14" height="40" fill="white" stroke="#0f172a" stroke-width="1.6"/>`;
if (R !== undefined) s += `<text x="${x+18}" y="${y+4}" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">R = ${R}</text>`;
}
return s;
}
function capacitorSymbol(x, y, C, orientation) {
orientation = orientation || 'h';
let s = '';
if (orientation === 'h') {
s += `<line x1="${x-3}" y1="${y-14}" x2="${x-3}" y2="${y+14}" stroke="#0f172a" stroke-width="2.5"/>`;
s += `<line x1="${x+3}" y1="${y-14}" x2="${x+3}" y2="${y+14}" stroke="#0f172a" stroke-width="2.5"/>`;
if (C !== undefined) s += `<text x="${x}" y="${y+30}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">C = ${C}</text>`;
}
return s;
}
function ammeterSymbol(x, y, r) {
r = r || 14;
return `<circle cx="${x}" cy="${y}" r="${r}" fill="white" stroke="#0f172a" stroke-width="1.6"/>`
+ `<text x="${x}" y="${y+5}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#d97706">A</text>`;
}
function voltmeterSymbol(x, y, r) {
r = r || 14;
return `<circle cx="${x}" cy="${y}" r="${r}" fill="white" stroke="#0f172a" stroke-width="1.6"/>`
+ `<text x="${x}" y="${y+5}" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#2563eb">V</text>`;
}
function lightbulbSymbol(x, y, r) {
r = r || 14;
let s = '';
s += `<circle cx="${x}" cy="${y}" r="${r}" fill="#fef3c7" stroke="#0f172a" stroke-width="1.6"/>`;
s += `<line x1="${x-r*0.7}" y1="${y-r*0.7}" x2="${x+r*0.7}" y2="${y+r*0.7}" stroke="#0f172a" stroke-width="1.4"/>`;
s += `<line x1="${x-r*0.7}" y1="${y+r*0.7}" x2="${x+r*0.7}" y2="${y-r*0.7}" stroke="#0f172a" stroke-width="1.4"/>`;
return s;
}
function inductorSymbol(x, y, L, orientation) {
orientation = orientation || 'h';
let s = '';
if (orientation === 'h') {
for (let i = 0; i < 4; i++) {
const cx = x - 15 + i * 10;
s += `<path d="M ${cx-5} ${y} A 5 5 0 0 1 ${cx+5} ${y}" fill="none" stroke="#0f172a" stroke-width="1.6"/>`;
}
if (L !== undefined) s += `<text x="${x}" y="${y+22}" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#0f172a">L = ${L}</text>`;
}
return s;
}
function wire(x1, y1, x2, y2) {
return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#0f172a" stroke-width="1.6"/>`;
}
// === Эталонные константы ===
const CONST = {
k: 9e9, // Кулона
e: 1.6e-19, // элементарный заряд
eps0: 8.85e-12, // электрическая постоянная
kB: 1.38e-23, // Больцмана
NA: 6.022e23, // Авогадро
R: 8.314, // универсальная газовая
c: 3e8, // скорость света
g: 9.8, // ускорение свободного падения
atm: 101325, // 1 атм в Па
T0: 273.15 // ноль Цельсия в К
};
// === Конвертеры единиц ===
function celsiusToKelvin(t) { return t + 273.15; }
function kelvinToCelsius(T) { return T - 273.15; }
function atmToPa(p) { return p * 101325; }
function paToAtm(p) { return p / 101325; }
function litersToM3(V) { return V / 1000; }
function m3ToLiters(V) { return V * 1000; }
// === Расширения для Физики 8 (тепловые явления) ===
// === Цвет по температуре: 240° (синий) → 0° (красный) ===
function tempColor(t, tMin, tMax) {
const u = Math.max(0, Math.min(1, (t - tMin) / (tMax - tMin)));
const hue = 240 * (1 - u);
return `hsl(${hue.toFixed(0)},72%,52%)`;
}
// === Термометр (вертикальная шкала, столбик ртути по температуре) ===
function thermometer(x, y, h, tMin, tMax, tValue) {
// x, y — верх корпуса; h — высота столбика
const reservoirR = 11;
const tubeW = 8;
const u = Math.max(0, Math.min(1, (tValue - tMin) / (tMax - tMin)));
const fillH = h * u;
const color = tempColor(tValue, tMin, tMax);
let s = '';
// Корпус (стекло)
s += `<rect x="${x - tubeW/2}" y="${y}" width="${tubeW}" height="${h}" rx="${tubeW/2}" fill="#fff" stroke="#0f172a" stroke-width="1.4"/>`;
// Столбик ртути
s += `<rect x="${x - tubeW/2 + 1.5}" y="${y + h - fillH}" width="${tubeW - 3}" height="${fillH}" rx="${(tubeW-3)/2}" fill="${color}"/>`;
// Резервуар
s += `<circle cx="${x}" cy="${y + h + reservoirR - 1}" r="${reservoirR}" fill="${color}" stroke="#0f172a" stroke-width="1.4"/>`;
// Деления (5)
for (let i = 0; i <= 5; i++) {
const ty = y + h * i / 5;
const tv = tMax - (tMax - tMin) * i / 5;
s += `<line x1="${x + tubeW/2 + 1}" y1="${ty}" x2="${x + tubeW/2 + 6}" y2="${ty}" stroke="#0f172a" stroke-width="1"/>`;
s += `<text x="${x + tubeW/2 + 9}" y="${ty + 3.5}" font-family="JetBrains Mono,monospace" font-size="10" fill="#0f172a">${tv.toFixed(0)}</text>`;
}
// Подпись текущего значения
s += `<text x="${x - tubeW/2 - 4}" y="${y - 4}" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="${color}">${tValue.toFixed(0)} &#176;C</text>`;
return s;
}
// === Калориметр-стакан с жидкостью ===
function calorimeter(x, y, w, h, fillFrac, liquidColor) {
fillFrac = Math.max(0, Math.min(1, fillFrac || 0.6));
liquidColor = liquidColor || '#60a5fa';
const wall = 4;
const innerH = h - wall;
const liqH = innerH * fillFrac;
let s = '';
// Внешний контур
s += `<path d="M ${x} ${y} L ${x} ${y+h} Q ${x} ${y+h+6} ${x+8} ${y+h+6} L ${x+w-8} ${y+h+6} Q ${x+w} ${y+h+6} ${x+w} ${y+h} L ${x+w} ${y} L ${x+w-wall} ${y} L ${x+w-wall} ${y+h-2} L ${x+wall} ${y+h-2} L ${x+wall} ${y} Z" fill="#e0f2fe" stroke="#0f172a" stroke-width="1.5"/>`;
// Жидкость
s += `<rect x="${x+wall+1}" y="${y + innerH - liqH + wall/2}" width="${w - 2*wall - 2}" height="${liqH}" fill="${liquidColor}" opacity="0.78"/>`;
// Мениск
s += `<line x1="${x+wall+1}" y1="${y + innerH - liqH + wall/2}" x2="${x+w-wall-1}" y2="${y + innerH - liqH + wall/2}" stroke="${liquidColor}" stroke-width="1.6" opacity="0.95"/>`;
return s;
}
// === Симуляция теплопроводности по стержню (одномерное уравнение тепла) ===
// Возвращает объект с .step(dt), .render(x, y, w, h), .reset(), .setTHot(t), .setTCold(t)
function createHeatBar(opts) {
opts = opts || {};
const N = opts.N || 30;
const tHot = opts.tHot != null ? opts.tHot : 200;
const tCold = opts.tCold != null ? opts.tCold : 0;
const alpha = opts.alpha || 0.25; // коэф. диффузии (отн. ед.)
const T = new Array(N);
for (let i = 0; i < N; i++) T[i] = tCold;
return {
N: N, alpha: alpha, T: T,
_tHot: tHot, _tCold: tCold,
setTHot(v) { this._tHot = v; },
setTCold(v) { this._tCold = v; },
reset() {
for (let i = 0; i < this.N; i++) this.T[i] = this._tCold;
},
step(dt) {
// Конечно-разностное уравнение теплопроводности с граничными условиями Дирихле.
const Tnew = new Array(this.N);
Tnew[0] = this._tHot;
Tnew[this.N - 1] = this._tCold;
for (let i = 1; i < this.N - 1; i++) {
Tnew[i] = this.T[i] + this.alpha * dt * (this.T[i-1] - 2*this.T[i] + this.T[i+1]);
}
this.T = Tnew;
},
render(x, y, w, h) {
const segW = w / this.N;
const tMin = Math.min(this._tCold, this._tHot);
const tMax = Math.max(this._tCold, this._tHot);
let s = '';
for (let i = 0; i < this.N; i++) {
const c = tempColor(this.T[i], tMin, tMax);
s += `<rect x="${(x + i*segW).toFixed(1)}" y="${y}" width="${(segW + 0.5).toFixed(1)}" height="${h}" fill="${c}"/>`;
}
// Контур стержня
s += `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#0f172a" stroke-width="1.5"/>`;
return s;
}
};
}
// === График фазовых переходов T(t) с горизонтальными плато ===
// points: массив сегментов [{tStart, tEnd, Tstart, Tend, label?}]
// W, H, pad — размеры графика; tMaxAll, TminAll, TmaxAll — диапазоны
function phaseGraphTT(W, H, pad, points, tMaxAll, TminAll, TmaxAll) {
const toX = t => pad + (W - 2*pad) * t / tMaxAll;
const toY = T => H - pad - (H - 2*pad) * (T - TminAll) / (TmaxAll - TminAll);
let s = '';
// Оси
s += `<line x1="${pad}" y1="${H-pad}" x2="${W-pad}" y2="${H-pad}" stroke="#0f172a" stroke-width="1.5"/>`;
s += `<line x1="${pad}" y1="${pad}" x2="${pad}" y2="${H-pad}" stroke="#0f172a" stroke-width="1.5"/>`;
s += `<text x="${W-pad+4}" y="${H-pad+4}" font-family="Inter,sans-serif" font-size="11" fill="#0f172a">t</text>`;
s += `<text x="${pad-4}" y="${pad-4}" text-anchor="end" font-family="Inter,sans-serif" font-size="11" fill="#0f172a">T,&#176;C</text>`;
// Сегменты
let d = '';
let first = true;
for (const seg of points) {
if (first) { d += `M ${toX(seg.tStart).toFixed(1)} ${toY(seg.Tstart).toFixed(1)} `; first = false; }
d += `L ${toX(seg.tEnd).toFixed(1)} ${toY(seg.Tend).toFixed(1)} `;
}
s += `<path d="${d}" stroke="#dc2626" stroke-width="2.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>`;
// Подписи сегментов
for (const seg of points) {
if (seg.label) {
const mx = (toX(seg.tStart) + toX(seg.tEnd)) / 2;
const my = (toY(seg.Tstart) + toY(seg.Tend)) / 2 - 8;
s += `<text x="${mx.toFixed(1)}" y="${my.toFixed(1)}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="600" fill="#475569">${seg.label}</text>`;
}
}
return { svg: s, toX: toX, toY: toY };
}
// === Электронные хелперы для электрических задач ===
// Параллельное и последовательное сопротивление
function Rseries() {
let R = 0;
for (let i = 0; i < arguments.length; i++) R += arguments[i];
return R;
}
function Rparallel() {
let inv = 0;
for (let i = 0; i < arguments.length; i++) {
if (arguments[i] > 0) inv += 1 / arguments[i];
}
return inv > 0 ? 1 / inv : Infinity;
}
// === Экспорт ===
window.PHYS = {
tempColor: tempColor,
thermometer: thermometer,
calorimeter: calorimeter,
createHeatBar: createHeatBar,
phaseGraphTT: phaseGraphTT,
Rseries: Rseries,
Rparallel: Rparallel,
drawArrow: drawArrow,
fieldLinesPointCharge: fieldLinesPointCharge,
chargeMark: chargeMark,
magneticFieldGrid: magneticFieldGrid,
molecule: molecule,
createGasSim: createGasSim,
batteryEMF: batteryEMF,
resistor: resistor,
capacitorSymbol: capacitorSymbol,
ammeterSymbol: ammeterSymbol,
voltmeterSymbol: voltmeterSymbol,
lightbulbSymbol: lightbulbSymbol,
inductorSymbol: inductorSymbol,
wire: wire,
CONST: CONST,
celsiusToKelvin: celsiusToKelvin,
kelvinToCelsius: kelvinToCelsius,
atmToPa: atmToPa,
paToAtm: paToAtm,
litersToM3: litersToM3,
m3ToLiters: m3ToLiters
};
})();