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=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: hub ellipse trace (deprecated — breaks under rotation). * 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 }; 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; return hubRy / GLOBE_VIEW.r;
} }
/** Filled EW band between two latitude rings (hub SVG ry → sphere half-width). */ function parallelBandStyle(par, z, bandPx, layer) {
function fillParallelBand(ctx, cx, cy, r, rotY, par, layoutScale) { 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 lat = par.lat;
const halfW = parallelBandHalfWidth(par); const halfW = parallelBandHalfWidth(par);
const latLo = Math.max(-Math.PI / 2 + 0.02, lat - halfW); const latLo = Math.max(-Math.PI / 2 + 0.02, lat - halfW);
const latHi = Math.min(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 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'; const rgb = par.bright ? '219,231,246' : '231,236,243';
let runO = [];
let runI = [];
for (let i = 0; i < n - 1; i++) { function flushFill() {
const views = [ if (runO.length < 2) {
bodyToView(inner[i], rotY), runO = [];
bodyToView(inner[i + 1], rotY), runI = [];
bodyToView(outer[i + 1], rotY), return;
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); 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 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)); if (t <= 0) {
const alpha = (par.bright ? 0.2 + 0.55 * t : 0.1 + 0.38 * t) * limbFade; runO = [];
if (alpha <= 0.008) { 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; continue;
} }
ctx.beginPath(); runO.push({ sx: cx + vo.x * r, sy: cy - vo.y * r, z: vo.z });
ctx.moveTo(cx + views[0].x * r, cy - views[0].y * r); runI.push({ sx: cx + vi.x * r, sy: cy - vi.y * r, z: vi.z });
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();
} }
flushFill();
strokeBodyRing( strokeBodyRing(
ctx, ctx,
@@ -372,19 +410,50 @@ function fillParallelBand(ctx, cx, cy, r, rotY, par, layoutScale) {
rotY, rotY,
parallelBodyRingAtLat(lat, SPHERE_AXIS_STEPS), parallelBodyRingAtLat(lat, SPHERE_AXIS_STEPS),
function (z) { function (z) {
const style = parallelStyle(par.bright, layoutScale, z, LIMB_Z); const style = parallelStyle(par.bright, 1, z, LIMB_Z);
if (!style) { if (!style) {
return null; return null;
} }
return { return {
color: style.color, 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 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. */ /** globe3d=6 — v4 sphere model with volumetric EW bands. */
function drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, opts) { function drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly); const parallelsOnly = !!(opts && opts.parallelsOnly);
@@ -394,7 +463,39 @@ function drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, opts) {
ctx.clip(); ctx.clip();
SPHERE_PARALLELS.forEach(function (par) { 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) { if (!parallelsOnly) {
@@ -518,6 +619,10 @@ function drawGlobeGrid(ctx, cx, cy, r, rotY, layoutScale, renderVersion) {
drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false }); drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return; return;
} }
if (renderVersion === 7) {
drawSphereGridWideStroke(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;
@@ -909,7 +1014,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 >= 6 renderVersion >= 7
? 'ew-sphere-stroke'
: renderVersion >= 6
? 'ew-sphere-band' ? 'ew-sphere-band'
: renderVersion >= 5 : renderVersion >= 5
? 'ew-hub-ellipse' ? 'ew-hub-ellipse'

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