1// hodge/scripts.js 2// 3// Cross-cutting init logic shared by all 6 Hodge calculators. Called once from 4// pages/hodge.js after every individual calculator's init() has run. 5// 6// Responsibilities: 7// 1. Calculator toggle buttons â show/hide container divs, mark active button 8// 2. Label alignment â equalize .input-row label widths within each container 9// 3. Preset button highlighting â keep preset buttons highlighted when active 10// 4. Dynamic diamond description â update the "Hodge diamond for a smooth ..." 11// caption above each diamond in real time as inputs change 12// 5. Coordinate annotation â stamp data-i / data-j attributes and tooltip titles 13// onto every diamond cell (used by the Twisted calculator's click handler) 14// 6. Zero-entry coloring â add .is-zero class to cells whose value is "0" 15// when the "Alternate color for zero entries" toggle is on 16// 7. Hide-top-half toggle â add .is-top-half to rows above the middle row 17// 8. Scroll wheel guard â prevent number inputs from changing on wheel scroll 18 19export function init() { 20 // --- Toggle Between Calculators --- 21 const toggleCompleteIntersection = document.getElementById("toggle-complete-intersection"); 22 const toggleAbelianVariety = document.getElementById("toggle-abelian-variety"); 23 const toggleGrassmannian = document.getElementById("toggle-grassmannian"); 24 const toggleFlag = document.getElementById("toggle-flag"); 25 const toggleTwisted = document.getElementById("toggle-twisted"); 26 const toggleProductGrassmannian = document.getElementById("toggle-product-grassmannian"); 27 28 const completeIntersectionContainer = document.getElementById("complete-intersection-container"); 29 const abelianVarietyContainer = document.getElementById("abelian-variety-container"); 30 const grassmannianContainer = document.getElementById("grassmannian-container"); 31 const flagContainer = document.getElementById("flag-container"); 32 const twistedContainer = document.getElementById("twisted-container"); 33 const productGrassmannianContainer = document.getElementById("product-grassmannian-container"); 34 35 const setActiveToggle = (active, ...inactives) => { 36 active.classList.add("pressed"); 37 inactives.forEach(el => el.classList.remove("pressed")); 38 }; 39 40 // Aligns all .input-row labels within a container to the same width so 41 // sliders line up. Must be called while the container is visible. 42 function alignLabelsInContainer(container) { 43 if (!container) return; 44 const labels = Array.from(container.querySelectorAll(".input-row label")); 45 labels.forEach(l => { l.style.minWidth = ""; }); 46 const maxW = labels.reduce((m, l) => Math.max(m, l.scrollWidth), 0); 47 labels.forEach(l => { l.style.minWidth = maxW + "px"; }); 48 } 49 50 const showContainer = (container) => { 51 completeIntersectionContainer.style.display = "none"; 52 abelianVarietyContainer.style.display = "none"; 53 grassmannianContainer.style.display = "none"; 54 flagContainer.style.display = "none"; 55 twistedContainer.style.display = "none"; 56 if (productGrassmannianContainer) productGrassmannianContainer.style.display = "none"; 57 container.style.display = "block"; 58 // Align labels now that the container is visible (scrollWidth is valid). 59 requestAnimationFrame(() => alignLabelsInContainer(container)); 60 // If the container has a registered update function, call it. 61 if (typeof container.updateCalculator === "function") { 62 container.updateCalculator(); 63 } 64 requestAnimationFrame(updateHodgeDiamondDescription); 65 }; 66 67 toggleCompleteIntersection.addEventListener("click", () => { 68 showContainer(completeIntersectionContainer); 69 setActiveToggle(toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleFlag, toggleTwisted, toggleProductGrassmannian); 70 }); 71 toggleAbelianVariety.addEventListener("click", () => { 72 showContainer(abelianVarietyContainer); 73 setActiveToggle(toggleAbelianVariety, toggleCompleteIntersection, toggleGrassmannian, toggleFlag, toggleTwisted, toggleProductGrassmannian); 74 }); 75 toggleGrassmannian.addEventListener("click", () => { 76 showContainer(grassmannianContainer);
77 setActiveToggle(toggleGrassmannian, toggleCompleteIntersection, toggleAbelianVariety, toggleFlag, toggleTwisted, toggleProductGrassmannian); 78 }); 79 toggleFlag.addEventListener("click", () => { 80 showContainer(flagContainer); 81 setActiveToggle(toggleFlag, toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleTwisted, toggleProductGrassmannian); 82 }); 83 toggleTwisted.addEventListener("click", () => { 84 showContainer(twistedContainer); 85 setActiveToggle(toggleTwisted, toggleFlag, toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleProductGrassmannian); 86 }); 87 if (toggleProductGrassmannian) { 88 toggleProductGrassmannian.addEventListener("click", () => { 89 showContainer(productGrassmannianContainer); 90 setActiveToggle(toggleProductGrassmannian, toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian, toggleFlag, toggleTwisted); 91 }); 92 } 93 94 // Show more / show less toggle for advanced calculators 95 const toggleMenu = document.getElementById("toggle-menu"); 96 const toggleAdvancedBtn = document.getElementById("toggle-advanced"); 97 const advancedContainers = [abelianVarietyContainer, flagContainer, productGrassmannianContainer]; 98 const advancedToggles = [toggleAbelianVariety, toggleFlag, toggleProductGrassmannian]; 99 if (toggleAdvancedBtn && toggleMenu) { 100 toggleAdvancedBtn.addEventListener("click", () => { 101 const expanding = !toggleMenu.classList.contains("show-advanced"); 102 toggleMenu.classList.toggle("show-advanced", expanding); 103 toggleAdvancedBtn.setAttribute("aria-expanded", expanding); 104 toggleAdvancedBtn.textContent = expanding ? "â² Show less" : "â¼ Show more"; 105 // If collapsing while an advanced calculator is visible, revert to CI 106 if (!expanding) { 107 const advancedVisible = advancedContainers.some(c => c && c.style.display !== "none"); 108 if (advancedVisible) { 109 showContainer(completeIntersectionContainer);
110 setActiveToggle(toggleCompleteIntersection, ...advancedToggles, toggleGrassmannian, toggleTwisted); 111 } 112 } 113 }); 114 } 115 116 // Align the default-visible container on first paint. 117 requestAnimationFrame(() => alignLabelsInContainer(completeIntersectionContainer)); 118 119 // --- Preset Button State Updates --- 120 const getDegrees = (container) => 121 [...(container?.children || [])].map(row => parseInt(row.querySelector("input")?.value || 0, 10)); 122 123 const updateAllPressedButtons = () => { 124 // Complete Intersection (P^n) 125 const nValue = parseInt(document.getElementById("n-value")?.value || 0, 10); 126 const degreeToggles = document.getElementById("degree-toggles"); 127 const ciDegrees = getDegrees(degreeToggles); 128 document.querySelectorAll("#complete-intersection-container .preset-button").forEach(btn => { 129 const n = parseInt(btn.dataset.n, 10); 130 const degrees = btn.dataset.degrees?.split(",").map(Number) ?? []; 131 const match = n === nValue && degrees.length === ciDegrees.length && 132 degrees.every((d, i) => d === ciDegrees[i]); 133 btn.classList.toggle("pressed", match); 134 }); 135 136 // Abelian Variety 137 const gValue = parseInt(document.getElementById("g-value")?.value || 0, 10); 138 document.querySelectorAll("#abelian-variety-container .preset-button").forEach(btn => { 139 btn.classList.toggle("pressed", parseInt(btn.dataset.g, 10) === gValue); 140 }); 141 142 // Grassmannian â apply Gr(k,n)â Gr(n-k,n) symmetry reduction before comparing 143 const grN = parseInt(document.getElementById("n-value-grassmannian")?.value || 0, 10); 144 const grKRaw = parseInt(document.getElementById("k-value-grassmannian")?.value || 0, 10); 145 const grK = Math.min(grKRaw, grN - grKRaw); // canonical form 146 const grR = parseInt(document.getElementById("r-value-grassmannian")?.value || 0, 10); 147 const grDegToggles = document.getElementById("degree-toggles-grassmannian"); 148 const grDegrees = getDegrees(grDegToggles); 149 document.querySelectorAll("#grassmannian-container .preset-button").forEach(btn => { 150 const n = parseInt(btn.dataset.n, 10); 151 const kRaw = parseInt(btn.dataset.k, 10); 152 const k = Math.min(kRaw, n - kRaw); // canonical form 153 const r = parseInt(btn.dataset.r, 10); 154 const degrees = btn.dataset.degrees?.split(",").map(Number) ?? []; 155 const match = n === grN && k === grK && r === grR && 156 degrees.length === grDegrees.length && degrees.every((d, i) => d === grDegrees[i]); 157 btn.classList.toggle("pressed", match); 158 }); 159 160 // Product Grassmannian 161 const prodR = parseInt(document.getElementById("r-value-product")?.value || 0, 10); 162 const prodDegToggles = document.getElementById("degree-toggles-product"); 163 // Each degree row has multiple inputs (one per factor) â read them all as a flat array 164 const prodDegrees = [...(prodDegToggles?.querySelectorAll("input") || [])].map(inp => parseInt(inp.value || 0, 10)); 165 const factorRows = [...(document.getElementById("factor-inputs-product")?.children || [])]; 166 const currentFactors = factorRows.flatMap(row => [ 167 parseInt(row.querySelector(".factor-k-value")?.value || 0, 10), 168 parseInt(row.querySelector(".factor-n-value")?.value || 0, 10), 169 ]); 170 document.querySelectorAll("#product-grassmannian-container .preset-button[data-factors]").forEach(btn => { 171 const btnFactors = btn.dataset.factors?.split(",").map(Number) ?? []; 172 const btnR = parseInt(btn.dataset.r, 10); 173 const btnDegrees = btn.dataset.degrees ? btn.dataset.degrees.split(",").map(Number) : []; 174 const match = btnR === prodR && 175 btnFactors.length === currentFactors.length && 176 btnFactors.every((f, i) => f === currentFactors[i]) && 177 btnDegrees.length === prodDegrees.length && 178 btnDegrees.every((d, i) => d === prodDegrees[i]); 179 btn.classList.toggle("pressed", match); 180 }); 181 }; 182 183 const delayedUpdate = () => setTimeout(updateAllPressedButtons, 0); 184 const attachListeners = (selector, eventType) => { 185 document.querySelectorAll(selector).forEach(el => el.addEventListener(eventType, delayedUpdate)); 186 }; 187 188 attachListeners("#n-slider, #k-slider, #g-slider", "input"); 189 attachListeners("#n-value, #k-value, #g-value", "input"); 190 attachListeners(".preset-button", "click"); 191 document.getElementById("degree-toggles")?.addEventListener("input", delayedUpdate); 192 attachListeners("#n-value-grassmannian, #k-value-grassmannian, #r-value-grassmannian, #n-slider-grassmannian, #k-slider-grassmannian, #r-slider-grassmannian", "input"); 193 document.getElementById("degree-toggles-grassmannian")?.addEventListener("input", delayedUpdate); 194 document.getElementById("factor-inputs-product")?.addEventListener("input", delayedUpdate); 195 document.getElementById("r-value-product")?.addEventListener("input", delayedUpdate); 196 document.getElementById("r-slider-product")?.addEventListener("input", delayedUpdate); 197 document.getElementById("degree-toggles-product")?.addEventListener("input", delayedUpdate); 198 199 updateAllPressedButtons(); 200 201 // --- Unified Dynamic Hodge Diamond Description --- 202 203 // Returns "\mathbb{P}^{n-1}" when Gr(k,n) is projective space (k=1 or k=n-1), 204 // otherwise returns "\mathrm{Gr}(k,n)". 205 function grOrPn(k, n) {
206 const ki = parseInt(k, 10), ni = parseInt(n, 10); 207 if (ki === 1 || ki === ni - 1) return `\\mathbb{P}^{${ni - 1}}`; 208 return `\\mathrm{Gr}(${k},${n})`; 209 } 210 211 function updateHodgeDiamondDescription() { 212 let descriptionText = ""; 213 214 // ----- Complete intersections in CP^n ----- 215 if (completeIntersectionContainer && completeIntersectionContainer.style.display !== "none") { 216 const nValueEl = document.getElementById("n-value"); 217 const degreeToggles = document.getElementById("degree-toggles"); 218 if (!nValueEl || !degreeToggles) return; 219 220 const nRaw = nValueEl.value.trim(); 221 const degreeInputs = Array.from(degreeToggles.querySelectorAll("input.hodge-input")); 222 223 if (nRaw === "" || isNaN(parseInt(nRaw, 10))) { 224 descriptionText = "Hodge diamond for a complete intersection in projective space"; 225 } else { 226 const n = parseInt(nRaw, 10); 227 const degreesRaw = degreeInputs.map(inp => (inp.value ?? "").trim()); 228 const anyBadDegree = degreesRaw.some(d => d === "" || isNaN(parseInt(d, 10))); 229 230 if (degreeInputs.length === 0 || anyBadDegree) { 231 descriptionText = `Hodge diamond for a smooth complete intersection in \\(\\mathbb{P}^{${n}}\\)`; 232 } else if (degreeInputs.length === 1) { 233 const degree = parseInt(degreesRaw[0], 10); 234 descriptionText = 235 `Hodge diamond for a smooth hypersurface of degree ${degree} in ` + 236 `\\(\\mathbb{P}^{${n}}\\)`; 237 } else { 238 const degrees = degreesRaw.map(d => parseInt(d, 10)); 239 const multidegreeStr = "(" + degrees.join(", ") + ")"; 240 descriptionText = 241 `Hodge diamond for a smooth complete intersection of multidegree ${multidegreeStr} ` + 242 `in \\(\\mathbb{P}^{${n}}\\)`; 243 } 244 const sElCI = document.getElementById("s-value"); 245 const sCI = sElCI ? (parseInt(sElCI.value, 10) || 0) : 0; 246 if (sCI > 0) { 247 descriptionText += `, blown up at \\(${sCI}\\) point${sCI === 1 ? "" : "s"} in general position`; 248 } 249 } 250 } 251 252 // For Grassmannians: 253 else if (grassmannianContainer && grassmannianContainer.style.display !== "none") { 254 const nValueEl = document.getElementById("n-value-grassmannian"); 255 const kValueEl = document.getElementById("k-value-grassmannian"); 256 const rValueEl = document.getElementById("r-value-grassmannian"); 257 const degreeToggles = document.getElementById("degree-toggles-grassmannian"); 258 if (!nValueEl || !kValueEl || !rValueEl || !degreeToggles) return; 259 const n = nValueEl.value; 260 const k = kValueEl.value; 261 const r = parseInt(rValueEl.value, 10); 262 const degreeInputs = Array.from(degreeToggles.querySelectorAll("input.hodge-input")); 263 const ambientGr = grOrPn(k, n); 264 if (degreeInputs.length === 0 || r === 0) { 265 descriptionText = `Hodge diamond for \\(${ambientGr}\\)`; 266 } else if (degreeInputs.length === 1) { 267 const degree = degreeInputs[0].value; 268 descriptionText = `Hodge diamond for a smooth hypersurface of degree ${degree} in \\(${ambientGr}\\)`; 269 } else { 270 const degrees = degreeInputs.map(input => input.value); 271 const multidegreeStr = "(" + degrees.join(", ") + ")"; 272 descriptionText = `Hodge diamond for a smooth complete intersection of multidegree ${multidegreeStr} in \\(${ambientGr}\\)`; 273 } 274 const sElGr = document.getElementById("s-value-grassmannian"); 275 const sGr = sElGr ? (parseInt(sElGr.value, 10) || 0) : 0; 276 if (sGr > 0) { 277 descriptionText += `, blown up at \\(${sGr}\\) point${sGr === 1 ? "" : "s"} in general position`; 278 } 279 } 280 // For Abelian Varieties: 281 else if (abelianVarietyContainer && abelianVarietyContainer.style.display !== "none") { 282 const gValueEl = document.getElementById("g-value"); 283 if (!gValueEl) return; 284 const g = gValueEl.value; 285 descriptionText = `Hodge diamond for an abelian variety of genus \\(g=${g}\\)`; 286 } 287 // For Flag Varieties: 288 else if (flagContainer && flagContainer.style.display !== "none") { 289 const dimsInput = document.getElementById("dims-input"); 290 const rValueFlag = document.getElementById("r-value-flag"); 291 const degreeToggles = document.getElementById("degree-toggles-flag"); 292 if (!dimsInput || !rValueFlag || !degreeToggles) return; 293 294 const dimsList = dimsInput.value 295 .split(",") 296 .map(s => s.trim()) 297 .filter(s => s !== ""); 298 const dimsStr = dimsList.join(", "); 299 300 const r = parseInt(rValueFlag.value, 10); 301 const degreeInputs = Array.from( 302 degreeToggles.querySelectorAll("input.hodge-input") 303 ); 304 305 if (!Number.isFinite(r) || r === 0 || degreeInputs.length === 0) { 306 descriptionText = `Hodge diamond for a partial flag of dimensions [${dimsStr}]`;
307 } else if (degreeInputs.length === 1) { 308 const block = (degreeInputs[0].value || "").trim(); 309 descriptionText = 310 `Hodge diamond for a hypersurface of multidegree [${block}] ` + 311 `in a partial flag of dimensions [${dimsStr}]`; 312 } else { 313 const blocks = degreeInputs.map(inp => { 314 const txt = (inp.value || "").trim(); 315 return `[${txt}]`; 316 }); 317 descriptionText = 318 `Hodge diamond for a complete intersection (r=${r}) ` + 319 `in a partial flag of dimensions [${dimsStr}] ` + 320 `with multidegrees ${blocks.join(", ")}`; 321 } 322 } 323 324 // For Product of Grassmannians: 325 else if (productGrassmannianContainer && productGrassmannianContainer.style.display !== "none") { 326 const r = parseInt(document.getElementById("r-value-product")?.value, 10); 327 const factorRows = Array.from( 328 document.querySelectorAll("#factor-inputs-product .factor-row-product") 329 ); 330 const factorStrs = factorRows.map(row => { 331 const k = row.querySelector(".factor-k-value")?.value; 332 const n = row.querySelector(".factor-n-value")?.value; 333 return grOrPn(k, n); 334 }); 335 const productStr = factorStrs.join("\\times "); 336 const degInputs = Array.from( 337 document.getElementById("degree-toggles-product") 338 ?.querySelectorAll(".degree-toggle") ?? [] 339 ); 340 if (!r || degInputs.length === 0) { 341 descriptionText = `Hodge diamond for \\(${productStr}\\)`; 342 } else { 343 const degs = degInputs.map(row => { 344 const vals = Array.from(row.querySelectorAll(".hodge-input")).map(i => i.value ?? "?"); 345 return `(${vals.join(",")})`; 346 }); 347 descriptionText = 348 `Hodge diamond for a smooth complete intersection of multidegree ${degs.join(", ")} ` + 349 `in \\(${productStr}\\)`; 350 } 351 } 352 // For Twisted Hodge Numbers: 353 else if (twistedContainer && twistedContainer.style.display !== "none") { 354 const nValueEl = document.getElementById("n-value-twisted"); 355 const kValueEl = document.getElementById("k-value-twisted"); 356 const tValueEl = document.getElementById("t-value-twisted"); 357 if (!nValueEl || !kValueEl || !tValueEl) return; 358 const n = nValueEl.value; 359 const k = kValueEl.value; 360 const t = parseInt(tValueEl.value, 10); 361 if (t === 0) { 362 descriptionText = `Hodge diamond for \\(\\text{Gr}(${k},${n})\\)`; 363 } else { 364 descriptionText = `Hodge diamond for \\(\\Omega_{\\text{Gr}(${k},${n})}\\otimes\\mathcal O_{\\text{Gr}(${k},${n})}(${t})\\)`; 365 } 366 } 367 else { 368 descriptionText = ""; 369 } 370 371 document.querySelectorAll(".hodge-diamond-description").forEach(el => { 372 el.innerHTML = descriptionText; 373 if (window.MathJax && MathJax.typesetPromise) { 374 MathJax.typesetPromise([el]).catch(err => console.error("MathJax typeset failed: " + err.message)); 375 } 376 }); 377 } 378 379 // --- Attach Event Listeners for Description Updates --- 380 // (unchanged listeners omitted for brevity â keep your existing ones) 381 const nSlider = document.getElementById("n-slider"); 382 const nValueEl = document.getElementById("n-value"); 383 const kSlider = document.getElementById("k-slider"); 384 const kValueEl = document.getElementById("k-value"); 385 const degreeTogglesEl = document.getElementById("degree-toggles"); 386 if (nSlider) { 387 nSlider.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 388 nSlider.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 389 } 390 if (nValueEl) nValueEl.addEventListener("input", updateHodgeDiamondDescription); 391 if (kSlider) { 392 kSlider.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 393 kSlider.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 394 } 395 if (kValueEl) kValueEl.addEventListener("input", updateHodgeDiamondDescription); 396 if (degreeTogglesEl) degreeTogglesEl.addEventListener("input", updateHodgeDiamondDescription); 397 398 const nSliderG = document.getElementById("n-slider-grassmannian"); 399 const nValueG = document.getElementById("n-value-grassmannian");
400 const kSliderG = document.getElementById("k-slider-grassmannian"); 401 const kValueG = document.getElementById("k-value-grassmannian"); 402 const rSliderG = document.getElementById("r-slider-grassmannian"); 403 const rValueG = document.getElementById("r-value-grassmannian"); 404 const degreeTogglesG = document.getElementById("degree-toggles-grassmannian"); 405 if (nSliderG) { 406 nSliderG.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 407 nSliderG.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 408 } 409 if (nValueG) nValueG.addEventListener("input", updateHodgeDiamondDescription); 410 if (kSliderG) { 411 kSliderG.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 412 kSliderG.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 413 } 414 if (kValueG) kValueG.addEventListener("input", updateHodgeDiamondDescription); 415 if (rSliderG) { 416 rSliderG.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 417 rSliderG.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 418 } 419 if (rValueG) rValueG.addEventListener("input", updateHodgeDiamondDescription); 420 if (degreeTogglesG) degreeTogglesG.addEventListener("input", updateHodgeDiamondDescription); 421 422 const sSliderCI = document.getElementById("s-slider"); 423 const sValueCI = document.getElementById("s-value"); 424 if (sSliderCI) { 425 sSliderCI.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 426 sSliderCI.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 427 } 428 if (sValueCI) sValueCI.addEventListener("input", updateHodgeDiamondDescription); 429 430 const sSliderGr = document.getElementById("s-slider-grassmannian"); 431 const sValueGr = document.getElementById("s-value-grassmannian"); 432 if (sSliderGr) { 433 sSliderGr.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 434 sSliderGr.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 435 } 436 if (sValueGr) sValueGr.addEventListener("input", updateHodgeDiamondDescription); 437 438 const gSlider = document.getElementById("g-slider"); 439 const gValueEl = document.getElementById("g-value"); 440 if (gSlider) { 441 gSlider.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 442 gSlider.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 443 } 444 if (gValueEl) gValueEl.addEventListener("input", updateHodgeDiamondDescription); 445 446 447 const nSliderT = document.getElementById("n-slider-twisted"); 448 const nValueT = document.getElementById("n-value-twisted"); 449 const kSliderT = document.getElementById("k-slider-twisted"); 450 const kValueT = document.getElementById("k-value-twisted"); 451 const tSliderT = document.getElementById("t-slider-twisted"); 452 const tValueT = document.getElementById("t-value-twisted"); 453 if (nSliderT) { 454 nSliderT.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 455 nSliderT.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 456 } 457 if (nValueT) nValueT.addEventListener("input", updateHodgeDiamondDescription); 458 if (kSliderT) { 459 kSliderT.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 460 kSliderT.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 461 } 462 if (kValueT) kValueT.addEventListener("input", updateHodgeDiamondDescription); 463 if (tSliderT) { 464 tSliderT.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 465 tSliderT.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 466 } 467 if (tValueT) tValueT.addEventListener("input", updateHodgeDiamondDescription); 468 469 // Product Grassmannian inputs â r slider/textbox 470 const rSliderProduct = document.getElementById("r-slider-product"); 471 const rValueProduct = document.getElementById("r-value-product"); 472 if (rSliderProduct) { 473 rSliderProduct.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 474 rSliderProduct.addEventListener("change", () => requestAnimationFrame(updateHodgeDiamondDescription)); 475 } 476 if (rValueProduct) rValueProduct.addEventListener("input", updateHodgeDiamondDescription); 477 // Factor rows and degree toggles are dynamically created; use event delegation 478 const factorInputsProduct = document.getElementById("factor-inputs-product"); 479 if (factorInputsProduct) { 480 factorInputsProduct.addEventListener("input", () => requestAnimationFrame(updateHodgeDiamondDescription)); 481 } 482 const degTogglesProduct = document.getElementById("degree-toggles-product"); 483 if (degTogglesProduct) degTogglesProduct.addEventListener("input", updateHodgeDiamondDescription); 484 // Re-describe after Add/Remove Factor buttons 485 ["add-factor-product","remove-factor-product"].forEach(id => { 486 const el = document.getElementById(id); 487 if (el) el.addEventListener("click", () => requestAnimationFrame(updateHodgeDiamondDescription)); 488 }); 489 490 const presetButtons = document.querySelectorAll(".preset-button"); 491 presetButtons.forEach(button => { 492 button.addEventListener("click", () => { 493 requestAnimationFrame(updateHodgeDiamondDescription); 494 }); 495 }); 496 497 // ----- Zero-color toggle ----- 498 const zeroToggle = document.getElementById("zero-color-toggle"); 499 500 // All diamond roots we render into 501 const diamondRoots = [ 502 document.getElementById("diamond-container"), 503 document.getElementById("diamond-container-abelian"), 504 document.getElementById("diamond-container-grassmannian"), 505 document.getElementById("diamond-container-flag"), 506 document.getElementById("diamond-container-twisted"), 507 document.getElementById("diamond-container-product"),
508 ].filter(Boolean); 509 510 // Stamp data-i, data-j, and a hover tooltip onto every cell of the given diamond root. 511 // 512 // Diamond convention: the diamond has 2N+1 rows. Row 0 (top) is h^{N,N}; row 2N 513 // (bottom) is h^{0,0}. In each row, the leftmost cell has the largest i value. 514 // 515 // For a row at DOM index idxTop (0 = top of diamond): 516 // - "antidiagonal index" r = i + j = (2N) - idxTop (bottom row â r=0, top row â r=2N) 517 // - i ranges from iMax = min(r, N) down to iMin = max(0, r-N), left to right 518 // - j = r - i 519 // 520 // These coordinates are consumed by the Twisted calculator's click handler to show 521 // which Schubert partitions contribute to the clicked h^{i,j} value. 522 function annotateDiamondCoords(root) { 523 if (!root) return; 524 525 const rows = Array.from( 526 root.querySelectorAll(".hodge-row, .diamond-row") 527 ); 528 if (!rows.length) return; 529 530 const L = rows.length; 531 if (L % 2 === 0) return; // expect 2N+1 rows; bail if something is wrong 532 const N = (L - 1) / 2; // half-dimension: diamond spans h^{p,q} with p,q in [0,N] 533 534 rows.forEach((row, idxTop) => { 535 const r = (L - 1) - idxTop; // antidiagonal: bottom row â r=0, top row â r=2N 536 537 const spans = Array.from( 538 row.querySelectorAll(".diamond-value, .hodge-cell, span") 539 ).filter(el => el.textContent && el.textContent.trim() !== ""); 540 if (!spans.length) return; 541 542 const iMin = Math.max(0, r - N); 543 const iMax = Math.min(r, N); 544 545 // Leftmost span has i = iMax; i decreases by 1 per step going right 546 spans.forEach((span, s) => { 547 const i = iMax - s; 548 const j = r - i; 549 550 if (i < 0 || j < 0 || i > N || j > N) return; 551 552 span.dataset.i = String(i); 553 span.dataset.j = String(j); 554 span.title = `(${i}, ${j})`; // native hover tooltip 555 }); 556 }); 557 } 558 559 560 function annotateAllDiamondCoords() { 561 diamondRoots.forEach(annotateDiamondCoords); 562 } 563 564 // Mark any element whose visible text is exactly "0" 565 function labelZeros(root) { 566 if (!root) return; 567 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); 568 const toMark = new Set(); 569 while (true) { 570 const n = walker.nextNode(); 571 if (!n) break; 572 const txt = n.nodeValue && n.nodeValue.trim(); 573 if (txt === "0") { 574 const host = n.parentElement; 575 if (host) toMark.add(host); 576 } 577 } 578 toMark.forEach(el => el.classList.add("is-zero")); 579 } 580 581 // Mark the top half (excluding the middle row) of each diamond 582 function labelTopHalf(root) { 583 if (!root) return; 584 585 const rows = Array.from( 586 root.querySelectorAll(".hodge-row, .diamond-row") 587 ); 588 if (!rows.length) return; 589 590 // querySelectorAll returns DOM order (topâbottom), no position sort needed 591 const mid = Math.floor(rows.length / 2); 592 593 rows.forEach((row, idx) => { 594 if (idx < mid) { 595 row.classList.add("is-top-half"); 596 } else { 597 row.classList.remove("is-top-half"); 598 } 599 }); 600 } 601 602 function relabelAllZeros() { 603 diamondRoots.forEach(labelZeros); 604 } 605 606 function relabelAllTopHalf() { 607 diamondRoots.forEach(labelTopHalf); 608 } 609 610 // Scroll #diamond-box so the diamond peak is centred in the visible area. 611 // Only activates when the diamond is wider than the box. 612 function centreDiamondScroll(root) { 613 const wrapper = root.closest('.diamond-scroll-wrapper'); 614 if (!wrapper) return; 615 requestAnimationFrame(() => { 616 const overflow = wrapper.scrollWidth - wrapper.clientWidth; 617 wrapper.scrollLeft = overflow > 0 ? overflow / 2 : 0; 618 }); 619 } 620 621 // Mutation observer: now also annotates coordinates 622 const observers = diamondRoots.map(root => { 623 const obs = new MutationObserver(() => { 624 annotateDiamondCoords(root); // NEW 625 if (document.body.classList.contains("zero-alt-on")) labelZeros(root); 626 labelTopHalf(root); 627 centreDiamondScroll(root); 628 }); 629 obs.observe(root, { childList: true, subtree: true }); 630 return obs; 631 }); 632 633 window.addEventListener('beforeunload', () =>
633 observers.forEach(o => o.disconnect())); 634 635 if (zeroToggle) { 636 zeroToggle.addEventListener("change", () => { 637 const on = zeroToggle.checked; 638 document.body.classList.toggle("zero-alt-on", on); 639 if (on) relabelAllZeros(); 640 }); 641 } 642 643 const hideTopHalfToggle = document.getElementById("hide-top-half-toggle"); 644 645 if (hideTopHalfToggle) { 646 hideTopHalfToggle.addEventListener("change", () => { 647 const on = hideTopHalfToggle.checked; 648 document.body.classList.toggle("hide-top-half", on); 649 if (on) { 650 relabelAllTopHalf(); 651 } 652 }); 653 } 654 655 const showBlowupsToggle = document.getElementById("show-blowups-toggle"); 656 if (showBlowupsToggle) { 657 showBlowupsToggle.addEventListener("change", () => { 658 document.body.classList.toggle("show-blowups", showBlowupsToggle.checked); 659 }); 660 } 661 662 // Initial pass 663 annotateAllDiamondCoords(); // NEW 664 665 if (zeroToggle?.checked) { 666 document.body.classList.add("zero-alt-on"); 667 relabelAllZeros(); 668 } 669 670 if (hideTopHalfToggle?.checked) { 671 document.body.classList.add("hide-top-half"); 672 } 673 relabelAllTopHalf(); 674 675 if (showBlowupsToggle?.checked) { 676 document.body.classList.add("show-blowups"); 677 } 678 679 updateHodgeDiamondDescription(); 680 681 // --- URL State: encode current calculator state into query params --- 682 function encodeState() { 683 const params = new URLSearchParams(); 684 685 let calc = 'ci'; 686 if (abelianVarietyContainer.style.display !== 'none') calc = 'abelian'; 687 else if (grassmannianContainer.style.display !== 'none') calc = 'grassmannian'; 688 else if (flagContainer.style.display !== 'none') calc = 'flag'; 689 else if (twistedContainer.style.display !== 'none') calc = 'twisted'; 690 else if (productGrassmannianContainer?.style.display !== 'none') calc = 'product'; 691 params.set('calc', calc); 692 693 if (calc === 'ci') { 694 params.set('n', document.getElementById('n-value').value); 695 const r = document.getElementById('r-value').value; 696 params.set('r', r); 697 if (parseInt(r) > 0) { 698 const degs = Array.from(document.querySelectorAll('#degree-toggles .hodge-input')).map(i => i.value); 699 if (degs.length) params.set('deg', degs.join(',')); 700 } 701 const s = document.getElementById('s-value')?.value; 702 if (s && s !== '0') params.set('s', s); 703 704 } else if (calc === 'abelian') { 705 params.set('g', document.getElementById('g-value').value); 706 707 } else if (calc === 'grassmannian') { 708 params.set('n', document.getElementById('n-value-grassmannian').value); 709 params.set('k', document.getElementById('k-value-grassmannian').value); 710 const r = document.getElementById('r-value-grassmannian').value; 711 params.set('r', r); 712 if (parseInt(r) > 0) { 713 const degs = Array.from(document.querySelectorAll('#degree-toggles-grassmannian .hodge-input')).map(i => i.value); 714 if (degs.length) params.set('deg', degs.join(',')); 715 } 716 const s = document.getElementById('s-value-grassmannian')?.value; 717 if (s && s !== '0') params.set('s', s); 718 719 } else if (calc === 'flag') { 720 params.set('dims', (document.getElementById('dims-input')?.value ?? '').replace(/\s/g, '')); 721 params.set('r', document.getElementById('r-value-flag')?.value ?? '0'); 722 const flagInputs = document.querySelectorAll('#degree-toggles-flag .hodge-input'); 723 if (flagInputs.length) { 724 params.set('md', Array.from(flagInputs).map(i => i.value.replace(/\s/g, '')).join('|')); 725 } 726 727 } else if (calc === 'twisted') { 728 params.set('n', document.getElementById('n-value-twisted').value); 729 params.set('k', document.getElementById('k-value-twisted').value); 730 params.set('t', document.getElementById('t-value-twisted').value); 731 732 } else if (calc === 'product') { 733 const factorRows = document.querySelectorAll('#factor-inputs-product .factor-row-product'); 734 const pairs = Array.from(factorRows).map(row => 735 row.querySelector('.factor-k-value').value + ',' + row.querySelector('.factor-n-value').value 736 ); 737 params.set('factors', pairs.join(',')); 738 const r = document.getElementById('r-value-product')?.value ?? '0'; 739 params.set('r', r); 740 if (parseInt(r) > 0) { 741 const degRows = document.querySelectorAll('#degree-toggles-product .degree-toggle'); 742 const tuples = Array.from(degRows).map(row => 743 Array.from(row.querySelectorAll('.hodge-input')).map(i => i.value).join(',') 744 ); 745 if (tuples.length) params.set('md', tuples.join('|')); 746 } 747 } 748 749 if (document.getElementById('zero-color-toggle')?.checked) params.set('color', '1'); 750 if (document.getElementById('hide-top-half-toggle')?.checked) params.set('half', '1'); 751 if (document.getElementById('show-blowups-toggle')?.checked) params.set('blowups', '1'); 752 753 return params; 754 } 755 756 // --- URL State: restore calculator state from query params on page load --- 757 function decodeState() { 758 const params = new URLSearchParams(window.location.search); 759 const calc = params.get('calc'); 760 if (!calc) return; 761 762 function fire(el) { el?.dispatchEvent(new Event('input', { bubbles: true })); } 763 function setVal(id, val) { 764 if (val === null) return; 765 const el = document.getElementById(id); 766 if (el) { el.value = val; fire(el); } 767 } 768 769 // Switch to the requested tab (CI is already active by default so skip its click) 770 const tabBtns = { abelian: toggleAbelianVariety, grassmannian: toggleGrassmannian,
771 flag: toggleFlag, twisted: toggleTwisted, product: toggleProductGrassmannian }; 772 if (tabBtns[calc]) tabBtns[calc].click(); 773 774 if (calc === 'ci') { 775 setVal('n-value', params.get('n')); 776 setVal('r-value', params.get('r')); 777 setVal('s-value', params.get('s') ?? '0'); 778 const deg = params.get('deg'); 779 if (deg) { 780 requestAnimationFrame(() => { 781 const inputs = document.querySelectorAll('#degree-toggles .hodge-input'); 782 deg.split(',').forEach((d, i) => { 783 if (inputs[i]) { inputs[i].value = d; fire(inputs[i]); } 784 }); 785 }); 786 } 787 788 } else if (calc === 'abelian') { 789 setVal('g-value', params.get('g')); 790 791 } else if (calc === 'grassmannian') { 792 setVal('n-value-grassmannian', params.get('n')); 793 setVal('k-value-grassmannian', params.get('k')); 794 setVal('r-value-grassmannian', params.get('r')); 795 setVal('s-value-grassmannian', params.get('s') ?? '0'); 796 const deg = params.get('deg'); 797 if (deg) { 798 requestAnimationFrame(() => { 799 const inputs = document.querySelectorAll('#degree-toggles-grassmannian .hodge-input'); 800 deg.split(',').forEach((d, i) => { 801 if (inputs[i]) { inputs[i].value = d; fire(inputs[i]); } 802 }); 803 }); 804 } 805 806 } else if (calc === 'flag') { 807 // Flag is lazily loaded â click the toggle to trigger the dynamic import, 808 // then wait for _flagInitPromise (set up in pages/hodge.js) to resolve 809 // before restoring state. This replaces the old fragile setTimeout(500/200). 810 toggleFlag?.click(); 811 (window._flagInitPromise || Promise.resolve()) 812 .catch(e => console.error('Flag init failed, skipping state restore:', e)) 813 .then(() => { 814 setVal('dims-input', (params.get('dims') ?? '').replace(/,/g, ', ')); 815 setVal('r-value-flag', params.get('r')); 816 const md = params.get('md'); 817 if (md) { 818 const inputs = document.querySelectorAll('#degree-toggles-flag .hodge-input'); 819 md.split('|').forEach((val, i) => { 820 if (inputs[i]) { inputs[i].value = val; fire(inputs[i]); } 821 }); 822 } 823 }); 824 825 } else if (calc === 'twisted') { 826 setVal('n-value-twisted', params.get('n')); 827 setVal('k-value-twisted', params.get('k')); 828 setVal('t-value-twisted', params.get('t')); 829 830 } else if (calc === 'product') { 831 const factorsStr = params.get('factors'); 832 if (factorsStr && productGrassmannianContainer?._loadPreset) { 833 const flat = factorsStr.split(',').map(Number); 834 const factors = []; 835 for (let i = 0; i + 1 < flat.length; i += 2) factors.push({ k: flat[i], n: flat[i + 1] }); 836 const rNum = parseInt(params.get('r')) || 0; 837 const md = params.get('md'); 838 const degrees = md ? md.split('|').map(row => row.split(',').map(Number)) : []; 839 productGrassmannianContainer._loadPreset(factors, rNum, degrees); 840 } 841 } 842 843 if (params.get('color') === '1') { 844 const el = document.getElementById('zero-color-toggle'); 845 if (el) { el.checked = true; el.dispatchEvent(new Event('change')); } 846 } 847 if (params.get('half') === '1') { 848 const el = document.getElementById('hide-top-half-toggle'); 849 if (el) { el.checked = true; el.dispatchEvent(new Event('change')); } 850 } 851 if (params.get('blowups') === '1') { 852 const el = document.getElementById('show-blowups-toggle'); 853 if (el) { el.checked = true; el.dispatchEvent(new Event('change')); } 854 } 855 } 856 857 // Keep the URL in sync with the current calculator state as inputs change 858 let _urlUpdateTimer = null; 859 function scheduleUrlUpdate() { 860 clearTimeout(_urlUpdateTimer); 861 _urlUpdateTimer = setTimeout(() => { 862 const params = encodeState(); 863 history.replaceState(history.state, '', '/hodge?' + params.toString()); 864 }, 300); 865 } 866 document.getElementById('app')?.addEventListener('input', scheduleUrlUpdate); 867 document.getElementById('app')?.addEventListener('change', scheduleUrlUpdate); 868 document.getElementById('app')?.addEventListener('click', (e) => { 869 if (e.target.closest('.preset-button')) scheduleUrlUpdate(); 870 }); 871 // Also update when switching tabs (tab buttons fire click, not input) 872 [toggleCompleteIntersection, toggleAbelianVariety, toggleGrassmannian,
873 toggleFlag, toggleTwisted, toggleProductGrassmannian].forEach(btn => { 874 btn?.addEventListener('click', () => setTimeout(scheduleUrlUpdate, 0)); 875 }); 876 ['add-factor-product', 'remove-factor-product'].forEach(id => { 877 document.getElementById(id)?.addEventListener('click', scheduleUrlUpdate); 878 }); 879 880 // Extend _pageCleanup to cancel any pending URL update timer on navigation 881 const prevCleanupUrl = window._pageCleanup; 882 window._pageCleanup = () => { 883 prevCleanupUrl?.(); 884 clearTimeout(_urlUpdateTimer); 885 }; 886 887 // Copy Link button: encode state to URL, update address bar, copy to clipboard 888 const copyLinkBtn = document.getElementById('copy-link-btn'); 889 if (copyLinkBtn) { 890 copyLinkBtn.addEventListener('click', () => { 891 clearTimeout(_urlUpdateTimer); 892 const params = encodeState(); 893 const search = '?' + params.toString(); 894 const fullUrl = window.location.origin + '/hodge' + search; 895 history.replaceState(history.state, '', '/hodge' + search); 896 897 const flash = (ok) => { 898 const orig = copyLinkBtn.textContent; 899 copyLinkBtn.textContent = ok ? 'Copied!' : 'Copy failed'; 900 copyLinkBtn.disabled = true; 901 setTimeout(() => { copyLinkBtn.textContent = orig; copyLinkBtn.disabled = false; }, 1500); 902 }; 903 904 if (navigator.clipboard?.writeText) { 905 navigator.clipboard.writeText(fullUrl).then(() => flash(true)).catch(() => flash(false)); 906 } else { 907 // Fallback for browsers without clipboard API 908 const ta = document.createElement('textarea'); 909 ta.value = fullUrl; 910 ta.style.cssText = 'position:fixed;opacity:0'; 911 document.body.appendChild(ta); 912 ta.select(); 913 try { document.execCommand('copy'); flash(true); } catch { flash(false); } 914 document.body.removeChild(ta); 915 } 916 }); 917 } 918 919 // Restore state from URL query params (shared links, back-button navigation) 920 decodeState(); 921 922}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.