diff --git a/examples/app_hub/assets/hub-globe.js b/examples/app_hub/assets/hub-globe.js index af9bbd6..9022152 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). - * Phase 1: E–W parallels only (SVG hub ellipses → body → Y-rotation → canvas stroke). - * Phase 2 (after confirm): N–S meridians from SVG. + * 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). */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; @@ -12,13 +12,42 @@ const FRONT_Z = 0.04; const RASTER_SCALE = 2; const AXIS_STEPS = 160; -/** Same ellipses as hub-logo-fragment-globe-grid-overlay.svg (E–W / parallel to equator). */ +/** hub-logo-fragment-globe-grid-overlay.svg — E–W parallels. */ const HUB_EW_PARALLELS = [ - { cx: 720, cy: 560, rx: 318, ry: 11, width: 7.5, bright: true }, - { cx: 720, cy: 392, rx: 260, ry: 39, width: 7.5, bright: false }, - { cx: 720, cy: 728, rx: 260, ry: 39, width: 7.5, bright: false }, + { cx: 720, cy: 560, rx: 318, ry: 11, bright: true }, + { cx: 720, cy: 392, rx: 260, ry: 39, bright: false }, + { cx: 720, cy: 728, rx: 260, ry: 39, bright: false }, ]; +/** Same SVG — N–S meridians (line + quadratic beziers). */ +const HUB_NS_MERIDIANS = [ + { prime: true, points: sampleLinePoints(720, 248, 720, 872, AXIS_STEPS) }, + { prime: false, points: sampleQuadPoints(620, 248, 358, 560, 620, 872, AXIS_STEPS) }, + { prime: false, points: sampleQuadPoints(820, 248, 1082, 560, 820, 872, AXIS_STEPS) }, +]; + +function sampleLinePoints(x0, y0, x1, y1, steps) { + const pts = []; + for (let i = 0; i <= steps; i++) { + const t = i / steps; + pts.push({ x: x0 + (x1 - x0) * t, y: y0 + (y1 - y0) * t }); + } + return pts; +} + +function sampleQuadPoints(x0, y0, cx, cy, x1, y1, steps) { + const pts = []; + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const u = 1 - t; + pts.push({ + x: u * u * x0 + 2 * u * t * cx + t * t * x1, + y: u * u * y0 + 2 * u * t * cy + t * t * y1, + }); + } + return pts; +} + function hubAsset(rel, build) { const link = document.querySelector('link[href*="hub.css"]'); const base = link ? (link.getAttribute('href') || '').replace(/[^/]*$/, '') : ''; @@ -137,6 +166,17 @@ function hubPixelToBody(hpX, hpY) { }; } +function hubPointsToBodyRing(points) { + const out = []; + for (let i = 0; i < points.length; i++) { + const body = hubPixelToBody(points[i].x, points[i].y); + if (body) { + out.push(body); + } + } + return out; +} + function ellipseBodyRing(ellipse, steps) { const out = []; for (let i = 0; i <= steps; i++) { @@ -165,6 +205,18 @@ 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))); + if (t <= 0) { + return null; + } + const alpha = prime ? 0.5 + 0.48 * t : 0.3 + 0.45 * t; + return { + color: 'rgba(238,245,255,' + alpha.toFixed(3) + ')', + width: Math.max(prime ? 3 : 2.5, scale * (prime ? 0.0095 : 0.008)) * (0.45 + 0.55 * t), + }; +} + function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) { let run = []; @@ -210,20 +262,25 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) { flush(); } -/** Phase 1 — E–W hub parallels (equator + pole ellipses from SVG). */ -function drawEwParallels(ctx, cx, cy, r, rotY, layoutScale) { +/** 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(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.clip(); HUB_EW_PARALLELS.forEach(function (ellipse) { - const ring = ellipseBodyRing(ellipse, AXIS_STEPS); - strokeBodyRing(ctx, cx, cy, r, rotY, ring, function (z) { + strokeBodyRing(ctx, cx, cy, r, rotY, ellipseBodyRing(ellipse, AXIS_STEPS), function (z) { return parallelStyle(ellipse.bright, layoutScale, z); }); }); + 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(); } @@ -254,12 +311,27 @@ function isLandPixel(r, g, b, a) { return a > 10 && r > 100 && g < 140 && b < 140; } +/** Light hub grid strokes baked into hub-logo-fragment-globe.svg (vector grid drawn separately). */ +function isGridPixel(r, g, b, a) { + if (a < 12) { + return false; + } + const lum = r * 0.299 + g * 0.587 + b * 0.114; + if (lum < 125) { + return false; + } + if (g < 155 || b < 155) { + return false; + } + return Math.max(r, g, b) - Math.min(r, g, b) < 85; +} + function sampleOceanRgba(globe, tex) { if (tex.z <= FRONT_Z) { return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; } const s = sampleHub(globe, tex); - if (s[3] > 8) { + 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]; @@ -271,6 +343,10 @@ function sampleLandRgba(continents, mercFlat, tex) { if (ortho[3] > 6) { return ortho; } + return null; + } + if (tex.z > -FRONT_Z) { + return null; } const ll = texLonLat(tex); const flat = sampleMercFlat(mercFlat, ll.lon, ll.lat); @@ -467,7 +543,7 @@ export async function mountHubGlobe(stage, opts) { return l || [0, 0, 0, 0]; }) ); - drawEwParallels(ctx, cx, cy, r, rotation, layoutScale); + drawHubGrid(ctx, cx, cy, r, rotation, layoutScale); drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale); ctx.save(); ctx.beginPath(); @@ -537,7 +613,7 @@ export async function mountHubGlobe(stage, opts) { raf = requestAnimationFrame(tick); stage.dataset.globe3d = 'active'; - stage.dataset.globeAxis = 'ew-parallels'; + stage.dataset.globeAxis = 'ew-ns-grid'; return { destroy() { diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php index 3aa2490..6fabeac 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 = '20260606globe12'; + var logoBuild = '20260606globe14'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');