Created
June 16, 2026 20:12
-
-
Save jmikedupont2/55fd5e8f6347066d05fafd8146b1b41c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
| <title>A Tour of the Monster</title> | |
| <style> | |
| :root { --gold:#ffd23f; --paper:#0c0c14; } | |
| * { box-sizing:border-box; } | |
| html,body { margin:0; height:100%; background:var(--paper); | |
| color:#f4f0e6; font-family:'Trebuchet MS',Verdana,sans-serif; overflow:hidden; } | |
| #wrap { position:fixed; inset:0; } | |
| canvas { display:block; width:100%; height:100%; } | |
| #titlecard { position:absolute; top:5%; left:50%; transform:translateX(-50%); | |
| text-align:center; pointer-events:none; text-shadow:0 2px 12px #000; } | |
| #title { font-size:clamp(22px,4vw,52px); font-weight:900; letter-spacing:2px; | |
| margin:0; color:#fff; -webkit-text-stroke:1px #000; } | |
| #theorem { font-family:'Courier New',monospace; color:var(--gold); | |
| font-size:clamp(11px,1.6vw,18px); margin-top:6px; } | |
| #caption { position:absolute; bottom:9%; left:50%; transform:translateX(-50%); | |
| width:min(860px,90%); background:rgba(8,8,16,.72); | |
| border:2px solid rgba(255,210,63,.55); border-radius:14px; | |
| padding:14px 20px; font-size:clamp(13px,1.8vw,19px); line-height:1.4; | |
| text-align:center; backdrop-filter:blur(3px); pointer-events:none; } | |
| #hud { position:absolute; bottom:2.4%; left:50%; transform:translateX(-50%); | |
| display:flex; gap:10px; align-items:center; z-index:5; } | |
| button { background:#1a1a2e; color:#f4f0e6; border:1px solid #444; | |
| border-radius:9px; padding:8px 14px; font-size:15px; cursor:pointer; } | |
| button:hover { background:#2a2a44; } | |
| #dots { display:flex; gap:7px; } | |
| .dot { width:11px; height:11px; border-radius:50%; background:#444; cursor:pointer; | |
| transition:all .25s; } | |
| .dot.on { background:var(--gold); transform:scale(1.35); } | |
| #brand { position:absolute; top:12px; left:16px; font-size:12px; color:#7a7a90; | |
| letter-spacing:1px; } | |
| #hint { position:absolute; top:12px; right:16px; font-size:12px; color:#7a7a90; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="wrap"> | |
| <canvas id="cv"></canvas> | |
| <div id="brand">OEIS A001379 - character degrees of the Monster M</div> | |
| <div id="hint">drag to look around - space = play/pause</div> | |
| <div id="titlecard"><h1 id="title"></h1><div id="theorem"></div></div> | |
| <div id="caption"></div> | |
| <div id="hud"> | |
| <button id="prev">◀ prev</button> | |
| <button id="play">❙❙ pause</button> | |
| <button id="next">next ▶</button> | |
| <div id="dots"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const DATA = {"M": [[46, 2, 0, 0, 2, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1], [42, 2, 1, 4, 0, 2, 0, 0, 1, 1, 0, 1, 0, 1, 0], [44, 0, 0, 6, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1], [46, 0, 0, 0, 2, 3, 0, 0, 1, 0, 1, 0, 1, 0, 0], [46, 0, 0, 0, 2, 3, 0, 0, 1, 0, 1, 0, 1, 0, 0], [42, 0, 7, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1], [43, 0, 0, 0, 2, 2, 0, 1, 1, 1, 0, 1, 0, 1, 0], [43, 0, 0, 0, 2, 2, 0, 1, 1, 1, 0, 1, 0, 1, 0], [43, 0, 0, 0, 2, 2, 0, 1, 1, 1, 0, 1, 0, 1, 0], [42, 0, 0, 4, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0], [42, 0, 0, 0, 0, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1], [32, 0, 9, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1], [18, 19, 0, 0, 0, 3, 0, 0, 0, 1, 1, 1, 0, 1, 1], [32, 0, 1, 0, 0, 3, 1, 1, 1, 1, 1, 0, 1, 1, 1], [31, 1, 0, 3, 2, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0], [31, 1, 0, 3, 2, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0], [28, 1, 0, 1, 1, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1], [18, 3, 8, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1], [18, 0, 8, 5, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1], [21, 0, 0, 6, 1, 3, 1, 1, 0, 1, 0, 1, 0, 1, 1], [20, 0, 2, 5, 0, 3, 1, 0, 1, 1, 1, 1, 0, 1, 1], [18, 3, 2, 1, 2, 3, 0, 1, 1, 1, 0, 1, 1, 1, 1], [20, 2, 0, 0, 2, 3, 1, 0, 1, 1, 1, 1, 1, 1, 1], [18, 0, 0, 6, 1, 3, 1, 1, 1, 1, 1, 1, 1, 0, 0], [18, 0, 0, 6, 1, 3, 1, 1, 1, 1, 1, 1, 1, 0, 0], [18, 0, 0, 6, 1, 3, 1, 1, 0, 1, 0, 1, 1, 1, 1], [0, 17, 7, 4, 2, 2, 0, 0, 0, 0, 1, 0, 0, 1, 1], [12, 6, 2, 5, 0, 3, 1, 1, 1, 0, 1, 0, 1, 1, 1], [0, 20, 0, 6, 2, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1], [2, 19, 0, 4, 0, 3, 0, 0, 0, 1, 1, 1, 1, 1, 1], [6, 17, 0, 4, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1], [1, 19, 0, 4, 2, 3, 1, 0, 1, 1, 0, 1, 0, 0, 0], [1, 19, 0, 4, 2, 3, 1, 0, 1, 1, 0, 1, 0, 0, 0], [18, 0, 0, 6, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 19, 5, 1, 2, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1], [16, 1, 0, 4, 2, 2, 1, 1, 0, 1, 1, 1, 1, 1, 1], [1, 19, 1, 1, 2, 3, 0, 1, 0, 1, 1, 1, 1, 1, 0], [12, 1, 9, 1, 0, 2, 1, 1, 0, 1, 1, 1, 1, 1, 1], [1, 19, 0, 4, 2, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1], [2, 19, 0, 0, 2, 3, 1, 0, 1, 1, 1, 1, 0, 1, 1], [0, 17, 7, 0, 1, 2, 0, 0, 0, 1, 1, 1, 1, 1, 1], [0, 17, 7, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], [18, 0, 0, 6, 1, 3, 1, 1, 0, 1, 0, 1, 0, 0, 0], [18, 0, 0, 6, 1, 3, 1, 1, 0, 1, 0, 1, 0, 0, 0], [18, 1, 2, 5, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1], [17, 0, 5, 0, 2, 3, 1, 1, 0, 0, 1, 0, 0, 1, 1], [17, 0, 5, 0, 2, 3, 1, 1, 0, 0, 1, 0, 0, 1, 1], [0, 18, 9, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1], [0, 18, 9, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1], [1, 17, 2, 5, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1], [1, 17, 1, 5, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1], [0, 19, 0, 1, 2, 3, 1, 1, 1, 1, 0, 1, 1, 0, 1], [0, 18, 0, 5, 0, 2, 1, 1, 0, 0, 1, 1, 1, 1, 1], [19, 1, 1, 1, 2, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1], [16, 3, 2, 0, 1, 2, 1, 0, 0, 1, 1, 1, 1, 1, 1], [13, 0, 2, 5, 2, 3, 0, 0, 1, 1, 0, 1, 1, 1, 1], [0, 19, 0, 0, 2, 3, 0, 1, 0, 1, 1, 1, 1, 1, 1], [0, 18, 0, 1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1], [3, 17, 0, 1, 1, 2, 1, 0, 1, 1, 1, 0, 0, 1, 1], [0, 19, 0, 0, 2, 3, 0, 1, 0, 1, 1, 1, 1, 1, 0], [0, 19, 0, 0, 2, 3, 1, 0, 1, 1, 0, 0, 1, 1, 1], [0, 19, 0, 0, 2, 3, 1, 0, 1, 1, 0, 0, 1, 1, 1], [9, 1, 8, 1, 1, 3, 0, 1, 1, 1, 1, 0, 1, 1, 1], [0, 17, 1, 0, 2, 2, 1, 1, 1, 0, 1, 1, 1, 1, 1], [3, 6, 7, 5, 1, 2, 0, 1, 0, 1, 1, 1, 0, 1, 1], [4, 7, 7, 1, 1, 3, 0, 1, 1, 0, 1, 1, 1, 1, 1], [0, 12, 5, 3, 2, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1], [0, 12, 7, 0, 0, 3, 1, 0, 1, 1, 1, 1, 1, 1, 1], [18, 0, 0, 0, 1, 3, 1, 1, 1, 0, 0, 1, 1, 1, 1], [4, 12, 0, 1, 2, 3, 0, 1, 0, 1, 1, 1, 1, 1, 1], [7, 9, 0, 0, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [10, 1, 0, 5, 2, 3, 0, 1, 1, 1, 1, 1, 1, 1, 1], [6, 0, 9, 4, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], [0, 12, 0, 6, 0, 3, 1, 1, 1, 1, 1, 1, 1, 0, 1], [1, 6, 8, 6, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], [0, 17, 2, 0, 0, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1], [10, 1, 2, 5, 0, 3, 0, 1, 0, 1, 1, 1, 1, 1, 1], [11, 2, 0, 3, 1, 3, 1, 0, 1, 1, 1, 1, 1, 1, 1], [0, 13, 0, 6, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1], [0, 13, 0, 6, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1], [3, 6, 8, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1], [2, 2, 9, 4, 2, 3, 0, 1, 1, 1, 1, 1, 0, 0, 1], [0, 12, 2, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [7, 2, 1, 4, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 19, 0, 0, 2, 3, 1, 0, 1, 1, 0, 0, 0, 0, 0], [0, 19, 0, 0, 2, 3, 1, 0, 1, 1, 0, 0, 0, 0, 0], [3, 1, 7, 5, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1], [2, 0, 9, 6, 0, 3, 1, 1, 0, 0, 1, 1, 1, 1, 1], [1, 12, 0, 6, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0], [1, 12, 0, 6, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0], [12, 1, 0, 1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1], [1, 12, 2, 1, 0, 3, 1, 0, 1, 1, 1, 1, 0, 1, 1], [0, 9, 7, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1], [3, 6, 0, 6, 1, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1], [0, 2, 9, 6, 2, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1], [1, 0, 9, 6, 2, 2, 1, 1, 0, 1, 1, 0, 1, 1, 0], [1, 0, 9, 6, 2, 2, 1, 1, 0, 1, 1, 0, 1, 1, 0], [3, 0, 8, 6, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1], [0, 2, 9, 6, 1, 2, 1, 1, 0, 0, 0, 1, 1, 1, 1], [2, 4, 1, 6, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 0], [2, 2, 9, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 0, 7, 5, 2, 3, 1, 0, 0, 1, 1, 1, 1, 1, 1], [0, 0, 9, 6, 2, 2, 1, 1, 1, 1, 0, 1, 0, 1, 1], [1, 0, 8, 6, 1, 3, 1, 0, 1, 1, 0, 1, 1, 1, 1], [1, 2, 9, 1, 2, 3, 0, 1, 1, 1, 1, 1, 1, 1, 1], [5, 7, 1, 1, 1, 3, 1, 1, 0, 1, 1, 1, 1, 0, 1], [6, 3, 0, 6, 1, 3, 1, 1, 0, 0, 0, 1, 1, 1, 1], [4, 3, 7, 0, 2, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1], [1, 1, 9, 6, 2, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1], [1, 1, 9, 6, 2, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1], [2, 0, 9, 6, 2, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1], [2, 0, 9, 6, 2, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1], [1, 3, 8, 4, 0, 3, 0, 0, 1, 1, 1, 1, 1, 1, 0], [6, 1, 2, 5, 1, 2, 1, 1, 0, 1, 1, 1, 1, 1, 1], [2, 0, 9, 4, 1, 3, 0, 1, 1, 0, 0, 1, 1, 1, 1], [2, 0, 7, 4, 2, 3, 0, 1, 0, 1, 1, 1, 1, 1, 1], [0, 0, 9, 3, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 0], [5, 0, 8, 4, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0], [2, 3, 7, 4, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1], [0, 6, 3, 2, 2, 3, 1, 1, 1, 1, 1, 1, 0, 1, 1], [0, 3, 9, 0, 1, 3, 1, 1, 1, 1, 0, 1, 1, 1, 1], [1, 2, 3, 4, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 1, 9, 0, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 4, 0, 6, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 4, 0, 6, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [3, 0, 7, 5, 0, 2, 0, 0, 1, 1, 1, 0, 1, 1, 1], [3, 2, 0, 6, 2, 3, 1, 0, 1, 0, 1, 1, 1, 1, 1], [1, 0, 8, 6, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], [1, 0, 8, 6, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], [1, 1, 8, 4, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], [3, 1, 1, 6, 2, 2, 1, 1, 1, 1, 1, 1, 0, 1, 1], [0, 3, 7, 1, 2, 3, 0, 0, 1, 1, 1, 1, 1, 1, 1], [3, 2, 0, 4, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 1, 2, 6, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 0, 2, 6, 2, 3, 1, 0, 1, 1, 1, 1, 1, 1, 1], [1, 0, 5, 4, 2, 3, 0, 1, 1, 1, 1, 1, 1, 1, 1], [2, 0, 1, 6, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [12, 0, 0, 4, 1, 2, 0, 0, 0, 0, 1, 0, 0, 1, 1], [1, 3, 8, 4, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1], [0, 0, 9, 6, 2, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1], [0, 0, 9, 6, 2, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1], [2, 0, 9, 0, 1, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1], [0, 0, 9, 1, 2, 3, 1, 1, 1, 1, 1, 1, 0, 0, 1], [0, 0, 9, 1, 2, 3, 1, 1, 1, 1, 1, 1, 0, 0, 1], [3, 0, 3, 5, 0, 3, 1, 1, 0, 1, 1, 1, 1, 1, 1], [2, 1, 2, 5, 2, 3, 0, 1, 1, 0, 1, 1, 1, 1, 1], [3, 0, 0, 6, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], [3, 0, 8, 1, 0, 3, 1, 1, 0, 1, 0, 1, 0, 1, 1], [0, 1, 9, 0, 2, 3, 0, 1, 1, 1, 1, 1, 0, 1, 0], [0, 1, 9, 0, 2, 3, 0, 1, 1, 1, 1, 1, 0, 1, 0], [0, 6, 0, 5, 0, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1], [3, 0, 2, 3, 2, 3, 1, 1, 1, 1, 0, 1, 1, 1, 1], [0, 3, 1, 6, 0, 3, 1, 1, 1, 1, 0, 1, 1, 1, 1], [0, 0, 5, 6, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 6, 7, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1], [7, 0, 2, 0, 1, 3, 0, 1, 1, 0, 1, 1, 1, 1, 1], [2, 2, 0, 6, 2, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1], [2, 2, 0, 5, 2, 2, 0, 1, 1, 0, 1, 1, 1, 1, 1], [0, 0, 9, 6, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 6, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 3, 7, 0, 1, 2, 1, 0, 1, 1, 0, 0, 1, 1, 1], [0, 1, 7, 5, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], [2, 0, 0, 6, 2, 3, 1, 0, 1, 0, 1, 1, 1, 1, 0], [2, 0, 0, 6, 2, 3, 1, 0, 1, 0, 1, 1, 1, 1, 0], [0, 0, 7, 1, 1, 2, 1, 1, 0, 1, 1, 1, 1, 1, 1], [3, 2, 0, 4, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1], [4, 0, 2, 0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 2, 4, 1, 3, 0, 1, 1, 1, 1, 1, 1, 0, 1], [1, 0, 0, 5, 2, 3, 0, 1, 1, 1, 1, 0, 1, 1, 1], [1, 0, 0, 5, 2, 3, 0, 1, 1, 1, 1, 0, 1, 1, 1], [2, 0, 7, 0, 0, 2, 0, 1, 0, 0, 1, 1, 1, 1, 1], [1, 1, 2, 5, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1], [0, 0, 1, 5, 0, 3, 0, 0, 1, 1, 1, 1, 1, 1, 1], [0, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 2, 1, 0, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 2, 5, 1, 2, 1, 0, 1, 0, 1, 0, 0, 1, 1], [1, 0, 2, 1, 1, 3, 1, 1, 0, 1, 1, 1, 1, 0, 1], [0, 0, 3, 1, 2, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1], [0, 6, 0, 1, 0, 2, 1, 1, 0, 0, 1, 0, 0, 1, 1], [3, 0, 0, 1, 0, 3, 1, 0, 0, 1, 1, 1, 1, 1, 1], [0, 0, 0, 5, 0, 2, 0, 1, 1, 1, 0, 1, 1, 1, 1], [0, 0, 2, 4, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1], [0, 3, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1], [0, 0, 0, 4, 1, 2, 0, 0, 0, 1, 0, 1, 1, 1, 1], [1, 0, 2, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 0, 1, 1, 2, 0, 1, 1, 0, 0, 1, 1, 1, 0], [0, 1, 0, 0, 1, 2, 1, 0, 1, 1, 1, 1, 1, 1, 0], [2, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1], [1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1], [1, 0, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 1, 1, 0], [0, 0, 0, 0, 0, 2, 0, 0, 1, 1, 0, 1, 0, 1, 1], [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], "IDX": [192, 174, 180, 101, 102, 139, 122, 123, 124, 132, 171, 147, 168, 158, 80, 81, 144, 96, 118, 136, 157, 145, 125, 134, 135, 156, 160, 172, 175, 186, 77, 104, 105, 111, 121, 141, 151, 161, 162, 163, 177, 188, 40, 41, 45, 58, 59, 70, 71, 130, 140, 159, 167, 49, 63, 120, 152, 185, 65, 100, 106, 107, 129, 142, 150, 154, 181, 193, 42, 112, 115, 153, 169, 173, 191, 76, 95, 97, 98, 99, 119, 155, 170, 187, 25, 26, 166, 182, 43, 44, 60, 67, 91, 117, 126, 127, 128, 133, 137, 148, 149, 183, 184, 189, 190, 56, 64, 78, 82, 83, 84, 85, 90, 94, 110, 143, 176, 48, 66, 109, 113, 164, 165, 178, 179, 62, 87, 88, 89, 92, 93, 103, 114, 116, 131, 138, 146, 14, 36, 54, 55, 69, 73, 74, 75, 79, 108, 35, 52, 53, 61, 68, 72, 86, 27, 28, 50, 51, 15, 16, 29, 30, 38, 39, 57, 23, 33, 37, 46, 47, 20, 19, 31, 32, 34, 18, 22, 24, 8, 17, 21, 9, 10, 12, 13, 7, 11, 4, 6, 3, 5, 2, 1, 0], "R": 194, "C": 15, "PRIMES": [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 47, 59, 71], "LANDMARKS": [{"id": "overview", "title": "THE MONSTER", "theorem": "Monster.lean - Sigma d_i^2 = |M|", "text": "194 irreducible representations of the Monster sporadic group, each a skyline of supersingular primes. We enter from -z.", "box": {"r0": 0, "c0": 0, "h": 194, "w": 15}, "view": "wide", "color": "#9b6cff"}, {"id": "ridge", "title": "MOUNT |M|", "theorem": "Monster.lean - prime_dvd_orderM_iff", "text": "The v2 ridge (column 2) towers to exponent 46 - the power of 2 in |M| = 2^46 . 3^20 . 5^9 . 7^6 . 11^2 . 13^3 . 17 . 19 . 23 ...", "box": {"r0": 0, "c0": 0, "h": 12, "w": 1}, "view": "left", "color": "#ff5d5d"}, {"id": "faithful", "title": "196883", "theorem": "Monster.lean - smallest_faithful", "text": "The smallest faithful representation: degree 47 . 59 . 71 = 196883, the q^1 coefficient of j minus 1 in Monstrous Moonshine.", "box": {"r0": 192, "c0": 11, "h": 1, "w": 4}, "view": "close", "color": "#ffd23f"}, {"id": "square", "title": "KA-BLOCK! square (side 6)", "theorem": "Shapes.lean - largest_solid_square", "text": "The biggest fully-filled square in the grid has side exactly 6, at row 119, col 4 (primes 11..23 all divide these degrees).", "box": {"r0": 119, "c0": 4, "h": 6, "w": 6}, "view": "iso", "color": "#e8413a"}, {"id": "tower", "title": "SKY-HIGH TOWER (24 x 2)", "theorem": "Shapes.lean - solid_rect_24x2 / no_solid_rect_25x2", "text": "A solid 24 x 2 wall over primes 59 & 71 - and 24 is the maximum height at width 2. The last two columns rarely vanish.", "box": {"r0": 118, "c0": 13, "h": 24, "w": 2}, "view": "right", "color": "#2f7bd6"}, {"id": "stair", "title": "THE STAIRCASE (height 7)", "theorem": "Shapes.lean - largest_triangle", "text": "The tallest filled right-triangle staircase has height exactly 7, climbing from row 118, col 4.", "box": {"r0": 118, "c0": 4, "h": 7, "w": 7}, "view": "iso", "color": "#28a55a"}, {"id": "diamond", "title": "THE DIAMOND (radius 3)", "theorem": "Shapes.lean - largest_circle", "text": "The largest discrete circle (an L1 ball / diamond - a true circle needs more than 15 columns) has radius 3, centred at row 71, col 11.", "box": {"r0": 68, "c0": 8, "h": 7, "w": 7}, "view": "close", "color": "#f2a324"}, {"id": "penteract", "title": "THE 5-CUBE SHADOW", "theorem": "Shadows.lean - cube5_shadow / no_cube6_shadow", "text": "Over primes {2,3,5,23,47} all 2^5 = 32 divisibility patterns occur: the matrix is the shadow of a penteract. 5 is the maximum.", "box": {"r0": 0, "c0": 0, "h": 194, "w": 15}, "view": "top", "color": "#37d6c4"}, {"id": "apex", "title": "THE APEX / VOID", "theorem": "GeometricPatterns.lean - apex_row_unique", "text": "Row 193 is the trivial representation, dimension 1: the unique all-zero row - flat ground at the far end of the Monster.", "box": {"r0": 193, "c0": 0, "h": 1, "w": 15}, "view": "close", "color": "#c9c9d6"}, {"id": "finale", "title": "THE WHOLE MONSTER", "theorem": "Monster.lean - each_degree_dvd_order", "text": "Every degree divides |M|, so only the 15 supersingular primes ever appear. One matrix, certified end to end by Lean.", "box": {"r0": 0, "c0": 0, "h": 194, "w": 15}, "view": "hero", "color": "#9b6cff"}]}; | |
| const {M, IDX, R, C, PRIMES, LANDMARKS} = DATA; | |
| // ---- colours per prime column (cool->warm rainbow) ----------------------- | |
| const COLS = ["#ff5d5d","#ff924c","#ffd23f","#a8e05f","#5fe08a","#3fd6c4", | |
| "#37b6ff","#5d8bff","#9b6cff","#c96cff","#ff6cd0","#ff6c97", | |
| "#ff8c5d","#ffd86c","#d6ff6c"]; | |
| function shade(hex, f){ // f<1 darken, f>1 lighten | |
| const n=parseInt(hex.slice(1),16); let r=(n>>16)&255,g=(n>>8)&255,b=n&255; | |
| r=Math.min(255,r*f)|0; g=Math.min(255,g*f)|0; b=Math.min(255,b*f)|0; | |
| return "rgb("+r+","+g+","+b+")"; | |
| } | |
| // ---- world geometry ------------------------------------------------------ | |
| // grid cell (row i, col j): x = (j - C/2)*SX, z = i*SZ, height = M[i][j]*HY | |
| const SX=2.2, SZ=1.0, HY=0.9; | |
| const cx = c => (c - C/2 + 0.5)*SX; | |
| const cz = r => r*SZ; | |
| // focus point (world) of a landmark box centre | |
| function boxCenter(b){ | |
| return { x: cx(b.c0 + b.w/2 - 0.5), y: 4, z: cz(b.r0 + b.h/2) }; | |
| } | |
| // camera keyframe for a landmark | |
| function camFor(L){ | |
| const ctr = boxCenter(L.box); | |
| const span = Math.max(L.box.h*SZ, L.box.w*SX, 6); | |
| const v = L.view; | |
| let dx=0, dy=span*0.7+18, dz=-span*0.9-22; // default: behind (-z) & above | |
| if(v==="wide"){ dx=0; dy=70; dz=-150; } | |
| else if(v==="hero"){ dx=60; dy=95; dz=-150; } | |
| else if(v==="top"){ dx=2; dy=170; dz=-30; } | |
| else if(v==="left"){ dx=-55; dy=34; dz=-46; } | |
| else if(v==="right"){ dx=55; dy=40; dz=-40; } | |
| else if(v==="iso"){ dx=34; dy=30; dz=-34; } | |
| else if(v==="close"){ dx=10; dy=14; dz=-26; } | |
| return { eye:{x:ctr.x+dx,y:ctr.y+dy,z:ctr.z+dz}, tgt:ctr }; | |
| } | |
| const KEYS = LANDMARKS.map(camFor); | |
| // ---- precompute towers (one box per nonzero cell) ------------------------ | |
| const TOWERS=[]; | |
| for(let i=0;i<R;i++) for(let j=0;j<C;j++){ | |
| const h=M[i][j]; if(h<=0) continue; | |
| TOWERS.push({i,j,h:h*HY, x:cx(j), z:cz(i), col:COLS[j%COLS.length]}); | |
| } | |
| // ---- camera + projection ------------------------------------------------- | |
| const cv=document.getElementById("cv"), ctx=cv.getContext("2d"); | |
| let W=0,H=0,DPR=Math.min(2,window.devicePixelRatio||1); | |
| function resize(){ W=cv.clientWidth; H=cv.clientHeight; | |
| cv.width=W*DPR; cv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0); } | |
| window.addEventListener("resize",resize); | |
| let eye={...KEYS[0].eye}, tgt={...KEYS[0].tgt}; | |
| let yawOff=0, pitchOff=0; // user drag look-around | |
| function project(p){ | |
| // build camera basis from eye->tgt with user yaw/pitch offsets | |
| let fx=tgt.x-eye.x, fy=tgt.y-eye.y, fz=tgt.z-eye.z; | |
| let fl=Math.hypot(fx,fy,fz)||1; fx/=fl; fy/=fl; fz/=fl; | |
| // apply yaw (around world up) and pitch offsets | |
| const cy=Math.cos(yawOff), sy=Math.sin(yawOff); | |
| let gx=fx*cy - fz*sy, gz=fx*sy + fz*cy, gy=fy; | |
| // right = forward x up | |
| let rx=gz, ry=0, rz=-gx; let rl=Math.hypot(rx,ry,rz)||1; rx/=rl; rz/=rl; | |
| // pitch: tilt forward up/down | |
| gy += pitchOff; let nl=Math.hypot(gx,gy,gz)||1; gx/=nl; gy/=nl; gz/=nl; | |
| // up = right x forward | |
| let ux=ry*gz-rz*gy, uy=rz*gx-rx*gz, uz=rx*gy-ry*gx; | |
| const dx=p.x-eye.x, dy=p.y-eye.y, dz=p.z-eye.z; | |
| const X=dx*rx+dy*ry+dz*rz; | |
| const Y=dx*ux+dy*uy+dz*uz; | |
| const Z=dx*gx+dy*gy+dz*gz; | |
| if(Z<=0.1) return null; | |
| const f=H*0.9; | |
| return { x:W/2 + X/Z*f, y:H/2 - Y/Z*f, z:Z }; | |
| } | |
| function quad(a,b,c,d,fill){ | |
| const A=project(a),B=project(b),Cc=project(c),D=project(d); | |
| if(!A||!B||!Cc||!D) return; | |
| ctx.beginPath(); ctx.moveTo(A.x,A.y); ctx.lineTo(B.x,B.y); | |
| ctx.lineTo(Cc.x,Cc.y); ctx.lineTo(D.x,D.y); ctx.closePath(); | |
| ctx.fillStyle=fill; ctx.fill(); | |
| } | |
| // ---- render -------------------------------------------------------------- | |
| let curIdx=0, glow=0; | |
| function activeBox(){ return LANDMARKS[curIdx].box; } | |
| function inBox(i,j,b){ return i>=b.r0&&i<b.r0+b.h&&j>=b.c0&&j<b.c0+b.w; } | |
| function frame(t){ | |
| ctx.fillStyle="#0c0c14"; ctx.fillRect(0,0,W,H); | |
| // floor grid glow | |
| const g=ctx.createRadialGradient(W/2,H*0.62,40,W/2,H*0.62,Math.max(W,H)); | |
| g.addColorStop(0,"rgba(40,30,70,.55)"); g.addColorStop(1,"rgba(8,8,18,0)"); | |
| ctx.fillStyle=g; ctx.fillRect(0,0,W,H); | |
| // depth sort towers (far first) | |
| const box=activeBox(); const hi=LANDMARKS[curIdx].color; | |
| const drawn=[]; | |
| for(const T of TOWERS){ | |
| const pc=project({x:T.x,y:0,z:T.z}); | |
| if(!pc) continue; | |
| drawn.push([pc.z,T]); | |
| } | |
| drawn.sort((a,b)=>b[0]-a[0]); | |
| const gl=0.5+0.5*Math.sin(glow); | |
| for(const [,T] of drawn){ | |
| const i=T.i,j=T.j,x=T.x,z=T.z,h=T.h,w=SX*0.42,d=SZ*0.42; | |
| const x0=x-w,x1=x+w,z0=z-d,z1=z+d; | |
| const lit = inBox(i,j,box); | |
| let top=T.col, side=shade(T.col,0.7), front=shade(T.col,0.5); | |
| if(lit){ top=shade(hi,1.25); side=shade(hi,0.85); front=shade(hi,0.6); } | |
| // back/left/right faces then top, then front (painter within tower) | |
| quad({x:x0,y:0,z:z1},{x:x1,y:0,z:z1},{x:x1,y:h,z:z1},{x:x0,y:h,z:z1}, side); // back | |
| quad({x:x0,y:0,z:z0},{x:x0,y:0,z:z1},{x:x0,y:h,z:z1},{x:x0,y:h,z:z0}, front);// left | |
| quad({x:x1,y:0,z:z0},{x:x1,y:0,z:z1},{x:x1,y:h,z:z1},{x:x1,y:h,z:z0}, front);// right | |
| quad({x:x0,y:h,z:z0},{x:x1,y:h,z:z0},{x:x1,y:h,z:z1},{x:x0,y:h,z:z1}, top); // top | |
| quad({x:x0,y:0,z:z0},{x:x1,y:0,z:z0},{x:x1,y:h,z:z0},{x:x0,y:h,z:z0}, | |
| lit?shade(hi,1.5):shade(T.col,0.85)); // front face | |
| if(lit){ | |
| ctx.save(); ctx.globalAlpha=0.25+0.45*gl; | |
| const top2=project({x:x,y:h+1.2,z:z}); | |
| if(top2){ ctx.fillStyle=hi; ctx.beginPath(); | |
| ctx.arc(top2.x,top2.y,3.2,0,7); ctx.fill(); } | |
| ctx.restore(); | |
| } | |
| } | |
| } | |
| // ---- tour state machine -------------------------------------------------- | |
| let playing=true, segT=0; | |
| const LINGER=2.6, TRAVEL=2.8; // seconds | |
| let phase="travel"; // travel -> linger | |
| let fromK=KEYS[0], toK=KEYS[0]; | |
| function setLandmark(k,instant){ | |
| curIdx=(k+LANDMARKS.length)%LANDMARKS.length; | |
| fromK={eye:{...eye},tgt:{...tgt}}; toK=KEYS[curIdx]; | |
| segT=0; phase=instant?"linger":"travel"; | |
| if(instant){ eye={...toK.eye}; tgt={...toK.tgt}; } | |
| updateHUD(); | |
| } | |
| function ease(x){ return x<.5 ? 4*x*x*x : 1-Math.pow(-2*x+2,3)/2; } | |
| function lerp(a,b,t){ return a+(b-a)*t; } | |
| function lerpPt(a,b,t){ return {x:lerp(a.x,b.x,t),y:lerp(a.y,b.y,t),z:lerp(a.z,b.z,t)}; } | |
| function updateHUD(){ | |
| const L=LANDMARKS[curIdx]; | |
| document.getElementById("title").textContent=L.title; | |
| document.getElementById("theorem").textContent=L.theorem; | |
| document.getElementById("caption").textContent=L.text; | |
| document.querySelectorAll(".dot").forEach((d,i)=>d.classList.toggle("on",i===curIdx)); | |
| } | |
| let last=performance.now(); | |
| function loop(now){ | |
| const dt=Math.min(0.05,(now-last)/1000); last=now; glow+=dt*3.2; | |
| if(playing){ | |
| segT+=dt; | |
| if(phase==="travel"){ | |
| const t=ease(Math.min(1,segT/TRAVEL)); | |
| eye=lerpPt(fromK.eye,toK.eye,t); tgt=lerpPt(fromK.tgt,toK.tgt,t); | |
| yawOff*=0.92; pitchOff*=0.92; | |
| if(segT>=TRAVEL){ phase="linger"; segT=0; } | |
| } else { | |
| // slow orbit drift while lingering | |
| yawOff=0.18*Math.sin(segT*0.6); | |
| if(segT>=LINGER){ setLandmark(curIdx+1,false); } | |
| } | |
| } | |
| frame(now); | |
| requestAnimationFrame(loop); | |
| } | |
| // ---- controls ------------------------------------------------------------ | |
| const dots=document.getElementById("dots"); | |
| LANDMARKS.forEach((L,i)=>{ const d=document.createElement("div"); | |
| d.className="dot"+(i===0?" on":""); d.title=L.title; | |
| d.onclick=()=>{ setLandmark(i,false); playing=true; pbtn.innerHTML="❙❙ pause"; }; | |
| dots.appendChild(d); }); | |
| const pbtn=document.getElementById("play"); | |
| pbtn.onclick=()=>{ playing=!playing; | |
| pbtn.innerHTML=playing?"❙❙ pause":"▶ play"; }; | |
| document.getElementById("next").onclick=()=>{ setLandmark(curIdx+1,false); }; | |
| document.getElementById("prev").onclick=()=>{ setLandmark(curIdx-1,false); }; | |
| window.addEventListener("keydown",e=>{ | |
| if(e.code==="Space"){ e.preventDefault(); pbtn.onclick(); } | |
| if(e.code==="ArrowRight") setLandmark(curIdx+1,false); | |
| if(e.code==="ArrowLeft") setLandmark(curIdx-1,false); }); | |
| // drag to look around | |
| let drag=false, lx=0, ly=0; | |
| cv.addEventListener("pointerdown",e=>{ drag=true; lx=e.clientX; ly=e.clientY; }); | |
| window.addEventListener("pointerup",()=>drag=false); | |
| window.addEventListener("pointermove",e=>{ if(!drag) return; | |
| yawOff += (e.clientX-lx)*0.004; pitchOff += (e.clientY-ly)*0.003; | |
| pitchOff=Math.max(-0.8,Math.min(0.8,pitchOff)); lx=e.clientX; ly=e.clientY; }); | |
| resize(); setLandmark(0,true); requestAnimationFrame(loop); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment