From 54c3ac4966f97e45af112d396709f2d2ebeda6f5 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Wed, 5 Aug 2026 16:10:44 +0200 Subject: [PATCH] Hub globe3d=3..5: fix E-W grid rotation iterations. Strip baked SVG grid from rotating ocean (solid fill v3+). v3 sphere parallels only; v4 +meridians; v5 hub ellipse trace. Continents skipped v3+ until wrap is revisited. Co-authored-by: Cursor --- assets/hub-globe.js | 188 ++++++++++++++++++++++++++++++++------------ index.php | 2 +- 2 files changed, 140 insertions(+), 50 deletions(-) diff --git a/assets/hub-globe.js b/assets/hub-globe.js index 90cdfa3..ef1bbbf 100644 --- a/assets/hub-globe.js +++ b/assets/hub-globe.js @@ -1,8 +1,11 @@ /** * Hub landing globe — orthographic Canvas2D (?globe3d=N). * globe3d=0: flat SVG layers (index.php). - * globe3d=1: legacy hybrid ortho front + mercator back. - * globe3d=2+: mercator wrap on visible hemisphere + full-sphere grid rings. + * globe3d=1: legacy hybrid ortho front + mercator back + hub SVG grid trace. + * globe3d=2: mercator land attempt + full-sphere grid (superseded). + * 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: solid ocean, E–W via hub ellipse → sphere trace (matches globe3d=0 art at rest). */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -15,11 +18,16 @@ const RASTER_SCALE = 2; const AXIS_STEPS = 160; const SPHERE_AXIS_STEPS = 180; -/** Latitudes derived from hub-logo-fragment-globe-grid-overlay.svg E–W ellipses. */ +/** Latitudes from hub grid overlay ellipse centers (meridian x = cx). */ +function hubParallelLat(hubCy) { + const texY = (GLOBE_VIEW.cy - hubCy) / GLOBE_VIEW.r; + return Math.asin(Math.max(-1, Math.min(1, texY))); +} + const SPHERE_PARALLELS = [ { lat: 0, bright: true }, - { lat: Math.asin((560 - 392) / 322), bright: false }, - { lat: Math.asin((560 - 728) / 322), bright: false }, + { lat: hubParallelLat(392), bright: false }, + { lat: hubParallelLat(728), bright: false }, ]; /** Longitudes at equator from hub N–S meridian artwork (x=620 / x=820). */ @@ -311,8 +319,9 @@ function meridianBodyRingAtLon(lon, steps) { return out; } -/** Full-sphere grid rings (globe3d>=2) — visible segments only, no front-hemisphere SVG trace. */ -function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale) { +/** Full-sphere grid rings (globe3d>=4). */ +function drawSphereGrid(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); @@ -333,26 +342,45 @@ function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale) { ); }); - 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 - ); + 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(); +} + +/** Hub SVG ellipses traced on sphere (globe3d=5) — matches static art at yaw 0. */ +function drawHubParallelGrid(ctx, cx, cy, r, rotY, layoutScale) { + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2); + ctx.clip(); + + HUB_EW_PARALLELS.forEach(function (ellipse) { + strokeBodyRing(ctx, cx, cy, r, rotY, ellipseBodyRing(ellipse, AXIS_STEPS), function (z) { + return parallelStyle(ellipse.bright, layoutScale, z, LIMB_Z); + }, LIMB_Z); }); ctx.restore(); } -/** Hub SVG grid: E–W ellipses + N–S meridian paths (matches globe3d=0 artwork geometry). */ -function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) { +/** Hub SVG grid: E–W ellipses + N–S meridian paths (globe3d=1). */ +function drawHubGrid(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); @@ -364,15 +392,55 @@ function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) { }); }); - HUB_NS_MERIDIANS.forEach(function (mer) { - strokeBodyRing(ctx, cx, cy, r, rotY, hubPointsToBodyRing(mer.points), function (z) { - return meridianStyle(mer.prime, layoutScale, z); + if (!parallelsOnly) { + HUB_NS_MERIDIANS.forEach(function (mer) { + strokeBodyRing(ctx, cx, cy, r, rotY, hubPointsToBodyRing(mer.points), function (z) { + return meridianStyle(mer.prime, layoutScale, z); + }); }); - }); + } ctx.restore(); } +function drawGlobeGrid(ctx, cx, cy, r, rotY, layoutScale, renderVersion) { + if (renderVersion === 3) { + drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: true }); + return; + } + if (renderVersion === 4) { + drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false }); + return; + } + if (renderVersion === 5) { + drawHubParallelGrid(ctx, cx, cy, r, rotY, layoutScale); + return; + } + if (renderVersion >= 2) { + drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false }); + return; + } + drawHubGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false }); +} + +function drawOceanRim(ctx, globeImg, cx, cy, r, rim, layoutScale, renderVersion) { + if (renderVersion >= 3) { + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, rim, 0, Math.PI * 2); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.clip(); + const g = ctx.createRadialGradient(cx, cy, r, cx, cy, rim); + g.addColorStop(0, 'rgba(28,48,74,0.98)'); + g.addColorStop(1, 'rgba(14,24,38,0.92)'); + ctx.fillStyle = g; + ctx.fill(); + ctx.restore(); + return; + } + drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale); +} + function sampleHub(layer, tex) { const hp = { x: GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r, @@ -415,15 +483,21 @@ function isGridPixel(r, g, b, a) { return Math.max(r, g, b) - Math.min(r, g, b) < 85; } -function sampleOceanRgba(globe, tex) { +const OCEAN_SOLID = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; + +function sampleOceanSolid() { + return OCEAN_SOLID; +} + +function sampleOceanClean(globe, tex) { if (tex.z <= FRONT_Z) { - return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; + return OCEAN_SOLID; } const s = sampleHub(globe, tex); if (s[3] > 8 && !isGridPixel(s[0], s[1], s[2], s[3])) { return [s[0], s[1], s[2], 255]; } - return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; + return OCEAN_SOLID; } function sampleLandRgbaLegacy(continents, mercFlat, tex) { @@ -633,28 +707,37 @@ export async function mountHubGlobe(stage, opts) { cy, r, fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { - return sampleOceanRgba(globe, tex); + if (renderVersion >= 3) { + return sampleOceanSolid(); + } + return sampleOceanClean(globe, tex); }) ); - blitLayer( - ctx, - cx, - cy, - r, - fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { - const l = - renderVersion >= 2 - ? sampleLandRgbaMercator(mercFlat, tex) - : sampleLandRgbaLegacy(continents, mercFlat, tex); - return l || [0, 0, 0, 0]; - }) - ); - if (renderVersion >= 2) { - drawSphereGrid(ctx, cx, cy, r, rotation, layoutScale); - } else { - drawHubGrid(ctx, cx, cy, r, rotation, layoutScale); + if (renderVersion === 1) { + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { + const l = sampleLandRgbaLegacy(continents, mercFlat, tex); + return l || [0, 0, 0, 0]; + }) + ); + } else if (renderVersion === 2) { + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { + const l = sampleLandRgbaMercator(mercFlat, tex); + return l || [0, 0, 0, 0]; + }) + ); } - drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale); + drawGlobeGrid(ctx, cx, cy, r, rotation, layoutScale, renderVersion); + drawOceanRim(ctx, globeImg, cx, cy, r, rim, layoutScale, renderVersion); ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); @@ -723,7 +806,14 @@ export async function mountHubGlobe(stage, opts) { raf = requestAnimationFrame(tick); stage.dataset.globe3d = String(renderVersion); - stage.dataset.globeAxis = renderVersion >= 2 ? 'ew-ns-sphere' : 'ew-ns-grid'; + stage.dataset.globeAxis = + renderVersion >= 5 + ? 'ew-hub-ellipse' + : renderVersion >= 3 + ? 'ew-sphere' + : renderVersion >= 2 + ? 'ew-ns-sphere' + : 'ew-ns-grid'; return { destroy() { diff --git a/index.php b/index.php index c8379a1..6b2ad7c 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 = '20260704globe2'; + var logoBuild = '20260704globe5'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');