Skip to content

Instantly share code, notes, and snippets.

@arivero
Last active June 18, 2026 21:12
Show Gist options
  • Select an option

  • Save arivero/16d17064d4c337ddf29bb8b460d131fa to your computer and use it in GitHub Desktop.

Select an option

Save arivero/16d17064d4c337ddf29bb8b460d131fa to your computer and use it in GitHub Desktop.
nuclear stability and fine structure constant
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EW–QCD Stability-Line Gadget</title>
<style>
:root {
--bg: #f3f6fa;
--panel: #ffffff;
--panel2: #f7f9fc;
--plot: #ffffff;
--plot-field: #fbfcfe;
--ink: #182235;
--muted: #5d687a;
--border: #d7dfe9;
--grid: #dbe2eb;
--axis: #748196;
--valley: #006f9d;
--band: rgba(0, 111, 157, 0.13);
--alpha: #b86000;
--fission: #c4385b;
--proton: #7051bd;
--neutron: #087d59;
--w: #007f9f;
--z: #7045b8;
--vh: #27833d;
--warn: #a45a00;
}
html, body { margin:0; padding:0; background:var(--bg); color:var(--ink); font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; }
main { max-width: 1480px; margin: 0 auto; padding: 18px; }
h1 { margin: 0 0 6px; font-size: 1.35rem; font-weight: 700; }
p { color: var(--muted); line-height: 1.42; }
code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
.layout { display: grid; grid-template-columns: minmax(660px, 1.5fr) minmax(380px, 0.95fr); gap: 16px; align-items: start; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; box-shadow: 0 10px 28px rgba(31,48,74,0.10); }
.plot-card { padding: 10px; }
#chart { width: 100%; aspect-ratio: 1 / 1; display:block; border-radius: 10px; background: var(--plot); touch-action: none; user-select: none; cursor: crosshair; }
#chart.dragging { cursor: grabbing; }
.side { padding: 14px; }
.controls { display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
label { display:block; font-size: 0.78rem; color: var(--muted); margin-bottom: 4px; }
input[type="number"], input[type="range"], select { width:100%; box-sizing:border-box; border: 1px solid #c9d3df; background: var(--panel2); color: var(--ink); border-radius: 8px; padding: 8px; font-size: 0.92rem; }
input[type="checkbox"] { transform: translateY(1px); margin-right: 6px; }
input[type="range"] { padding: 0; }
button { border: 1px solid #b9c7d8; background: #edf3f9; color: var(--ink); border-radius: 9px; padding: 9px 10px; cursor:pointer; font-weight:600; }
button:hover { background: #e2ebf5; }
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin: 10px 0; }
.row > * { flex: 1; min-width: 130px; }
.small { font-size: 0.84rem; color: var(--muted); }
.warn { color: var(--warn); }
table { width:100%; border-collapse: collapse; margin-top: 10px; font-size: 0.88rem; }
td, th { border-bottom: 1px solid #e0e6ee; padding: 6px 7px; vertical-align: top; }
th { color: var(--muted); text-align:left; font-weight:600; }
td.num { text-align:right; font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.legend { display:flex; flex-wrap:wrap; gap:9px 14px; padding: 8px 2px 0; color: var(--muted); font-size:0.86rem; }
.chip { display:inline-flex; align-items:center; gap:6px; }
.swatch { width:18px; height:3px; border-radius:3px; display:inline-block; }
details { margin-top: 14px; background: #ffffff; border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; }
summary { cursor:pointer; font-weight: 700; color: var(--ink); }
.formula { overflow-x:auto; background:#f7f9fc; border:1px solid #d7dfe9; border-radius:10px; padding: 10px; color:#263650; }
.footer-note { margin-top: 12px; font-size: 0.82rem; color: var(--muted); }
.drag-note { margin: 0 0 8px; padding: 8px 10px; border: 1px solid #cfd9e5; border-radius: 9px; background: #f7fafc; color: #42516a; font-size: 0.86rem; }
.embed-code { white-space: pre-wrap; overflow-wrap: anywhere; }
body.embed main { max-width: none; padding: 8px; }
body.embed h1, body.embed .intro, body.embed details { display: none; }
body.embed .layout { margin: 0; }
@media (max-width: 1050px) { .layout { grid-template-columns: 1fr; } .controls { grid-template-columns: 1fr 1fr; } }
</style>
</head>
<body>
<main>
<h1>EW–QCD Stability-Line Gadget</h1>
<p class="intro">
Move the three diagonal mass-number lines <span class="mono">A=N+Z=M/u</span>. The gadget converts
<span class="mono">M_W</span>, <span class="mono">M_Z</span>, and <span class="mono">v/√2</span> into a tree-level electromagnetic coupling,
inserts it into a liquid-drop Coulomb coefficient, and repaints the beta-stability valley plus its first stopping surface.
</p>
<div class="layout">
<section class="card plot-card">
<div class="drag-note"><strong>Interactive:</strong> move the three colored diagonal markers—W, Z, and the Higgs scale v/√2—or edit their values in the panel. The circular handle stays under the pointer.</div>
<svg id="chart" viewBox="0 0 900 900" role="img" aria-label="N Z stability chart with interactive electroweak mass markers"></svg>
<div class="legend">
<span class="chip"><span class="swatch" style="background:var(--w)"></span>interactive W marker</span>
<span class="chip"><span class="swatch" style="background:var(--z)"></span>interactive Z marker</span>
<span class="chip"><span class="swatch" style="background:var(--vh)"></span>interactive v/√2 marker</span>
<span class="chip"><span class="swatch" style="background:var(--valley)"></span>beta-stability valley</span>
<span class="chip"><span class="swatch" style="background:var(--band)"></span>finite-time beta-survival band</span>
<span class="chip"><span class="swatch" style="background:var(--alpha)"></span>S<sub>α</sub>=0 dripline / endpoint</span>
<span class="chip"><span class="swatch" style="background:var(--fission)"></span>fissility X=1 / endpoint</span>
<span class="chip"><span class="swatch" style="background:var(--proton)"></span>S<sub>p</sub>=0 dripline / endpoint</span>
<span class="chip"><span class="swatch" style="background:var(--neutron)"></span>S<sub>n</sub>=0 dripline / endpoint</span>
</div>
</section>
<aside class="card side">
<div class="controls">
<div>
<label for="Aw">W line: A<sub>W</sub>=M<sub>W</sub>/u</label>
<input id="Aw" type="number" step="0.001" value="86.288" />
</div>
<div>
<label for="Az">Z line: A<sub>Z</sub>=M<sub>Z</sub>/u</label>
<input id="Az" type="number" step="0.001" value="97.894" />
</div>
<div>
<label for="Avh">vev line: A<sub>v/√2</sub>=v/(√2u)</label>
<input id="Avh" type="number" step="0.001" value="186.906" />
</div>
</div>
<div class="row">
<button id="reset">Reset PDG-like values</button>
<div>
<label for="deltaMode">beta valley</label>
<select id="deltaMode">
<option value="atomic" selected>atomic Δβ = mₙ − m_H</option>
<option value="isospin">isospin-clean Δβ = 0</option>
</select>
</div>
<div>
<label for="alphaMode">alpha boundary</label>
<select id="alphaMode">
<option value="ldpair" selected>self-consistent LD + pairing α</option>
<option value="empirical">empirical α binding, Coulomb-shifted</option>
<option value="off">ignore α decay</option>
</select>
</div>
</div>
<div class="row">
<div>
<label for="q0">β finite-time threshold Q<sub>0</sub> at physical G<sub>F</sub> [MeV]</label>
<input id="q0" type="range" min="0.05" max="5" step="0.05" value="1" />
<div class="small">Q<sub>0</sub> = <span id="q0Text">1.00</span> MeV. Band obeys λ<sub>β</sub>∝G<sub>F</sub><sup>2</sup>Q<sup>5</sup>.</div>
</div>
<div>
<label for="showDrips"><input id="showDrips" type="checkbox" checked style="width:auto; margin-right:6px; vertical-align:middle;">paint driplines</label>
<div class="small">Contours S<sub>α</sub>=0, S<sub>p</sub>=0, S<sub>n</sub>=0, and X<sub>fiss</sub>=1.</div>
</div>
<div>
<label for="constants">constants</label>
<button id="toggleConstants">Show / hide model constants</button>
</div>
</div>
<div class="row">
<div>
<label for="lockAlpha"><input id="lockAlpha" type="checkbox">keep α<sub>em</sub>* fixed when moving v</label>
<div class="small" id="alphaLockText">If enabled, moving or typing the v/√2 line rescales W and Z together, so α*=M<sub>W</sub><sup>2</sup>(1-M<sub>W</sub><sup>2</sup>/M<sub>Z</sub><sup>2</sup>)/(πv<sup>2</sup>) stays fixed. With Δβ and the nuclear coefficients held fixed, that leaves the static stability valley unchanged and changes G<sub>F</sub>, hence beta-decay times and the finite-time survival band.</div>
</div>
<div>
<label for="lockAlphaV"><input id="lockAlphaV" type="checkbox">keep α<sub>em</sub>* and v fixed when moving W or Z</label>
<div class="small" id="alphaVLockText">If enabled, moving W adjusts Z, and moving Z adjusts W, with v held fixed. For a given Z there are two W branches; the gadget keeps the branch closest to the current W.</div>
</div>
<div>
<label for="showMagicGrid"><input id="showMagicGrid" type="checkbox" checked>use magic-number grid instead of uniform grid</label>
<div class="small">Uses grid lines and tick labels at N,Z = 2, 8, 20, 28, 50, 82, 126, 184, rather than evenly spaced ticks.</div>
</div>
</div>
<div id="constantsBox" style="display:none; margin-top:10px;" class="controls">
<div><label>a<sub>v</sub> [MeV]</label><input id="av" type="number" step="0.01" value="15.75"></div>
<div><label>a<sub>s</sub> [MeV]</label><input id="as" type="number" step="0.01" value="17.80"></div>
<div><label>a<sub>I</sub> [MeV]</label><input id="aI" type="number" step="0.01" value="23.70"></div>
<div><label>a<sub>c,phys</sub> [MeV]</label><input id="ac" type="number" step="0.001" value="0.711"></div>
<div><label>a<sub>p</sub> [MeV]</label><input id="ap" type="number" step="0.01" value="11.18"></div>
<div><label>α<sub>phys</sub><sup>−1</sup></label><input id="ainv" type="number" step="0.0001" value="137.035999084"></div>
</div>
<table id="summary"></table>
<div id="status" class="footer-note"></div>
</aside>
</div>
<details open class="theory-details">
<summary>Equations used by the gadget</summary>
<div class="formula mono">
knobs: x=v², y=M_W², z=M_Z²<br>
α*(x,y,z) = y/(πx) · (1 − y/z)<br>
c* = (a_c,phys / α_phys) α*<br><br>
q = (N−Z)/(2Z), &nbsp; D(q)=Δβ+(4a_I+Δβ)q<br>
A(q;c) = [D(q)/c]^{3/2} &nbsp; for endpoint equations<br>
direct valley rendering: Z_β(A;c)=(4a_I+Δβ)A/(8a_I+2cA^{2/3}), &nbsp; N=A−Z_β<br>
Z(q;c) = A/[2(1+q)], &nbsp; N(q;c)=A(1+2q)/[2(1+q)]<br><br>
B(A,I;c)=a_v A − a_s A^{2/3} − c(A−I)²A^{−1/3}/4 − a_I I²/A<br><br>
valley endpoints are intersections of A(q;c) with the contour equations:<br>
alpha dripline: &nbsp; S_α=B(A,I) − B(A−4,I) − B_α(c)=0<br>
fission line: X_fiss=cZ²/(2a_sA)=1, equivalently D(q)^3 − 64a_s²c(1+q)^4=0 on the valley<br>
proton dripline: &nbsp; S_p=B(A,I) − B(A−1,I+1)=0<br>
neutron dripline: S_n=B(A,I) − B(A−1,I−1)=0<br><br>
q_max = min(q_α, q_F, q_p, q_n), &nbsp; Ä=A(q_max;c).
</div>
<p>
The mass parabola at fixed A is moved by the Coulomb coefficient and the neutron-proton mass term.
The drawn valley is parametrized directly by A, not by q, because q becomes a singular coordinate when
c→0 with the atomic beta mass term Δβ>0. The A=1 point is anchored to hydrogen; the liquid-drop formula is
only a smooth heavy-nucleus approximation there.
In this liquid-drop model the static mass parabola at fixed A depends on the strong coefficients, the Coulomb coefficient c, and Δβ, but not directly on G_F. Thus, at fixed α, moving v changes only beta-decay rates <em>provided Δβ and the nuclear coefficients are frozen</em>. Along a more Standard-Model-like trajectory with fixed Yukawa couplings, changing v also changes quark and electron masses and therefore Δβ; this shifts the parabola minimum, although its curvature remains controlled by c and a_I. Direct weak-contact contributions to nuclear masses are neglected here because they are tiny. The blue translucent band is a finite-time diagnostic:
if λ<sub>β</sub>∝G<sub>F</sub><sup>2</sup>Q<sup>5</sup>, then the Q-threshold for a fixed lifetime scales as
Q<sub>τ</sub>∝G<sub>F</sub><sup>−2/5</sup>. For the discrete beta step of a quadratic mass parabola, the displayed half-width is approximately 1/2+Q<sub>τ</sub>/K<sub>Z</sub>. Increasing v weakens G<sub>F</sub> and widens the finite-time beta-survival band.
</p>
</details>
<details class="embed-help">
<summary>Embed this standalone gadget elsewhere</summary>
<p>This file has no external JavaScript, CSS, fonts, or network dependencies. Put the HTML file on any static host and embed it with an iframe. Add <code>?embed=1</code> to hide the title, introductory text, and explanatory panels.</p>
<div class="formula mono embed-code">&lt;iframe
id="ew-qcd-gadget"
src="/path/ew_qcd_stability_gadget.html?embed=1"
title="EW–QCD stability-line gadget"
loading="lazy"
sandbox="allow-scripts"
style="width:100%; height:1050px; border:0;"
&gt;&lt;/iframe&gt;</div>
<p>For automatic height, the gadget posts <code>{type: "ew-qcd-gadget-height", height: ...}</code> to its parent. A parent page may listen for that message and update the iframe height after validating the message origin.</p>
<div class="formula mono embed-code">const frame = document.getElementById('ew-qcd-gadget');
window.addEventListener('message', event =&gt; {
if (event.data?.type === 'ew-qcd-gadget-height') {
frame.style.height = `${event.data.height}px`;
}
});</div>
</details>
</main>
<script>
(() => {
const embedMode = new URLSearchParams(window.location.search).get('embed') === '1';
if (embedMode) document.body.classList.add('embed');
const svg = document.getElementById('chart');
const NS = 'http://www.w3.org/2000/svg';
const U_GEV = 0.93149410242;
const GF_PHYS = 1.1663787e-5;
const B_ALPHA_PHYS = 28.295674;
const DELTA_ATOMIC = 0.782333; // m_n - atomic hydrogen, MeV
const SQRT2 = Math.sqrt(2);
const PI = Math.PI;
const ids = ['Aw','Az','Avh','deltaMode','alphaMode','q0','showDrips','lockAlpha','lockAlphaV','showMagicGrid','av','as','aI','ac','ap','ainv'];
const el = Object.fromEntries(ids.map(id => [id, document.getElementById(id)]));
const summary = document.getElementById('summary');
const status = document.getElementById('status');
const q0Text = document.getElementById('q0Text');
const alphaLockText = document.getElementById('alphaLockText');
const alphaVLockText = document.getElementById('alphaVLockText');
const state = {
dragging: null,
dragGeom: null,
dragDom: null,
Aw: +el.Aw.value,
Az: +el.Az.value,
Avh: +el.Avh.value,
lockNotice: '',
handleFraction: { Aw: 0.34, Az: 0.66, Avh: 0.50 }
};
document.getElementById('reset').onclick = () => {
state.Aw = 80.377 / U_GEV;
state.Az = 91.1876 / U_GEV;
state.Avh = 246.21965 / (SQRT2 * U_GEV);
state.lockNotice = '';
el.Aw.value = state.Aw.toFixed(3);
el.Az.value = state.Az.toFixed(3);
el.Avh.value = state.Avh.toFixed(3);
update();
};
document.getElementById('toggleConstants').onclick = () => {
const box = document.getElementById('constantsBox');
box.style.display = box.style.display === 'none' ? 'grid' : 'none';
};
function setAvhLocked(newAvh) {
const oldAvh = state.Avh;
if (el.lockAlpha.checked && oldAvh > 0 && newAvh > 0) {
const scale = newAvh / oldAvh;
state.Aw *= scale;
state.Az *= scale;
el.Aw.value = state.Aw.toFixed(3);
el.Az.value = state.Az.toFixed(3);
}
state.Avh = newAvh;
el.Avh.value = state.Avh.toFixed(3);
}
function alphaFromStateValues(Aw, Az, Avh) {
if (!(Aw > 0) || !(Az > Aw) || !(Avh > 0)) return NaN;
return (Aw * Aw) / (2 * PI * Avh * Avh) * (1 - (Aw * Aw) / (Az * Az));
}
function solveZFromAlphaWV(alphaTarget, Aw, Avh) {
const K = 2 * PI * Avh * Avh * alphaTarget;
if (!(alphaTarget >= 0) || !(Aw > 0) || !(Avh > 0) || !(Aw * Aw > K)) return null;
const denom = 1 - K / (Aw * Aw);
if (!(denom > 0)) return null;
const Az = Aw / Math.sqrt(denom);
return (Az > Aw) ? Az : null;
}
function solveWFromAlphaZV(alphaTarget, Az, Avh, preferAw) {
const K = 2 * PI * Avh * Avh * alphaTarget;
if (!(alphaTarget >= 0) || !(Az > 0) || !(Avh > 0)) return null;
const disc = 1 - 4 * K / (Az * Az);
if (!(disc >= 0)) return null;
const s = Math.sqrt(Math.max(0, disc));
const wLow2 = 0.5 * Az * Az * (1 - s);
const wHigh2 = 0.5 * Az * Az * (1 + s);
const cands = [Math.sqrt(Math.max(0, wLow2)), Math.sqrt(Math.max(0, wHigh2))].filter(w => w > 0 && w < Az);
if (!cands.length) return null;
if (!(preferAw > 0)) return cands[0];
cands.sort((a,b) => Math.abs(a - preferAw) - Math.abs(b - preferAw));
return cands[0];
}
function applyWZLock(changedKey, oldAlpha) {
state.lockNotice = '';
if (!el.lockAlphaV.checked) return;
if (!(oldAlpha >= 0) || !(state.Avh > 0)) return;
const K = 2 * PI * state.Avh * state.Avh * oldAlpha;
const eps = 1e-9;
if (changedKey === 'Aw') {
const minAw = Math.sqrt(Math.max(K, 0));
if (!(state.Aw > minAw)) {
state.Aw = minAw * (1 + 1e-6);
el.Aw.value = state.Aw.toFixed(3);
state.lockNotice = 'W reached the lower domain boundary compatible with the locked α and v.';
}
const AzNew = solveZFromAlphaWV(oldAlpha, state.Aw, state.Avh);
if (AzNew !== null) {
state.Az = Math.max(state.Aw + eps, AzNew);
el.Az.value = state.Az.toFixed(3);
}
} else if (changedKey === 'Az') {
const minAz = 2 * Math.sqrt(Math.max(K, 0));
if (!(state.Az > minAz)) {
state.Az = minAz * (1 + 1e-6);
el.Az.value = state.Az.toFixed(3);
state.lockNotice = 'Z reached the branch point compatible with the locked α and v.';
}
const AwNew = solveWFromAlphaZV(oldAlpha, state.Az, state.Avh, state.Aw);
if (AwNew !== null) {
state.Aw = Math.min(state.Az - eps, AwNew);
el.Aw.value = state.Aw.toFixed(3);
}
}
}
for (const id of ids) {
el[id].addEventListener('input', () => {
state.lockNotice = '';
const oldAlpha = alphaFromStateValues(state.Aw, state.Az, state.Avh);
if (id === 'Aw') { state.Aw = +el.Aw.value; applyWZLock('Aw', oldAlpha); }
if (id === 'Az') { state.Az = +el.Az.value; applyWZLock('Az', oldAlpha); }
if (id === 'Avh') setAvhLocked(+el.Avh.value);
update();
});
}
function constants() {
return {
av: +el.av.value,
as: +el.as.value,
aI: +el.aI.value,
acPhys: +el.ac.value,
ap: +el.ap.value,
alphaPhys: 1 / (+el.ainv.value),
delta: el.deltaMode.value === 'atomic' ? DELTA_ATOMIC : 0,
alphaMode: el.alphaMode.value,
showDrips: el.showDrips.checked,
showMagicGrid: el.showMagicGrid.checked,
q0: +el.q0.value
};
}
function params() {
const C = constants();
const Aw = state.Aw, Az = state.Az, Avh = state.Avh;
const MW = Aw * U_GEV;
const MZ = Az * U_GEV;
const v = SQRT2 * Avh * U_GEV;
const alpha = (Aw * Aw) / (2 * PI * Avh * Avh) * (1 - (Aw * Aw) / (Az * Az));
const c = (C.acPhys / C.alphaPhys) * alpha;
const GF = 1 / (SQRT2 * v * v);
return { ...C, Aw, Az, Avh, MW, MZ, v, alpha, c, GF, betaScale: (GF / GF_PHYS) ** 2 };
}
function D(q, P) { return P.delta + (4 * P.aI + P.delta) * q; }
function ZbetaA(A, P) {
// Continuous beta-stability valley as a function of A. This remains
// regular when c -> 0; the q-parametrization A=[D(q)/c]^(3/2) does not.
if (!(A > 0)) return NaN;
const denom = 8 * P.aI + 2 * P.c * Math.pow(A, 2/3);
if (!(denom > 0)) return NaN;
return ((4 * P.aI + P.delta) * A) / denom;
}
function valleyNZA(A, P) {
let Z = ZbetaA(A, P);
if (!Number.isFinite(Z)) return null;
// The liquid-drop parabola is not a light-nucleus mass table; clamp only
// to keep the display physical near A=1 and under extreme knob values.
Z = Math.max(0, Math.min(A, Z));
return { A, Z, N: A - Z };
}
function Aof(q, P) {
const d = D(q, P);
if (!(P.c > 0) || d <= 0) return NaN;
return Math.pow(d / P.c, 1.5);
}
function Iof(q, P) {
const A = Aof(q, P);
return q / (1 + q) * A;
}
function B(A, I, P) {
if (!(A > 0) || Math.abs(I) > A) return NaN;
return P.av * A - P.as * Math.pow(A, 2/3) - (P.c / 4) * (A - I) * (A - I) * Math.pow(A, -1/3) - P.aI * I * I / A;
}
function Balpha(P) {
const kappa = Math.pow(4, 2/3);
if (P.alphaMode === 'off') return NaN;
if (P.alphaMode === 'empirical') return B_ALPHA_PHYS + kappa * (P.acPhys - P.c);
return 4 * P.av - kappa * (P.as + P.c) + P.ap / 2;
}
function fAlpha(q, P) {
if (P.alphaMode === 'off') return NaN;
const A = Aof(q, P), I = Iof(q, P), ba = Balpha(P);
if (!(A > 4) || !(ba > 0)) return NaN;
return B(A, I, P) - B(A - 4, I, P) - ba;
}
function fFission(q, P) { return Math.pow(D(q, P), 3) - 64 * P.as * P.as * P.c * Math.pow(1 + q, 4); }
function fProton(q, P) {
const A = Aof(q, P), I = Iof(q, P);
if (!(A > 1)) return NaN;
return B(A, I, P) - B(A - 1, I + 1, P);
}
function fNeutron(q, P) {
const A = Aof(q, P), I = Iof(q, P);
if (!(A > 1)) return NaN;
return B(A, I, P) - B(A - 1, I - 1, P);
}
function fAlphaNZ(N, Z, P) {
if (P.alphaMode === 'off') return NaN;
const A = N + Z, I = N - Z, ba = Balpha(P);
if (!(N >= 2 && Z >= 2 && A > 4) || !(ba > 0)) return NaN;
return B(A, I, P) - B(A - 4, I, P) - ba;
}
function fProtonNZ(N, Z, P) {
const A = N + Z, I = N - Z;
if (!(Z >= 1 && A > 1)) return NaN;
return B(A, I, P) - B(A - 1, I + 1, P);
}
function fNeutronNZ(N, Z, P) {
const A = N + Z, I = N - Z;
if (!(N >= 1 && A > 1)) return NaN;
return B(A, I, P) - B(A - 1, I - 1, P);
}
function fFissionNZ(N, Z, P) {
const A = N + Z;
if (!(A > 1 && Z > 0)) return NaN;
return (P.c * Z * Z) / (2 * P.as * A) - 1;
}
function rootsInRange(fn, P, qMin=1e-7, qMax=8, steps=900) {
const roots = [];
let qPrev = qMin, fPrev = fn(qPrev, P);
for (let i = 1; i <= steps; i++) {
// Mildly logarithmic grid; gives detail near q=0 and still reaches large q.
const t = i / steps;
const q = qMin * Math.pow(qMax / qMin, t);
const f = fn(q, P);
if (Number.isFinite(fPrev) && Number.isFinite(f) && fPrev * f < 0) {
let lo = qPrev, hi = q, flo = fPrev, fhi = f;
for (let j = 0; j < 60; j++) {
const mid = 0.5 * (lo + hi);
const fm = fn(mid, P);
if (!Number.isFinite(fm)) break;
if (flo * fm <= 0) { hi = mid; fhi = fm; }
else { lo = mid; flo = fm; }
}
roots.push(0.5 * (lo + hi));
}
qPrev = q;
fPrev = f;
}
return roots.filter(r => Number.isFinite(r) && r > 0);
}
function endpoint(P) {
if (!(P.Az > P.Aw) || !(P.Avh > 0) || !(P.alpha >= 0)) {
return { valid: false, reason: 'Invalid EW knobs: require M_Z > M_W, v > 0, and α* ≥ 0.' };
}
if (!(P.c > 1e-14)) {
const entries = [
{ key: 'alpha', label: 'alpha', q: NaN, A: Infinity, color: 'var(--alpha)' },
{ key: 'fission', label: 'fission', q: NaN, A: Infinity, color: 'var(--fission)' },
{ key: 'proton', label: 'proton drip', q: NaN, A: Infinity, color: 'var(--proton)' },
{ key: 'neutron', label: 'neutron drip', q: NaN, A: Infinity, color: 'var(--neutron)' }
];
return { valid: true, entries, winner: { key: 'unbounded', label: 'unbounded Coulomb-free limit', q: NaN, A: Infinity, color: 'var(--warn)' } };
}
const alphaRoots = rootsInRange(fAlpha, P, 1e-7, 8);
const alphaRoot = alphaRoots.filter(q => Aof(q, P) > 8).sort((a,b) => b-a)[0];
const fissionRoot = rootsInRange(fFission, P, 1e-7, 8).sort((a,b) => a-b)[0];
const protonRoot = rootsInRange(fProton, P, 1e-7, 8).sort((a,b) => a-b)[0];
const neutronRoot = rootsInRange(fNeutron, P, 1e-7, 8).sort((a,b) => a-b)[0];
const entries = [
{ key: 'alpha', label: 'alpha', q: alphaRoot, color: 'var(--alpha)' },
{ key: 'fission', label: 'fission', q: fissionRoot, color: 'var(--fission)' },
{ key: 'proton', label: 'proton drip', q: protonRoot, color: 'var(--proton)' },
{ key: 'neutron', label: 'neutron drip', q: neutronRoot, color: 'var(--neutron)' }
].map(e => ({ ...e, A: Number.isFinite(e.q) ? Aof(e.q, P) : Infinity }));
const finite = entries.filter(e => Number.isFinite(e.A));
const winner = finite.sort((a,b) => a.A - b.A)[0] || null;
return { valid: true, entries, winner };
}
function fmt(x, digits=4) {
if (!Number.isFinite(x)) return '∞';
const ax = Math.abs(x);
if (ax !== 0 && (ax < 1e-3 || ax >= 1e5)) return x.toExponential(3);
return x.toFixed(digits);
}
function fmtInt(x) { return Number.isFinite(x) ? Math.round(x).toString() : '∞'; }
function ptFromNZ(N, Z, geom) {
const x = geom.m.l + (N / geom.dom) * geom.w;
const y = geom.m.t + (1 - Z / geom.dom) * geom.h;
return [x, y];
}
function NZFromClient(evt, geom) {
const rect = svg.getBoundingClientRect();
const x = (evt.clientX - rect.left) * (900 / rect.width);
const y = (evt.clientY - rect.top) * (900 / rect.height);
const N = ((x - geom.m.l) / geom.w) * geom.dom;
const Z = (1 - (y - geom.m.t) / geom.h) * geom.dom;
return { x, y, N, Z };
}
function make(tag, attrs={}, text='') {
const node = document.createElementNS(NS, tag);
for (const [k,v] of Object.entries(attrs)) node.setAttribute(k, v);
if (text) node.textContent = text;
return node;
}
function pathFromPoints(points) {
return points.map((p,i) => (i ? 'L' : 'M') + p[0].toFixed(2) + ',' + p[1].toFixed(2)).join(' ');
}
function lineSegmentForA(A, geom) {
const dom = geom.dom;
let p1, p2;
if (A <= dom) { p1 = ptFromNZ(0, A, geom); p2 = ptFromNZ(A, 0, geom); }
else { p1 = ptFromNZ(A - dom, dom, geom); p2 = ptFromNZ(dom, A - dom, geom); }
return [p1, p2];
}
function visibleHandleFraction(A, geom, fraction) {
if (!(A > 0)) return 0.5;
const lo = Math.max(0, (A - geom.dom) / A);
const hi = Math.min(1, geom.dom / A);
return Math.max(lo, Math.min(hi, fraction));
}
function pointerFractionOnLine(pos, A, geom) {
if (!(A > 0)) return 0.5;
// Orthogonal projection onto N+Z=A. During motion A=N_mouse+Z_mouse,
// so this is exactly the pointer position unless a lock has clamped A.
const nProjected = 0.5 * (pos.N - pos.Z + A);
return visibleHandleFraction(A, geom, nProjected / A);
}
function distanceToSegment(px, py, x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
const l2 = dx*dx + dy*dy;
if (l2 === 0) return Math.hypot(px-x1, py-y1);
let t = ((px-x1)*dx + (py-y1)*dy) / l2;
t = Math.max(0, Math.min(1, t));
return Math.hypot(px - (x1 + t*dx), py - (y1 + t*dy));
}
function contourPath(fnNZ, P, geom, grid=125) {
const vals = Array.from({ length: grid + 1 }, () => new Float64Array(grid + 1));
const finite = Array.from({ length: grid + 1 }, () => new Uint8Array(grid + 1));
for (let i = 0; i <= grid; i++) {
const N = geom.dom * i / grid;
for (let j = 0; j <= grid; j++) {
const Z = geom.dom * j / grid;
const v = fnNZ(N, Z, P);
vals[i][j] = v;
finite[i][j] = Number.isFinite(v) ? 1 : 0;
}
}
let d = '';
const interp = (a, b) => {
const fa = a.f, fb = b.f;
const t = Math.max(0, Math.min(1, fa / (fa - fb)));
return { N: a.N + t * (b.N - a.N), Z: a.Z + t * (b.Z - a.Z) };
};
for (let i = 0; i < grid; i++) {
const N0 = geom.dom * i / grid, N1 = geom.dom * (i + 1) / grid;
for (let j = 0; j < grid; j++) {
const Z0 = geom.dom * j / grid, Z1 = geom.dom * (j + 1) / grid;
const c0 = { N:N0, Z:Z0, f:vals[i][j], ok:finite[i][j] };
const c1 = { N:N1, Z:Z0, f:vals[i+1][j], ok:finite[i+1][j] };
const c2 = { N:N1, Z:Z1, f:vals[i+1][j+1], ok:finite[i+1][j+1] };
const c3 = { N:N0, Z:Z1, f:vals[i][j+1], ok:finite[i][j+1] };
const cs = [c0,c1,c2,c3];
const ps = [];
for (let k = 0; k < 4; k++) {
const a = cs[k], b = cs[(k+1)&3];
if (!a.ok || !b.ok) continue;
if (a.f === 0 && b.f === 0) continue;
if ((a.f <= 0 && b.f >= 0) || (a.f >= 0 && b.f <= 0)) ps.push(interp(a,b));
}
if (ps.length >= 2) {
for (let k = 0; k + 1 < ps.length; k += 2) {
const pA = ptFromNZ(ps[k].N, ps[k].Z, geom);
const pB = ptFromNZ(ps[k+1].N, ps[k+1].Z, geom);
d += `M${pA[0].toFixed(2)},${pA[1].toFixed(2)}L${pB[0].toFixed(2)},${pB[1].toFixed(2)}`;
}
}
}
}
return d;
}
let lastGeom = null;
function draw(P, E) {
svg.replaceChildren();
const Awin = E.valid && E.winner ? E.winner.A : 250;
// Do not let a huge endpoint under weak Coulomb zoom the origin away.
// User mass lines still expand the plot; endpoints are capped for display.
const endpointForZoom = Number.isFinite(Awin) ? Math.min(Awin, 1600) : 0;
const autoDom = Math.max(260, 1.18 * Math.max(state.Aw, state.Az, state.Avh, endpointForZoom));
const dom = state.dragDom || autoDom;
const geom = { dom, m: { l: 58, r: 22, t: 28, b: 58 } };
geom.w = 900 - geom.m.l - geom.m.r;
geom.h = 900 - geom.m.t - geom.m.b;
lastGeom = geom;
// background
svg.appendChild(make('rect', { x:0, y:0, width:900, height:900, fill:'var(--plot)', rx:10 }));
svg.appendChild(make('rect', { x:geom.m.l, y:geom.m.t, width:geom.w, height:geom.h, fill:'var(--plot-field)', stroke:'var(--border)' }));
// grid and axes
const magicValsAll = [2, 8, 20, 28, 50, 82, 126, 184];
const tickVals = P.showMagicGrid
? [0].concat(magicValsAll.filter(v => v <= dom))
: (() => {
const niceStep = dom <= 300 ? 25 : dom <= 800 ? 50 : dom <= 1600 ? 100 : 250;
const arr = [];
for (let v = 0; v <= dom + 1e-9; v += niceStep) arr.push(v);
return arr;
})();
for (const v of tickVals) {
const [x0,y0] = ptFromNZ(v, 0, geom);
const [x1,y1] = ptFromNZ(v, dom, geom);
svg.appendChild(make('line', { x1:x0, y1:y0, x2:x1, y2:y1, stroke:'var(--grid)', 'stroke-width':1 }));
if (v > 0) svg.appendChild(make('text', { x:x0, y:geom.m.t+geom.h+22, fill:'var(--muted)', 'font-size':12, 'text-anchor':'middle' }, String(Math.round(v))));
const [xx0,yy0] = ptFromNZ(0, v, geom);
const [xx1,yy1] = ptFromNZ(dom, v, geom);
svg.appendChild(make('line', { x1:xx0, y1:yy0, x2:xx1, y2:yy1, stroke:'var(--grid)', 'stroke-width':1 }));
if (v > 0) svg.appendChild(make('text', { x:geom.m.l-10, y:yy0+4, fill:'var(--muted)', 'font-size':12, 'text-anchor':'end' }, String(Math.round(v))));
}
svg.appendChild(make('line', { x1:geom.m.l, y1:geom.m.t+geom.h, x2:geom.m.l+geom.w, y2:geom.m.t+geom.h, stroke:'var(--axis)', 'stroke-width':1.5 }));
svg.appendChild(make('line', { x1:geom.m.l, y1:geom.m.t, x2:geom.m.l, y2:geom.m.t+geom.h, stroke:'var(--axis)', 'stroke-width':1.5 }));
svg.appendChild(make('text', { x:geom.m.l+geom.w/2, y:888, fill:'var(--ink)', 'font-size':15, 'text-anchor':'middle' }, 'N'));
svg.appendChild(make('text', { x:18, y:geom.m.t+geom.h/2, fill:'var(--ink)', 'font-size':15, 'text-anchor':'middle', transform:`rotate(-90 18 ${geom.m.t+geom.h/2})` }, 'Z'));
// N=Z line
const pNZ0 = ptFromNZ(0,0,geom), pNZ1 = ptFromNZ(dom,dom,geom);
svg.appendChild(make('line', { x1:pNZ0[0], y1:pNZ0[1], x2:pNZ1[0], y2:pNZ1[1], stroke:'#b4bfcd', 'stroke-width':1.2, 'stroke-dasharray':'5 5' }));
svg.appendChild(make('text', { x:pNZ1[0]-8, y:pNZ1[1]+15, fill:'#6f7c8f', 'font-size':12, 'text-anchor':'end' }, 'N=Z'));
// Diagonal draggable A lines.
const lines = [
['Aw', state.Aw, 'W', 'var(--w)'],
['Az', state.Az, 'Z', 'var(--z)'],
['Avh', state.Avh, 'v/√2', 'var(--vh)']
];
for (const [key, A, label, color] of lines) {
if (!(A > 0) || A > 2*dom) continue;
const [p1,p2] = lineSegmentForA(A, geom);
svg.appendChild(make('line', { x1:p1[0], y1:p1[1], x2:p2[0], y2:p2[1], stroke:color, 'stroke-width':15, 'stroke-linecap':'round', opacity:0.001, cursor:'grab', 'data-line':key }));
svg.appendChild(make('line', { x1:p1[0], y1:p1[1], x2:p2[0], y2:p2[1], stroke:color, 'stroke-width':3.2, 'stroke-linecap':'round', 'data-line':key, opacity:0.95, cursor:'grab' }));
const handleT = visibleHandleFraction(A, geom, state.handleFraction[key] ?? 0.5);
const handleN = handleT * A;
const handleZ = (1 - handleT) * A;
const mid = ptFromNZ(handleN, handleZ, geom);
svg.appendChild(make('circle', { cx:mid[0], cy:mid[1], r:8, fill:'#ffffff', stroke:color, 'stroke-width':3, cursor:'grab', 'data-line':key, 'aria-label':`${label} control` }));
const labelText = key === 'Avh' ? `v/√2 (Higgs scale) · A=${A.toFixed(1)}` : `${label} · A=${A.toFixed(1)}`;
const boxWidth = key === 'Avh' ? 190 : 112;
const placeLeft = mid[0] > geom.m.l + 0.68 * geom.w;
const boxX = placeLeft ? mid[0] - boxWidth - 12 : mid[0] + 12;
const textX = boxX + 8;
svg.appendChild(make('rect', { x:boxX, y:mid[1]-25, width:boxWidth, height:24, rx:6, fill:'#ffffff', stroke:color, 'stroke-width':1.2, opacity:0.96, cursor:'grab', 'data-line':key }));
svg.appendChild(make('text', { x:textX, y:mid[1]-8, fill:color, 'font-size':13, 'font-weight':700, cursor:'grab', 'data-line':key }, labelText));
}
if (!E.valid || !E.winner) {
svg.appendChild(make('text', { x:450, y:450, fill:'var(--warn)', 'font-size':18, 'text-anchor':'middle' }, E.reason || 'No finite endpoint found'));
return;
}
// Full one-particle/alpha driplines as implicit zero contours.
if (P.showDrips) {
const dripDefs = [
[fAlphaNZ, 'var(--alpha)', '6 5', 2.0],
[fFissionNZ, 'var(--fission)', '3 4', 1.7],
[fProtonNZ, 'var(--proton)', '7 4', 2.0],
[fNeutronNZ, 'var(--neutron)', '7 4', 2.0]
];
for (const [fn, color, dash, width] of dripDefs) {
const d = contourPath(fn, P, geom, 115);
if (d) svg.appendChild(make('path', { d, fill:'none', stroke:color, 'stroke-width':width, 'stroke-dasharray':dash, opacity:0.75 }));
}
}
// Beta finite-time survival band and valley, parametrized by A rather than q.
// This is regular in the W -> 0, alpha* -> 0 limit and visibly starts at hydrogen.
const nPts = 620;
const lower = [], upper = [], valley = [];
const Qcrit = P.q0 * Math.pow(GF_PHYS / P.GF, 2/5);
const endpointA = E.winner && Number.isFinite(E.winner.A) ? E.winner.A : Infinity;
const AStop = Math.max(2, Math.min(endpointA, 2 * geom.dom));
// Physical light anchor: the continuum liquid-drop parabola is not valid at A=1.
// With the atomic beta mass term, A=1 is hydrogen, not the fractional LD point.
if (el.deltaMode.value === 'atomic') valley.push(ptFromNZ(0, 1, geom));
for (let i = 0; i <= nPts; i++) {
const t = i / nPts;
// Slightly concave sampling: more detail near the origin without losing the tail.
const A = 1 + (AStop - 1) * t * t;
const nz = valleyNZA(A, P);
if (!nz || !Number.isFinite(nz.N) || !Number.isFinite(nz.Z)) continue;
const {N, Z} = nz;
if (N < -1e-6 || Z < -1e-6 || N > geom.dom*1.05 || Z > geom.dom*1.05) continue;
valley.push(ptFromNZ(N, Z, geom));
const Kz = 2 * P.c * Math.pow(A, -1/3) + 8 * P.aI / A;
const dZ = Math.min(60, 0.5 + Qcrit / Math.max(Kz, 1e-6));
const Zhi = Math.min(A, Z + dZ), Nhi = A - Zhi;
const Zlo = Math.max(0, Z - dZ), Nlo = A - Zlo;
upper.push(ptFromNZ(Nhi, Zhi, geom));
lower.push(ptFromNZ(Nlo, Zlo, geom));
}
if (upper.length > 3) {
const bandPoints = upper.concat(lower.reverse());
svg.appendChild(make('path', { d: pathFromPoints(bandPoints) + ' Z', fill:'var(--band)', stroke:'none' }));
}
if (valley.length > 2) svg.appendChild(make('path', { d:pathFromPoints(valley), fill:'none', stroke:'var(--valley)', 'stroke-width':3.2, 'stroke-linecap':'round' }));
// Mark the hydrogen anchor explicitly; it is a light-nucleus boundary condition, not LD physics.
if (el.deltaMode.value === 'atomic') {
const hp = ptFromNZ(0, 1, geom);
svg.appendChild(make('circle', { cx:hp[0], cy:hp[1], r:4.5, fill:'var(--ink)', stroke:'#ffffff', 'stroke-width':1.5 }));
svg.appendChild(make('text', { x:hp[0]+7, y:hp[1]-7, fill:'var(--ink)', 'font-size':12, 'font-weight':700 }, 'H'));
}
// Endpoint dots.
for (const ep of E.entries) {
if (!Number.isFinite(ep.A) || !Number.isFinite(ep.q)) continue;
const Z = ep.A / (2*(1+ep.q));
const N = ep.A - Z;
const [x,y] = ptFromNZ(N, Z, geom);
svg.appendChild(make('circle', { cx:x, cy:y, r: ep === E.winner ? 7 : 5, fill:ep.color, stroke:'#ffffff', 'stroke-width':2 }));
svg.appendChild(make('text', { x:x+8, y:y-8, fill:ep.color, 'font-size':12, 'font-weight':700 }, `${ep.label} A≈${Math.round(ep.A)}`));
}
}
function updateTable(P, E) {
q0Text.textContent = (+el.q0.value).toFixed(2);
const rows = [];
rows.push(['M_W', `${fmt(P.MW,4)} GeV`, `A_W=${fmt(P.Aw,3)}`]);
rows.push(['M_Z', `${fmt(P.MZ,4)} GeV`, `A_Z=${fmt(P.Az,3)}`]);
rows.push(['v', `${fmt(P.v,4)} GeV`, `A_v=${fmt(SQRT2*P.Avh,3)}, A_{v/√2}=${fmt(P.Avh,3)}`]);
rows.push(['G_F', `${fmt(P.GF,7)} GeV⁻²`, `rate scale ${(P.betaScale).toExponential(3)}`]);
rows.push(['α*', `${fmt(P.alpha,8)}`, `1/α*=${fmt(1/P.alpha,3)}`]);
rows.push(['c*', `${fmt(P.c,4)} MeV`, `c*/a_c,phys=${fmt(P.c/P.acPhys,4)}`]);
rows.push(['Δβ treatment', el.deltaMode.value === 'atomic' ? `${DELTA_ATOMIC.toFixed(6)} MeV` : '0', 'held fixed while the EW mass markers move']);
rows.push(['α lock on v', el.lockAlpha.checked ? 'on' : 'off', el.lockAlpha.checked ? 'changing v rescales W and Z together, leaving α* fixed' : 'moving v changes α* unless W and Z are adjusted manually']);
rows.push(['α,v lock on W/Z', el.lockAlphaV.checked ? 'on' : 'off', el.lockAlphaV.checked ? 'moving W adjusts Z and moving Z adjusts W at fixed v' : 'W and Z move independently']);
rows.push(['W branch at fixed Z', P.Aw < P.Az / SQRT2 ? 'lower-W' : 'upper-W', 'the two branches meet at W=Z/√2']);
rows.push(['grid mode', P.showMagicGrid ? 'magic' : 'uniform', P.showMagicGrid ? 'magic numbers replace the ordinary coordinate grid' : 'ordinary evenly spaced coordinate grid']);
if (E.valid && E.winner) {
for (const ep of E.entries) rows.push([`A_${ep.key}`, fmtInt(ep.A), `q=${fmt(ep.q,4)}`]);
const q = E.winner.q, A = E.winner.A;
if (Number.isFinite(A) && Number.isFinite(q)) {
const Z = A / (2*(1+q)), N = A - Z;
const Kz = 2 * P.c * Math.pow(A, -1/3) + 8 * P.aI / A;
const Qcrit = P.q0 * Math.pow(GF_PHYS / P.GF, 2/5);
const betaHalf = 0.5 + Qcrit / Math.max(Kz, 1e-9);
rows.push(['winner', E.winner.label, `Ä≈${fmt(A,2)}, Z≈${fmt(Z,1)}, N≈${fmt(N,1)}`]);
rows.push(['K_Z at endpoint', `${fmt(Kz,4)} MeV/charge²`, `β-band halfwidth≈${fmt(betaHalf,2)} charge units`]);
} else {
rows.push(['winner', E.winner.label, 'Ä=∞ in this liquid-drop limit']);
rows.push(['near origin', el.deltaMode.value === 'atomic' ? 'anchored at H' : 'N=Z line', 'display is A-parametrized, not q-parametrized']);
}
}
summary.innerHTML = '<tr><th>quantity</th><th class="num">value</th><th>note</th></tr>' + rows.map(r => `<tr><td>${r[0]}</td><td class="num">${r[1]}</td><td>${r[2]}</td></tr>`).join('');
if (!E.valid) status.innerHTML = `<span class="warn">${E.reason}</span>`;
else status.textContent = (state.lockNotice ? state.lockNotice + ' ' : '') + 'Move any colored diagonal marker, or type a value, to repaint the chart. Endpoints are exact finite differences except fission, which is the usual fissility condition.';
alphaLockText.textContent = el.lockAlpha.checked ? 'Lock is on: changing the v/√2 line rescales W and Z together, preserving α*. With Δβ and the nuclear coefficients frozen, the static valley stays put while G_F changes beta-decay times and the finite-time survival band.' : 'If enabled, moving or typing the v/√2 line rescales W and Z together, so α*=M_W²(1-M_W²/M_Z²)/(πv²) stays fixed.';
alphaVLockText.textContent = el.lockAlphaV.checked ? 'Lock is on: v is held fixed while moving W or Z, and the companion line is adjusted to preserve α*. For a given Z the code keeps the W branch closest to the current one.' : 'If enabled, moving W adjusts Z, and moving Z adjusts W, with v held fixed. For a given Z there are two W branches; the gadget keeps the branch closest to the current W.';
}
function reportHeight() {
if (window.parent === window) return;
requestAnimationFrame(() => {
window.parent.postMessage({ type: 'ew-qcd-gadget-height', height: document.documentElement.scrollHeight }, '*');
});
}
function update() {
const P = params();
const E = endpoint(P);
draw(P, E);
updateTable(P, E);
reportHeight();
}
svg.addEventListener('pointerdown', (evt) => {
if (!lastGeom) return;
const geom = lastGeom;
const pos = NZFromClient(evt, geom);
let best = null;
const directKey = evt.target && evt.target.getAttribute ? evt.target.getAttribute('data-line') : null;
if (directKey && ['Aw','Az','Avh'].includes(directKey)) {
best = { key: directKey, d: 0 };
} else {
for (const [key,A] of [['Aw', state.Aw], ['Az', state.Az], ['Avh', state.Avh]]) {
if (!(A > 0) || A > 2*geom.dom) continue;
const [p1,p2] = lineSegmentForA(A, geom);
const d = distanceToSegment(pos.x, pos.y, p1[0], p1[1], p2[0], p2[1]);
if (best === null || d < best.d) best = { key, d };
}
}
if (best && best.d < 18) {
const selectedA = state[best.key];
state.handleFraction[best.key] = pointerFractionOnLine(pos, selectedA, geom);
state.dragging = best.key;
svg.classList.add('dragging');
state.dragGeom = { ...geom, m: { ...geom.m } };
state.dragDom = geom.dom;
svg.setPointerCapture(evt.pointerId);
evt.preventDefault();
}
});
svg.addEventListener('pointermove', (evt) => {
if (!state.dragging || !state.dragGeom) return;
const pos = NZFromClient(evt, state.dragGeom);
let A = pos.N + pos.Z;
A = Math.max(1, Math.min(5000, A));
state.lockNotice = '';
const oldAlpha = alphaFromStateValues(state.Aw, state.Az, state.Avh);
if (state.dragging === 'Avh') setAvhLocked(A);
else if (state.dragging === 'Aw') {
state.Aw = A;
el.Aw.value = A.toFixed(3);
applyWZLock('Aw', oldAlpha);
} else if (state.dragging === 'Az') {
state.Az = A;
el.Az.value = A.toFixed(3);
applyWZLock('Az', oldAlpha);
}
const activeA = state[state.dragging];
state.handleFraction[state.dragging] = pointerFractionOnLine(pos, activeA, state.dragGeom);
update();
});
function endDrag() {
if (!state.dragging) return;
state.dragging = null;
svg.classList.remove('dragging');
state.dragGeom = null;
state.dragDom = null;
update();
}
svg.addEventListener('pointerup', endDrag);
svg.addEventListener('pointercancel', endDrag);
svg.addEventListener('lostpointercapture', endDrag);
window.addEventListener('resize', update);
update();
})();
</script>
</body>
</html>
@arivero

arivero commented Jun 18, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment