diff --git a/assets/hub-globe.js b/assets/hub-globe.js index 9022152..90cdfa3 100644 --- a/assets/hub-globe.js +++ b/assets/hub-globe.js @@ -1,7 +1,8 @@ /** - * Hub landing globe — orthographic Canvas2D (globe3d=1). - * Grid: hub SVG ellipses (E–W) + meridian paths (N–S) traced on sphere, then Y-rotated. - * Land: ortho hub disk on front face; mercator-flat only on back wrap (avoids front squeeze). + * 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. */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -9,8 +10,24 @@ const AUTO_SPIN_RAD_S = 0.035; const DRAG_ROTATION_PER_PX = 0.004; const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 }; const FRONT_Z = 0.04; +const LIMB_Z = 0.002; 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. */ +const SPHERE_PARALLELS = [ + { lat: 0, bright: true }, + { lat: Math.asin((560 - 392) / 322), bright: false }, + { lat: Math.asin((560 - 728) / 322), bright: false }, +]; + +/** Longitudes at equator from hub N–S meridian artwork (x=620 / x=820). */ +const SPHERE_MERIDIANS = [ + { lon: 0, prime: true }, + { lon: Math.atan2((620 - 720) / 322, Math.sqrt(Math.max(0, 1 - Math.pow((620 - 720) / 322, 2)))), prime: false }, + { lon: Math.atan2((820 - 720) / 322, Math.sqrt(Math.max(0, 1 - Math.pow((820 - 720) / 322, 2)))), prime: false }, +]; /** hub-logo-fragment-globe-grid-overlay.svg — E–W parallels. */ const HUB_EW_PARALLELS = [ @@ -192,8 +209,9 @@ function ellipseBodyRing(ellipse, steps) { return out; } -function parallelStyle(bright, scale, z) { - const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z))); +function parallelStyle(bright, scale, z, limbZ) { + const zCut = limbZ != null ? limbZ : FRONT_Z; + const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut))); if (t <= 0) { return null; } @@ -205,8 +223,9 @@ function parallelStyle(bright, scale, z) { }; } -function meridianStyle(prime, scale, z) { - const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z))); +function meridianStyle(prime, scale, z, limbZ) { + const zCut = limbZ != null ? limbZ : FRONT_Z; + const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut))); if (t <= 0) { return null; } @@ -217,8 +236,9 @@ function meridianStyle(prime, scale, z) { }; } -function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) { +function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt, limbZ) { let run = []; + const zCut = limbZ != null ? limbZ : FRONT_Z; function flush() { if (run.length < 2) { @@ -249,7 +269,7 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) { for (let i = 0; i < bodies.length; i++) { const view = bodyToView(bodies[i], rotY); - if (view.z <= FRONT_Z) { + if (view.z <= zCut) { flush(); continue; } @@ -262,6 +282,75 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) { flush(); } +function parallelBodyRingAtLat(lat, steps) { + const out = []; + const cosLat = Math.cos(lat); + const sinLat = Math.sin(lat); + for (let i = 0; i <= steps; i++) { + const lon = -Math.PI + (2 * Math.PI * i) / steps; + out.push({ + x: cosLat * Math.sin(lon), + y: sinLat, + z: cosLat * Math.cos(lon), + }); + } + return out; +} + +function meridianBodyRingAtLon(lon, steps) { + const out = []; + for (let i = 0; i <= steps; i++) { + const lat = -Math.PI / 2 + (Math.PI * i) / steps; + const cosLat = Math.cos(lat); + out.push({ + x: cosLat * Math.sin(lon), + y: Math.sin(lat), + z: cosLat * Math.cos(lon), + }); + } + return out; +} + +/** Full-sphere grid rings (globe3d>=2) — visible segments only, no front-hemisphere SVG trace. */ +function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale) { + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2); + ctx.clip(); + + SPHERE_PARALLELS.forEach(function (par) { + strokeBodyRing( + ctx, + cx, + cy, + r, + rotY, + parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS), + function (z) { + return parallelStyle(par.bright, layoutScale, z, LIMB_Z); + }, + LIMB_Z + ); + }); + + 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 grid: E–W ellipses + N–S meridian paths (matches globe3d=0 artwork geometry). */ function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) { ctx.save(); @@ -337,7 +426,7 @@ function sampleOceanRgba(globe, tex) { return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; } -function sampleLandRgba(continents, mercFlat, tex) { +function sampleLandRgbaLegacy(continents, mercFlat, tex) { if (tex.z > FRONT_Z) { const ortho = sampleHub(continents, tex); if (ortho[3] > 6) { @@ -356,6 +445,19 @@ function sampleLandRgba(continents, mercFlat, tex) { return null; } +/** globe3d>=2: mercator-flat on entire visible hemisphere (z>0). */ +function sampleLandRgbaMercator(mercFlat, tex) { + if (tex.z <= LIMB_Z) { + return null; + } + const ll = texLonLat(tex); + const flat = sampleMercFlat(mercFlat, ll.lon, ll.lat); + if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) { + return [flat[0], flat[1], flat[2], Math.min(255, flat[3])]; + } + return null; +} + function fillDiskRegion(ctx, cx, cy, r, rotY, fn) { const r2 = r * r; const x0 = Math.max(0, Math.floor(cx - r)); @@ -454,6 +556,7 @@ function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) { export async function mountHubGlobe(stage, opts) { const build = opts.build || 'earth3d'; const reducedMotion = !!opts.reducedMotion; + const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1); const canvas = document.createElement('canvas'); canvas.className = 'hub-globe-canvas'; @@ -539,11 +642,18 @@ export async function mountHubGlobe(stage, opts) { cy, r, fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { - const l = sampleLandRgba(continents, mercFlat, tex); + const l = + renderVersion >= 2 + ? sampleLandRgbaMercator(mercFlat, tex) + : sampleLandRgbaLegacy(continents, mercFlat, tex); return l || [0, 0, 0, 0]; }) ); - drawHubGrid(ctx, cx, cy, r, rotation, layoutScale); + if (renderVersion >= 2) { + drawSphereGrid(ctx, cx, cy, r, rotation, layoutScale); + } else { + drawHubGrid(ctx, cx, cy, r, rotation, layoutScale); + } drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale); ctx.save(); ctx.beginPath(); @@ -612,8 +722,8 @@ export async function mountHubGlobe(stage, opts) { }; raf = requestAnimationFrame(tick); - stage.dataset.globe3d = 'active'; - stage.dataset.globeAxis = 'ew-ns-grid'; + stage.dataset.globe3d = String(renderVersion); + stage.dataset.globeAxis = renderVersion >= 2 ? 'ew-ns-sphere' : 'ew-ns-grid'; return { destroy() { diff --git a/hub-logo-layers.css b/hub-logo-layers.css index 943ae3d..de3d6f0 100644 --- a/hub-logo-layers.css +++ b/hub-logo-layers.css @@ -26,7 +26,7 @@ z-index: 1; } -/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */ +/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d>=1 */ .hub-globe-canvas { position: fixed; z-index: 1; diff --git a/index.php b/index.php index 6f7137e..c8379a1 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 = '20260606globe14'; + var logoBuild = '20260704globe2'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]'); @@ -253,7 +253,10 @@ function hub_h(string $s): string { return base + '?v=' + logoBuild; } - var useGlobe3d = logoEnabled && (params.get('globe3d') || '1') !== '0'; + var globe3dRaw = params.get('globe3d'); + var globe3dVersion = globe3dRaw === null || globe3dRaw === '' ? 1 : parseInt(globe3dRaw, 10); + if (!isFinite(globe3dVersion)) globe3dVersion = 1; + var useGlobe3d = logoEnabled && globe3dVersion !== 0; var globe3dLayers = { globe: true, continents: true, gridOverlay: true }; function appendStaticLayers() { @@ -291,7 +294,8 @@ function hub_h(string $s): string { var globeYawDeg = parseFloat(params.get('globeYawDeg')); var globeOpts = { build: logoBuild, - reducedMotion: reducedMotion + reducedMotion: reducedMotion, + renderVersion: globe3dVersion }; if (isFinite(globeYawDeg)) { globeOpts.initialRotationY = globeYawDeg * Math.PI / 180; @@ -302,7 +306,7 @@ function hub_h(string $s): string { return mod.mountHubGlobe(stage, globeOpts); }).then(function (handle) { if (handle) { - stage.dataset.globe3d = 'active'; + stage.dataset.globe3d = String(globe3dVersion); } if (!handle) { ['globe', 'continents', 'gridOverlay'].forEach(function (name) {