fix(opticsbench): источник — вертикальное положение + фикс плавающего FX

- источник можно двигать по вертикали: слайдер «Положение ↕» (для любого
  типа) + вертикальное перетаскивание; эмиссия/отрисовка/хит-тест через _sy()
- фикс бага: FX-вспышка рисовалась на ay−source.h даже для точечного
  источника (h оставалась 70) → «звезда» улетала вверх; теперь FX привязан
  к реальной точке источника (поднятая вершина только у стрелки-предмета)
- object «Высота» → «Размер стрелки» (чтобы не путать с вертик. положением)
- bump opticsbench.js?v=8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 13:21:30 +03:00
parent 016786ac50
commit a97896d293
2 changed files with 21 additions and 12 deletions
+20 -11
View File
@@ -2545,7 +2545,7 @@ class BenchSim {
this._drag = null;
this._nextId = 1;
// source: object arrow by default. `ang` (deg) aims point/single/laser/parallel.
this.source = { kind: 'object', xf: 0.07, h: 70, spread: 0.32, rays: 9, ang: 0 };
this.source = { kind: 'object', xf: 0.07, yf: 0, h: 70, spread: 0.32, rays: 9, ang: 0 };
// elements along the bench, positioned by x-fraction; centred on the axis
this.elements = [
this._mk('lens', { xf: 0.40, f: 130, ap: 95 }),
@@ -2612,10 +2612,11 @@ class BenchSim {
/* ── geometry helpers ── */
_ex(el) { return el.xf * this.W; }
_ay() { return this.H / 2; }
_sy() { return this._ay() + (this.source.yf || 0) * (this.H / 2 - 14); } // source y (vertical position)
/* Emit the initial rays from the source. */
_emitRays() {
const ay = this._ay();
const ay = this._sy(); // emission height respects the source vertical position
const sx = this.source.xf * this.W;
const rays = [];
// white light → one sub-ray per spectral sample (they coincide until a prism disperses them)
@@ -2795,7 +2796,9 @@ class BenchSim {
this._drawScreenHits(ctx, rays);
if (typeof _drawOBFXLayer === 'function') {
_drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: ay - (this.source.h || 0) });
// 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);
_drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: fxY });
}
}
@@ -2819,7 +2822,8 @@ class BenchSim {
ctx.restore();
}
_drawSource(ctx, ay) {
_drawSource(ctx, _ayIgnored) {
const ay = this._sy(); // draw at the source vertical position
const sx = this.source.xf * this.W;
const aim = (this.source.ang || 0) * Math.PI / 180;
ctx.save();
@@ -2938,11 +2942,12 @@ class BenchSim {
};
const hit = (mx, my) => {
const ay = this._ay();
// source first (it can sit off-axis), grab around its actual vertical position
const sx = this.source.xf * this.W, sy = this._sy();
if (Math.abs(mx - sx) < 16 && Math.abs(my - sy) < 70) return { kind: 'src' };
for (const el of this.elements) {
if (Math.abs(mx - this._ex(el)) < 14 && Math.abs(my - ay) < 120) return { kind: 'el', id: el.id };
}
const sx = this.source.xf * this.W;
if (Math.abs(mx - sx) < 16 && Math.abs(my - ay) < 120) return { kind: 'src' };
return null;
};
on(cv, 'pointerdown', e => {
@@ -2957,10 +2962,14 @@ class BenchSim {
});
on(cv, 'pointermove', e => {
if (!this._drag) { const { mx, my } = pos(e); cv.style.cursor = hit(mx, my) ? 'grab' : 'default'; return; }
const { mx } = pos(e);
const { mx, my } = pos(e);
const xf = Math.max(0.02, Math.min(0.98, mx / this.W));
if (this._drag.kind === 'src') this.source.xf = xf;
else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; }
if (this._drag.kind === 'src') {
this.source.xf = xf;
// vertical drag too → move the source up/down off the axis
const yf = (my - this._ay()) / (this.H / 2 - 14);
this.source.yf = Math.max(-0.95, Math.min(0.95, yf));
} else { const el = this.elements.find(x => x.id === this._drag.id); if (el) el.xf = xf; }
this._redraw(); // position drag → redraw canvas, keep inspector intact
});
on(cv, 'pointerup', e => { this._drag = null; try { cv.releasePointerCapture(e.pointerId); } catch (_) {} });
@@ -4444,9 +4453,9 @@ function _benchPropsHTML() {
let h = '<div class="gp-section-title" style="margin:4px 0 6px">Источник</div>';
h += _benchBtnRow(['object:Предмет', 'point:Точка', 'parallel:Параллель', 'single:Луч', 'laser:Лазер'],
k => s.kind === k, k => "benchSourceKind('" + k + "')");
if (s.kind === 'object') h += _benchCtl('Высота', 0, 'h', 20, 120, 2, s.h, true);
h += _benchCtl('Положение ↕', 0, 'yf', -0.9, 0.9, 0.02, +(s.yf || 0).toFixed(2), true); // vertical position (any kind)
if (s.kind === 'object') h += _benchCtl('Размер стрелки', 0, 'h', 20, 120, 2, s.h, true);
if (s.kind === 'point') h += _benchCtl('Раствор', 0, 'spread', 0.1, 0.6, 0.02, s.spread, true);
if (s.kind !== 'object' && s.kind !== 'parallel') {} // no spread for single/laser
if (s.kind !== 'object') h += _benchCtl('Угол°', 0, 'ang', -60, 60, 1, s.ang || 0, true);
return h;
}
+1 -1
View File
@@ -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=7"></script>
<script src="/js/labs/opticsbench.js?v=8"></script>
<script src="/js/labs/isoprocess.js"></script>
<script src="/js/labs/titration.js"></script>
<script src="/js/labs/probability.js"></script>