feat(opticsbench): учебное построение характеристических лучей
Для «Предмет» + «Характ. лучи» (один предмет, одна линза): - подписи лучей 1/2/3 у предмета - точка изображения = пересечение финальных отрезков лучей 1 и 2 - стрелка-изображение (основание на оси → вершина в точке изображения) - мнимое изображение: пунктирные продления расходящихся лучей назад к мнимой точке (слева от линзы); подпись «изображение»/«мнимое изобр.» - проверено численно: предмет за 2F → реальное справа, внутри F → мнимое слева - bump opticsbench.js?v=10 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2621,8 +2621,8 @@ class BenchSim {
|
||||
const rays = [];
|
||||
// white light → one sub-ray per spectral sample (they coincide until a prism disperses them)
|
||||
const wls = window._obWhiteLight ? OB_SPECTRAL.map(s => s.nm) : [window._obWavelength || 540];
|
||||
const push = (x, y, ang) => {
|
||||
for (const wl of wls) rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), wl, pts: [{ x, y }], alive: true, bounces: 0 });
|
||||
const push = (x, y, ang, role) => {
|
||||
for (const wl of wls) rays.push({ x, y, dx: Math.cos(ang), dy: Math.sin(ang), wl, role: role || null, pts: [{ x, y }], alive: true, bounces: 0 });
|
||||
};
|
||||
const aim = (this.source.ang || 0) * Math.PI / 180;
|
||||
if (this.source.kind === 'single') {
|
||||
@@ -2652,11 +2652,11 @@ class BenchSim {
|
||||
// textbook construction: 2–3 characteristic rays from the tip + axial ray from the base
|
||||
const lensX = firstLens.xf * this.W, f = firstLens.f;
|
||||
const aimAt = (tx, ty) => Math.atan2(ty - tipY, tx - sx);
|
||||
push(sx, tipY, 0); // 1) parallel to axis → through far focus F'
|
||||
push(sx, tipY, aimAt(lensX, axis)); // 2) through the optical centre → straight
|
||||
const Fx = lensX - f; // front focal point
|
||||
if (f > 0 && Fx > sx + 5) push(sx, tipY, aimAt(Fx, axis)); // 3) through F → emerges parallel
|
||||
push(sx, baseY, 0); // base lies on the axis
|
||||
push(sx, tipY, 0, 'char1'); // 1) parallel to axis → through far focus F'
|
||||
push(sx, tipY, aimAt(lensX, axis), 'char2'); // 2) through the optical centre → straight
|
||||
const Fx = lensX - f; // front focal point
|
||||
if (f > 0 && Fx > sx + 5) push(sx, tipY, aimAt(Fx, axis), 'char3'); // 3) through F → emerges parallel
|
||||
push(sx, baseY, 0, 'base'); // base lies on the axis
|
||||
} else {
|
||||
// physical bundle: a fan from tip and base
|
||||
const n = Math.max(2, this.source.rays | 0), A = this.source.spread;
|
||||
@@ -2810,6 +2810,11 @@ class BenchSim {
|
||||
// image formed on screens (where rays land)
|
||||
this._drawScreenHits(ctx, rays);
|
||||
|
||||
// textbook construction overlay (labels, image arrow, dashed extensions)
|
||||
if (this.source.kind === 'object' && (this.source.rayMode || 'char') === 'char') {
|
||||
this._drawCharConstruction(ctx, rays, ay);
|
||||
}
|
||||
|
||||
if (typeof _drawOBFXLayer === 'function') {
|
||||
// FX anchored at the actual source point (only the object arrow has a raised tip)
|
||||
const fxY = this._sy() - (this.source.kind === 'object' ? this.source.h : 0);
|
||||
@@ -2837,6 +2842,59 @@ class BenchSim {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
_lineIntersect(a, b) {
|
||||
const den = a.dx * b.dy - a.dy * b.dx;
|
||||
if (Math.abs(den) < 1e-9) return null; // parallel → no finite intersection
|
||||
const t = ((b.x - a.x) * b.dy - (b.y - a.y) * b.dx) / den;
|
||||
return { x: a.x + t * a.dx, y: a.y + t * a.dy };
|
||||
}
|
||||
|
||||
// Textbook overlay for single-lens characteristic construction:
|
||||
// labels 1/2/3, the image arrow, and dashed back-extensions for a virtual image.
|
||||
_drawCharConstruction(ctx, rays, axis) {
|
||||
const lenses = this.elements.filter(e => e.type === 'lens');
|
||||
if (lenses.length !== 1) return; // construction is only clean for one lens
|
||||
const lensX = this._ex(lenses[0]);
|
||||
|
||||
ctx.save();
|
||||
// ── ray labels 1/2/3 near the object ──
|
||||
ctx.font = 'bold 11px Manrope, system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
const labelRay = (role, txt) => {
|
||||
const r = rays.find(x => x.role === role); if (!r || r.pts.length < 2) return;
|
||||
const p0 = r.pts[0], p1 = r.pts[1];
|
||||
const d = Math.hypot(p1.x - p0.x, p1.y - p0.y) || 1;
|
||||
const k = Math.min(40, d * 0.45);
|
||||
const lx = p0.x + (p1.x - p0.x) / d * k, ly = p0.y + (p1.y - p0.y) / d * k;
|
||||
ctx.fillStyle = 'rgba(13,13,26,0.7)'; ctx.beginPath(); ctx.arc(lx, ly, 8, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#7BF5A4'; ctx.fillText(txt, lx, ly);
|
||||
};
|
||||
labelRay('char1', '1'); labelRay('char2', '2'); labelRay('char3', '3');
|
||||
|
||||
// ── image point = intersection of the final segments of rays 1 and 2 ──
|
||||
const finalLine = (role) => {
|
||||
const r = rays.find(x => x.role === role); if (!r || r.pts.length < 2) return null;
|
||||
const b = r.pts[r.pts.length - 1], a = r.pts[r.pts.length - 2];
|
||||
return { x: a.x, y: a.y, dx: b.x - a.x, dy: b.y - a.y };
|
||||
};
|
||||
const L1 = finalLine('char1'), L2 = finalLine('char2');
|
||||
const P = (L1 && L2) ? this._lineIntersect(L1, L2) : null;
|
||||
if (P && isFinite(P.x) && isFinite(P.y)) {
|
||||
const real = P.x > lensX + 2; // real image forms to the right of the lens
|
||||
const col = real ? '#EF476F' : '#FFD166';
|
||||
if (!real) {
|
||||
// virtual image: extend the diverging rays backward (dashed) to the apparent source P
|
||||
ctx.strokeStyle = col; ctx.setLineDash([5, 4]); ctx.lineWidth = 1;
|
||||
[L1, L2].forEach(L => { if (!L) return; ctx.beginPath(); ctx.moveTo(L.x, L.y); ctx.lineTo(P.x, P.y); ctx.stroke(); });
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
this._arrow(ctx, P.x, axis, P.x, P.y, col); // image arrow: base (axis) → tip (P)
|
||||
ctx.fillStyle = col; ctx.beginPath(); ctx.arc(P.x, P.y, 3.5, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.font = '10px Manrope, system-ui, sans-serif';
|
||||
ctx.fillText(real ? 'изображение' : 'мнимое изобр.', P.x, P.y + (P.y < axis ? -14 : 16));
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
_drawSource(ctx, _ayIgnored) {
|
||||
const ay = this._sy(); // draw at the source vertical position
|
||||
const sx = this.source.xf * this.W;
|
||||
|
||||
+2
-2
@@ -4847,7 +4847,7 @@
|
||||
<script src="/js/labs/graphtransform.js"></script>
|
||||
<script src="/js/labs/pendulum.js"></script>
|
||||
<script src="/js/labs/equilibrium.js"></script>
|
||||
<script src="/js/labs/opticsbench.js?v=9"></script>
|
||||
<script src="/js/labs/opticsbench.js?v=10"></script>
|
||||
<script src="/js/labs/isoprocess.js"></script>
|
||||
<script src="/js/labs/titration.js"></script>
|
||||
<script src="/js/labs/probability.js"></script>
|
||||
@@ -4864,7 +4864,7 @@
|
||||
<script src="/js/labs/_periodic_data.js" defer></script>
|
||||
<script src="/js/labs/periodic.js" defer></script>
|
||||
<script src="/js/labs/qualanalysis.js" defer></script>
|
||||
<script src="/js/labs/_pilots.js" defer></script>
|
||||
<script src="/js/labs/_register-all.js" defer></script>
|
||||
<script>
|
||||
/* Sync sound toggle button icon with localStorage state on load */
|
||||
(function() {
|
||||
|
||||
Reference in New Issue
Block a user