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:
@@ -6,7 +6,8 @@
|
||||
* globe3d=3: solid ocean, E–W 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 E–W 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 E–W 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,7 +1014,9 @@ export async function mountHubGlobe(stage, opts) {
|
||||
raf = requestAnimationFrame(tick);
|
||||
stage.dataset.globe3d = String(renderVersion);
|
||||
stage.dataset.globeAxis =
|
||||
renderVersion >= 6
|
||||
renderVersion >= 7
|
||||
? 'ew-sphere-stroke'
|
||||
: renderVersion >= 6
|
||||
? 'ew-sphere-band'
|
||||
: renderVersion >= 5
|
||||
? 'ew-hub-ellipse'
|
||||
|
||||
@@ -187,7 +187,7 @@ function hub_h(string $s): string {
|
||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
||||
|
||||
var logoBuild = '20260704globe6';
|
||||
var logoBuild = '20260704globe7';
|
||||
|
||||
function hubAsset(rel) {
|
||||
var link = document.querySelector('link[href*="hub.css"]');
|
||||
|
||||
Reference in New Issue
Block a user