From a13485d678f320ac069b488c928234276d4be46e Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sat, 6 Jun 2026 19:22:28 +0200 Subject: [PATCH] sync 4 --- examples/app_hub/assets/hub-globe.js | 268 ++++++++++++++++----------- examples/app_hub/index.php | 2 +- 2 files changed, 163 insertions(+), 107 deletions(-) diff --git a/examples/app_hub/assets/hub-globe.js b/examples/app_hub/assets/hub-globe.js index 53e8154..fe39d2f 100644 --- a/examples/app_hub/assets/hub-globe.js +++ b/examples/app_hub/assets/hub-globe.js @@ -1,18 +1,23 @@ /** * Hub landing globe — orthographic Canvas2D (globe3d=1). - * 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). + * Phase 1: E–W parallels only (SVG hub ellipses → body → Y-rotation → canvas stroke). + * Phase 2 (after confirm): N–S meridians from SVG. */ export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 }; 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.02; +const FRONT_Z = 0.04; const RASTER_SCALE = 2; -const GRID_PARALLELS = [0, 0.521, -0.521]; -const GRID_MERIDIAN_SPACING = Math.PI / 6; -const GRID_LINE_RAD = 0.018; +const AXIS_STEPS = 160; + +/** Same ellipses as hub-logo-fragment-globe-grid-overlay.svg (E–W / parallel to equator). */ +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 }, +]; function hubAsset(rel, build) { const link = document.querySelector('link[href*="hub.css"]'); @@ -103,18 +108,125 @@ function inverseRotateY(nx, ny, nz, rotY) { }; } -/** 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 texLonLat(tex) { +function bodyToView(body, rotY) { + const cosR = Math.cos(rotY); + const sinR = Math.sin(rotY); return { - lon: Math.atan2(tex.x, tex.z), - lat: Math.asin(Math.max(-1, Math.min(1, tex.y))), + x: body.x * cosR + body.z * sinR, + y: body.y, + z: -body.x * sinR + body.z * cosR, }; } +/** Hub disk pixel → unit-sphere body (front hemisphere, matches globe3d=0). */ +function hubPixelToBody(hpX, hpY) { + const texX = (hpX - GLOBE_VIEW.cx) / GLOBE_VIEW.r; + const texY = (GLOBE_VIEW.cy - hpY) / GLOBE_VIEW.r; + const rr = texX * texX + texY * texY; + if (rr > 1.001) { + return null; + } + return { + x: texX, + y: -texY, + z: Math.sqrt(Math.max(0, 1 - rr)), + }; +} + +function ellipseBodyRing(ellipse, steps) { + const out = []; + for (let i = 0; i <= steps; i++) { + const a = (i / steps) * Math.PI * 2; + const body = hubPixelToBody( + ellipse.cx + ellipse.rx * Math.cos(a), + ellipse.cy + ellipse.ry * Math.sin(a) + ); + if (body) { + out.push(body); + } + } + return out; +} + +function parallelStyle(bright, scale, z) { + const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z))); + if (t <= 0) { + return null; + } + const alpha = bright ? 0.35 + 0.55 * t : 0.15 + 0.45 * t; + const rgb = bright ? '219,231,246' : '231,236,243'; + return { + color: 'rgba(' + rgb + ',' + alpha.toFixed(3) + ')', + width: Math.max(bright ? 2.5 : 1.5, scale * (bright ? 0.007 : 0.0055)) * (0.4 + 0.6 * t), + }; +} + +function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) { + let run = []; + + function flush() { + if (run.length < 2) { + run = []; + return; + } + let zSum = 0; + for (let i = 0; i < run.length; i++) { + zSum += run[i].z; + } + const style = styleAt(zSum / run.length); + if (!style) { + run = []; + return; + } + ctx.beginPath(); + ctx.moveTo(run[0].sx, run[0].sy); + for (let i = 1; i < run.length; i++) { + ctx.lineTo(run[i].sx, run[i].sy); + } + ctx.strokeStyle = style.color; + ctx.lineWidth = style.width; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.stroke(); + run = []; + } + + for (let i = 0; i < bodies.length; i++) { + const view = bodyToView(bodies[i], rotY); + if (view.z <= FRONT_Z) { + flush(); + continue; + } + run.push({ + sx: cx + view.x * r, + sy: cy - view.y * r, + z: view.z, + }); + } + flush(); +} + +/** Phase 1 — E–W hub parallels (equator + pole ellipses from SVG). */ +function drawEwParallels(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) { + return parallelStyle(ellipse.bright, layoutScale, z); + }); + }); + + ctx.restore(); +} + function sampleHub(layer, tex) { const hp = { x: GLOBE_VIEW.cx + tex.x * GLOBE_VIEW.r, @@ -125,6 +237,13 @@ function sampleHub(layer, tex) { return sampleImageDataBilinear(layer.data, layer.w, layer.h, sx, sy); } +function texLonLat(tex) { + return { + lon: Math.atan2(tex.x, tex.z), + lat: Math.asin(Math.max(-1, Math.min(1, tex.y))), + }; +} + function sampleMercFlat(mercFlat, lon, lat) { const u = ((lon + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1); const v = (0.5 - lat / Math.PI) * (mercFlat.h - 1); @@ -135,62 +254,17 @@ 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 > 240; -} - -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); - const onMeridian = merDist < GRID_LINE_RAD; - let onParallel = false; - let bright = false; - for (let i = 0; i < GRID_PARALLELS.length; i++) { - const targetLat = Math.asin(Math.max(-1, Math.min(1, GRID_PARALLELS[i]))); - if (Math.abs(lat - targetLat) < GRID_LINE_RAD) { - onParallel = true; - bright = i === 0; - } - } - if (!onMeridian && !onParallel) { - return [0, 0, 0, 0]; - } - if (onMeridian) { - 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, 200] : [231, 236, 243, 140]; -} - 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 && !isGridPixel(s[0], s[1], s[2], s[3])) { + if (s[3] > 8) { return [s[0], s[1], s[2], 255]; } return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; } -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]; - } - const ll = texLonLat(tex); - const g = wrapGridRgba(ll.lon, ll.lat); - return g[3] > 0 ? g : [0, 0, 0, 0]; -} - function sampleLandRgba(continents, mercFlat, tex) { if (tex.z > FRONT_Z) { const ortho = sampleHub(continents, tex); @@ -214,7 +288,9 @@ function fillDiskRegion(ctx, cx, cy, r, rotY, fn) { const y1 = Math.min(ctx.canvas.height - 1, Math.ceil(cy + r)); const w = x1 - x0 + 1; const h = y1 - y0 + 1; - if (w <= 0 || h <= 0) return null; + if (w <= 0 || h <= 0) { + return null; + } const out = ctx.createImageData(w, h); const px = out.data; @@ -247,7 +323,9 @@ function fillDiskRegion(ctx, cx, cy, r, rotY, fn) { } function blitLayer(ctx, cx, cy, r, layer) { - if (!layer) return; + if (!layer) { + return; + } ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); @@ -256,44 +334,6 @@ function blitLayer(ctx, cx, cy, r, layer) { ctx.restore(); } -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, 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, 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) { const drawW = GLOBE_VIEW.imgW * layoutScale; const drawH = GLOBE_VIEW.imgH * layoutScale; @@ -321,19 +361,16 @@ export async function mountHubGlobe(stage, opts) { const urls = { globe: hubAsset('assets/hub-logo-fragment-globe.svg', build), continents: hubAsset('assets/hub-logo-continents-mercator-globe.svg', build), - gridOverlay: hubAsset('assets/hub-logo-fragment-globe-grid-overlay.svg', build), mercatorFlat: hubAsset('assets/hub-logo-continents-mercator-flat.svg', build), }; let globeImg; let continentsImg; - let gridOverlayImg; let mercatorFlatImg; try { - [globeImg, continentsImg, gridOverlayImg, mercatorFlatImg] = await Promise.all([ + [globeImg, continentsImg, mercatorFlatImg] = await Promise.all([ loadImage(urls.globe), loadImage(urls.continents), - loadImage(urls.gridOverlay), loadImage(urls.mercatorFlat), ]); } catch (e) { @@ -344,7 +381,6 @@ export async function mountHubGlobe(stage, opts) { const globe = rasterizeImage(globeImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, RASTER_SCALE); const continents = rasterizeImage(continentsImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, RASTER_SCALE); - const gridOverlay = rasterizeImage(gridOverlayImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, RASTER_SCALE); const mercW = (mercatorFlatImg.naturalWidth || 800) * 2; const mercH = (mercatorFlatImg.naturalHeight || 519) * 2; const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1); @@ -383,17 +419,35 @@ export async function mountHubGlobe(stage, opts) { const cy = (rect.cy - rect.top) * dpr; const r = rect.r * dpr; const rim = rect.rim * dpr; + const layoutScale = rect.scale * dpr; ctx.clearRect(0, 0, px, px); - 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); + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { + return sampleOceanRgba(globe, tex); + }) + ); + drawEwParallels(ctx, cx, cy, r, rotation, layoutScale); + blitLayer( + ctx, + cx, + cy, + r, + fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) { + const l = sampleLandRgba(continents, mercFlat, tex); + return l || [0, 0, 0, 0]; + }) + ); + drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale); ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(10,14,20,0.55)'; - ctx.lineWidth = Math.max(2, rect.scale * dpr * 0.006); + ctx.lineWidth = Math.max(2, layoutScale * 0.006); ctx.stroke(); ctx.restore(); }; @@ -457,6 +511,7 @@ export async function mountHubGlobe(stage, opts) { raf = requestAnimationFrame(tick); stage.dataset.globe3d = 'active'; + stage.dataset.globeAxis = 'ew-parallels'; return { destroy() { @@ -464,6 +519,7 @@ export async function mountHubGlobe(stage, opts) { window.removeEventListener('resize', resize); canvas.remove(); delete stage.dataset.globe3d; + delete stage.dataset.globeAxis; }, }; } diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php index 5a8d69c..7d39342 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 = '20260606globe10'; + var logoBuild = '20260606globe11'; function hubAsset(rel) { var link = document.querySelector('link[href*="hub.css"]');