diff --git a/assets/hub-globe.js b/assets/hub-globe.js index 448eb5a..4ae14f9 100644 --- a/assets/hub-globe.js +++ b/assets/hub-globe.js @@ -12,7 +12,7 @@ * globe3d=9: v4 sphere grid only (thin E–W + N–S) — proper rotation baseline. * globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped. * globe3d=11: v10 grid + geographic mercator-flat land (360° U-wrap). - * ?f=0.66 — N–S latitude factor (default 0.66; lower = more squeeze). + * ?latf=1.25&lngf=1&zy=0&zx=0 — land plate zoom (linear + non-linear). */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -30,14 +30,39 @@ const CONTINENT_INNER_SX = 1.2408477842003853; const CONTINENT_INNER_SY = 1.2408477842003853; /** Map plate center longitude (extract_red_polygons.py CENTER_LON). */ const MERCATOR_LON_CENTER = (12 * Math.PI) / 180; -/** Default N–S latitude factor (override with ?f= on globe3d=11). */ -const CONTINENT_LAT_FACTOR_DEFAULT = 0.66; +/** Default land-plate zoom (?latf / ?lngf / ?zy / ?zx on globe3d=11). */ +const LAND_ZOOM_DEFAULT = { latf: 1.25, lngf: 1, zy: 0, zx: 0 }; -function resolveContinentLatFactor(raw) { - if (typeof raw !== 'number' || !isFinite(raw)) { - return CONTINENT_LAT_FACTOR_DEFAULT; +function clampZoomNum(v, min, max, fallback) { + if (typeof v !== 'number' || !isFinite(v)) { + return fallback; } - return Math.max(0.05, Math.min(2, raw)); + return Math.max(min, Math.min(max, v)); +} + +function resolveLandZoom(raw) { + const r = raw && typeof raw === 'object' ? raw : {}; + return { + latf: clampZoomNum(r.latf, 0.05, 4, LAND_ZOOM_DEFAULT.latf), + lngf: clampZoomNum(r.lngf, 0.05, 4, LAND_ZOOM_DEFAULT.lngf), + zy: clampZoomNum(r.zy, -4, 4, LAND_ZOOM_DEFAULT.zy), + zx: clampZoomNum(r.zx, -4, 4, LAND_ZOOM_DEFAULT.zx), + }; +} + +/** + * Linear latf/lngf on sphere lon/lat; zy/zx bias zoom toward equator/ prime meridian: + * mul = factor × (1 + z×(1−t²)), t = |coord|/pole (1 at pole, 0 at center line). + */ +function landZoomLonLat(lon, lat, zoom) { + const latT = Math.min(1, Math.abs(lat) / (Math.PI / 2)); + const lonT = Math.min(1, Math.abs(lon) / Math.PI); + const latMul = zoom.latf * (1 + zoom.zy * (1 - latT * latT)); + const lonMul = zoom.lngf * (1 + zoom.zx * (1 - lonT * lonT)); + return { + lon: wrapLon(lon * lonMul + MERCATOR_LON_CENTER), + lat: Math.max(-1.52, Math.min(1.52, lat * latMul)), + }; } const RASTER_SCALE = 2; const AXIS_STEPS = 160; @@ -961,11 +986,10 @@ function sampleFlatPlateFromHub(mercFlat, hpX, hpY) { } /** Sphere tex → mercator-flat plate (lon center + inverse Mercator Y). */ -function sampleMercFlatFromTex(mercFlat, tex, latFactor) { +function sampleMercFlatFromTex(mercFlat, tex, landZoom) { const ll = texLonLat(tex); - const lon = wrapLon(ll.lon + MERCATOR_LON_CENTER); - const lat = Math.max(-1.52, Math.min(1.52, ll.lat * latFactor)); - return sampleMercFlat(mercFlat, lon, lat); + const mapped = landZoomLonLat(ll.lon, ll.lat, landZoom); + return sampleMercFlat(mercFlat, mapped.lon, mapped.lat); } function isLandPixel(r, g, b, a) { @@ -1077,11 +1101,11 @@ function sampleLandRgbaMercator(mercFlat, tex) { } /** globe3d=11 — geographic mercator wrap (360°); z > LIMB_Z only. */ -function sampleLandVisibleHemisphere(mercFlat, tex, latFactor) { +function sampleLandVisibleHemisphere(mercFlat, tex, landZoom) { if (tex.z <= LIMB_Z) { return null; } - const flat = sampleMercFlatFromTex(mercFlat, tex, latFactor); + const flat = sampleMercFlatFromTex(mercFlat, tex, landZoom); if (isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) { return hubContinentRgba(flat); } @@ -1187,7 +1211,7 @@ 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 continentLatFactor = resolveContinentLatFactor(opts.continentLatFactor); + const landZoom = resolveLandZoom(opts.landZoom); const canvas = document.createElement('canvas'); canvas.className = 'hub-globe-canvas'; @@ -1299,7 +1323,7 @@ export async function mountHubGlobe(stage, opts) { cy, r, fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { - const l = sampleLandVisibleHemisphere(mercFlat, tex, continentLatFactor); + const l = sampleLandVisibleHemisphere(mercFlat, tex, landZoom); return l || [0, 0, 0, 0]; }) ); @@ -1375,7 +1399,10 @@ export async function mountHubGlobe(stage, opts) { raf = requestAnimationFrame(tick); stage.dataset.globe3d = String(renderVersion); if (renderVersion >= 11) { - stage.dataset.globeLatFactor = String(continentLatFactor); + stage.dataset.globeLatf = String(landZoom.latf); + stage.dataset.globeLngf = String(landZoom.lngf); + stage.dataset.globeZy = String(landZoom.zy); + stage.dataset.globeZx = String(landZoom.zx); } stage.dataset.globeAxis = renderVersion === 11 @@ -1405,7 +1432,10 @@ export async function mountHubGlobe(stage, opts) { canvas.remove(); delete stage.dataset.globe3d; delete stage.dataset.globeAxis; - delete stage.dataset.globeLatFactor; + delete stage.dataset.globeLatf; + delete stage.dataset.globeLngf; + delete stage.dataset.globeZy; + delete stage.dataset.globeZx; }, }; } diff --git a/index.php b/index.php index 1056c59..2d389de 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 = '20260704globe25'; + var logoBuild = '20260704globe26'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]'); @@ -256,10 +256,18 @@ function hub_h(string $s): string { var globe3dRaw = params.get('globe3d'); var globe3dVersion = globe3dRaw === null || globe3dRaw === '' ? 1 : parseInt(globe3dRaw, 10); if (!isFinite(globe3dVersion)) globe3dVersion = 1; - var globeLatFactorRaw = params.get('f'); - var globeLatFactor = globeLatFactorRaw === null || globeLatFactorRaw === '' - ? NaN - : parseFloat(globeLatFactorRaw); + var globeLandZoom = {}; + ['latf', 'lngf', 'zy', 'zx'].forEach(function (key) { + var raw = params.get(key); + if (raw === null || raw === '') return; + var n = parseFloat(raw); + if (isFinite(n)) globeLandZoom[key] = n; + }); + var globeLatfLegacy = params.get('f'); + if (globeLatfLegacy !== null && globeLatfLegacy !== '' && !isFinite(globeLandZoom.latf)) { + var fLegacy = parseFloat(globeLatfLegacy); + if (isFinite(fLegacy)) globeLandZoom.latf = fLegacy; + } var useGlobe3d = logoEnabled && globe3dVersion !== 0; var globe3dLayers = { globe: true, continents: true, gridOverlay: true }; @@ -304,8 +312,8 @@ function hub_h(string $s): string { if (isFinite(globeYawDeg)) { globeOpts.initialRotationY = globeYawDeg * Math.PI / 180; } - if (isFinite(globeLatFactor)) { - globeOpts.continentLatFactor = globeLatFactor; + if (Object.keys(globeLandZoom).length > 0) { + globeOpts.landZoom = globeLandZoom; } var globeModuleUrl = new URL('assets/hub-globe.js', window.location.href); globeModuleUrl.searchParams.set('v', logoBuild);