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

Hub globe3d=6/7: fix E-W band comb artifacts.

Replace per-longitude quad strips with continuous arc path fill (v6)
or dual wide centerline strokes (v7).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-08-05 16:23:19 +02:00
parent 55f770313a
commit be46fa2d11
2 changed files with 147 additions and 40 deletions

View File

@@ -6,7 +6,8 @@
* globe3d=3: solid ocean, EW parallels only (true latitude rings) — no baked grid bleed.
* globe3d=4: solid ocean, full sphere grid (parallels + meridians).
* globe3d=5: hub ellipse trace (deprecated — breaks under rotation).
* globe3d=6: v4 math + hub ry band fill (latitude strips, limb fade).
* globe3d=6: v4 math + latitude band fill (continuous arc paths).
* globe3d=7: v4 math + wide centerline stroke bands (no fill quads).
*/
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
@@ -325,44 +326,81 @@ function parallelBandHalfWidth(par) {
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) {
function parallelBandStyle(par, z, bandPx, layer) {
const t = Math.max(0, Math.min(1, (z - LIMB_Z) / (0.95 - LIMB_Z)));
if (t <= 0) {
return null;
}
const rgb = par.bright ? '219,231,246' : '231,236,243';
if (layer === 'halo') {
return {
color: 'rgba(' + rgb + ',' + (0.1 + 0.2 * t).toFixed(3) + ')',
width: bandPx * 1.08,
};
}
return {
color: 'rgba(' + rgb + ',' + (par.bright ? 0.4 + 0.52 * t : 0.24 + 0.4 * t).toFixed(3) + ')',
width: bandPx * (0.58 + 0.42 * t),
};
}
/** One filled ribbon per visible arc — no per-longitude quads (avoids comb artifacts). */
function fillParallelBandPath(ctx, cx, cy, r, rotY, par) {
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 inner = parallelBodyRingAtLat(latLo, SPHERE_AXIS_STEPS);
const n = Math.min(outer.length, inner.length);
const rgb = par.bright ? '219,231,246' : '231,236,243';
let runO = [];
let runI = [];
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;
function flushFill() {
if (runO.length < 2) {
runO = [];
runI = [];
return;
}
const zMin = Math.min(views[0].z, views[1].z, views[2].z, views[3].z);
let zSum = 0;
for (let k = 0; k < runO.length; k++) {
zSum += runO[k].z;
}
const zAvg = zSum / runO.length;
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) {
if (t <= 0) {
runO = [];
runI = [];
return;
}
const alpha = (par.bright ? 0.18 + 0.52 * t : 0.1 + 0.36 * t).toFixed(3);
ctx.beginPath();
ctx.moveTo(runO[0].sx, runO[0].sy);
for (let k = 1; k < runO.length; k++) {
ctx.lineTo(runO[k].sx, runO[k].sy);
}
for (let k = runI.length - 1; k >= 0; k--) {
ctx.lineTo(runI[k].sx, runI[k].sy);
}
ctx.closePath();
ctx.fillStyle = 'rgba(' + rgb + ',' + alpha + ')';
ctx.fill();
runO = [];
runI = [];
}
for (let i = 0; i < n; i++) {
const vo = bodyToView(outer[i], rotY);
const vi = bodyToView(inner[i], rotY);
if (vo.z <= LIMB_Z || vi.z <= LIMB_Z) {
flushFill();
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();
runO.push({ sx: cx + vo.x * r, sy: cy - vo.y * r, z: vo.z });
runI.push({ sx: cx + vi.x * r, sy: cy - vi.y * r, z: vi.z });
}
flushFill();
strokeBodyRing(
ctx,
@@ -372,19 +410,50 @@ function fillParallelBand(ctx, cx, cy, r, rotY, par, layoutScale) {
rotY,
parallelBodyRingAtLat(lat, SPHERE_AXIS_STEPS),
function (z) {
const style = parallelStyle(par.bright, layoutScale, z, LIMB_Z);
const style = parallelStyle(par.bright, 1, z, LIMB_Z);
if (!style) {
return null;
}
return {
color: style.color,
width: style.width * (par.bright ? 1.15 : 1.05),
width: Math.max(style.width, (par.bright ? 1.8 : 1.4)),
};
},
LIMB_Z
);
}
/** Wide dual stroke on centerline — globe3d=7. */
function strokeParallelBandWide(ctx, cx, cy, r, rotY, par, layoutScale) {
const halfW = parallelBandHalfWidth(par);
const bandPx = Math.max(par.bright ? 6 : 10, 2 * halfW * r);
const ring = parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS);
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
ring,
function (z) {
return parallelBandStyle(par, z, bandPx, 'halo');
},
LIMB_Z
);
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
ring,
function (z) {
return parallelBandStyle(par, z, bandPx, 'core');
},
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);
@@ -394,7 +463,39 @@ function drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, opts) {
ctx.clip();
SPHERE_PARALLELS.forEach(function (par) {
fillParallelBand(ctx, cx, cy, r, rotY, par, layoutScale);
fillParallelBandPath(ctx, cx, cy, r, rotY, par);
});
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();
}
/** globe3d=7 — v4 sphere model, wide stroke bands (no path fill). */
function drawSphereGridWideStroke(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) {
strokeParallelBandWide(ctx, cx, cy, r, rotY, par, layoutScale);
});
if (!parallelsOnly) {
@@ -518,6 +619,10 @@ function drawGlobeGrid(ctx, cx, cy, r, rotY, layoutScale, renderVersion) {
drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion === 7) {
drawSphereGridWideStroke(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion >= 2) {
drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
@@ -909,15 +1014,17 @@ export async function mountHubGlobe(stage, opts) {
raf = requestAnimationFrame(tick);
stage.dataset.globe3d = String(renderVersion);
stage.dataset.globeAxis =
renderVersion >= 6
? 'ew-sphere-band'
: renderVersion >= 5
? 'ew-hub-ellipse'
: renderVersion >= 3
? 'ew-sphere'
: renderVersion >= 2
? 'ew-ns-sphere'
: 'ew-ns-grid';
renderVersion >= 7
? 'ew-sphere-stroke'
: renderVersion >= 6
? 'ew-sphere-band'
: renderVersion >= 5
? 'ew-hub-ellipse'
: renderVersion >= 3
? 'ew-sphere'
: renderVersion >= 2
? 'ew-ns-sphere'
: 'ew-ns-grid';
return {
destroy() {