1
0
mirror of git://f0xx.org/ac/ac-be-hub synced 2026-08-09 20:59:23 +03:00

fix(globe3d=11): ortho hub land + affine flat wrap fallback

Restore sampleHub for reference N-S proportions; wrap via inverse
inner-layer affine on mercator-flat with seamless U bilinear.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-08-05 18:23:07 +02:00
parent 7b10062604
commit c0c99b7cce
2 changed files with 80 additions and 11 deletions

View File

@@ -11,7 +11,7 @@
* globe3d=8: hub SVG ellipse trace (breaks under rotation — deprecated).
* globe3d=9: v4 sphere grid only (thin EW + NS) — 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 NS) + 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];
})
);

View File

@@ -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"]');