33a91900a8
Полная инфраструктура курса «Физика 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>
285 lines
14 KiB
JavaScript
285 lines
14 KiB
JavaScript
// optics.js — модуль хелперов геометрической оптики для учебника Физика 8
|
|
// Экспорт в window.OPTICS = { ... }
|
|
(function(){
|
|
'use strict';
|
|
|
|
// === Палитра ===
|
|
const COLOR = {
|
|
ray: '#fbbf24',
|
|
rayIncident:'#0891b2',
|
|
rayReflected:'#10b981',
|
|
rayRefracted:'#a855f7',
|
|
normal: '#94a3b8',
|
|
lensConv: '#22c55e',
|
|
lensDiv: '#f97316',
|
|
mirror: '#475569',
|
|
hatch: '#94a3b8',
|
|
axis: '#cbd5e1',
|
|
focus: '#1d4ed8',
|
|
imageReal: '#7c3aed',
|
|
imageVirtual:'#a78bfa'
|
|
};
|
|
|
|
// === Стрелка-наконечник (внутренний помощник) ===
|
|
function arrowHead(x, y, ux, uy, size, color){
|
|
const w = size * 0.55;
|
|
const px = -uy, py = ux;
|
|
const bx = x - ux*size, by = y - uy*size;
|
|
const lx = bx + px*w, ly = by + py*w;
|
|
const rx = bx - px*w, ry = by - py*w;
|
|
return `<polygon points="${x.toFixed(1)},${y.toFixed(1)} ${lx.toFixed(1)},${ly.toFixed(1)} ${rx.toFixed(1)},${ry.toFixed(1)}" fill="${color}"/>`;
|
|
}
|
|
|
|
// === Луч (линия + стрелка посередине) ===
|
|
// dashed: true → пунктир (для виртуальных продолжений)
|
|
function ray(x1, y1, x2, y2, color, dashed){
|
|
color = color || COLOR.ray;
|
|
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 dash = dashed ? ' stroke-dasharray="6 4"' : '';
|
|
let s = '';
|
|
s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"${dash}/>`;
|
|
// Стрелка-наконечник в точке (x2,y2)
|
|
if (!dashed) s += arrowHead(x2, y2, ux, uy, 9, color);
|
|
return s;
|
|
}
|
|
|
|
// === Падающий, отражённый, преломлённый лучи + нормаль ===
|
|
// На границе двух сред (горизонтальной) с нормалью вверх
|
|
function refractRay(x0, y0, angleInDeg, n1, n2, len){
|
|
// angleInDeg — угол падения от нормали (в градусах)
|
|
// Возвращает SVG: падающий луч (сверху-слева к (x0,y0)) + нормаль + отражённый + преломлённый
|
|
len = len || 80;
|
|
const a1 = angleInDeg * Math.PI / 180;
|
|
const sinA2 = (n1/n2) * Math.sin(a1);
|
|
const tir = Math.abs(sinA2) > 1; // полное внутреннее отражение
|
|
const a2 = tir ? 0 : Math.asin(sinA2);
|
|
|
|
let s = '';
|
|
// Граница
|
|
s += `<line x1="${x0-len*1.5}" y1="${y0}" x2="${x0+len*1.5}" y2="${y0}" stroke="${COLOR.mirror}" stroke-width="1.5"/>`;
|
|
// Нормаль (вверх и вниз пунктиром)
|
|
s += `<line x1="${x0}" y1="${y0-len*0.9}" x2="${x0}" y2="${y0+len*0.9}" stroke="${COLOR.normal}" stroke-width="1.2" stroke-dasharray="5 3"/>`;
|
|
// Падающий луч (приходит сверху-слева)
|
|
const xi = x0 - len*Math.sin(a1), yi = y0 - len*Math.cos(a1);
|
|
s += ray(xi, yi, x0, y0, COLOR.rayIncident);
|
|
// Отражённый луч (вверх-вправо)
|
|
const xr = x0 + len*Math.sin(a1), yr = y0 - len*Math.cos(a1);
|
|
s += ray(x0, y0, xr, yr, COLOR.rayReflected);
|
|
// Преломлённый луч (вниз)
|
|
if (!tir){
|
|
const xt = x0 + len*Math.sin(a2), yt = y0 + len*Math.cos(a2);
|
|
s += ray(x0, y0, xt, yt, COLOR.rayRefracted);
|
|
}
|
|
return { svg: s, a1: a1, a2: a2, tir: tir };
|
|
}
|
|
|
|
// === Отражение от плоского зеркала ===
|
|
// Зеркало горизонтальное на y = y0
|
|
function reflectRay(x0, y0, angleInDeg, len){
|
|
len = len || 80;
|
|
const a = angleInDeg * Math.PI / 180;
|
|
let s = '';
|
|
// Зеркало
|
|
s += `<line x1="${x0-len*1.4}" y1="${y0}" x2="${x0+len*1.4}" y2="${y0}" stroke="${COLOR.mirror}" stroke-width="2"/>`;
|
|
// Штриховка с обратной стороны
|
|
for (let i = -len; i <= len; i += 8){
|
|
s += `<line x1="${x0+i}" y1="${y0}" x2="${x0+i+5}" y2="${y0+6}" stroke="${COLOR.hatch}" stroke-width="1"/>`;
|
|
}
|
|
// Нормаль
|
|
s += `<line x1="${x0}" y1="${y0-len*0.9}" x2="${x0}" y2="${y0+10}" stroke="${COLOR.normal}" stroke-width="1.2" stroke-dasharray="5 3"/>`;
|
|
// Падающий
|
|
const xi = x0 - len*Math.sin(a), yi = y0 - len*Math.cos(a);
|
|
s += ray(xi, yi, x0, y0, COLOR.rayIncident);
|
|
// Отражённый
|
|
const xr = x0 + len*Math.sin(a), yr = y0 - len*Math.cos(a);
|
|
s += ray(x0, y0, xr, yr, COLOR.rayReflected);
|
|
return s;
|
|
}
|
|
|
|
// === Плоское зеркало (отдельный элемент) ===
|
|
function mirrorPlane(x, y, len, angleDeg){
|
|
angleDeg = angleDeg || 0;
|
|
const a = angleDeg * Math.PI / 180;
|
|
const ca = Math.cos(a), sa = Math.sin(a);
|
|
const x1 = x - len/2*ca, y1 = y - len/2*sa;
|
|
const x2 = x + len/2*ca, y2 = y + len/2*sa;
|
|
const nx = -sa, ny = ca; // нормаль "наружу"
|
|
let s = `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${COLOR.mirror}" stroke-width="2.5"/>`;
|
|
// Штриховка с тыльной стороны
|
|
for (let t = -len/2 + 3; t <= len/2; t += 8){
|
|
const bx = x + t*ca, by = y + t*sa;
|
|
s += `<line x1="${bx.toFixed(1)}" y1="${by.toFixed(1)}" x2="${(bx - nx*6).toFixed(1)}" y2="${(by - ny*6).toFixed(1)}" stroke="${COLOR.hatch}" stroke-width="1"/>`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// === Сферическое зеркало (вогнутое/выпуклое) ===
|
|
// kind: 'concave' (вогнутое — фокусирует) | 'convex' (выпуклое — рассеивает)
|
|
function mirrorSpherical(cx, cy, R, kind, halfH){
|
|
halfH = halfH || R * 0.6;
|
|
const sign = (kind === 'convex') ? -1 : 1;
|
|
// Дуга
|
|
const x1 = cx - sign*R*0.15, y1 = cy - halfH;
|
|
const x2 = cx - sign*R*0.15, y2 = cy + halfH;
|
|
const sweep = sign > 0 ? 0 : 1;
|
|
let s = `<path d="M ${x1} ${y1} A ${R} ${R} 0 0 ${sweep} ${x2} ${y2}" fill="none" stroke="${COLOR.mirror}" stroke-width="2.5"/>`;
|
|
// Главная оптическая ось
|
|
s += `<line x1="${cx-R}" y1="${cy}" x2="${cx+R}" y2="${cy}" stroke="${COLOR.axis}" stroke-width="1" stroke-dasharray="4 3"/>`;
|
|
return s;
|
|
}
|
|
|
|
// === Тонкая линза ===
|
|
// kind: 'converging' (двусторонне-выпуклая, |) | 'diverging' (двусторонне-вогнутая, |)
|
|
// Возвращает SVG с осью, фокусами F и 2F
|
|
function thinLens(cx, cy, halfH, F, kind){
|
|
halfH = halfH || 70;
|
|
F = F || 80;
|
|
const color = (kind === 'diverging') ? COLOR.lensDiv : COLOR.lensConv;
|
|
let s = '';
|
|
// Главная оптическая ось
|
|
s += `<line x1="${cx - 2.5*F}" y1="${cy}" x2="${cx + 2.5*F}" y2="${cy}" stroke="${COLOR.axis}" stroke-width="1.2"/>`;
|
|
// Линза — вертикальный овал
|
|
s += `<line x1="${cx}" y1="${cy-halfH}" x2="${cx}" y2="${cy+halfH}" stroke="${color}" stroke-width="2.5"/>`;
|
|
if (kind === 'diverging'){
|
|
// Стрелки наружу (рассеивающая)
|
|
s += arrowHead(cx, cy - halfH, 0, -1, 10, color);
|
|
s += arrowHead(cx, cy + halfH, 0, 1, 10, color);
|
|
} else {
|
|
// Стрелки внутрь (собирающая) — вершины на оси
|
|
s += arrowHead(cx - 6, cy - halfH + 8, -0.6, -0.8, 10, color);
|
|
s += arrowHead(cx + 6, cy - halfH + 8, 0.6, -0.8, 10, color);
|
|
s += arrowHead(cx - 6, cy + halfH - 8, -0.6, 0.8, 10, color);
|
|
s += arrowHead(cx + 6, cy + halfH - 8, 0.6, 0.8, 10, color);
|
|
}
|
|
// Фокусы F и 2F
|
|
s += `<circle cx="${cx-F}" cy="${cy}" r="3" fill="${COLOR.focus}"/>`;
|
|
s += `<circle cx="${cx+F}" cy="${cy}" r="3" fill="${COLOR.focus}"/>`;
|
|
s += `<text x="${cx-F-3}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="${COLOR.focus}">F</text>`;
|
|
s += `<text x="${cx+F-3}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="${COLOR.focus}">F</text>`;
|
|
s += `<circle cx="${cx-2*F}" cy="${cy}" r="2.5" fill="${COLOR.focus}" opacity=".6"/>`;
|
|
s += `<circle cx="${cx+2*F}" cy="${cy}" r="2.5" fill="${COLOR.focus}" opacity=".6"/>`;
|
|
s += `<text x="${cx-2*F-5}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="11" opacity=".7" fill="${COLOR.focus}">2F</text>`;
|
|
s += `<text x="${cx+2*F-5}" y="${cy+18}" font-family="JetBrains Mono,monospace" font-size="11" opacity=".7" fill="${COLOR.focus}">2F</text>`;
|
|
return s;
|
|
}
|
|
|
|
// === Построение изображения в тонкой линзе ===
|
|
// F — фокусное расстояние (положительное для собирающей, отрицательное для рассеивающей)
|
|
// d — расстояние от предмета до линзы (положительное)
|
|
// h — высота предмета (положительная)
|
|
// Возвращает { f: расстояние до изображения, h2: высота, virtual: bool, kind: 'real'|'virtual' }
|
|
function buildLensImage(F, d, h){
|
|
// 1/F = 1/d + 1/f → f = d·F / (d - F)
|
|
if (Math.abs(d - F) < 1e-6){
|
|
return { f: Infinity, h2: -Infinity, virtual: false, kind: 'infinity' };
|
|
}
|
|
const f = d * F / (d - F);
|
|
const h2 = -h * f / d; // увеличение: h2/h = -f/d
|
|
const virtual = (F > 0) ? (d < F) : true; // для рассеивающей всегда мнимое
|
|
return { f: f, h2: h2, virtual: virtual, kind: virtual ? 'virtual' : 'real' };
|
|
}
|
|
|
|
// === Три "золотых" луча для построения изображения ===
|
|
// Возвращает SVG трёх лучей: через центр, параллельно оси, через передний фокус.
|
|
// objX, objY — координата предмета (вершина стрелки)
|
|
// lensX, lensY — центр линзы
|
|
// F (px) — фокусное расстояние (в пикселях)
|
|
function goldenRays(objX, objY, lensX, lensY, F){
|
|
let s = '';
|
|
const dx = lensX - objX;
|
|
const dy = lensY - objY;
|
|
// Луч 1: параллельно оси → проходит через F с другой стороны
|
|
const y1At = objY; // приходит на линзу на высоте objY
|
|
s += ray(objX, y1At, lensX, y1At, COLOR.rayIncident);
|
|
s += ray(lensX, y1At, lensX + 2.5*F, lensY + (y1At - lensY) * (-2.5*F)/(F * (objY < lensY ? 1 : -1)), COLOR.rayIncident);
|
|
// Луч 2: через центр — продолжается без преломления
|
|
s += ray(objX, objY, lensX + dx, lensY + dy, COLOR.rayReflected);
|
|
// Луч 3: через передний фокус → выходит параллельно оси
|
|
// (вычислим точку пересечения с линзой)
|
|
const slope = (lensY - objY) / (objX - (lensX - F));
|
|
const yAtLens = lensY - slope * F; // упрощённо
|
|
s += ray(objX, objY, lensX, yAtLens, COLOR.rayRefracted);
|
|
s += ray(lensX, yAtLens, lensX + 2*F, yAtLens, COLOR.rayRefracted);
|
|
return s;
|
|
}
|
|
|
|
// === Глаз (упрощённая схема) ===
|
|
// accommodation: 0..1, где 0 — расслабленный (дальний предмет), 1 — напряжённый (близкий)
|
|
function eyeDiagram(cx, cy, R, accommodation){
|
|
accommodation = accommodation || 0;
|
|
let s = '';
|
|
// Глазное яблоко
|
|
s += `<circle cx="${cx}" cy="${cy}" r="${R}" fill="#fff" stroke="#0f172a" stroke-width="1.6"/>`;
|
|
// Роговица — выпуклость спереди (слева)
|
|
s += `<path d="M ${cx-R*0.95} ${cy-R*0.5} Q ${cx-R*1.15} ${cy} ${cx-R*0.95} ${cy+R*0.5}" fill="#bfdbfe" stroke="#0f172a" stroke-width="1.4"/>`;
|
|
// Хрусталик — эллипс изнутри, форма зависит от accommodation
|
|
const lensHalfH = R * 0.45;
|
|
const lensW = R * (0.10 + 0.10 * accommodation); // толще при напряжении
|
|
s += `<ellipse cx="${cx-R*0.6}" cy="${cy}" rx="${lensW}" ry="${lensHalfH}" fill="#fef3c7" stroke="${COLOR.lensConv}" stroke-width="1.6"/>`;
|
|
// Сетчатка — задняя часть
|
|
s += `<path d="M ${cx+R*0.95} ${cy-R*0.8} A ${R} ${R} 0 0 1 ${cx+R*0.95} ${cy+R*0.8}" fill="none" stroke="${COLOR.imageReal}" stroke-width="2"/>`;
|
|
// Зрительный нерв
|
|
s += `<path d="M ${cx+R} ${cy} L ${cx+R*1.4} ${cy+R*0.3}" stroke="#0f172a" stroke-width="2.5"/>`;
|
|
return s;
|
|
}
|
|
|
|
// === Источник света / предмет ===
|
|
// kind: 'point' (точечный звезда) | 'arrow' (вертикальная стрелка-предмет)
|
|
function lightObject(x, y, h, kind){
|
|
kind = kind || 'arrow';
|
|
if (kind === 'point'){
|
|
let s = `<circle cx="${x}" cy="${y}" r="6" fill="${COLOR.ray}" stroke="#0f172a" stroke-width="1"/>`;
|
|
// Лучики
|
|
for (let i = 0; i < 8; i++){
|
|
const a = i * Math.PI / 4;
|
|
const x1 = x + 8*Math.cos(a), y1 = y + 8*Math.sin(a);
|
|
const x2 = x + 13*Math.cos(a), y2 = y + 13*Math.sin(a);
|
|
s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${COLOR.ray}" stroke-width="1.5" stroke-linecap="round"/>`;
|
|
}
|
|
return s;
|
|
}
|
|
// Стрелка-предмет: основание на оси (y), вершина на (y - h)
|
|
const tipY = y - h;
|
|
const color = '#0f172a';
|
|
let s = `<line x1="${x}" y1="${y}" x2="${x}" y2="${tipY}" stroke="${color}" stroke-width="2.5"/>`;
|
|
s += arrowHead(x, tipY, 0, -1, 9, color);
|
|
return s;
|
|
}
|
|
|
|
// === Тень и полутень ===
|
|
// Источник в (sx, sy), непрозрачный объект — отрезок [ox-or .. ox+or] на высоте oy.
|
|
// Возвращает SVG полупрозрачных треугольников тени/полутени, падающих на экран y = screenY.
|
|
function shadowTriangle(sx, sy, ox, oy, or, screenY){
|
|
// Касательные от источника к краям объекта дают полутень.
|
|
// Лучи через центр объекта дают полную тень.
|
|
// Простейшая реализация для точечного источника: только тень.
|
|
const dy = screenY - sy;
|
|
const k = dy / (oy - sy);
|
|
const x1 = sx + (ox - or - sx) * k;
|
|
const x2 = sx + (ox + or - sx) * k;
|
|
let s = '';
|
|
s += `<polygon points="${(ox-or).toFixed(1)},${oy} ${(ox+or).toFixed(1)},${oy} ${x2.toFixed(1)},${screenY} ${x1.toFixed(1)},${screenY}" fill="rgba(15,23,42,.35)" stroke="none"/>`;
|
|
return s;
|
|
}
|
|
|
|
// === Экспорт ===
|
|
window.OPTICS = {
|
|
COLOR: COLOR,
|
|
ray: ray,
|
|
refractRay: refractRay,
|
|
reflectRay: reflectRay,
|
|
mirrorPlane: mirrorPlane,
|
|
mirrorSpherical: mirrorSpherical,
|
|
thinLens: thinLens,
|
|
buildLensImage: buildLensImage,
|
|
goldenRays: goldenRays,
|
|
eyeDiagram: eyeDiagram,
|
|
lightObject: lightObject,
|
|
shadowTriangle: shadowTriangle
|
|
};
|
|
|
|
})();
|