diff --git a/assets/hub-globe.js b/assets/hub-globe.js index 76858ca..ac07eaf 100644 --- a/assets/hub-globe.js +++ b/assets/hub-globe.js @@ -10,7 +10,7 @@ * globe3d=7: v4 math + wide centerline stroke bands (no fill quads). * globe3d=8: hub SVG ellipse trace (breaks under rotation — deprecated). * globe3d=9: v4 sphere grid only (thin E–W + N–S) — proper rotation baseline. - * globe3d=10: v4 latitude arcs + screen-Y strip (no center stroke). + * globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped. */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -550,7 +550,60 @@ function drawHubEllipseGridRotated(ctx, cx, cy, r, rotY, layoutScale) { ctx.restore(); } -/** v4 latitude arc with hub ry as vertical screen thickness — rotates correctly. */ +function hubRyForParallel(par) { + if (par.bright) { + return HUB_EW_PARALLELS[0].ry; + } + return par.lat > 0 ? HUB_EW_PARALLELS[1].ry : HUB_EW_PARALLELS[2].ry; +} + +/** v4 visible arc bounds → 2D ellipse (curved ends via disk clip). */ +function drawParallelScreenEllipse(ctx, cx, cy, r, rotY, par) { + let xMin = Infinity; + let xMax = -Infinity; + let zSum = 0; + let n = 0; + const ring = parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS); + for (let i = 0; i < ring.length; i++) { + const view = bodyToView(ring[i], rotY); + if (view.z <= LIMB_Z) { + continue; + } + xMin = Math.min(xMin, view.x); + xMax = Math.max(xMax, view.x); + zSum += view.z; + n++; + } + if (n < 2 || xMax <= xMin) { + return; + } + + const t = Math.max(0, Math.min(1, (zSum / n - LIMB_Z) / (0.95 - LIMB_Z))); + const rx = ((xMax - xMin) / 2) * r; + const ry = hubRyForParallel(par) * (r / GLOBE_VIEW.r); + if (rx < 0.5 || ry < 0.5) { + return; + } + const ecx = cx + ((xMin + xMax) / 2) * r; + const ecy = cy - Math.sin(par.lat) * r; + const rgb = par.bright ? '219,231,246' : '231,236,243'; + const alpha = par.bright ? 0.32 + 0.58 * t : 0.2 + 0.44 * t; + + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2); + ctx.clip(); + ctx.beginPath(); + ctx.ellipse(ecx, ecy, rx, ry, 0, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(' + rgb + ',' + alpha.toFixed(3) + ')'; + ctx.fill(); + ctx.strokeStyle = 'rgba(' + rgb + ',' + Math.min(1, alpha + 0.12).toFixed(3) + ')'; + ctx.lineWidth = par.bright ? 1.6 : 1.1; + ctx.stroke(); + ctx.restore(); +} + +/** @deprecated screen-Y strips — flat rectangles; use drawParallelScreenEllipse. */ function fillParallelScreenStrip(ctx, cx, cy, r, rotY, par) { const halfPx = (par.bright ? HUB_EW_PARALLELS[0].ry : HUB_EW_PARALLELS[1].ry) * (r / GLOBE_VIEW.r); const ring = parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS); diff --git a/index.php b/index.php index 17d53fe..90b2bcb 100644 --- a/index.php +++ b/index.php @@ -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 = '20260704globe11'; + var logoBuild = '20260704globe12'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');