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

Hub globe3d=6: v4 sphere math with volumetric E-W bands.

Fill latitude strips using hub ellipse ry; keep v4/v5 for comparison.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-08-05 16:20:03 +02:00
parent 54c3ac4966
commit 55f770313a
2 changed files with 113 additions and 9 deletions

View File

@@ -5,7 +5,8 @@
* globe3d=2: mercator land attempt + full-sphere grid (superseded). * globe3d=2: mercator land attempt + full-sphere grid (superseded).
* globe3d=3: solid ocean, EW parallels only (true latitude rings) — no baked grid bleed. * globe3d=3: solid ocean, EW parallels only (true latitude rings) — no baked grid bleed.
* globe3d=4: solid ocean, full sphere grid (parallels + meridians). * globe3d=4: solid ocean, full sphere grid (parallels + meridians).
* globe3d=5: solid ocean, EW via hub ellipse → sphere trace (matches globe3d=0 art at rest). * globe3d=5: hub ellipse trace (deprecated — breaks under rotation).
* globe3d=6: v4 math + hub ry band fill (latitude strips, limb fade).
*/ */
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 };
@@ -319,6 +320,103 @@ function meridianBodyRingAtLon(lon, steps) {
return out; return out;
} }
function parallelBandHalfWidth(par) {
const hubRy = par.bright ? HUB_EW_PARALLELS[0].ry : HUB_EW_PARALLELS[1].ry;
return hubRy / GLOBE_VIEW.r;
}
/** Filled EW band between two latitude rings (hub SVG ry → sphere half-width). */
function fillParallelBand(ctx, cx, cy, r, rotY, par, layoutScale) {
const lat = par.lat;
const halfW = parallelBandHalfWidth(par);
const latLo = Math.max(-Math.PI / 2 + 0.02, lat - halfW);
const latHi = Math.min(Math.PI / 2 - 0.02, lat + halfW);
const inner = parallelBodyRingAtLat(latLo, SPHERE_AXIS_STEPS);
const outer = parallelBodyRingAtLat(latHi, SPHERE_AXIS_STEPS);
const n = Math.min(inner.length, outer.length);
const rgb = par.bright ? '219,231,246' : '231,236,243';
for (let i = 0; i < n - 1; i++) {
const views = [
bodyToView(inner[i], rotY),
bodyToView(inner[i + 1], rotY),
bodyToView(outer[i + 1], rotY),
bodyToView(outer[i], rotY),
];
const zAvg = (views[0].z + views[1].z + views[2].z + views[3].z) / 4;
if (zAvg <= LIMB_Z) {
continue;
}
const zMin = Math.min(views[0].z, views[1].z, views[2].z, views[3].z);
const t = Math.max(0, Math.min(1, (zAvg - LIMB_Z) / (0.95 - LIMB_Z)));
const limbFade = Math.min(1, Math.max(0, (zMin - LIMB_Z) / 0.12));
const alpha = (par.bright ? 0.2 + 0.55 * t : 0.1 + 0.38 * t) * limbFade;
if (alpha <= 0.008) {
continue;
}
ctx.beginPath();
ctx.moveTo(cx + views[0].x * r, cy - views[0].y * r);
ctx.lineTo(cx + views[1].x * r, cy - views[1].y * r);
ctx.lineTo(cx + views[2].x * r, cy - views[2].y * r);
ctx.lineTo(cx + views[3].x * r, cy - views[3].y * r);
ctx.closePath();
ctx.fillStyle = 'rgba(' + rgb + ',' + alpha.toFixed(3) + ')';
ctx.fill();
}
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
parallelBodyRingAtLat(lat, SPHERE_AXIS_STEPS),
function (z) {
const style = parallelStyle(par.bright, layoutScale, z, LIMB_Z);
if (!style) {
return null;
}
return {
color: style.color,
width: style.width * (par.bright ? 1.15 : 1.05),
};
},
LIMB_Z
);
}
/** globe3d=6 — v4 sphere model with volumetric EW bands. */
function drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly);
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
SPHERE_PARALLELS.forEach(function (par) {
fillParallelBand(ctx, cx, cy, r, rotY, par, layoutScale);
});
if (!parallelsOnly) {
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();
}
/** Full-sphere grid rings (globe3d>=4). */ /** Full-sphere grid rings (globe3d>=4). */
function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, opts) { function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly); const parallelsOnly = !!(opts && opts.parallelsOnly);
@@ -416,6 +514,10 @@ function drawGlobeGrid(ctx, cx, cy, r, rotY, layoutScale, renderVersion) {
drawHubParallelGrid(ctx, cx, cy, r, rotY, layoutScale); drawHubParallelGrid(ctx, cx, cy, r, rotY, layoutScale);
return; return;
} }
if (renderVersion === 6) {
drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion >= 2) { if (renderVersion >= 2) {
drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false }); drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return; return;
@@ -807,7 +909,9 @@ export async function mountHubGlobe(stage, opts) {
raf = requestAnimationFrame(tick); raf = requestAnimationFrame(tick);
stage.dataset.globe3d = String(renderVersion); stage.dataset.globe3d = String(renderVersion);
stage.dataset.globeAxis = stage.dataset.globeAxis =
renderVersion >= 5 renderVersion >= 6
? 'ew-sphere-band'
: renderVersion >= 5
? 'ew-hub-ellipse' ? 'ew-hub-ellipse'
: renderVersion >= 3 : renderVersion >= 3
? 'ew-sphere' ? 'ew-sphere'

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 = '20260704globe5'; var logoBuild = '20260704globe6';
function hubAsset(rel) { function hubAsset(rel) {
var link = document.querySelector('link[href*="hub.css"]'); var link = document.querySelector('link[href*="hub.css"]');