1
0
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:
Anton Afanasyeu
2026-08-05 15:51:47 +02:00
parent 2881206db3
commit 07bdf85220
3 changed files with 133 additions and 19 deletions

View File

@@ -1,7 +1,8 @@
/** /**
* Hub landing globe — orthographic Canvas2D (globe3d=1). * Hub landing globe — orthographic Canvas2D (?globe3d=N).
* Grid: hub SVG ellipses (EW) + meridian paths (NS) traced on sphere, then Y-rotated. * globe3d=0: flat SVG layers (index.php).
* Land: ortho hub disk on front face; mercator-flat only on back wrap (avoids front squeeze). * 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 }; 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 DRAG_ROTATION_PER_PX = 0.004;
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 }; const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
const FRONT_Z = 0.04; const FRONT_Z = 0.04;
const LIMB_Z = 0.002;
const RASTER_SCALE = 2; const RASTER_SCALE = 2;
const AXIS_STEPS = 160; const AXIS_STEPS = 160;
const SPHERE_AXIS_STEPS = 180;
/** Latitudes derived from hub-logo-fragment-globe-grid-overlay.svg EW 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 NS 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 — EW parallels. */ /** hub-logo-fragment-globe-grid-overlay.svg — EW parallels. */
const HUB_EW_PARALLELS = [ const HUB_EW_PARALLELS = [
@@ -192,8 +209,9 @@ function ellipseBodyRing(ellipse, steps) {
return out; return out;
} }
function parallelStyle(bright, scale, z) { function parallelStyle(bright, scale, z, limbZ) {
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z))); const zCut = limbZ != null ? limbZ : FRONT_Z;
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
if (t <= 0) { if (t <= 0) {
return null; return null;
} }
@@ -205,8 +223,9 @@ function parallelStyle(bright, scale, z) {
}; };
} }
function meridianStyle(prime, scale, z) { function meridianStyle(prime, scale, z, limbZ) {
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z))); const zCut = limbZ != null ? limbZ : FRONT_Z;
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
if (t <= 0) { if (t <= 0) {
return null; 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 = []; let run = [];
const zCut = limbZ != null ? limbZ : FRONT_Z;
function flush() { function flush() {
if (run.length < 2) { 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++) { for (let i = 0; i < bodies.length; i++) {
const view = bodyToView(bodies[i], rotY); const view = bodyToView(bodies[i], rotY);
if (view.z <= FRONT_Z) { if (view.z <= zCut) {
flush(); flush();
continue; continue;
} }
@@ -262,6 +282,75 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
flush(); 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: EW ellipses + NS meridian paths (matches globe3d=0 artwork geometry). */ /** Hub SVG grid: EW ellipses + NS meridian paths (matches globe3d=0 artwork geometry). */
function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) { function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) {
ctx.save(); ctx.save();
@@ -337,7 +426,7 @@ function sampleOceanRgba(globe, tex) {
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; 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) { if (tex.z > FRONT_Z) {
const ortho = sampleHub(continents, tex); const ortho = sampleHub(continents, tex);
if (ortho[3] > 6) { if (ortho[3] > 6) {
@@ -356,6 +445,19 @@ function sampleLandRgba(continents, mercFlat, tex) {
return null; 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) { function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
const r2 = r * r; const r2 = r * r;
const x0 = Math.max(0, Math.floor(cx - 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) { export async function mountHubGlobe(stage, opts) {
const build = opts.build || 'earth3d'; const build = opts.build || 'earth3d';
const reducedMotion = !!opts.reducedMotion; const reducedMotion = !!opts.reducedMotion;
const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1);
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.className = 'hub-globe-canvas'; canvas.className = 'hub-globe-canvas';
@@ -539,11 +642,18 @@ export async function mountHubGlobe(stage, opts) {
cy, cy,
r, r,
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { 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]; return l || [0, 0, 0, 0];
}) })
); );
if (renderVersion >= 2) {
drawSphereGrid(ctx, cx, cy, r, rotation, layoutScale);
} else {
drawHubGrid(ctx, cx, cy, r, rotation, layoutScale); drawHubGrid(ctx, cx, cy, r, rotation, layoutScale);
}
drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale); drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale);
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
@@ -612,8 +722,8 @@ export async function mountHubGlobe(stage, opts) {
}; };
raf = requestAnimationFrame(tick); raf = requestAnimationFrame(tick);
stage.dataset.globe3d = 'active'; stage.dataset.globe3d = String(renderVersion);
stage.dataset.globeAxis = 'ew-ns-grid'; stage.dataset.globeAxis = renderVersion >= 2 ? 'ew-ns-sphere' : 'ew-ns-grid';
return { return {
destroy() { destroy() {

View File

@@ -26,7 +26,7 @@
z-index: 1; z-index: 1;
} }
/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */ /* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d>=1 */
.hub-globe-canvas { .hub-globe-canvas {
position: fixed; position: fixed;
z-index: 1; z-index: 1;

View File

@@ -187,7 +187,7 @@ function hub_h(string $s): string {
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover')); var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
var logoEnabled = !!(stage && supportsSvg && supportsLayout); var logoEnabled = !!(stage && supportsSvg && supportsLayout);
var logoBuild = '20260606globe14'; var logoBuild = '20260704globe2';
function hubAsset(rel) { function hubAsset(rel) {
var link = document.querySelector('link[href*="hub.css"]'); var link = document.querySelector('link[href*="hub.css"]');
@@ -253,7 +253,10 @@ function hub_h(string $s): string {
return base + '?v=' + logoBuild; return base + '?v=' + logoBuild;
} }
var useGlobe3d = logoEnabled && (params.get('globe3d') || '1') !== '0'; var globe3dRaw = params.get('globe3d');
var globe3dVersion = globe3dRaw === null || globe3dRaw === '' ? 1 : parseInt(globe3dRaw, 10);
if (!isFinite(globe3dVersion)) globe3dVersion = 1;
var useGlobe3d = logoEnabled && globe3dVersion !== 0;
var globe3dLayers = { globe: true, continents: true, gridOverlay: true }; var globe3dLayers = { globe: true, continents: true, gridOverlay: true };
function appendStaticLayers() { function appendStaticLayers() {
@@ -291,7 +294,8 @@ function hub_h(string $s): string {
var globeYawDeg = parseFloat(params.get('globeYawDeg')); var globeYawDeg = parseFloat(params.get('globeYawDeg'));
var globeOpts = { var globeOpts = {
build: logoBuild, build: logoBuild,
reducedMotion: reducedMotion reducedMotion: reducedMotion,
renderVersion: globe3dVersion
}; };
if (isFinite(globeYawDeg)) { if (isFinite(globeYawDeg)) {
globeOpts.initialRotationY = globeYawDeg * Math.PI / 180; globeOpts.initialRotationY = globeYawDeg * Math.PI / 180;
@@ -302,7 +306,7 @@ function hub_h(string $s): string {
return mod.mountHubGlobe(stage, globeOpts); return mod.mountHubGlobe(stage, globeOpts);
}).then(function (handle) { }).then(function (handle) {
if (handle) { if (handle) {
stage.dataset.globe3d = 'active'; stage.dataset.globe3d = String(globe3dVersion);
} }
if (!handle) { if (!handle) {
['globe', 'continents', 'gridOverlay'].forEach(function (name) { ['globe', 'continents', 'gridOverlay'].forEach(function (name) {