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=3: solid ocean, E–W 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 E–W 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 E–W bands. */
|
/** globe3d=6 — v4 sphere model with volumetric E–W 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'
|
||||||
|
|||||||
@@ -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"]');
|
||||||
|
|||||||
Reference in New Issue
Block a user