From 39603e976360bdc0199c5ae830fedd42ad3ee7b6 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sat, 6 Jun 2026 18:50:28 +0200 Subject: [PATCH] 1 --- examples/app_hub/assets/hub-globe.js | 263 +++++++++++++-------------- examples/app_hub/index.php | 2 +- 2 files changed, 129 insertions(+), 136 deletions(-) diff --git a/examples/app_hub/assets/hub-globe.js b/examples/app_hub/assets/hub-globe.js index fdc1d11..c8f283c 100644 --- a/examples/app_hub/assets/hub-globe.js +++ b/examples/app_hub/assets/hub-globe.js @@ -1,7 +1,7 @@ /** * Hub landing globe — orthographic Canvas2D (globe3d=1). - * Matches globe3d=0 stack: globe (ocean+base grid) → grid overlay → continents. - * Grid/land sampled from hub SVG disks in body-fixed frame (Mercator ortho, not geodesic lines). + * Bakes globe3d=0 stack to equirect maps once; runtime always samples by lon/lat + * after inverse Y-rotation (rotation-invariant, full sphere wrap). */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -10,12 +10,11 @@ const DRAG_ROTATION_PER_PX = 0.004; const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 }; const FRONT_Z = 0.02; const RASTER_SCALE = 2; -const EQ_W = 1024; -const EQ_H = 512; -/** Hub ortho disk parallels ≈ sin(lat) offset from SVG pole ellipses. */ +const EQ_W = 2048; +const EQ_H = 1024; const GRID_PARALLELS = [0, 0.521, -0.521]; const GRID_MERIDIAN_SPACING = Math.PI / 6; -const GRID_LINE_RAD = 0.012; +const GRID_LINE_RAD = 0.022; function hubAsset(rel, build) { const link = document.querySelector('link[href*="hub.css"]'); @@ -106,47 +105,58 @@ function inverseRotateY(nx, ny, nz, rotY) { }; } -/** Hub SVG disk is N-up; negate Y for texture lookup only. */ -function toTextureBody(body) { - return { x: body.x, y: -body.y, z: body.z }; -} - -function hubPixelF(tex) { +function sphereTexFromLonLat(lon, lat) { + const cosLat = Math.cos(lat); return { - x: GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r, - y: GLOBE_VIEW.cy - tex.y * GLOBE_VIEW.r, + x: cosLat * Math.sin(lon), + y: -Math.sin(lat), + z: cosLat * Math.cos(lon), }; } -function mercatorFlatUV(lon, lat, w, h) { - return { - x: ((lon + Math.PI) / (2 * Math.PI)) * (w - 1), - y: (0.5 - lat / Math.PI) * (h - 1), - }; -} - -function isLandPixel(r, g, b, a) { - return a > 12 && r > 100 && g < 140 && b < 140; -} - -function isGridPixel(r, g, b, a) { - return a > 10 && r + g + b > 280; -} - -function bodyLonLat(tex) { +function bodyLonLat(body) { + const tex = { x: body.x, y: -body.y, z: body.z }; return { lon: Math.atan2(tex.x, tex.z), lat: Math.asin(Math.max(-1, Math.min(1, tex.y))), }; } -function sampleEquator(map, lon, lat) { - const u = (lon + Math.PI) / (2 * Math.PI); - const v = 0.5 - lat / Math.PI; - return sampleImageDataBilinear(map.data, map.w, map.h, u * (map.w - 1), v * (map.h - 1)); +function eqUV(lon, lat, w, h) { + return { + x: ((lon + Math.PI) / (2 * Math.PI)) * (w - 1), + y: (0.5 - lat / Math.PI) * (h - 1), + }; +} + +function sampleEq(map, lon, lat) { + const uv = eqUV(lon, lat, map.w, map.h); + return sampleImageDataBilinear(map.data, map.w, map.h, uv.x, uv.y); +} + +function sampleHub(layer, tex) { + const hp = { + x: GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r, + y: GLOBE_VIEW.cy - tex.y * GLOBE_VIEW.r, + }; + const sx = (hp.x / GLOBE_VIEW.imgW) * (layer.w - 1); + const sy = (hp.y / GLOBE_VIEW.imgH) * (layer.h - 1); + return sampleImageDataBilinear(layer.data, layer.w, layer.h, sx, sy); +} + +function sampleMercFlat(mercFlat, lon, lat) { + const uv = eqUV(lon, lat, mercFlat.w, mercFlat.h); + return sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, uv.x, uv.y); +} + +function isLandPixel(r, g, b, a) { + return a > 10 && r > 100 && g < 140 && b < 140; +} + +function isGridPixel(r, g, b, a) { + return a > 8 && r + g + b > 260; } -/** Procedural Mercator grid on wrap-around hemisphere (matches hub 30° meridians + pole ellipses). */ function analyticGridRgba(lon, lat) { const wrapped = ((lon + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI); const cell = wrapped % GRID_MERIDIAN_SPACING; @@ -165,45 +175,27 @@ function analyticGridRgba(lon, lat) { return [0, 0, 0, 0]; } if (onMeridian) { - const prime = merDist < GRID_LINE_RAD && (Math.abs(lon) < GRID_LINE_RAD || Math.abs(Math.abs(lon) - Math.PI) < GRID_LINE_RAD); - return prime ? [238, 245, 255, 220] : [238, 245, 255, 170]; + const prime = Math.abs(lon) < GRID_LINE_RAD * 1.5 || Math.abs(Math.abs(lon) - Math.PI) < GRID_LINE_RAD * 1.5; + return prime ? [238, 245, 255, 235] : [238, 245, 255, 185]; } - return bright ? [219, 231, 246, 190] : [231, 236, 243, 130]; + return bright ? [219, 231, 246, 210] : [231, 236, 243, 155]; } -/** - * Equirect grid: ortho hub artwork on front body face + analytic lines on back wrap. - */ -function bakeGridEquator(gridOverlay, globe) { +function bakeOceanEquator(globe) { const out = new Uint8ClampedArray(EQ_W * EQ_H * 4); for (let j = 0; j < EQ_H; j++) { for (let i = 0; i < EQ_W; i++) { const lon = (i / EQ_W) * 2 * Math.PI - Math.PI; const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI; - const cosLat = Math.cos(lat); - const tex = toTextureBody({ - x: cosLat * Math.sin(lon), - y: Math.sin(lat), - z: cosLat * Math.cos(lon), - }); + const tex = sphereTexFromLonLat(lon, lat); const p = (j * EQ_W + i) * 4; - let rgba = [0, 0, 0, 0]; + let rgba = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; if (tex.z > FRONT_Z) { - const overlay = sampleOverlayRgba(gridOverlay, tex); - if (isGridPixel(overlay[0], overlay[1], overlay[2], overlay[3])) { - rgba = overlay; - } else { - const hp = hubPixelF(tex); - const sx = (hp.x / GLOBE_VIEW.imgW) * (globe.w - 1); - const sy = (hp.y / GLOBE_VIEW.imgH) * (globe.h - 1); - const base = sampleImageDataBilinear(globe.data, globe.w, globe.h, sx, sy); - if (isGridPixel(base[0], base[1], base[2], base[3])) { - rgba = base; - } + const s = sampleHub(globe, tex); + if (!isGridPixel(s[0], s[1], s[2], s[3]) && s[3] > 8) { + rgba = [s[0], s[1], s[2], 255]; } - } else { - rgba = analyticGridRgba(lon, lat); } out[p] = rgba[0]; @@ -215,69 +207,68 @@ function bakeGridEquator(gridOverlay, globe) { return { data: out, w: EQ_W, h: EQ_H }; } -function sampleOceanRgba(globe, tex) { - const hp = hubPixelF(tex); - const sx = (hp.x / GLOBE_VIEW.imgW) * (globe.w - 1); - const sy = (hp.y / GLOBE_VIEW.imgH) * (globe.h - 1); - const ocean = sampleImageDataBilinear(globe.data, globe.w, globe.h, sx, sy); - if (ocean[3] < 8) { - return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; - } - return ocean; -} +function bakeGridEquator(globe, gridOverlay) { + const out = new Uint8ClampedArray(EQ_W * EQ_H * 4); + for (let j = 0; j < EQ_H; j++) { + for (let i = 0; i < EQ_W; i++) { + const lon = (i / EQ_W) * 2 * Math.PI - Math.PI; + const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI; + const tex = sphereTexFromLonLat(lon, lat); + const p = (j * EQ_W + i) * 4; + let rgba = analyticGridRgba(lon, lat); -function sampleOverlayRgba(layer, tex) { - const hp = hubPixelF(tex); - const sx = (hp.x / GLOBE_VIEW.imgW) * (layer.w - 1); - const sy = (hp.y / GLOBE_VIEW.imgH) * (layer.h - 1); - return sampleImageDataBilinear(layer.data, layer.w, layer.h, sx, sy); -} + if (tex.z > FRONT_Z) { + const overlay = sampleHub(gridOverlay, tex); + if (isGridPixel(overlay[0], overlay[1], overlay[2], overlay[3])) { + rgba = overlay; + } else { + const base = sampleHub(globe, tex); + if (isGridPixel(base[0], base[1], base[2], base[3])) { + rgba = base; + } + } + } -function sampleLandRgba(continents, mercFlat, body) { - const tex = toTextureBody(body); - if (tex.z > FRONT_Z) { - const hp = hubPixelF(tex); - const sx = (hp.x / GLOBE_VIEW.imgW) * (continents.w - 1); - const sy = (hp.y / GLOBE_VIEW.imgH) * (continents.h - 1); - const ortho = sampleImageDataBilinear(continents.data, continents.w, continents.h, sx, sy); - if (ortho[3] > 6) { - return ortho; + out[p] = rgba[0]; + out[p + 1] = rgba[1]; + out[p + 2] = rgba[2]; + out[p + 3] = rgba[3]; } } - - const ll = bodyLonLat(tex); - const uv = mercatorFlatUV(ll.lon, ll.lat, mercFlat.w, mercFlat.h); - const flat = sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, uv.x, uv.y); - if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) { - return [flat[0], flat[1], flat[2], Math.min(255, flat[3])]; - } - - return null; + return { data: out, w: EQ_W, h: EQ_H }; } -function sampleGridRgba(body, gridEq, gridOverlay, globe) { - const tex = toTextureBody(body); - if (tex.z > FRONT_Z) { - const overlay = sampleOverlayRgba(gridOverlay, tex); - if (overlay[3] >= 10) { - return overlay; - } - const hp = hubPixelF(tex); - const sx = (hp.x / GLOBE_VIEW.imgW) * (globe.w - 1); - const sy = (hp.y / GLOBE_VIEW.imgH) * (globe.h - 1); - const base = sampleImageDataBilinear(globe.data, globe.w, globe.h, sx, sy); - if (isGridPixel(base[0], base[1], base[2], base[3])) { - return base; - } - return [0, 0, 0, 0]; - } +function bakeLandEquator(continents, mercFlat) { + const out = new Uint8ClampedArray(EQ_W * EQ_H * 4); + for (let j = 0; j < EQ_H; j++) { + for (let i = 0; i < EQ_W; i++) { + const lon = (i / EQ_W) * 2 * Math.PI - Math.PI; + const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI; + const tex = sphereTexFromLonLat(lon, lat); + const p = (j * EQ_W + i) * 4; + let rgba = [0, 0, 0, 0]; - const ll = bodyLonLat(tex); - const eq = sampleEquator(gridEq, ll.lon, ll.lat); - if (eq[3] < 8) { - return [0, 0, 0, 0]; + if (tex.z > FRONT_Z) { + const ortho = sampleHub(continents, tex); + if (ortho[3] > 6) { + rgba = [ortho[0], ortho[1], ortho[2], ortho[3]]; + } + } + + if (rgba[3] < 12) { + const flat = sampleMercFlat(mercFlat, lon, lat); + if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) { + rgba = [flat[0], flat[1], flat[2], Math.min(255, flat[3])]; + } + } + + out[p] = rgba[0]; + out[p + 1] = rgba[1]; + out[p + 2] = rgba[2]; + out[p + 3] = rgba[3]; + } } - return eq; + return { data: out, w: EQ_W, h: EQ_H }; } function fillDiskRegion(ctx, cx, cy, r, rotY, fn) { @@ -330,35 +321,34 @@ function blitLayer(ctx, cx, cy, r, layer) { ctx.restore(); } -function drawOceanLayer(ctx, cx, cy, r, rotY, globe) { +function drawOceanLayer(ctx, cx, cy, r, rotY, oceanEq) { const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) { - const tex = toTextureBody(body); - if (tex.z > FRONT_Z) { - return sampleOceanRgba(globe, tex); - } - return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; + const ll = bodyLonLat(body); + return sampleEq(oceanEq, ll.lon, ll.lat); }); blitLayer(ctx, cx, cy, r, layer); } -function drawGridLayer(ctx, cx, cy, r, rotY, gridEq, gridOverlay, globe) { +function drawGridLayer(ctx, cx, cy, r, rotY, gridEq) { const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) { - const px = sampleGridRgba(body, gridEq, gridOverlay, globe); - if (px[3] < 8) { + const ll = bodyLonLat(body); + const g = sampleEq(gridEq, ll.lon, ll.lat); + if (g[3] < 8) { return [0, 0, 0, 0]; } - return px; + return g; }); blitLayer(ctx, cx, cy, r, layer); } -function drawLandLayer(ctx, cx, cy, r, rotY, continents, mercFlat) { +function drawLandLayer(ctx, cx, cy, r, rotY, landEq) { const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) { - const rgba = sampleLandRgba(continents, mercFlat, body); - if (!rgba) { + const ll = bodyLonLat(body); + const l = sampleEq(landEq, ll.lon, ll.lat); + if (l[3] < 8) { return [0, 0, 0, 0]; } - return rgba; + return l; }); blitLayer(ctx, cx, cy, r, layer); } @@ -417,7 +407,10 @@ export async function mountHubGlobe(stage, opts) { const mercW = (mercatorFlatImg.naturalWidth || 800) * 2; const mercH = (mercatorFlatImg.naturalHeight || 519) * 2; const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1); - const gridEq = bakeGridEquator(gridOverlay, globe); + + const oceanEq = bakeOceanEquator(globe); + const gridEq = bakeGridEquator(globe, gridOverlay); + const landEq = bakeLandEquator(continents, mercFlat); let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0; let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S; @@ -455,9 +448,9 @@ export async function mountHubGlobe(stage, opts) { const rim = rect.rim * dpr; ctx.clearRect(0, 0, px, px); - drawOceanLayer(ctx, cx, cy, r, rotation, globe); - drawGridLayer(ctx, cx, cy, r, rotation, gridEq, gridOverlay, globe); - drawLandLayer(ctx, cx, cy, r, rotation, continents, mercFlat); + drawOceanLayer(ctx, cx, cy, r, rotation, oceanEq); + drawGridLayer(ctx, cx, cy, r, rotation, gridEq); + drawLandLayer(ctx, cx, cy, r, rotation, landEq); drawRim(ctx, globeImg, cx, cy, r, rim, rect.scale * dpr); ctx.save(); ctx.beginPath(); diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php index 3550ce6..de953f6 100644 --- a/examples/app_hub/index.php +++ b/examples/app_hub/index.php @@ -180,7 +180,7 @@ unset($__platformDir); var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover')); var logoEnabled = !!(stage && supportsSvg && supportsLayout); - var logoBuild = '20260606globe7'; + var logoBuild = '20260606globe8'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');