diff --git a/assets/hub-globe.js b/assets/hub-globe.js index a8e9ae9..f97cfd1 100644 --- a/assets/hub-globe.js +++ b/assets/hub-globe.js @@ -11,7 +11,7 @@ * globe3d=8: hub SVG ellipse trace (breaks under rotation — deprecated). * globe3d=9: v4 sphere grid only (thin E–W + N–S) — proper rotation baseline. * globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped. - * globe3d=11: v10 grid + mercator-flat land wrap (lon center 12°, inverse Mercator Y). + * globe3d=11: v10 grid + ortho hub land + affine flat-plate wrap fallback. */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -20,8 +20,13 @@ const DRAG_ROTATION_PER_PX = 0.004; const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 }; const FRONT_Z = 0.04; const LIMB_Z = 0.002; -/** hub-logo-continents ortho / flat map center (extract_red_polygons.py). */ -const MERCATOR_LON_CENTER = (12 * Math.PI) / 180; +/** hub-logo-continents inner layer fit (continents-meta.json / extract_red_polygons.py). */ +const CONTINENT_FLAT_W = 800; +const CONTINENT_FLAT_H = 519; +const CONTINENT_INNER_TX = 223.66088631984587; +const CONTINENT_INNER_TY = 238.0; +const CONTINENT_INNER_SX = 1.2408477842003853; +const CONTINENT_INNER_SY = 1.2408477842003853; const RASTER_SCALE = 2; const AXIS_STEPS = 160; const SPHERE_AXIS_STEPS = 180; @@ -135,6 +140,34 @@ function rasterizeImage(img, w, h, scale) { return { data: ctx.getImageData(0, 0, rw, rh).data, w: rw, h: rh }; } +function sampleImageDataBilinearWrapU(imgData, w, h, fx, fy) { + const y = Math.max(0, Math.min(h - 1.001, fy)); + let x = fx % w; + if (x < 0) { + x += w; + } + const x0 = x | 0; + const tx = x - x0; + const x1 = (x0 + 1) % w; + const y0 = y | 0; + const y1 = Math.min(h - 1, y0 + 1); + const ty = y - y0; + const i00 = (y0 * w + x0) * 4; + const i10 = (y0 * w + x1) * 4; + const i01 = (y1 * w + x0) * 4; + const i11 = (y1 * w + x1) * 4; + const out = [0, 0, 0, 0]; + for (let c = 0; c < 4; c++) { + out[c] = Math.round( + imgData[i00 + c] * (1 - tx) * (1 - ty) + + imgData[i10 + c] * tx * (1 - ty) + + imgData[i01 + c] * (1 - tx) * ty + + imgData[i11 + c] * tx * ty + ); + } + return out; +} + function sampleImageDataBilinear(imgData, w, h, fx, fy) { const x = Math.max(0, Math.min(w - 1.001, fx)); const y = Math.max(0, Math.min(h - 1.001, fy)); @@ -900,13 +933,25 @@ function latToMercatorRow(lat, h) { function sampleMercFlat(mercFlat, lon, lat) { const u = ((wrapLon(lon) + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1); const v = latToMercatorRow(lat, mercFlat.h); - return sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, u, v); + return sampleImageDataBilinearWrapU(mercFlat.data, mercFlat.w, mercFlat.h, u, v); +} + +/** Inverse hub-disk → flat plate (same affine as globe SVG), X wraps 360°. */ +function sampleFlatPlateFromHub(mercFlat, hpX, hpY) { + const fx = (hpX - CONTINENT_INNER_TX) / CONTINENT_INNER_SX; + const fy = (hpY - CONTINENT_INNER_TY) / CONTINENT_INNER_SY; + if (fy < 0 || fy > CONTINENT_FLAT_H - 1) { + return null; + } + const sx = (fx / CONTINENT_FLAT_W) * mercFlat.w; + const sy = (fy / CONTINENT_FLAT_H) * mercFlat.h; + return sampleImageDataBilinearWrapU(mercFlat.data, mercFlat.w, mercFlat.h, sx, sy); } /** Sphere tex → mercator-flat plate (lon offset + inverse Mercator Y). */ function sampleMercFlatFromTex(mercFlat, tex) { const ll = texLonLat(tex); - const lon = wrapLon(ll.lon + MERCATOR_LON_CENTER); + const lon = wrapLon(ll.lon + (12 * Math.PI) / 180); return sampleMercFlat(mercFlat, lon, ll.lat); } @@ -936,6 +981,24 @@ function hubContinentRgba(sample) { return [sample[0], sample[1], sample[2], 255]; } +/** Solid fill from rasterized globe SVG — skip gradient fringe / drop-shadow. */ +function isSolidContinentPixel(r, g, b, a) { + if (a < 72) { + return false; + } + if (r > 168 && g > 198 && b > 218) { + return false; + } + if (r < 58 && g < 68 && b < 95) { + return false; + } + const lum = r * 0.299 + g * 0.587 + b * 0.114; + if (lum < 102 || lum > 218) { + return false; + } + return b >= 105 && g >= 100 && r >= 72; +} + /** Light hub grid strokes baked into hub-logo-fragment-globe.svg (vector grid drawn separately). */ function isGridPixel(r, g, b, a) { if (a < 12) { @@ -1000,13 +1063,19 @@ function sampleLandRgbaMercator(mercFlat, tex) { return null; } -/** globe3d=11 — visible hemisphere (z > LIMB_Z); mercator-flat wrap (single source, no ortho ghost). */ -function sampleLandVisibleHemisphere(mercFlat, tex) { +/** globe3d=11 — ortho hub (reference N–S) + wrapped flat-plate fallback. */ +function sampleLandVisibleHemisphere(continents, mercFlat, tex) { if (tex.z <= LIMB_Z) { return null; } - const flat = sampleMercFlatFromTex(mercFlat, tex); - if (isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) { + const ortho = sampleHub(continents, tex); + if (isSolidContinentPixel(ortho[0], ortho[1], ortho[2], ortho[3])) { + return hubContinentRgba(ortho); + } + const hpX = GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r; + const hpY = GLOBE_VIEW.cy - tex.y * GLOBE_VIEW.r; + const flat = sampleFlatPlateFromHub(mercFlat, hpX, hpY); + if (flat && isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) { return hubContinentRgba(flat); } return null; @@ -1222,7 +1291,7 @@ export async function mountHubGlobe(stage, opts) { cy, r, fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { - const l = sampleLandVisibleHemisphere(mercFlat, tex); + const l = sampleLandVisibleHemisphere(continents, mercFlat, tex); return l || [0, 0, 0, 0]; }) ); diff --git a/index.php b/index.php index 37e30cc..c3f6e87 100644 --- a/index.php +++ b/index.php @@ -187,7 +187,7 @@ function hub_h(string $s): string { var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover')); var logoEnabled = !!(stage && supportsSvg && supportsLayout); - var logoBuild = '20260704globe15'; + var logoBuild = '20260704globe16'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');