mirror of
git://f0xx.org/ac/ac-be-hub
synced 2026-08-09 20:59:23 +03:00
Hub globe3d=2: mercator hemisphere wrap + sphere grid rings.
globe3d=1 keeps legacy hybrid ortho/mercator; globe3d=2 samples hub-logo-continents-mercator-flat on visible z>0 and draws full parallel/meridian rings so axes and continents survive rotation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Hub landing globe — orthographic Canvas2D (globe3d=1).
|
* Hub landing globe — orthographic Canvas2D (?globe3d=N).
|
||||||
* Grid: hub SVG ellipses (E–W) + meridian paths (N–S) traced on sphere, then Y-rotated.
|
* globe3d=0: flat SVG layers (index.php).
|
||||||
* Land: ortho hub disk on front face; mercator-flat only on back wrap (avoids front squeeze).
|
* globe3d=1: legacy hybrid ortho front + mercator back.
|
||||||
|
* globe3d=2+: mercator wrap on visible hemisphere + full-sphere grid rings.
|
||||||
*/
|
*/
|
||||||
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
|
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
|
||||||
|
|
||||||
@@ -9,8 +10,24 @@ const AUTO_SPIN_RAD_S = 0.035;
|
|||||||
const DRAG_ROTATION_PER_PX = 0.004;
|
const DRAG_ROTATION_PER_PX = 0.004;
|
||||||
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
|
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
|
||||||
const FRONT_Z = 0.04;
|
const FRONT_Z = 0.04;
|
||||||
|
const LIMB_Z = 0.002;
|
||||||
const RASTER_SCALE = 2;
|
const RASTER_SCALE = 2;
|
||||||
const AXIS_STEPS = 160;
|
const AXIS_STEPS = 160;
|
||||||
|
const SPHERE_AXIS_STEPS = 180;
|
||||||
|
|
||||||
|
/** Latitudes derived from hub-logo-fragment-globe-grid-overlay.svg E–W ellipses. */
|
||||||
|
const SPHERE_PARALLELS = [
|
||||||
|
{ lat: 0, bright: true },
|
||||||
|
{ lat: Math.asin((560 - 392) / 322), bright: false },
|
||||||
|
{ lat: Math.asin((560 - 728) / 322), bright: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Longitudes at equator from hub N–S meridian artwork (x=620 / x=820). */
|
||||||
|
const SPHERE_MERIDIANS = [
|
||||||
|
{ lon: 0, prime: true },
|
||||||
|
{ lon: Math.atan2((620 - 720) / 322, Math.sqrt(Math.max(0, 1 - Math.pow((620 - 720) / 322, 2)))), prime: false },
|
||||||
|
{ lon: Math.atan2((820 - 720) / 322, Math.sqrt(Math.max(0, 1 - Math.pow((820 - 720) / 322, 2)))), prime: false },
|
||||||
|
];
|
||||||
|
|
||||||
/** hub-logo-fragment-globe-grid-overlay.svg — E–W parallels. */
|
/** hub-logo-fragment-globe-grid-overlay.svg — E–W parallels. */
|
||||||
const HUB_EW_PARALLELS = [
|
const HUB_EW_PARALLELS = [
|
||||||
@@ -192,8 +209,9 @@ function ellipseBodyRing(ellipse, steps) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parallelStyle(bright, scale, z) {
|
function parallelStyle(bright, scale, z, limbZ) {
|
||||||
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z)));
|
const zCut = limbZ != null ? limbZ : FRONT_Z;
|
||||||
|
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
|
||||||
if (t <= 0) {
|
if (t <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -205,8 +223,9 @@ function parallelStyle(bright, scale, z) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function meridianStyle(prime, scale, z) {
|
function meridianStyle(prime, scale, z, limbZ) {
|
||||||
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (0.95 - FRONT_Z)));
|
const zCut = limbZ != null ? limbZ : FRONT_Z;
|
||||||
|
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
|
||||||
if (t <= 0) {
|
if (t <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -217,8 +236,9 @@ function meridianStyle(prime, scale, z) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
|
function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt, limbZ) {
|
||||||
let run = [];
|
let run = [];
|
||||||
|
const zCut = limbZ != null ? limbZ : FRONT_Z;
|
||||||
|
|
||||||
function flush() {
|
function flush() {
|
||||||
if (run.length < 2) {
|
if (run.length < 2) {
|
||||||
@@ -249,7 +269,7 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
|
|||||||
|
|
||||||
for (let i = 0; i < bodies.length; i++) {
|
for (let i = 0; i < bodies.length; i++) {
|
||||||
const view = bodyToView(bodies[i], rotY);
|
const view = bodyToView(bodies[i], rotY);
|
||||||
if (view.z <= FRONT_Z) {
|
if (view.z <= zCut) {
|
||||||
flush();
|
flush();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -262,6 +282,75 @@ function strokeBodyRing(ctx, cx, cy, r, rotY, bodies, styleAt) {
|
|||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parallelBodyRingAtLat(lat, steps) {
|
||||||
|
const out = [];
|
||||||
|
const cosLat = Math.cos(lat);
|
||||||
|
const sinLat = Math.sin(lat);
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const lon = -Math.PI + (2 * Math.PI * i) / steps;
|
||||||
|
out.push({
|
||||||
|
x: cosLat * Math.sin(lon),
|
||||||
|
y: sinLat,
|
||||||
|
z: cosLat * Math.cos(lon),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function meridianBodyRingAtLon(lon, steps) {
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const lat = -Math.PI / 2 + (Math.PI * i) / steps;
|
||||||
|
const cosLat = Math.cos(lat);
|
||||||
|
out.push({
|
||||||
|
x: cosLat * Math.sin(lon),
|
||||||
|
y: Math.sin(lat),
|
||||||
|
z: cosLat * Math.cos(lon),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full-sphere grid rings (globe3d>=2) — visible segments only, no front-hemisphere SVG trace. */
|
||||||
|
function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
|
ctx.clip();
|
||||||
|
|
||||||
|
SPHERE_PARALLELS.forEach(function (par) {
|
||||||
|
strokeBodyRing(
|
||||||
|
ctx,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
r,
|
||||||
|
rotY,
|
||||||
|
parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS),
|
||||||
|
function (z) {
|
||||||
|
return parallelStyle(par.bright, layoutScale, z, LIMB_Z);
|
||||||
|
},
|
||||||
|
LIMB_Z
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SPHERE_MERIDIANS.forEach(function (mer) {
|
||||||
|
strokeBodyRing(
|
||||||
|
ctx,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
r,
|
||||||
|
rotY,
|
||||||
|
meridianBodyRingAtLon(mer.lon, SPHERE_AXIS_STEPS),
|
||||||
|
function (z) {
|
||||||
|
return meridianStyle(mer.prime, layoutScale, z, LIMB_Z);
|
||||||
|
},
|
||||||
|
LIMB_Z
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
/** Hub SVG grid: E–W ellipses + N–S meridian paths (matches globe3d=0 artwork geometry). */
|
/** Hub SVG grid: E–W ellipses + N–S meridian paths (matches globe3d=0 artwork geometry). */
|
||||||
function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) {
|
function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -337,7 +426,7 @@ function sampleOceanRgba(globe, tex) {
|
|||||||
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
|
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
|
||||||
}
|
}
|
||||||
|
|
||||||
function sampleLandRgba(continents, mercFlat, tex) {
|
function sampleLandRgbaLegacy(continents, mercFlat, tex) {
|
||||||
if (tex.z > FRONT_Z) {
|
if (tex.z > FRONT_Z) {
|
||||||
const ortho = sampleHub(continents, tex);
|
const ortho = sampleHub(continents, tex);
|
||||||
if (ortho[3] > 6) {
|
if (ortho[3] > 6) {
|
||||||
@@ -356,6 +445,19 @@ function sampleLandRgba(continents, mercFlat, tex) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** globe3d>=2: mercator-flat on entire visible hemisphere (z>0). */
|
||||||
|
function sampleLandRgbaMercator(mercFlat, tex) {
|
||||||
|
if (tex.z <= LIMB_Z) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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) {
|
function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
|
||||||
const r2 = r * r;
|
const r2 = r * r;
|
||||||
const x0 = Math.max(0, Math.floor(cx - r));
|
const x0 = Math.max(0, Math.floor(cx - r));
|
||||||
@@ -454,6 +556,7 @@ function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) {
|
|||||||
export async function mountHubGlobe(stage, opts) {
|
export async function mountHubGlobe(stage, opts) {
|
||||||
const build = opts.build || 'earth3d';
|
const build = opts.build || 'earth3d';
|
||||||
const reducedMotion = !!opts.reducedMotion;
|
const reducedMotion = !!opts.reducedMotion;
|
||||||
|
const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1);
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.className = 'hub-globe-canvas';
|
canvas.className = 'hub-globe-canvas';
|
||||||
@@ -539,11 +642,18 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
cy,
|
cy,
|
||||||
r,
|
r,
|
||||||
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
|
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
|
||||||
const l = sampleLandRgba(continents, mercFlat, tex);
|
const l =
|
||||||
|
renderVersion >= 2
|
||||||
|
? sampleLandRgbaMercator(mercFlat, tex)
|
||||||
|
: sampleLandRgbaLegacy(continents, mercFlat, tex);
|
||||||
return l || [0, 0, 0, 0];
|
return l || [0, 0, 0, 0];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (renderVersion >= 2) {
|
||||||
|
drawSphereGrid(ctx, cx, cy, r, rotation, layoutScale);
|
||||||
|
} else {
|
||||||
drawHubGrid(ctx, cx, cy, r, rotation, layoutScale);
|
drawHubGrid(ctx, cx, cy, r, rotation, layoutScale);
|
||||||
|
}
|
||||||
drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale);
|
drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -612,8 +722,8 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
raf = requestAnimationFrame(tick);
|
raf = requestAnimationFrame(tick);
|
||||||
stage.dataset.globe3d = 'active';
|
stage.dataset.globe3d = String(renderVersion);
|
||||||
stage.dataset.globeAxis = 'ew-ns-grid';
|
stage.dataset.globeAxis = renderVersion >= 2 ? 'ew-ns-sphere' : 'ew-ns-grid';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */
|
/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d>=1 */
|
||||||
.hub-globe-canvas {
|
.hub-globe-canvas {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
12
index.php
12
index.php
@@ -187,7 +187,7 @@ function hub_h(string $s): string {
|
|||||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||||
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
||||||
|
|
||||||
var logoBuild = '20260606globe14';
|
var logoBuild = '20260704globe2';
|
||||||
|
|
||||||
function hubAsset(rel) {
|
function hubAsset(rel) {
|
||||||
var link = document.querySelector('link[href*="hub.css"]');
|
var link = document.querySelector('link[href*="hub.css"]');
|
||||||
@@ -253,7 +253,10 @@ function hub_h(string $s): string {
|
|||||||
return base + '?v=' + logoBuild;
|
return base + '?v=' + logoBuild;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useGlobe3d = logoEnabled && (params.get('globe3d') || '1') !== '0';
|
var globe3dRaw = params.get('globe3d');
|
||||||
|
var globe3dVersion = globe3dRaw === null || globe3dRaw === '' ? 1 : parseInt(globe3dRaw, 10);
|
||||||
|
if (!isFinite(globe3dVersion)) globe3dVersion = 1;
|
||||||
|
var useGlobe3d = logoEnabled && globe3dVersion !== 0;
|
||||||
var globe3dLayers = { globe: true, continents: true, gridOverlay: true };
|
var globe3dLayers = { globe: true, continents: true, gridOverlay: true };
|
||||||
|
|
||||||
function appendStaticLayers() {
|
function appendStaticLayers() {
|
||||||
@@ -291,7 +294,8 @@ function hub_h(string $s): string {
|
|||||||
var globeYawDeg = parseFloat(params.get('globeYawDeg'));
|
var globeYawDeg = parseFloat(params.get('globeYawDeg'));
|
||||||
var globeOpts = {
|
var globeOpts = {
|
||||||
build: logoBuild,
|
build: logoBuild,
|
||||||
reducedMotion: reducedMotion
|
reducedMotion: reducedMotion,
|
||||||
|
renderVersion: globe3dVersion
|
||||||
};
|
};
|
||||||
if (isFinite(globeYawDeg)) {
|
if (isFinite(globeYawDeg)) {
|
||||||
globeOpts.initialRotationY = globeYawDeg * Math.PI / 180;
|
globeOpts.initialRotationY = globeYawDeg * Math.PI / 180;
|
||||||
@@ -302,7 +306,7 @@ function hub_h(string $s): string {
|
|||||||
return mod.mountHubGlobe(stage, globeOpts);
|
return mod.mountHubGlobe(stage, globeOpts);
|
||||||
}).then(function (handle) {
|
}).then(function (handle) {
|
||||||
if (handle) {
|
if (handle) {
|
||||||
stage.dataset.globe3d = 'active';
|
stage.dataset.globe3d = String(globe3dVersion);
|
||||||
}
|
}
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
['globe', 'continents', 'gridOverlay'].forEach(function (name) {
|
['globe', 'continents', 'gridOverlay'].forEach(function (name) {
|
||||||
|
|||||||
Reference in New Issue
Block a user