| … | … | @@ -45,7 +45,8 @@ canvas{position:absolute;inset:0;width:100%;height:100%;display:block} |
| 45 | 45 | <button id="gomtn">the mountain region</button> |
| 46 | 46 | <button id="clearq">stop prefetching</button> |
| 47 | 47 | </div> |
| 48 | | <div class="hud" id="help">drag to pan · wheel to zoom · one tile generates at a time, by design</div> |
| 48 | <div class="hud" id="help">drag to pan · wheel to zoom · one tile generates at a time, by design |
| 49 | <span style="color:var(--warn)">amber tint</span> = coarse placeholder (7.68 km/px), sharpens as detail tiles arrive · <span style="color:var(--accent)">cyan</span> = generating now</div> |
| 49 | 50 | |
| 50 | 51 | <script> |
| 51 | 52 | const TILE_PX = 256; // must match the server's tile_px |
| … | … | @@ -53,6 +54,12 @@ const TILE_PX = 256; // must match the server's tile_px |
| 53 | 54 | // hour, so without this in the URL a shading or apron change is invisible until |
| 54 | 55 | // the cache expires -- the page keeps drawing images from a build that is gone. |
| 55 | 56 | let TILE_VERSION = 3; |
| 57 | const COARSE_RATIO = 256; // 1 coarse px = 256 detail px = 7.68 km |
| 58 | // The coarse stage is ~700x cheaper per km2 than the detail stage (34,000 vs |
| 59 | // ~50 km2/s measured). It is drawn as a BACKDROP under everything, so a |
| 60 | // zoomed-out view is instant and correct instead of a field of dark holes, and |
| 61 | // detail tiles simply paint over it as they arrive. |
| 62 | let coarseImg = null, coarseBox = null, coarsePending = false; |
| 56 | 63 | const M_PER_PX = 30; // model native |
| 57 | 64 | const TILE_M = TILE_PX * M_PER_PX; // 7.68 km per tile |
| 58 | 65 | |
| … | … | @@ -111,10 +118,23 @@ function tileImage(tx, ty) { |
| 111 | 118 | return "pending"; |
| 112 | 119 | } |
| 113 | 120 | |
| 121 | let nDetail = 0, nCoarse = 0; |
| 114 | 122 | function draw() { |
| 123 | nDetail = 0; nCoarse = 0; |
| 115 | 124 | ctx.fillStyle = "#0d1114"; |
| 116 | 125 | ctx.fillRect(0, 0, innerWidth, innerHeight); |
| 117 | 126 | const s = view.scale, size = TILE_PX * s; |
| 127 | |
| 128 | // backdrop first |
| 129 | if (coarseImg && coarseBox) { |
| 130 | const bx = innerWidth/2 + (coarseBox.cj1 * COARSE_RATIO - view.x) * s; |
| 131 | const by = innerHeight/2 + (coarseBox.ci1 * COARSE_RATIO - view.y) * s; |
| 132 | const bw = (coarseBox.cj2 - coarseBox.cj1) * COARSE_RATIO * s; |
| 133 | const bh = (coarseBox.ci2 - coarseBox.ci1) * COARSE_RATIO * s; |
| 134 | ctx.imageSmoothingEnabled = true; // the coarse layer SHOULD interpolate |
| 135 | ctx.drawImage(coarseImg, bx, by, bw, bh); |
| 136 | ctx.imageSmoothingEnabled = false; |
| 137 | } |
| 118 | 138 | const cxs = innerWidth/2, cys = innerHeight/2; |
| 119 | 139 | const showGrid = $("grid").checked; |
| 120 | 140 | |
| … | … | @@ -124,13 +144,22 @@ function draw() { |
| 124 | 144 | const im = tileImage(tx, ty); |
| 125 | 145 | if (im && im !== "pending") { |
| 126 | 146 | ctx.drawImage(im, sx, sy, size + 1, size + 1); |
| 147 | nDetail++; |
| 127 | 148 | } else { |
| 149 | nCoarse++; |
| 150 | // No opaque fill any more -- the coarse backdrop shows through, which is |
| 151 | // the whole point of having it. Only mark what is actively generating. |
| 128 | 152 | const st = tileState[tx + "," + ty] || "missing"; |
| 129 | | ctx.fillStyle = st === "busy" ? "#1d3038" : "#141a20"; |
| 130 | | ctx.fillRect(sx, sy, size, size); |
| 131 | | if (size < 48) { // below the request threshold: say so, do not pretend |
| 132 | | ctx.fillStyle = "#243039"; |
| 133 | | ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); |
| 153 | if (st === "busy") { |
| 154 | ctx.fillStyle = "rgba(116,194,205,0.10)"; |
| 155 | ctx.fillRect(sx, sy, size, size); |
| 156 | } else if (showGrid) { |
| 157 | // These areas are the COARSE backdrop at 7.68 km/px, not detail. They |
| 158 | // look soft because they ARE soft -- 256x less resolution. Without a |
| 159 | // marker it reads as "the model is blurry over there", which is the |
| 160 | // question this page kept provoking. |
| 161 | ctx.fillStyle = "rgba(215,164,92,0.055)"; |
| 162 | ctx.fillRect(sx, sy, size, size); |
| 134 | 163 | } |
| 135 | 164 | } |
| 136 | 165 | if (showGrid) { |
| … | … | @@ -155,7 +184,27 @@ function frame() { if (dirty) draw(); requestAnimationFrame(frame); } |
| 155 | 184 | requestAnimationFrame(frame); |
| 156 | 185 | |
| 157 | 186 | // ---- prefetch: ask for what is visible, plus N rings, centred on the view ---- |
| 187 | async function ensureCoarse() { |
| 188 | if (coarsePending) return; |
| 189 | const w = innerWidth / view.scale, h = innerHeight / view.scale; |
| 190 | const pad = COARSE_RATIO * 4; |
| 191 | const ci1 = Math.floor((view.y - h/2 - pad) / COARSE_RATIO); |
| 192 | const ci2 = Math.ceil ((view.y + h/2 + pad) / COARSE_RATIO); |
| 193 | const cj1 = Math.floor((view.x - w/2 - pad) / COARSE_RATIO); |
| 194 | const cj2 = Math.ceil ((view.x + w/2 + pad) / COARSE_RATIO); |
| 195 | if (coarseBox && ci1 >= coarseBox.ci1 && ci2 <= coarseBox.ci2 && |
| 196 | cj1 >= coarseBox.cj1 && cj2 <= coarseBox.cj2) return; // still covered |
| 197 | if ((ci2-ci1) * (cj2-cj1) > 90000) return; |
| 198 | coarsePending = true; |
| 199 | const box = {ci1, cj1, ci2, cj2}; |
| 200 | const im = new Image(); |
| 201 | im.onload = () => { coarseImg = im; coarseBox = box; coarsePending = false; dirty = true; }; |
| 202 | im.onerror = () => { coarsePending = false; }; |
| 203 | im.src = `/api/coarse.png?ci1=${ci1}&cj1=${cj1}&ci2=${ci2}&cj2=${cj2}&v=${TILE_VERSION}`; |
| 204 | } |
| 205 | |
| 158 | 206 | async function poll() { |
| 207 | ensureCoarse(); |
| 159 | 208 | const vis = visibleTiles(); |
| 160 | 209 | const rings = +$("pre").value; |
| 161 | 210 | // DO NOT queue what you cannot see the detail of. At low zoom a 7.68 km tile |
| … | … | @@ -242,7 +291,10 @@ function hud() { |
| 242 | 291 | |
| 243 | 292 | <span class="k">generated </span>${s.generated||0} tiles <span class="k">=</span> ${(s.km2||0).toFixed(0)} km² |
| 244 | 293 | <span class="k">avg gen </span>${(s.avg_gen_s||0).toFixed(2)} s/tile ${(s.km2_per_s||0).toFixed(1)} km²/s |
| 245 | | <span class="k">queue </span><span class="${(s.queue||0)>12?'w':'k'}">${s.queue||0}</span><span class="k"> / cap 12</span> <span class="k">busy</span> <span class="a">${(s.busy||[]).join(" ")||"—"}</span> |
| 294 | <span class="k">queue </span><span class="${(s.queue||0)>12?'w':'k'}">${s.queue||0}</span><span class="k"> / cap 16</span> <span class="k">busy</span> <span class="a">${(s.busy||[]).join(" ")||"—"}</span> |
| 295 | <span class="k">on screen </span><span class="g">${nDetail} detail</span> @ 30 m/px · <span class="w">${nCoarse} coarse</span> @ 7.68 km/px |
| 296 | <span class="k"> </span>coarse is 256x lower resolution — it is a PLACEHOLDER, not the model being blurry |
| 297 | <span class="k">coarse </span>${coarseImg ? `${((coarseBox.cj2-coarseBox.cj1)*7.68).toFixed(0)} x ${((coarseBox.ci2-coarseBox.ci1)*7.68).toFixed(0)} km backdrop` : "loading…"} |
| 246 | 298 | <span class="k">cache </span>${s.disk_tiles||0} on disk, ${s.mem_tiles||0} in memory |
| 247 | 299 | <span class="k">hits </span>${s.from_disk||0} disk, ${s.from_mem||0} mem${(s.errors||0)?`\n<span class="w">errors ${s.errors}</span>`:""}`; |
| 248 | 300 | } |