diff --git a/examples/app_hub/assets/hub-globe.js b/examples/app_hub/assets/hub-globe.js index 27b0563..53e8154 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). - * Bakes globe3d=0 stack to equirect maps once; runtime always samples by lon/lat - * after inverse Y-rotation (rotation-invariant, full sphere wrap). + * Matches globe3d=0: sample hub SVG disks in body-fixed ortho coords (N-up via bodyToTex). + * Mercator-flat lon/lat fill only where the ortho disk has no paint (rotation wrap). */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -10,11 +10,9 @@ 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 = 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.022; +const GRID_LINE_RAD = 0.018; function hubAsset(rel, build) { const link = document.querySelector('link[href*="hub.css"]'); @@ -105,40 +103,18 @@ function inverseRotateY(nx, ny, nz, rotY) { }; } -/** Hub texture coords: N-up artwork on orthographic disk (matches globe3d=0). */ -function texFromLonLat(lon, lat) { - const cosLat = Math.cos(lat); - return { - x: cosLat * Math.sin(lon), - y: Math.sin(lat), - z: cosLat * Math.cos(lon), - }; -} - +/** Map view body → hub disk coords (globe3d=0, N-up artwork). */ function bodyToTex(body) { return { x: body.x, y: -body.y, z: body.z }; } -function bodyLonLat(body) { - const tex = bodyToTex(body); +function texLonLat(tex) { return { lon: Math.atan2(tex.x, tex.z), lat: Math.asin(Math.max(-1, Math.min(1, tex.y))), }; } -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, @@ -150,8 +126,9 @@ function sampleHub(layer, tex) { } 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); + const u = ((lon + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1); + const v = (0.5 - lat / Math.PI) * (mercFlat.h - 1); + return sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, u, v); } function isLandPixel(r, g, b, a) { @@ -159,10 +136,10 @@ function isLandPixel(r, g, b, a) { } function isGridPixel(r, g, b, a) { - return a > 8 && r + g + b > 260; + return a > 8 && r + g + b > 240; } -function analyticGridRgba(lon, lat) { +function wrapGridRgba(lon, lat) { const wrapped = ((lon + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI); const cell = wrapped % GRID_MERIDIAN_SPACING; const merDist = Math.min(cell, GRID_MERIDIAN_SPACING - cell); @@ -180,100 +157,53 @@ function analyticGridRgba(lon, lat) { return [0, 0, 0, 0]; } if (onMeridian) { - 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]; + const prime = Math.abs(lon) < GRID_LINE_RAD * 1.5; + return prime ? [238, 245, 255, 230] : [238, 245, 255, 175]; } - return bright ? [219, 231, 246, 210] : [231, 236, 243, 155]; + return bright ? [219, 231, 246, 200] : [231, 236, 243, 140]; } -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 tex = texFromLonLat(lon, lat); - const p = (j * EQ_W + i) * 4; - let rgba = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; - - if (tex.z > FRONT_Z) { - 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]; - } - } - - out[p] = rgba[0]; - out[p + 1] = rgba[1]; - out[p + 2] = rgba[2]; - out[p + 3] = rgba[3]; - } +function sampleOceanRgba(globe, tex) { + if (tex.z <= FRONT_Z) { + return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; } - return { data: out, w: EQ_W, h: EQ_H }; + 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]; } -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 = texFromLonLat(lon, lat); - const p = (j * EQ_W + i) * 4; - let rgba = analyticGridRgba(lon, lat); - - 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; - } - } - } - - out[p] = rgba[0]; - out[p + 1] = rgba[1]; - out[p + 2] = rgba[2]; - out[p + 3] = rgba[3]; +function sampleGridRgba(globe, gridOverlay, tex) { + if (tex.z > FRONT_Z) { + const overlay = sampleHub(gridOverlay, tex); + if (overlay[3] >= 12) { + return overlay; } + const base = sampleHub(globe, tex); + if (isGridPixel(base[0], base[1], base[2], base[3])) { + return base; + } + return [0, 0, 0, 0]; } - return { data: out, w: EQ_W, h: EQ_H }; + const ll = texLonLat(tex); + const g = wrapGridRgba(ll.lon, ll.lat); + return g[3] > 0 ? g : [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 = texFromLonLat(lon, lat); - const p = (j * EQ_W + i) * 4; - let rgba = [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]; +function sampleLandRgba(continents, mercFlat, tex) { + if (tex.z > FRONT_Z) { + const ortho = sampleHub(continents, tex); + if (ortho[3] > 6) { + return ortho; } } - return { data: out, w: EQ_W, h: EQ_H }; + 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) { @@ -304,8 +234,8 @@ function fillDiskRegion(ctx, cx, cy, r, rotY, fn) { const nx = ox / r; const ny = oy / r; const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny)); - const body = inverseRotateY(nx, ny, nz, rotY); - const rgba = fn(body); + const tex = bodyToTex(inverseRotateY(nx, ny, nz, rotY)); + const rgba = fn(tex); px[p++] = rgba[0]; px[p++] = rgba[1]; px[p++] = rgba[2]; @@ -326,36 +256,42 @@ function blitLayer(ctx, cx, cy, r, layer) { ctx.restore(); } -function drawOceanLayer(ctx, cx, cy, r, rotY, oceanEq) { - const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) { - const ll = bodyLonLat(body); - return sampleEq(oceanEq, ll.lon, ll.lat); - }); - blitLayer(ctx, cx, cy, r, layer); +function drawOceanLayer(ctx, cx, cy, r, rotY, globe) { + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotY, function (tex) { + return sampleOceanRgba(globe, tex); + }) + ); } -function drawGridLayer(ctx, cx, cy, r, rotY, gridEq) { - const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) { - const ll = bodyLonLat(body); - const g = sampleEq(gridEq, ll.lon, ll.lat); - if (g[3] < 8) { - return [0, 0, 0, 0]; - } - return g; - }); - blitLayer(ctx, cx, cy, r, layer); +function drawGridLayer(ctx, cx, cy, r, rotY, globe, gridOverlay) { + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotY, function (tex) { + const g = sampleGridRgba(globe, gridOverlay, tex); + return g[3] < 8 ? [0, 0, 0, 0] : g; + }) + ); } -function drawLandLayer(ctx, cx, cy, r, rotY, landEq) { - const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) { - const ll = bodyLonLat(body); - const l = sampleEq(landEq, ll.lon, ll.lat); - if (l[3] < 8) { - return [0, 0, 0, 0]; - } - return l; - }); - blitLayer(ctx, cx, cy, r, layer); +function drawLandLayer(ctx, cx, cy, r, rotY, continents, mercFlat) { + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotY, function (tex) { + const l = sampleLandRgba(continents, mercFlat, tex); + return l || [0, 0, 0, 0]; + }) + ); } function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) { @@ -413,10 +349,6 @@ export async function mountHubGlobe(stage, opts) { const mercH = (mercatorFlatImg.naturalHeight || 519) * 2; const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1); - 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; let dragging = false; @@ -453,9 +385,9 @@ export async function mountHubGlobe(stage, opts) { const rim = rect.rim * dpr; ctx.clearRect(0, 0, px, px); - drawOceanLayer(ctx, cx, cy, r, rotation, oceanEq); - drawGridLayer(ctx, cx, cy, r, rotation, gridEq); - drawLandLayer(ctx, cx, cy, r, rotation, landEq); + drawOceanLayer(ctx, cx, cy, r, rotation, globe); + drawGridLayer(ctx, cx, cy, r, rotation, globe, gridOverlay); + drawLandLayer(ctx, cx, cy, r, rotation, continents, mercFlat); 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 586bd26..5a8d69c 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 = '20260606globe9'; + var logoBuild = '20260606globe10'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');