feat(opticsbench): конструктор Фаза 3 — изображение на экране + экспорт PNG

- _drawScreenHits: светящиеся пятна (additive) в точках попадания лучей на
  экран, по длине волны — видно формирование изображения и спектр
- benchExportPng + кнопка «Снимок PNG»; подсказка про λ/белый свет
- bump opticsbench.js?v=4

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 12:40:37 +03:00
parent 353a6cb8a9
commit 1c7d8e9d95
3 changed files with 48 additions and 8 deletions
+36 -1
View File
@@ -2705,7 +2705,7 @@ class BenchSim {
return true;
}
if (el.type === 'screen') {
ray.hitY = ray.y; ray.alive = false; // absorbed, hit recorded
ray.hitY = ray.y; ray.hitEl = el.id; ray.alive = false; // absorbed, hit recorded
return true;
}
if (el.type === 'prism') {
@@ -2754,11 +2754,34 @@ class BenchSim {
this._drawSource(ctx, ay);
for (const el of this.elements) this._drawElement(ctx, el, ay);
// image formed on screens (where rays land)
this._drawScreenHits(ctx, rays);
if (typeof _drawOBFXLayer === 'function') {
_drawOBFXLayer(ctx, 'freebuild', { srcX: this.source.xf * W, srcY: ay - (this.source.h || 0) });
}
}
// Glowing spots on each screen where rays land → the image becomes visible.
_drawScreenHits(ctx, rays) {
const screens = this.elements.filter(e => e.type === 'screen');
if (!screens.length) return;
ctx.save();
ctx.globalCompositeOperation = 'lighter'; // additive → overlapping rays brighten
for (const sc of screens) {
const x = this._ex(sc);
for (const r of rays) {
if (r.hitEl !== sc.id) continue;
const col = (typeof wavelengthToRGB === 'function') ? wavelengthToRGB(r.wl) : '#fff';
const g = ctx.createRadialGradient(x, r.hitY, 0, x, r.hitY, 9);
g.addColorStop(0, col); g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g; ctx.globalAlpha = 0.5;
ctx.beginPath(); ctx.arc(x, r.hitY, 9, 0, Math.PI * 2); ctx.fill();
}
}
ctx.restore();
}
_drawSource(ctx, ay) {
const sx = this.source.xf * this.W;
ctx.save();
@@ -2880,6 +2903,10 @@ class BenchSim {
on(cv, 'pointerup', e => { this._drag = null; try { cv.releasePointerCapture(e.pointerId); } catch (_) {} });
}
exportPng() {
try { return this.canvas.toDataURL('image/png'); } catch (_) { return null; }
}
dispose() {
if (this._ro) { this._ro.disconnect(); this._ro = null; }
if (this._listeners) { for (const [t, ty, fn, o] of this._listeners) t.removeEventListener(ty, fn, o); this._listeners = []; }
@@ -4404,6 +4431,14 @@ function benchClear() {
if (!benchSim) return;
benchSim.elements = []; benchSim.selectedId = null; benchSim._changed(); _benchUpdateUI();
}
function benchExportPng() {
if (!benchSim) return;
const url = benchSim.exportPng();
if (!url) return;
const a = document.createElement('a');
a.href = url; a.download = 'optical-bench.png';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
}
function benchPreset(name) {
if (!benchSim) return;
const P = {
+6 -3
View File
@@ -3205,8 +3205,11 @@
<button class="preset-btn" style="font-size:.68rem" onclick="benchPreset('projector')">Проектор</button>
<button class="preset-btn" style="font-size:.68rem" onclick="benchPreset('folded')">Зеркальная</button>
</div>
<button class="preset-btn" style="width:100%;margin-bottom:6px" onclick="benchClear()">Очистить</button>
<div class="pp-hint">Тащи элементы и источник по оси. Клик — выбрать и настроить.</div>
<div style="display:flex;gap:4px;margin-bottom:6px">
<button class="preset-btn" style="flex:1" onclick="benchClear()">Очистить</button>
<button class="preset-btn" style="flex:1" onclick="benchExportPng()">Снимок PNG</button>
</div>
<div class="pp-hint">Тащи элементы и источник по оси. Клик — выбрать и настроить. λ и «Белый свет» — сверху.</div>
</div>
<!-- ── Interference control panel (Agent C) ── -->
<div id="ob-ctrl-interf" class="proj-panel" style="width:240px;gap:0;flex-shrink:0;display:none">
@@ -4841,7 +4844,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=3"></script>
<script src="/js/labs/opticsbench.js?v=4"></script>
<script src="/js/labs/isoprocess.js"></script>
<script src="/js/labs/titration.js"></script>
<script src="/js/labs/probability.js"></script>
+6 -4
View File
@@ -34,10 +34,12 @@
- [x] 2.1 Вогнутое/выпуклое зеркало — сделано ещё в Фазе 1 (кик f=R/2, разворот хода, лимит отражений).
- [x] 2.2 Призма: тонкопризменное отклонение δ=(n−1)·A к основанию + хроматическая дисперсия n(λ). Белый свет — пучки по `OB_SPECTRAL`, каждый луч красится по λ (`wavelengthToRGB`); до призмы совпадают, после — расходятся в спектр. Управление через общий λ-бар скамьи. Проверено численно (фиолетовый отклоняется сильнее красного).
### Фаза 3 — Сохранение состояния + полировка — [ ]
- [ ] 3.1 Расширить `_obGetState/_obApplyState` на конструктор (снимок/embed).
- [ ] 3.2 Пресеты систем (микроскоп, телескоп, глаз, проектор), экспорт PNG.
- [ ] 3.3 Полировка: апертурное отсечение, подписи, тач, a11y.
### Фаза 3 — Сохранение состояния + полировка — [x]
- [x] 3.1 Сохранение состояния конструктора — сделано в Фазе 1 (`_obGetState/_obApplyState` + `bench`).
- [x] 3.2 Пресеты систем (микроскоп/телескоп/проектор/зеркальная) — Фаза 1; экспорт PNG (`benchExportPng`, кнопка «Снимок PNG»).
- [x] 3.3 Экран ловит изображение: светящиеся пятна (additive `lighter`) в точках попадания лучей, по λ — видно формирование изображения и спектр.
Бэклог: точная двухгранная призма (Снеллиус на гранях вместо тонкопризменного), апертурное отсечение лучей вне линзы (сейчас проходят прямо), профиль интенсивности на экране, поворот элементов, удаление legacy `FreeBuildSim`.
---
История: создан 2026-05-30.