mirror of
git://f0xx.org/ac/ac-be-hub
synced 2026-08-09 20:59:23 +03:00
Hub globe3d=2: mercator hemisphere wrap + sphere grid rings.
globe3d=1 keeps legacy hybrid ortho/mercator; globe3d=2 samples hub-logo-continents-mercator-flat on visible z>0 and draws full parallel/meridian rings so axes and continents survive rotation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Hub landing globe — orthographic Canvas2D (globe3d=1).
|
||||
* Grid: hub SVG ellipses (E–W) + meridian paths (N–S) traced on sphere, then Y-rotated.
|
||||
* Land: ortho hub disk on front face; mercator-flat only on back wrap (avoids front squeeze).
|
||||
* Hub landing globe — orthographic Canvas2D (?globe3d=N).
|
||||
* globe3d=0: flat SVG layers (index.php).
|
||||
* globe3d=1: legacy hybrid ortho front + mercator back.
|
||||
* globe3d=2+: mercator wrap on visible hemisphere + full-sphere grid rings.
|
||||
*/
|
||||
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
|
||||
|
||||
@@ -9,8 +10,24 @@ const AUTO_SPIN_RAD_S = 0.035;
|
||||
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;
|
||||
const RASTER_SCALE = 2;
|
||||
const AXIS_STEPS = 160;
|
||||
const SPHERE_AXIS_STEPS = 180;
|
||||
|
||||
/** Latitudes derived from hub-logo-fragment-globe-grid-overlay.svg E–W ellipses. */
|
||||
const SPHERE_PARALLELS = [
|
||||
{ lat: 0, bright: true },
|
||||
{ lat: Math.asin((560 - 392) / 322), bright: false },
|
||||
{ lat: Math.asin((560 - 728) / 322), bright: false },
|
||||
];
|
||||
|
||||
/** Longitudes at equator from hub N–S meridian artwork (x=620 / x=820). */
|
||||
const SPHERE_MERIDIANS = [
|
||||
{ lon: 0, prime: true },
|
||||
{ lon: Math.atan2((620 - 720) / 322, Math.sqrt(Math.max(0, 1 - Math.pow((620 - 720) / 322, 2)))), prime: false },
|
||||
{ lon: Math.atan2((820 - 720) / 322, Math.sqrt(Math.max(0, 1 - Math.pow((820 - 720) / 322, 2)))), prime: false },
|
||||
];
|
||||
|
||||
/** hub-logo-fragment-globe-grid-overlay.svg — E–W parallels. */
|
||||
const HUB_EW_PARALLELS = [
|
||||
@@ -192,8 +209,9 @@ function ellipseBodyRing(ellipse, steps) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parallelStyle(bright, scale, z) {
|
||||
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z)));
|
||||
function parallelStyle(bright, scale, z, limbZ) {
|
||||
const zCut = limbZ != null ? limbZ : FRONT_Z;
|
||||
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
|
||||
if (t <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -205,8 +223,9 @@ function parallelStyle(bright, scale, z) {
|
||||
};
|
||||
}
|
||||
|
||||
function meridianStyle(prime, scale, z) {
|
||||
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z)));
|
||||
function meridianStyle(prime, scale, z, limbZ) {
|
||||
const zCut = limbZ != null ? limbZ : FRONT_Z;
|
||||
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
|
||||
if (t <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -217,8 +236,9 @@ function meridianStyle(prime, scale, z) {
|
||||
};
|
||||
}
|
||||
|
||||
function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
|
||||
function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt, limbZ) {
|
||||
let run = [];
|
||||
const zCut = limbZ != null ? limbZ : FRONT_Z;
|
||||
|
||||
function flush() {
|
||||
if (run.length < 2) {
|
||||
@@ -249,7 +269,7 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
|
||||
|
||||
for (let i = 0; i < bodies.length; i++) {
|
||||
const view = bodyToView(bodies[i], rotY);
|
||||
if (view.z <= FRONT_Z) {
|
||||
if (view.z <= zCut) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
@@ -262,6 +282,75 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
|
||||
flush();
|
||||
}
|
||||
|
||||
function parallelBodyRingAtLat(lat, steps) {
|
||||
const out = [];
|
||||
const cosLat = Math.cos(lat);
|
||||
const sinLat = Math.sin(lat);
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const lon = -Math.PI + (2 * Math.PI * i) / steps;
|
||||
out.push({
|
||||
x: cosLat * Math.sin(lon),
|
||||
y: sinLat,
|
||||
z: cosLat * Math.cos(lon),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function meridianBodyRingAtLon(lon, steps) {
|
||||
const out = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const lat = -Math.PI / 2 + (Math.PI * i) / steps;
|
||||
const cosLat = Math.cos(lat);
|
||||
out.push({
|
||||
x: cosLat * Math.sin(lon),
|
||||
y: Math.sin(lat),
|
||||
z: cosLat * Math.cos(lon),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Full-sphere grid rings (globe3d>=2) — visible segments only, no front-hemisphere SVG trace. */
|
||||
function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
|
||||
SPHERE_PARALLELS.forEach(function (par) {
|
||||
strokeBodyRing(
|
||||
ctx,
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
rotY,
|
||||
parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS),
|
||||
function (z) {
|
||||
return parallelStyle(par.bright, layoutScale, z, LIMB_Z);
|
||||
},
|
||||
LIMB_Z
|
||||
);
|
||||
});
|
||||
|
||||
SPHERE_MERIDIANS.forEach(function (mer) {
|
||||
strokeBodyRing(
|
||||
ctx,
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
rotY,
|
||||
meridianBodyRingAtLon(mer.lon, SPHERE_AXIS_STEPS),
|
||||
function (z) {
|
||||
return meridianStyle(mer.prime, layoutScale, z, LIMB_Z);
|
||||
},
|
||||
LIMB_Z
|
||||
);
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/** Hub SVG grid: E–W ellipses + N–S meridian paths (matches globe3d=0 artwork geometry). */
|
||||
function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) {
|
||||
ctx.save();
|
||||
@@ -337,7 +426,7 @@ function sampleOceanRgba(globe, tex) {
|
||||
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
|
||||
}
|
||||
|
||||
function sampleLandRgba(continents, mercFlat, tex) {
|
||||
function sampleLandRgbaLegacy(continents, mercFlat, tex) {
|
||||
if (tex.z > FRONT_Z) {
|
||||
const ortho = sampleHub(continents, tex);
|
||||
if (ortho[3] > 6) {
|
||||
@@ -356,6 +445,19 @@ function sampleLandRgba(continents, mercFlat, tex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** globe3d>=2: mercator-flat on entire visible hemisphere (z>0). */
|
||||
function sampleLandRgbaMercator(mercFlat, tex) {
|
||||
if (tex.z <= LIMB_Z) {
|
||||
return null;
|
||||
}
|
||||
const ll = texLonLat(tex);
|
||||
const flat = sampleMercFlat(mercFlat, ll.lon, ll.lat);
|
||||
if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) {
|
||||
return [flat[0], flat[1], flat[2], Math.min(255, flat[3])];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
|
||||
const r2 = r * r;
|
||||
const x0 = Math.max(0, Math.floor(cx - r));
|
||||
@@ -454,6 +556,7 @@ function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) {
|
||||
export async function mountHubGlobe(stage, opts) {
|
||||
const build = opts.build || 'earth3d';
|
||||
const reducedMotion = !!opts.reducedMotion;
|
||||
const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'hub-globe-canvas';
|
||||
@@ -539,11 +642,18 @@ export async function mountHubGlobe(stage, opts) {
|
||||
cy,
|
||||
r,
|
||||
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
|
||||
const l = sampleLandRgba(continents, mercFlat, tex);
|
||||
const l =
|
||||
renderVersion >= 2
|
||||
? sampleLandRgbaMercator(mercFlat, tex)
|
||||
: sampleLandRgbaLegacy(continents, mercFlat, tex);
|
||||
return l || [0, 0, 0, 0];
|
||||
})
|
||||
);
|
||||
drawHubGrid(ctx, cx, cy, r, rotation, layoutScale);
|
||||
if (renderVersion >= 2) {
|
||||
drawSphereGrid(ctx, cx, cy, r, rotation, layoutScale);
|
||||
} else {
|
||||
drawHubGrid(ctx, cx, cy, r, rotation, layoutScale);
|
||||
}
|
||||
drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
@@ -612,8 +722,8 @@ export async function mountHubGlobe(stage, opts) {
|
||||
};
|
||||
|
||||
raf = requestAnimationFrame(tick);
|
||||
stage.dataset.globe3d = 'active';
|
||||
stage.dataset.globeAxis = 'ew-ns-grid';
|
||||
stage.dataset.globe3d = String(renderVersion);
|
||||
stage.dataset.globeAxis = renderVersion >= 2 ? 'ew-ns-sphere' : 'ew-ns-grid';
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
|
||||
Reference in New Issue
Block a user