Files
Learn_System/frontend/js/labs/radioactive.js
T
Maxim Dolgolyov 6afe928c0d feat(labs): visual polish wave — LabFX foundation + 33 sims juiced up
ФУНДАМЕНТ (4 новых файла):
- _fx_core.js: LabFX namespace, glow.drawGlow, glow.pulse, haptic, shake
- _fx_particles.js: пул 1500 объектов, 6 shapes (dot/spark/ring/smoke/splash/dust)
- _fx_motion.js: tween + 12 easings + critically-damped spring
- _fx_sound.js: 9 procedural synth-звуков (click/tick/whoosh/chime/fizz/spark/bounce/pour/drone), Web Audio API
- Sound toggle в шапке lab.html с localStorage-persist

UX МИКРО (CSS + JS):
- Button states: hover scale+brightness, active scale-down, disabled grayscale
- Slider polish: custom thumb с тенью, filled-track gradient, hover/active
- Focus rings через :focus-visible
- Tooltip system .tt-host data-tt= с 400ms hover, fade-in
- Marching ants для selection
- Loading skeleton с shimmer
- Empty state .sim-empty-* паттерн
- Toast: progress bar внизу, icons по типу
- Cursor states utility classes
- View Transitions API для smooth sim-switch, fallback на CSS fade

PHASE 2 — визуальные эффекты для 33 симуляций:

Physics motion: projectile (launch whoosh + landing splash/shake/haptic + target chime), pendulum (max-extension tick + bob glow), collision (bounce + sparks + shake), angrybirds (whoosh/bounce/fizz/chime + confetti), newton (rocket flame trail + scene transitions), forcesandbox (spring glow + impact sparks)

Physics fields: emfield (field-lines glow + particle trail + lightning при high field + Gauss-drag haptic + rod motion sparks), circuit (energized-wire glow ∝ current + LED bloom + short-circuit shake/spark + heat shimmer smoke + place/erase/switch sounds), opticsbench (beam glow на всех режимах + caustics dust у фокуса + TIR/Brewster one-shot sounds)

Thermo+waves: waves (mode-switch whoosh + Mach-cone particles + spectrum harmonic chime + waveform glow), hydrostatics (pour sound + splash при погружении + valve click), isoprocess (PV-trail dust), heatengine (drone цикла + hot/cold reservoir smoke/dust + phase-change ticks), radioactive (Geiger tick throttle + decay sparks + half-life chime + α/β/γ glow)

Chemistry: chemsandbox (pour splash + fizz bubbles/dust/spark по типу реакции + shake при горении), equilibrium/electrolysis/reactions/titration/flask/ionexchange/redox (pour/fizz/spark/chime по событиям, частицы при ключевых моментах), stoichiometry (fizz bubbles + recipe-change click)

Math+geom: geometry (tool-click + object-create tick + locus glow + challenge confetti via LabFX + haptic), triangle (vertex-drop tick + special-point glow), stereo (figure-change whoosh + cross-section chime, no particles — Three.js), trigcircle (drag-haptic + pitch∝angle tick + sin/cos glow), graph/graphtransform/quadratic (slider-tick throttled + curve glow + discriminant-cross chime), probability (bounce + finish-chime burst), normaldist (pulsing shade + bell glow)

Bio+misc: celldivision (phase-change whoosh + prophase dust + anaphase poles spark + cytokinesis chime), photosynthesis (photon tick + ATP chime + glucose sparkle), bohratom (electron-jump chime ∝ levels + photon spark), orbitals/crystal (mode-change whoosh — Three.js, sound only), logic (gate-place + wire-connect + LED bloom + HIGH wire flowing dots + preset chime), gas/brownian/diffusion/states (preset whoosh, throttled tick по событиям)

Все LabFX вызовы обёрнуты в if (window.LabFX) guard — graceful degradation если фундамент не загружен. 47 JS файлов прошли syntax check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:58:49 +03:00

