viewer: tile-loading spinner + zoom-adaptive point sizing
Browse files- index.html +29 -7
index.html
CHANGED
|
@@ -117,13 +117,20 @@
|
|
| 117 |
border-radius: 7px; padding: 4px 8px; cursor: pointer; font: inherit; }
|
| 118 |
#hud button:hover { color: var(--ink); border-color: var(--accent); }
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
#err { position: fixed; inset: 0; display: none; place-items: center; padding: 24px; text-align: center; }
|
| 121 |
#err div { max-width: 400px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
| 122 |
@media (max-width: 560px) {
|
| 123 |
#rail { width: calc(100vw - 32px); }
|
| 124 |
#legend { max-width: calc(100vw - 32px); }
|
| 125 |
}
|
| 126 |
-
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
| 127 |
</style>
|
| 128 |
</head>
|
| 129 |
<body>
|
|
@@ -145,6 +152,7 @@
|
|
| 145 |
</section>
|
| 146 |
|
| 147 |
<aside id="legend" class="panel" aria-label="Legend"></aside>
|
|
|
|
| 148 |
<div id="tip" role="tooltip"></div>
|
| 149 |
<div id="hud" class="panel">
|
| 150 |
<span><b id="fps">β</b> fps</span><span><b id="tiles">0</b> tiles</span>
|
|
@@ -202,12 +210,12 @@
|
|
| 202 |
const prog = gl.createProgram();
|
| 203 |
gl.attachShader(prog, sh(gl.VERTEX_SHADER, `
|
| 204 |
attribute vec2 a_xy; attribute vec3 a_col; attribute float a_deg; attribute float a_alpha;
|
| 205 |
-
uniform vec2 u_scale; uniform vec2 u_off; uniform float u_pt; uniform float u_matchOnly;
|
| 206 |
varying vec3 v_col; varying float v_alpha;
|
| 207 |
void main() {
|
| 208 |
vec2 p = a_xy * u_scale + u_off;
|
| 209 |
gl_Position = vec4(p, 0.0, 1.0);
|
| 210 |
-
gl_PointSize = u_pt * (0.65 + 1.9 * a_deg);
|
| 211 |
v_col = a_col; v_alpha = a_alpha;
|
| 212 |
// foreground pass: draw only the filter-matched points (alpha==1), cull the dimmed rest
|
| 213 |
if (u_matchOnly > 0.5 && a_alpha < 0.99) { gl_PointSize = 0.0; gl_Position = vec4(2.0, 2.0, 2.0, 1.0); }
|
|
@@ -230,7 +238,7 @@
|
|
| 230 |
const U = {
|
| 231 |
scale: gl.getUniformLocation(prog, "u_scale"), off: gl.getUniformLocation(prog, "u_off"),
|
| 232 |
pt: gl.getUniformLocation(prog, "u_pt"), alpha: gl.getUniformLocation(prog, "u_alpha"),
|
| 233 |
-
matchOnly: gl.getUniformLocation(prog, "u_matchOnly"),
|
| 234 |
};
|
| 235 |
gl.enableVertexAttribArray(A.xy); gl.enableVertexAttribArray(A.col);
|
| 236 |
gl.enableVertexAttribArray(A.deg); gl.enableVertexAttribArray(A.alpha);
|
|
@@ -393,8 +401,15 @@
|
|
| 393 |
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
| 394 |
gl.uniform2f(U.scale, view.s * asp, view.s);
|
| 395 |
gl.uniform2f(U.off, -view.cx * view.s * asp, -view.cy * view.s);
|
| 396 |
-
//
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
// pass 1: whole scene (filtered-out points already dimmed to ~10% via per-vertex alpha)
|
| 399 |
gl.uniform1f(U.matchOnly, 0);
|
| 400 |
gl.uniform1f(U.alpha, N > 4e6 ? 0.5 : 0.75);
|
|
@@ -591,8 +606,15 @@
|
|
| 591 |
window.addEventListener("resize", resize);
|
| 592 |
|
| 593 |
// ββ render loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 594 |
-
let fFrames = 0, fT = performance.now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
function loop(now) {
|
|
|
|
| 596 |
if (dirty) {
|
| 597 |
updateActive(); draw(); dirty = false;
|
| 598 |
$("zoom").textContent = view.s.toFixed(view.s < 10 ? 1 : 0) + "Γ";
|
|
|
|
| 117 |
border-radius: 7px; padding: 4px 8px; cursor: pointer; font: inherit; }
|
| 118 |
#hud button:hover { color: var(--ink); border-color: var(--accent); }
|
| 119 |
|
| 120 |
+
#spin { position: fixed; top: 16px; right: 16px; z-index: 45; width: 17px; height: 17px;
|
| 121 |
+
border-radius: 50%; border: 2.5px solid var(--line); border-top-color: var(--accent);
|
| 122 |
+
box-shadow: var(--shadow); opacity: 0; pointer-events: none; transition: opacity .2s;
|
| 123 |
+
animation: spin .7s linear infinite; }
|
| 124 |
+
#spin.on { opacity: .95; }
|
| 125 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 126 |
+
|
| 127 |
#err { position: fixed; inset: 0; display: none; place-items: center; padding: 24px; text-align: center; }
|
| 128 |
#err div { max-width: 400px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
| 129 |
@media (max-width: 560px) {
|
| 130 |
#rail { width: calc(100vw - 32px); }
|
| 131 |
#legend { max-width: calc(100vw - 32px); }
|
| 132 |
}
|
| 133 |
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } #spin { animation: none; } }
|
| 134 |
</style>
|
| 135 |
</head>
|
| 136 |
<body>
|
|
|
|
| 152 |
</section>
|
| 153 |
|
| 154 |
<aside id="legend" class="panel" aria-label="Legend"></aside>
|
| 155 |
+
<div id="spin" role="status" aria-label="Loading tiles" aria-hidden="true"></div>
|
| 156 |
<div id="tip" role="tooltip"></div>
|
| 157 |
<div id="hud" class="panel">
|
| 158 |
<span><b id="fps">β</b> fps</span><span><b id="tiles">0</b> tiles</span>
|
|
|
|
| 210 |
const prog = gl.createProgram();
|
| 211 |
gl.attachShader(prog, sh(gl.VERTEX_SHADER, `
|
| 212 |
attribute vec2 a_xy; attribute vec3 a_col; attribute float a_deg; attribute float a_alpha;
|
| 213 |
+
uniform vec2 u_scale; uniform vec2 u_off; uniform float u_pt; uniform float u_ptMin; uniform float u_matchOnly;
|
| 214 |
varying vec3 v_col; varying float v_alpha;
|
| 215 |
void main() {
|
| 216 |
vec2 p = a_xy * u_scale + u_off;
|
| 217 |
gl_Position = vec4(p, 0.0, 1.0);
|
| 218 |
+
gl_PointSize = max(u_ptMin, u_pt * (0.65 + 1.9 * a_deg));
|
| 219 |
v_col = a_col; v_alpha = a_alpha;
|
| 220 |
// foreground pass: draw only the filter-matched points (alpha==1), cull the dimmed rest
|
| 221 |
if (u_matchOnly > 0.5 && a_alpha < 0.99) { gl_PointSize = 0.0; gl_Position = vec4(2.0, 2.0, 2.0, 1.0); }
|
|
|
|
| 238 |
const U = {
|
| 239 |
scale: gl.getUniformLocation(prog, "u_scale"), off: gl.getUniformLocation(prog, "u_off"),
|
| 240 |
pt: gl.getUniformLocation(prog, "u_pt"), alpha: gl.getUniformLocation(prog, "u_alpha"),
|
| 241 |
+
ptMin: gl.getUniformLocation(prog, "u_ptMin"), matchOnly: gl.getUniformLocation(prog, "u_matchOnly"),
|
| 242 |
};
|
| 243 |
gl.enableVertexAttribArray(A.xy); gl.enableVertexAttribArray(A.col);
|
| 244 |
gl.enableVertexAttribArray(A.deg); gl.enableVertexAttribArray(A.alpha);
|
|
|
|
| 401 |
gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
| 402 |
gl.uniform2f(U.scale, view.s * asp, view.s);
|
| 403 |
gl.uniform2f(U.off, -view.cx * view.s * asp, -view.cy * view.s);
|
| 404 |
+
// point size = a hump in log-zoom: grow through the hub-only overview (declutter), then SHRINK
|
| 405 |
+
// once LOD reveals the dense low-degree crowd at high zoom (screen density peaks there, so smaller
|
| 406 |
+
// dots keep the map readable). Peaks ~4Γ zoom, decays toward a 2px floor at 150β500Γ+.
|
| 407 |
+
const L = Math.log2(view.s);
|
| 408 |
+
gl.uniform1f(U.pt, Math.max(1.2, dpr * (2.0 + 5.0 * Math.exp(-(L - 2.0) * (L - 2.0) / 8.0))));
|
| 409 |
+
// floor on-screen size so points stay visible when zoomed way in: off through the good low/mid
|
| 410 |
+
// zoom (β€~22Γ), then ramps in to a 10px minimum by ~128Γ (per-point max, so it lifts only the
|
| 411 |
+
// too-small dots rather than inflating the whole map).
|
| 412 |
+
gl.uniform1f(U.ptMin, dpr * 10 * Math.min(1, Math.max(0, (L - 4.6) / 2.4)));
|
| 413 |
// pass 1: whole scene (filtered-out points already dimmed to ~10% via per-vertex alpha)
|
| 414 |
gl.uniform1f(U.matchOnly, 0);
|
| 415 |
gl.uniform1f(U.alpha, N > 4e6 ? 0.5 : 0.75);
|
|
|
|
| 606 |
window.addEventListener("resize", resize);
|
| 607 |
|
| 608 |
// ββ render loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 609 |
+
let fFrames = 0, fT = performance.now(), spinOn = false, spinUntil = 0;
|
| 610 |
+
function syncSpinner(now) { // ring visible while any tile is queued/fetching
|
| 611 |
+
const busy = inflight > 0 || queue.length > 0; // ...held β₯350ms so fast local loads still flash
|
| 612 |
+
if (busy) spinUntil = now + 350;
|
| 613 |
+
const show = busy || now < spinUntil;
|
| 614 |
+
if (show !== spinOn) { spinOn = show; $("spin").classList.toggle("on", show); }
|
| 615 |
+
}
|
| 616 |
function loop(now) {
|
| 617 |
+
syncSpinner(now);
|
| 618 |
if (dirty) {
|
| 619 |
updateActive(); draw(); dirty = false;
|
| 620 |
$("zoom").textContent = view.s.toFixed(view.s < 10 ? 1 : 0) + "Γ";
|