feat(phys7): Phase 0 — фундамент учебника Физики 7
Полная инфраструктура: hub, 5 ch-скелетов, lab-скелет, миграция 039, расширение phys.js на 11 хелперов + 2 класса симуляций для новых тем 7-го класса. ФАЙЛЫ: - backend/src/db/migrations/039_physics_7_hub.sql — self-sufficient миграция (parent physics-7 + 6 children: ch1..ch5 + lab). Palette: sky/blue для hub, глав: indigo/violet/red/amber/emerald/cyan. - frontend/textbooks/physics_7_hub.html (862 строки) — hub с прогресс-картами 6 разделов, шпаргалкой курса в 5 mini-карточках, 10 интегрированных боссов финала курса (через ачивку «Магистр физики 7», +150 XP), темой/lang storage через ключи physics7_*. Sidebar-фикс на десктопе встроен. - frontend/textbooks/physics_7_ch1..ch5.html (350-390 строк каждый) — скелеты глав с header, paragraph selector, sidebar, прогресс/XP, goTo, search-модалом, KaTeX с delimiters, sidebar-фиксом, cache-busting ?v=20260530. Каждая глава имеет правильное число параграфов (7/6/14/8/7) + sec-finalN. - frontend/textbooks/physics_7_lab.html (306 строк) — скелет лаб. практикума на 6 ЛР с teal/cyan палитрой и ачивкой «Лаборант 7 класса» (+80 XP). - backend/scripts/gen_phys7_ch.js / gen_phys7_lab.js — генераторы из единого шаблона (для регенерации при правках инфраструктуры). PHYS.JS НОВЫЕ ХЕЛПЕРЫ (всё работает, smoke-test пройден): - forceVector(x,y,F,angle,color,label) — стрелка силы с подписью - dynamometer(x,y,h,Fmax,F) — динамометр с пружиной и шкалой - blockOnSurface(x,y,w,h,label,weights) — брусок со стопкой гирь - connectedVessels(x,y,kindA,kindB,levelY) — сообщающиеся сосуды - hydraulicPress(x,y,sSmall,sLarge,fSmall) — гидравлический пресс - mercuryBarometer(x,y,hMm) — ртутный барометр Торричелли - aneroidBarometer(cx,cy,r,p) — стрелочный барометр-анероид - uManometer(x,y,w,h,deltaH) — U-образный жидкостный манометр - rulerWithError(x,y,lenCm,mmPerDiv) — линейка со шкалой и ценой деления - bimetal(x,y,w,h,deltaT) — биметаллическая пластина (гнётся от ΔT) - expandingRod(x,y,l0,alpha,deltaT) — стержень с тепловым расширением - class HillSlideSim — тележка на горке (§42, закон сохранения; графики Ek/Ep/Etot) - class PendulumSim — математический маятник (§42, осцилляции) Все 13 экспортированы в window.PHYS, smoke-test показал физически разумные значения энергий. Parse-check + node --check проходят. Уроки phys 9 учтены сразу: cache-busting на phys.js, sidebar-фикс @media min-width:981px, delimiters для renderMathInElement. PHASE 0 DONE. Дальше: Phase 1 Wave 1 — §§1-2 (Физика как наука + Тело/явление/величина).
This commit is contained in:
+328
-1
@@ -443,6 +443,319 @@ function Rparallel() {
|
||||
return inv > 0 ? 1 / inv : Infinity;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// PHYSICS 7 HELPERS — силы, давление, гидростатика, барометры, энергия.
|
||||
// Все размеры в SVG-пикселях; физические величины в СИ.
|
||||
// ======================================================================
|
||||
|
||||
function forceVector(x, y, F, angleDeg, color, label, scale) {
|
||||
scale = scale || 1.5;
|
||||
const a = -angleDeg * Math.PI / 180;
|
||||
const x2 = x + F * scale * Math.cos(a);
|
||||
const y2 = y + F * scale * Math.sin(a);
|
||||
let s = drawArrow(x, y, x2, y2, color || '#0f172a', 2.5, 9);
|
||||
if (label) {
|
||||
const lx = x2 + 8 * Math.cos(a);
|
||||
const ly = y2 + 8 * Math.sin(a) + 4;
|
||||
s += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="${color || '#0f172a'}">${label}</text>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function dynamometer(x, y, h, Fmax, Fcurr) {
|
||||
const w = 30;
|
||||
const springTop = y + 14, springBot = y + h - 28;
|
||||
const stretch = Math.max(0, Math.min(1, Fcurr / Fmax)) * (springBot - springTop);
|
||||
const pointerY = springTop + stretch;
|
||||
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="4"/>`;
|
||||
const coils = 8;
|
||||
const coilH = (pointerY - springTop) / coils;
|
||||
let path = `M ${x + w/2} ${springTop}`;
|
||||
for (let i = 0; i < coils; i++) {
|
||||
path += ` L ${x + (i%2 ? w-6 : 6)} ${springTop + (i+0.5)*coilH}`;
|
||||
}
|
||||
path += ` L ${x + w/2} ${pointerY}`;
|
||||
s += `<path d="${path}" fill="none" stroke="#92400e" stroke-width="1.5"/>`;
|
||||
const ticks = 10;
|
||||
for (let i = 0; i <= ticks; i++) {
|
||||
const ty = springTop + (springBot - springTop) * i / ticks;
|
||||
s += `<line x1="${x + w}" y1="${ty}" x2="${x + w + 6}" y2="${ty}" stroke="#92400e" stroke-width="1.2"/>`;
|
||||
if (i % 2 === 0) {
|
||||
s += `<text x="${x + w + 10}" y="${ty + 4}" font-family="Inter,sans-serif" font-size="9" fill="#92400e">${(Fmax * i / ticks).toFixed(Fmax < 5 ? 1 : 0)}</text>`;
|
||||
}
|
||||
}
|
||||
s += `<line x1="${x + 5}" y1="${pointerY}" x2="${x + w - 5}" y2="${pointerY}" stroke="#dc2626" stroke-width="2.5"/>`;
|
||||
s += `<circle cx="${x + w/2}" cy="${pointerY + 8}" r="4" fill="#374151"/>`;
|
||||
s += `<text x="${x + w/2}" y="${y + h + 18}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#374151">F = ${Fcurr.toFixed(1)} Н</text>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function blockOnSurface(x, y, w, h, label, weights) {
|
||||
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#a78bfa" stroke="#5b21b6" stroke-width="1.5" rx="3"/>`;
|
||||
if (label) s += `<text x="${x + w/2}" y="${y + h/2 + 4}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#fff">${label}</text>`;
|
||||
if (weights && weights > 0) {
|
||||
const wH = 10, wW = w * 0.7;
|
||||
for (let i = 0; i < weights; i++) {
|
||||
const wy = y - (i + 1) * (wH + 2);
|
||||
s += `<rect x="${x + (w - wW)/2}" y="${wy}" width="${wW}" height="${wH}" fill="#475569" stroke="#1e293b" stroke-width="1" rx="2"/>`;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function connectedVessels(x, y, kindA, kindB, levelY, fluidColor) {
|
||||
fluidColor = fluidColor || '#60a5fa';
|
||||
const widths = { cylinder: 60, wide: 100, narrow: 35 };
|
||||
const wA = widths[kindA] || 60, wB = widths[kindB] || 60;
|
||||
const h = 140, gap = 50;
|
||||
const xA = x, xB = x + wA + gap;
|
||||
let s = '';
|
||||
s += `<line x1="${xA}" y1="${y}" x2="${xA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xA + wA}" y1="${y}" x2="${xA + wA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xB}" y1="${y}" x2="${xB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xB + wB}" y1="${y}" x2="${xB + wB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xA}" y1="${y + h}" x2="${xA + wA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xB}" y1="${y + h}" x2="${xB + wB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xA + wA}" y1="${y + h - 6}" x2="${xB}" y2="${y + h - 6}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<line x1="${xA + wA}" y1="${y + h}" x2="${xB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
const fillH = (y + h) - levelY;
|
||||
s += `<rect x="${xA + 1}" y="${levelY}" width="${wA - 2}" height="${fillH}" fill="${fluidColor}" opacity="0.75"/>`;
|
||||
s += `<rect x="${xB + 1}" y="${levelY}" width="${wB - 2}" height="${fillH}" fill="${fluidColor}" opacity="0.75"/>`;
|
||||
s += `<rect x="${xA + wA}" y="${y + h - 6}" width="${gap}" height="6" fill="${fluidColor}" opacity="0.75"/>`;
|
||||
s += `<line x1="${xA - 8}" y1="${levelY}" x2="${xB + wB + 8}" y2="${levelY}" stroke="#dc2626" stroke-width="1" stroke-dasharray="4 3"/>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function hydraulicPress(x, y, sSmall, sLarge, fSmall, fluidColor) {
|
||||
fluidColor = fluidColor || '#60a5fa';
|
||||
const fLarge = fSmall * sLarge / sSmall;
|
||||
const scale = 6;
|
||||
const wSmall = Math.max(20, Math.sqrt(sSmall) * scale);
|
||||
const wLarge = Math.max(40, Math.sqrt(sLarge) * scale);
|
||||
const cylH = 60, baseY = y + 110, gap = 40;
|
||||
const xS = x, xL = x + wSmall + gap;
|
||||
let s = '';
|
||||
s += `<rect x="${xS}" y="${y + 50}" width="${wSmall}" height="${cylH}" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
|
||||
s += `<rect x="${xS - 3}" y="${y + 40}" width="${wSmall + 6}" height="14" fill="#374151" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
|
||||
s += `<rect x="${xL}" y="${y + 50}" width="${wLarge}" height="${cylH}" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
|
||||
s += `<rect x="${xL - 3}" y="${y + 40}" width="${wLarge + 6}" height="14" fill="#374151" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
|
||||
s += `<rect x="${xS}" y="${baseY}" width="${(xL + wLarge) - xS}" height="10" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
|
||||
s += drawArrow(xS + wSmall/2, y + 20, xS + wSmall/2, y + 40, '#dc2626', 2.5, 8);
|
||||
s += drawArrow(xL + wLarge/2, y + 20, xL + wLarge/2, y + 40, '#10b981', 2.5, 8);
|
||||
s += `<text x="${xS + wSmall/2}" y="${y + 12}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">F_1 = ${fSmall.toFixed(0)} Н</text>`;
|
||||
s += `<text x="${xL + wLarge/2}" y="${y + 12}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#10b981">F_2 = ${fLarge.toFixed(0)} Н</text>`;
|
||||
s += `<text x="${xS + wSmall/2}" y="${y + 80}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#0f172a">S_1 = ${sSmall} см²</text>`;
|
||||
s += `<text x="${xL + wLarge/2}" y="${y + 80}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#0f172a">S_2 = ${sLarge} см²</text>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function mercuryBarometer(x, y, hMm) {
|
||||
hMm = hMm == null ? 760 : hMm;
|
||||
const maxMm = 800, scale = 0.4;
|
||||
const tubeH = maxMm * scale, tubeW = 14;
|
||||
const cupY = y + tubeH + 10, cupW = 70, cupH = 26;
|
||||
const colH = hMm * scale, colTop = y + tubeH - colH;
|
||||
let s = '';
|
||||
s += `<rect x="${x - cupW/2 + tubeW/2}" y="${cupY}" width="${cupW}" height="${cupH}" fill="#94a3b8" stroke="#0f172a" stroke-width="1.5" rx="3"/>`;
|
||||
s += `<rect x="${x - cupW/2 + tubeW/2 + 3}" y="${cupY + 5}" width="${cupW - 6}" height="${cupH - 8}" fill="#475569" opacity="0.85"/>`;
|
||||
s += `<rect x="${x}" y="${y}" width="${tubeW}" height="${tubeH + 12}" fill="#e0f2fe" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||
s += `<rect x="${x + 1}" y="${colTop}" width="${tubeW - 2}" height="${colH + 12}" fill="#475569"/>`;
|
||||
s += `<text x="${x + tubeW + 8}" y="${y + 14}" font-family="Inter,sans-serif" font-size="10" font-style="italic" fill="#64748b">вакуум</text>`;
|
||||
for (let mm = 0; mm <= maxMm; mm += 50) {
|
||||
const ty = y + tubeH - mm * scale;
|
||||
s += `<line x1="${x + tubeW}" y1="${ty}" x2="${x + tubeW + 6}" y2="${ty}" stroke="#0f172a" stroke-width="1"/>`;
|
||||
if (mm % 100 === 0) s += `<text x="${x + tubeW + 10}" y="${ty + 3}" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">${mm}</text>`;
|
||||
}
|
||||
s += `<line x1="${x - 12}" y1="${colTop}" x2="${x}" y2="${colTop}" stroke="#dc2626" stroke-width="2"/>`;
|
||||
s += `<text x="${x - 14}" y="${colTop + 4}" text-anchor="end" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">${hMm} мм</text>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function aneroidBarometer(cx, cy, r, pressurePa) {
|
||||
const pMm = pressurePa / 133.322;
|
||||
const pMin = 720, pMax = 800;
|
||||
const angDeg = -210 + 240 * Math.max(0, Math.min(1, (pMm - pMin) / (pMax - pMin)));
|
||||
const angRad = angDeg * Math.PI / 180;
|
||||
let s = '';
|
||||
s += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="#fff" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<circle cx="${cx}" cy="${cy}" r="${r - 8}" fill="none" stroke="#cbd5e1" stroke-width="1"/>`;
|
||||
for (let p = pMin; p <= pMax; p += 10) {
|
||||
const a = (-210 + 240 * (p - pMin) / (pMax - pMin)) * Math.PI / 180;
|
||||
const r1 = r - 4, r2 = r - 12;
|
||||
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);
|
||||
s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="#0f172a" stroke-width="${p % 20 === 0 ? 2 : 1}"/>`;
|
||||
if (p % 20 === 0) {
|
||||
const rT = r - 22;
|
||||
const xT = cx + rT * Math.cos(a), yT = cy + rT * Math.sin(a) + 3;
|
||||
s += `<text x="${xT.toFixed(1)}" y="${yT.toFixed(1)}" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">${p}</text>`;
|
||||
}
|
||||
}
|
||||
const tipX = cx + (r - 18) * Math.cos(angRad), tipY = cy + (r - 18) * Math.sin(angRad);
|
||||
s += `<line x1="${cx}" y1="${cy}" x2="${tipX.toFixed(1)}" y2="${tipY.toFixed(1)}" stroke="#dc2626" stroke-width="3" stroke-linecap="round"/>`;
|
||||
s += `<circle cx="${cx}" cy="${cy}" r="4" fill="#374151"/>`;
|
||||
s += `<text x="${cx}" y="${cy + r + 16}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#374151">${pMm.toFixed(0)} мм рт. ст.</text>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function uManometer(x, y, w, h, deltaH, fluidColor) {
|
||||
fluidColor = fluidColor || '#0891b2';
|
||||
const tube = 14;
|
||||
const xL = x, xR = x + w - tube;
|
||||
const baseY = y + h;
|
||||
let s = `<path d="M ${xL} ${y} L ${xL} ${baseY - tube/2} Q ${xL} ${baseY} ${xL + tube/2} ${baseY} L ${xR + tube/2} ${baseY} Q ${xR + tube} ${baseY} ${xR + tube} ${baseY - tube/2} L ${xR + tube} ${y}" fill="none" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||
s += `<path d="M ${xL + tube} ${y} L ${xL + tube} ${baseY - tube/2 - 8}" fill="none" stroke="#0f172a" stroke-width="1.5" opacity="0.4"/>`;
|
||||
s += `<path d="M ${xR} ${y} L ${xR} ${baseY - tube/2 - 8}" fill="none" stroke="#0f172a" stroke-width="1.5" opacity="0.4"/>`;
|
||||
const lLevel = y + h * 0.45 + deltaH / 2;
|
||||
const rLevel = y + h * 0.45 - deltaH / 2;
|
||||
s += `<rect x="${xL + 1}" y="${lLevel}" width="${tube - 2}" height="${baseY - lLevel}" fill="${fluidColor}" opacity="0.85"/>`;
|
||||
s += `<rect x="${xR + 1}" y="${rLevel}" width="${tube - 2}" height="${baseY - rLevel}" fill="${fluidColor}" opacity="0.85"/>`;
|
||||
if (Math.abs(deltaH) > 1) {
|
||||
s += `<line x1="${xR + tube + 6}" y1="${lLevel}" x2="${xR + tube + 6}" y2="${rLevel}" stroke="#dc2626" stroke-width="1.5"/>`;
|
||||
s += `<text x="${xR + tube + 10}" y="${(lLevel + rLevel)/2 + 4}" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#dc2626">Δh</text>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function rulerWithError(x, y, lenCm, mmPerDiv) {
|
||||
mmPerDiv = mmPerDiv || 1;
|
||||
const pxPerCm = 30;
|
||||
const w = lenCm * pxPerCm, h = 28;
|
||||
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="2"/>`;
|
||||
const totalDivs = (lenCm * 10) / mmPerDiv;
|
||||
for (let i = 0; i <= totalDivs; i++) {
|
||||
const tx = x + (i * mmPerDiv / 10) * pxPerCm;
|
||||
const mm = i * mmPerDiv;
|
||||
const isCm = mm % 10 === 0, isHalfCm = mm % 5 === 0;
|
||||
const tickH = isCm ? 12 : (isHalfCm ? 8 : 5);
|
||||
s += `<line x1="${tx}" y1="${y}" x2="${tx}" y2="${y + tickH}" stroke="#0f172a" stroke-width="${isCm ? 1.5 : 1}"/>`;
|
||||
if (isCm) s += `<text x="${tx}" y="${y + h - 4}" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" font-weight="600" fill="#0f172a">${mm/10}</text>`;
|
||||
}
|
||||
s += `<text x="${x + w + 6}" y="${y + h/2 + 4}" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#92400e">см</text>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function bimetal(x, y, w, h, deltaT) {
|
||||
const ang = Math.max(-30, Math.min(30, deltaT * 0.3));
|
||||
let s = `<g transform="translate(${x},${y}) rotate(${ang} 0 ${h/2})">`;
|
||||
s += `<rect x="0" y="0" width="${w}" height="${h/2}" fill="#fbbf24" stroke="#92400e" stroke-width="1"/>`;
|
||||
s += `<rect x="0" y="${h/2}" width="${w}" height="${h/2}" fill="#94a3b8" stroke="#374151" stroke-width="1"/>`;
|
||||
s += `<text x="${w/2}" y="${h + 14}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#374151">ΔT = ${deltaT.toFixed(0)} °C</text>`;
|
||||
s += `</g>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
function expandingRod(x, y, l0, alpha, deltaT) {
|
||||
const dl = l0 * alpha * deltaT * 1000;
|
||||
const len = l0 + dl;
|
||||
const h = 16;
|
||||
const color = tempColor(deltaT, 0, 100);
|
||||
let s = `<rect x="${x}" y="${y}" width="${len.toFixed(1)}" height="${h}" fill="${color}" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
|
||||
s += `<line x1="${x + l0}" y1="${y - 4}" x2="${x + l0}" y2="${y + h + 4}" stroke="#0f172a" stroke-width="1" stroke-dasharray="3 2"/>`;
|
||||
if (dl > 1) {
|
||||
s += drawArrow(x + l0, y + h + 14, x + len, y + h + 14, '#dc2626', 1.8, 6);
|
||||
s += `<text x="${x + l0 + dl/2}" y="${y + h + 28}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#dc2626">Δl = ${dl.toFixed(1)} px</text>`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// === HillSlideSim — тележка скатывается с горки (для §42, закон сохранения) ===
|
||||
class HillSlideSim {
|
||||
constructor(opts) {
|
||||
opts = opts || {};
|
||||
this.x0 = opts.x0 || 30;
|
||||
this.y0 = opts.y0 || 200;
|
||||
this.hStart = opts.hStart != null ? opts.hStart : 5;
|
||||
this.m = opts.mass || 1;
|
||||
this.g = opts.g || 9.8;
|
||||
this.friction = opts.friction || 0;
|
||||
this.scale = opts.scale || 30;
|
||||
this.reset();
|
||||
}
|
||||
reset() { this.t = 0; this.x = 0; this.v = 0; this.h = this.hStart; }
|
||||
step(dt) {
|
||||
const gEff = this.g * (1 - this.friction);
|
||||
this.t += dt;
|
||||
const dropped = this.hStart - this.h;
|
||||
if (this.h <= 0) { this.h = 0; this.v = Math.sqrt(2 * gEff * this.hStart); return; }
|
||||
this.v = Math.sqrt(2 * gEff * Math.max(0, dropped));
|
||||
this.x += this.v * dt;
|
||||
const L = this.hStart * 4;
|
||||
const xRel = Math.min(this.x, L) / L;
|
||||
this.h = this.hStart * Math.pow(1 - xRel, 2);
|
||||
}
|
||||
getEnergies() {
|
||||
return {
|
||||
Ek: 0.5 * this.m * this.v * this.v,
|
||||
Ep: this.m * this.g * this.h,
|
||||
Etot: 0.5 * this.m * this.v * this.v + this.m * this.g * this.h
|
||||
};
|
||||
}
|
||||
renderProfile() {
|
||||
const L = this.hStart * 4;
|
||||
const pxL = L * this.scale, pxH = this.hStart * this.scale;
|
||||
const baseY = this.y0;
|
||||
let path = `M ${this.x0} ${baseY - pxH}`;
|
||||
const N = 40;
|
||||
for (let i = 1; i <= N; i++) {
|
||||
const xRel = i / N;
|
||||
const yRel = Math.pow(1 - xRel, 2);
|
||||
path += ` L ${(this.x0 + pxL * xRel).toFixed(1)} ${(baseY - pxH * yRel).toFixed(1)}`;
|
||||
}
|
||||
let s = '';
|
||||
s += `<line x1="${this.x0 - 10}" y1="${baseY}" x2="${this.x0 + pxL + 30}" y2="${baseY}" stroke="#0f172a" stroke-width="2"/>`;
|
||||
s += `<path d="${path} L ${this.x0 + pxL} ${baseY} L ${this.x0} ${baseY} Z" fill="#86efac" opacity="0.6" stroke="#10b981" stroke-width="1.5"/>`;
|
||||
const cartX = this.x0 + this.x * this.scale;
|
||||
const cartY = baseY - this.h * this.scale;
|
||||
s += `<rect x="${(cartX - 10).toFixed(1)}" y="${(cartY - 14).toFixed(1)}" width="20" height="12" fill="#dc2626" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
|
||||
s += `<circle cx="${(cartX - 5).toFixed(1)}" cy="${(cartY - 2).toFixed(1)}" r="3" fill="#0f172a"/>`;
|
||||
s += `<circle cx="${(cartX + 5).toFixed(1)}" cy="${(cartY - 2).toFixed(1)}" r="3" fill="#0f172a"/>`;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// === PendulumSim — математический маятник (для §42) ===
|
||||
class PendulumSim {
|
||||
constructor(opts) {
|
||||
opts = opts || {};
|
||||
this.cx = opts.cx || 150;
|
||||
this.cy = opts.cy || 40;
|
||||
this.L = opts.length || 1.2;
|
||||
this.m = opts.mass || 0.5;
|
||||
this.g = opts.g || 9.8;
|
||||
this.phi0 = (opts.angleDeg || 25) * Math.PI / 180;
|
||||
this.scale = opts.scale || 80;
|
||||
this.reset();
|
||||
}
|
||||
reset() { this.t = 0; this.phi = this.phi0; this.omega = 0; }
|
||||
step(dt) {
|
||||
const alpha = -(this.g / this.L) * Math.sin(this.phi);
|
||||
this.omega += alpha * dt;
|
||||
this.phi += this.omega * dt;
|
||||
this.t += dt;
|
||||
}
|
||||
getEnergies() {
|
||||
const h = this.L * (1 - Math.cos(this.phi));
|
||||
const v = Math.abs(this.omega) * this.L;
|
||||
return {
|
||||
Ek: 0.5 * this.m * v * v,
|
||||
Ep: this.m * this.g * h,
|
||||
Etot: 0.5 * this.m * v * v + this.m * this.g * h
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const pxL = this.L * this.scale;
|
||||
const bx = this.cx + pxL * Math.sin(this.phi);
|
||||
const by = this.cy + pxL * Math.cos(this.phi);
|
||||
let s = '';
|
||||
s += `<rect x="${this.cx - 30}" y="${this.cy - 8}" width="60" height="8" fill="#475569" stroke="#0f172a" stroke-width="1"/>`;
|
||||
s += `<line x1="${this.cx}" y1="${this.cy}" x2="${bx.toFixed(1)}" y2="${by.toFixed(1)}" stroke="#0f172a" stroke-width="1.5"/>`;
|
||||
s += `<circle cx="${bx.toFixed(1)}" cy="${by.toFixed(1)}" r="9" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5"/>`;
|
||||
s += `<circle cx="${this.cx}" cy="${this.cy}" r="3" fill="#0f172a"/>`;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// === Экспорт ===
|
||||
window.PHYS = {
|
||||
tempColor: tempColor,
|
||||
@@ -473,6 +786,20 @@ window.PHYS = {
|
||||
atmToPa: atmToPa,
|
||||
paToAtm: paToAtm,
|
||||
litersToM3: litersToM3,
|
||||
m3ToLiters: m3ToLiters
|
||||
m3ToLiters: m3ToLiters,
|
||||
// Physics 7 — Phase 0
|
||||
forceVector: forceVector,
|
||||
dynamometer: dynamometer,
|
||||
blockOnSurface: blockOnSurface,
|
||||
connectedVessels: connectedVessels,
|
||||
hydraulicPress: hydraulicPress,
|
||||
mercuryBarometer: mercuryBarometer,
|
||||
aneroidBarometer: aneroidBarometer,
|
||||
uManometer: uManometer,
|
||||
rulerWithError: rulerWithError,
|
||||
bimetal: bimetal,
|
||||
expandingRod: expandingRod,
|
||||
HillSlideSim: HillSlideSim,
|
||||
PendulumSim: PendulumSim
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user