646 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* RadioactiveSim — Radioactive decay simulation.
* Left panel: particle canvas (circles colored by species).
* Right panel: N(t) graph with theoretical curve overlay.
* Supports single-step decays and short decay chains.
*
* Decay chains are simplified to 4-5 prominent steps;
* the full U-238 chain (14 nuclides) is condensed to 5.
*/
class RadioactiveSim {
constructor(canvas, graphCanvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.graphCanvas = graphCanvas;
this.gCtx = graphCanvas.getContext('2d');
/* layout */
this.W = 0; this.H = 0;
this.GW = 0; this.GH = 0;
this._dpr = 1;
/* simulation state */
this.particles = []; // [{x, y, vx, vy, step, flash, flashT}]
this.history = []; // [{t, counts:[...per step]}]
this._raf = null;
this._last = 0;
this.simTime = 0; // sim time in seconds (scaled)
this.playing = false;
/* parameters */
this.isotope = 'C-14';
this.N0 = 500;
this.speed = 10; // time multiplier
/* callbacks */
this.onUpdate = null;
/* load preset */
this._loadIsotope(this.isotope);
this._spawn();
new ResizeObserver(() => { this.fit(); }).observe(canvas.parentElement || canvas);
}
/* ══════════════ isotope presets ══════════════ */
static ISOTOPES = {
'C-14': {
label: '¹⁴C',
steps: [
{ name: '¹⁴C', T_half: 5730 * 3.156e7, type: 'β⁻', color: '#9B5DE5' },
{ name: '¹⁴N', T_half: Infinity, type: null, color: '#4CAF50' },
]
},
'I-131': {
label: '¹³¹I',
steps: [
{ name: '¹³¹I', T_half: 8.02 * 86400, type: 'β⁻', color: '#F15BB5' },
{ name: '¹³¹Xe', T_half: Infinity, type: null, color: '#06D6E0' },
]
},
'Cs-137': {
label: '¹³⁷Cs',
steps: [
{ name: '¹³⁷Cs', T_half: 30.2 * 3.156e7, type: 'β⁻', color: '#FFD166' },
{ name: '¹³⁷Ba', T_half: Infinity, type: null, color: '#7BF5A4' },
]
},
'Ra-226': {
label: '²²⁶Ra',
steps: [
{ name: '²²⁶Ra', T_half: 1600 * 3.156e7, type: 'α', color: '#EF476F' },
{ name: '²²²Rn', T_half: 3.82 * 86400, type: 'α', color: '#FF9F1C' },
{ name: '²¹⁸Po', T_half: 3.05 * 60, type: 'α', color: '#F15BB5' },
{ name: '²¹⁴Pb', T_half: 26.8 * 60, type: 'β⁻', color: '#9B5DE5' },
{ name: '²⁰⁶Pb', T_half: Infinity, type: null, color: '#4CAF50' },
]
},
'K-40': {
label: '⁴⁰K',
steps: [
{ name: '⁴⁰K', T_half: 1.248e9 * 3.156e7, type: 'β⁻/EC', color: '#06D6E0' },
{ name: '⁴⁰Ca/⁴⁰Ar', T_half: Infinity, type: null, color: '#7BF5A4' },
]
},
'U-238': {
label: '²³⁸U',
// Condensed chain: U-238 → Th-234 → Ra-226 → Rn-222 → Pb-206 (stable)
// Full chain has 14 steps; we keep 5 most prominent
steps: [
{ name: '²³⁸U', T_half: 4.468e9 * 3.156e7, type: 'α', color: '#FFD166' },
{ name: '²³⁴Th', T_half: 24.1 * 86400, type: 'β⁻', color: '#F15BB5' },
{ name: '²²⁶Ra', T_half: 1600 * 3.156e7, type: 'α', color: '#EF476F' },
{ name: '²²²Rn', T_half: 3.82 * 86400, type: 'α', color: '#9B5DE5' },
{ name: '²⁰⁶Pb', T_half: Infinity, type: null, color: '#4CAF50' },
]
},
'U-235': {
label: '²³⁵U',
// Condensed: U-235 → Pa-231 → Ac-227 → Bi-211 → Pb-207 (stable)
steps: [
{ name: '²³⁵U', T_half: 7.04e8 * 3.156e7, type: 'α', color: '#FF9F1C' },
{ name: '²³¹Pa', T_half: 32760 * 3.156e7, type: 'α', color: '#F15BB5' },
{ name: '²²⁷Ac', T_half: 21.77 * 3.156e7, type: 'β⁻', color: '#9B5DE5' },
{ name: '²¹¹Bi', T_half: 2.14 * 60, type: 'α', color: '#06D6E0' },
{ name: '²⁰⁷Pb', T_half: Infinity, type: null, color: '#4CAF50' },
]
},
};
_loadIsotope(id) {
this.isotope = id;
const preset = RadioactiveSim.ISOTOPES[id];
this.steps = preset.steps;
// λ for each step
this.lambdas = this.steps.map(s =>
s.T_half === Infinity ? 0 : Math.LN2 / s.T_half
);
this.simTime = 0;
this.history = [];
this._lastHalfLifeMark = 0;
this._fxDecayCount = 0;
}
/* ══════════════ public API ══════════════ */
fit() {
const dpr = window.devicePixelRatio || 1;
this._dpr = dpr;
const pw = this.canvas.offsetWidth || 480;
const ph = this.canvas.offsetHeight || 400;
this.canvas.width = pw * dpr;
this.canvas.height = ph * dpr;
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.W = pw; this.H = ph;
const gw = this.graphCanvas.offsetWidth || 340;
const gh = this.graphCanvas.offsetHeight || 400;
this.graphCanvas.width = gw * dpr;
this.graphCanvas.height = gh * dpr;
this.gCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.GW = gw; this.GH = gh;
this._layoutParticles();
this.draw();
}
reset() {
this.pause();
this._loadIsotope(this.isotope);
this._spawn();
this.draw();
this._emit();
}
play() {
if (this.playing) return;
this.playing = true;
this._last = performance.now();
this._raf = requestAnimationFrame(ts => this._tick(ts));
}
pause() {
this.playing = false;
if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
}
stop() { this.pause(); }
setIsotope(id) {
if (!RadioactiveSim.ISOTOPES[id]) return;
this.isotope = id;
this.reset();
}
setSpeed(v) { this.speed = Math.max(1, Math.min(1000, +v)); }
setN0(v) { this.N0 = Math.max(50, Math.min(2000, +v)); this.reset(); }
getParams() {
return { isotope: this.isotope, N0: this.N0, speed: this.speed };
}
info() {
const counts = this._counts();
const T = this.steps[0].T_half;
const periods = T === Infinity ? 0 : this.simTime / T;
const decayed = this.N0 > 0 ? 1 - counts[0] / this.N0 : 0;
const lambda0 = this.lambdas[0];
const activity = Math.round(counts[0] * lambda0);
return {
periods: periods.toFixed(2),
decayPct: (decayed * 100).toFixed(1),
activity,
counts,
names: this.steps.map(s => s.name),
};
}
/* ══════════════ internal ══════════════ */
_spawn() {
this.particles = [];
this._flashes = [];
const simW = this.W || 480;
const simH = this.H || 400;
for (let i = 0; i < this.N0; i++) {
this.particles.push({
x: Math.random() * simW,
y: Math.random() * simH,
vx: (Math.random() - 0.5) * 30,
vy: (Math.random() - 0.5) * 30,
step: 0, // index into this.steps
flash: false,
flashT: 0,
flashSymbol: '',
});
}
}
_layoutParticles() {
// re-distribute within new canvas size after fit
const W = this.W, H = this.H;
if (!W || !H) return;
for (const p of this.particles) {
if (p.x > W) p.x = Math.random() * W;
if (p.y > H) p.y = Math.random() * H;
}
}
_counts() {
const c = new Array(this.steps.length).fill(0);
for (const p of this.particles) {
if (p.step < this.steps.length) c[p.step]++;
}
return c;
}
_tick(ts) {
if (!this.playing) return;
const wallDt = Math.min((ts - this._last) / 1000, 0.05); // s, capped
this._last = ts;
const dt = wallDt * this.speed; // scaled sim time step
// physics + decay
const W = this.W, H = this.H;
for (const p of this.particles) {
// move
p.x += p.vx * wallDt;
p.y += p.vy * wallDt;
// bounce off walls
if (p.x < 0) { p.x = 0; p.vx = Math.abs(p.vx); }
if (p.x > W) { p.x = W; p.vx = -Math.abs(p.vx); }
if (p.y < 0) { p.y = 0; p.vy = Math.abs(p.vy); }
if (p.y > H) { p.y = H; p.vy = -Math.abs(p.vy); }
// decay (only if not at final stable step)
const step = p.step;
const lambda = this.lambdas[step];
if (lambda > 0 && Math.random() < lambda * dt) {
p.step = Math.min(step + 1, this.steps.length - 1);
// emit flash
const decayType = this.steps[step].type || '';
const sym = decayType.startsWith('α') ? 'α'
: decayType.startsWith('β') ? 'β'
: 'γ';
this._flashes.push({ x: p.x, y: p.y, t: 0, maxT: 0.35, sym });
// LabFX decay effects (throttled)
if (window.LabFX) {
this._fxFrameDecays = (this._fxFrameDecays || 0) + 1;
LabFX.particles.emit({
ctx: this.ctx, x: p.x, y: p.y,
count: 6, color: '#FFD700', speed: 60,
spread: Math.PI * 2, angle: 0, gravity: 0,
life: 300, shape: 'spark', glow: true, size: 2,
});
}
}
// age flash on particle itself
if (p.flash) {
p.flashT -= wallDt;
if (p.flashT <= 0) p.flash = false;
}
}
// age global flashes
for (let i = this._flashes.length - 1; i >= 0; i--) {
this._flashes[i].t += wallDt;
if (this._flashes[i].t >= this._flashes[i].maxT) {
this._flashes.splice(i, 1);
}
}
this.simTime += dt;
// LabFX half-life crossing + throttled tick sound
if (window.LabFX) {
/* throttle tick: play at most once per frame, only if ≤10 decays/s effective */
const frameDecays = this._fxFrameDecays || 0;
this._fxFrameDecays = 0;
if (frameDecays > 0) {
const decaysPerSec = frameDecays / Math.max(wallDt, 0.001);
const N = Math.max(1, Math.round(decaysPerSec / 10));
if (Math.random() < 1 / N) {
LabFX.sound.play('tick', { pitch: 0.8 + Math.random() * 0.4, volume: 0.08 });
}
}
LabFX.particles.update(wallDt);
const T0 = this.steps[0].T_half;
if (T0 !== Infinity) {
const halfLifesElapsed = Math.floor(this.simTime / T0);
if (halfLifesElapsed > (this._lastHalfLifeMark || 0)) {
this._lastHalfLifeMark = halfLifesElapsed;
LabFX.sound.play('chime', {
pitch: 0.6 + halfLifesElapsed * 0.1,
volume: 0.3,
});
}
}
}
// record history every ~2 ticks (≈30ms)
const last = this.history[this.history.length - 1];
if (!last || this.simTime - last.t > this.steps[0].T_half * 0.005 || this.history.length < 5) {
this._recordHistory();
}
this.draw();
this._emit();
this._raf = requestAnimationFrame(ts2 => this._tick(ts2));
}
_recordHistory() {
this.history.push({ t: this.simTime, counts: this._counts() });
// keep last 500 points
if (this.history.length > 500) this.history.shift();
}
_emit() {
if (this.onUpdate) this.onUpdate(this.info());
}
/* ══════════════ drawing ══════════════ */
draw() {
this._drawParticles();
this._drawGraph();
}
_drawParticles() {
const ctx = this.ctx;
const W = this.W, H = this.H;
if (!W || !H) return;
ctx.clearRect(0, 0, W, H);
// background
ctx.fillStyle = '#0D0D1A';
ctx.fillRect(0, 0, W, H);
// subtle grid
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
const step = 40;
ctx.beginPath();
for (let x = 0; x < W; x += step) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
for (let y = 0; y < H; y += step) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
ctx.stroke();
// draw flashes first (under particles)
for (const fl of this._flashes) {
const alpha = 1 - fl.t / fl.maxT;
const r = 6 + fl.t / fl.maxT * 12;
ctx.beginPath();
ctx.arc(fl.x, fl.y, r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,200,${alpha * 0.45})`;
ctx.fill();
const symSize = Math.round(8 + alpha * 4);
ctx.font = `bold ${symSize}px Manrope,sans-serif`;
const symColor = `rgba(255,255,180,${alpha})`;
ctx.fillStyle = symColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (window.LabFX) {
LabFX.glow.drawGlow(ctx, () => {
ctx.font = `bold ${symSize}px Manrope,sans-serif`;
ctx.fillStyle = symColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(fl.sym, fl.x, fl.y - r - 4);
}, { color: '#FFFFFF', intensity: 6 });
} else {
ctx.fillText(fl.sym, fl.x, fl.y - r - 4);
}
}
// draw particles
const R = 4;
for (const p of this.particles) {
const s = this.steps[p.step];
ctx.beginPath();
ctx.arc(p.x, p.y, R, 0, Math.PI * 2);
ctx.fillStyle = s.color;
ctx.fill();
}
// legend
const lx = 10, ly = 10;
ctx.font = '11px Manrope,sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
for (let i = 0; i < this.steps.length; i++) {
const s = this.steps[i];
const y = ly + i * 18;
ctx.fillStyle = s.color;
ctx.beginPath();
ctx.arc(lx + 5, y + 6, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.75)';
ctx.fillText(s.name, lx + 15, y);
}
if (window.LabFX) LabFX.particles.draw(ctx);
}
_drawGraph() {
const ctx = this.gCtx;
const W = this.GW, H = this.GH;
if (!W || !H) return;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0D0D1A';
ctx.fillRect(0, 0, W, H);
const pad = { l: 40, r: 14, t: 20, b: 36 };
const gW = W - pad.l - pad.r;
const gH = H - pad.t - pad.b;
// grid
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i <= 4; i++) {
const y = pad.t + gH - i * gH / 4;
ctx.moveTo(pad.l, y); ctx.lineTo(pad.l + gW, y);
}
for (let i = 0; i <= 5; i++) {
const x = pad.l + i * gW / 5;
ctx.moveTo(x, pad.t); ctx.lineTo(x, pad.t + gH);
}
ctx.stroke();
// axes
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(pad.l, pad.t); ctx.lineTo(pad.l, pad.t + gH);
ctx.moveTo(pad.l, pad.t + gH); ctx.lineTo(pad.l + gW, pad.t + gH);
ctx.stroke();
// axis labels
ctx.font = '10px Manrope,sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.45)';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let i = 0; i <= 4; i++) {
const y = pad.t + gH - i * gH / 4;
const val = Math.round(this.N0 * i / 4);
ctx.fillText(val, pad.l - 4, y);
}
const T0 = this.steps[0].T_half;
const tMax = T0 === Infinity ? Math.max(this.simTime * 1.1, 1e-6) : T0 * 5;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (let i = 0; i <= 5; i++) {
const x = pad.l + i * gW / 5;
const tVal = tMax * i / 5;
const label = T0 === Infinity ? tVal.toFixed(0) + 's' : (tVal / T0).toFixed(1) + 'T';
ctx.fillText(label, x, pad.t + gH + 4);
}
// axis title
ctx.font = '9px Manrope,sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.textAlign = 'left';
ctx.fillText('N', pad.l + 2, pad.t + 2);
ctx.textAlign = 'right';
ctx.fillText(T0 === Infinity ? 't, с' : 't / T½', pad.l + gW, pad.t + gH + 28);
if (this.history.length < 2) return;
const tx = t => pad.l + (t / tMax) * gW;
const ty = n => pad.t + gH - (n / this.N0) * gH;
// theoretical decay curve for step 0 (semi-transparent)
if (T0 !== Infinity) {
const lam = this.lambdas[0];
ctx.beginPath();
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
const nPts = 80;
for (let i = 0; i <= nPts; i++) {
const t = tMax * i / nPts;
const n = this.N0 * Math.exp(-lam * t);
const x = tx(t), y = ty(n);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
ctx.setLineDash([]);
}
// actual curves per species
for (let si = 0; si < this.steps.length; si++) {
const color = this.steps[si].color;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
let first = true;
for (const pt of this.history) {
const x = tx(pt.t);
const y = ty(pt.counts[si]);
if (x < pad.l || x > pad.l + gW) continue;
first ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
first = false;
}
ctx.stroke();
}
// current time marker
const curX = tx(this.simTime);
if (curX >= pad.l && curX <= pad.l + gW) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 1;
ctx.setLineDash([2, 3]);
ctx.moveTo(curX, pad.t);
ctx.lineTo(curX, pad.t + gH);
ctx.stroke();
ctx.setLineDash([]);
}
}
}
/* ══════════════════════════════════════════════
_openRadioactive — wiring
══════════════════════════════════════════════ */
var radioactiveSim = null;
function _openRadioactive() {
document.getElementById('sim-topbar-title').textContent = 'Радиоактивный распад';
document.getElementById('ctrl-radioactive').style.display = '';
_simShow('sim-radioactive');
_registerSimState('radioactive', () => radioactiveSim?.getParams(),
st => { if (radioactiveSim && st) {
if (st.isotope) radioactiveSim.setIsotope(st.isotope);
if (st.N0) radioactiveSim.setN0(st.N0);
if (st.speed) radioactiveSim.setSpeed(st.speed);
}});
if (typeof _embedMode !== 'undefined' && _embedMode) _startStateEmit('radioactive');
requestAnimationFrame(() => requestAnimationFrame(() => {
if (!radioactiveSim) {
radioactiveSim = new RadioactiveSim(
document.getElementById('radioactive-canvas'),
document.getElementById('radioactive-graph')
);
radioactiveSim.onUpdate = _radioactiveUpdateHUD;
}
radioactiveSim.fit();
radioactiveSim.reset();
radioactiveSim.play();
_radioactiveUpdateHUD(radioactiveSim.info());
}));
}
function radioactiveIsotope(id) {
if (radioactiveSim) {
radioactiveSim.setIsotope(id);
radioactiveSim.play();
}
}
function radioactiveSpeed(val) {
if (radioactiveSim) radioactiveSim.setSpeed(+val);
const el = document.getElementById('rd-speed-val');
if (el) el.textContent = '×' + (+val).toFixed(0);
}
function radioactiveN0(val) {
if (radioactiveSim) radioactiveSim.setN0(+val);
const el = document.getElementById('rd-n0-val');
if (el) el.textContent = val;
}
function radioactivePlay() {
if (!radioactiveSim) return;
if (window.LabFX) LabFX.sound.play('click');
if (radioactiveSim.playing) {
radioactiveSim.pause();
document.getElementById('rd-play-btn').textContent = 'Старт';
} else {
radioactiveSim.play();
document.getElementById('rd-play-btn').textContent = 'Пауза';
}
}
function radioactiveReset() {
if (!radioactiveSim) return;
if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 0.5, volume: 0.3 });
radioactiveSim._lastHalfLifeMark = 0;
radioactiveSim._fxDecayCount = 0;
radioactiveSim.reset();
document.getElementById('rd-play-btn').textContent = 'Старт';
}
function _radioactiveUpdateHUD(info) {
const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
v('rd-hud-periods', info.periods + ' T½');
v('rd-hud-decayed', info.decayPct + '%');
v('rd-hud-activity', info.activity + ' Бк');
}
/* ── dating mode ── */
function radioactiveDating(pctLeft) {
// pct of parent remaining (0-100)
const ratio = Math.max(0.001, Math.min(0.999, (+pctLeft) / 100));
const T = radioactiveSim ? radioactiveSim.steps[0].T_half : null;
if (!T || T === Infinity) return;
const lambda = Math.LN2 / T;
const age = -Math.log(ratio) / lambda;
const el = document.getElementById('rd-dating-result');
if (el) {
const years = (age / 3.156e7).toExponential(3);
el.textContent = 'Возраст: ' + years + ' лет';
}
const pctEl = document.getElementById('rd-dating-pct-val');
if (pctEl) pctEl.textContent = (+pctLeft).toFixed(0) + '% осталось';
}