malteos commited on
Commit
efb3310
Β·
verified Β·
1 Parent(s): 1cf25db

viewer: tile-loading spinner + zoom-adaptive point sizing

Browse files
Files changed (1) hide show
  1. 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
- // sub-linear (sqrt) point growth: gaps outpace dots on zoom-in so clusters declutter
397
- gl.uniform1f(U.pt, Math.max(1.2, dpr * (1.5 + 3.4 * Math.sqrt(view.s))));
 
 
 
 
 
 
 
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) + "Γ—";