1
0
mirror of git://f0xx.org/ac/ac-be-hub synced 2026-08-09 20:59:23 +03:00
Files
ac-be-hub/assets/hub-globe.js
Anton Afanasyeu a4e582d212 feat(globe3d=11): latf/lngf/zy/zx land plate zoom URL params
Default latf=1.25; linear multiply on lon/lat plus optional non-linear
bias via zy/zx toward equator and prime meridian.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:34:26 +02:00

1442 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Hub landing globe — orthographic Canvas2D (?globe3d=N).
* globe3d=0: flat SVG layers (index.php).
* globe3d=1: legacy hybrid ortho front + mercator back + hub SVG grid trace.
* globe3d=2: mercator land attempt + full-sphere grid (superseded).
* globe3d=3: solid ocean, EW parallels only (true latitude rings) — no baked grid bleed.
* globe3d=4: solid ocean, full sphere grid (parallels + meridians).
* globe3d=5: hub ellipse trace (deprecated — breaks under rotation).
* globe3d=6: v4 math + latitude band fill (continuous arc paths).
* globe3d=7: v4 math + wide centerline stroke bands (no fill quads).
* globe3d=8: hub SVG ellipse trace (breaks under rotation — deprecated).
* globe3d=9: v4 sphere grid only (thin EW + NS) — proper rotation baseline.
* globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped.
* globe3d=11: v10 grid + geographic mercator-flat land (360° U-wrap).
* ?latf=1.25&lngf=1&zy=0&zx=0 — land plate zoom (linear + non-linear).
*/
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.04;
const LIMB_Z = 0.002;
/** hub-logo-continents inner layer fit (continents-meta.json / extract_red_polygons.py). */
const CONTINENT_FLAT_W = 800;
const CONTINENT_FLAT_H = 519;
const CONTINENT_INNER_TX = 223.66088631984587;
const CONTINENT_INNER_TY = 238.0;
const CONTINENT_INNER_SX = 1.2408477842003853;
const CONTINENT_INNER_SY = 1.2408477842003853;
/** Map plate center longitude (extract_red_polygons.py CENTER_LON). */
const MERCATOR_LON_CENTER = (12 * Math.PI) / 180;
/** Default land-plate zoom (?latf / ?lngf / ?zy / ?zx on globe3d=11). */
const LAND_ZOOM_DEFAULT = { latf: 1.25, lngf: 1, zy: 0, zx: 0 };
function clampZoomNum(v, min, max, fallback) {
if (typeof v !== 'number' || !isFinite(v)) {
return fallback;
}
return Math.max(min, Math.min(max, v));
}
function resolveLandZoom(raw) {
const r = raw && typeof raw === 'object' ? raw : {};
return {
latf: clampZoomNum(r.latf, 0.05, 4, LAND_ZOOM_DEFAULT.latf),
lngf: clampZoomNum(r.lngf, 0.05, 4, LAND_ZOOM_DEFAULT.lngf),
zy: clampZoomNum(r.zy, -4, 4, LAND_ZOOM_DEFAULT.zy),
zx: clampZoomNum(r.zx, -4, 4, LAND_ZOOM_DEFAULT.zx),
};
}
/**
* Linear latf/lngf on sphere lon/lat; zy/zx bias zoom toward equator/ prime meridian:
* mul = factor × (1 + z×(1t²)), t = |coord|/pole (1 at pole, 0 at center line).
*/
function landZoomLonLat(lon, lat, zoom) {
const latT = Math.min(1, Math.abs(lat) / (Math.PI / 2));
const lonT = Math.min(1, Math.abs(lon) / Math.PI);
const latMul = zoom.latf * (1 + zoom.zy * (1 - latT * latT));
const lonMul = zoom.lngf * (1 + zoom.zx * (1 - lonT * lonT));
return {
lon: wrapLon(lon * lonMul + MERCATOR_LON_CENTER),
lat: Math.max(-1.52, Math.min(1.52, lat * latMul)),
};
}
const RASTER_SCALE = 2;
const AXIS_STEPS = 160;
const SPHERE_AXIS_STEPS = 180;
/** Latitudes from hub grid overlay ellipse centers (meridian x = cx). */
function hubParallelLat(hubCy) {
const texY = (GLOBE_VIEW.cy - hubCy) / GLOBE_VIEW.r;
return Math.asin(Math.max(-1, Math.min(1, texY)));
}
const SPHERE_PARALLELS = [
{ lat: 0, bright: true },
{ lat: hubParallelLat(392), bright: false },
{ lat: hubParallelLat(728), bright: false },
];
/** Longitudes at equator from hub NS 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 — EW parallels. */
const HUB_EW_PARALLELS = [
{ 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 — NS 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(/[^/]*$/, '') : '';
const sep = rel.indexOf('?') >= 0 ? '&' : '?';
return base + rel + sep + 'v=' + build;
}
export function globeCoverRect() {
const vw = window.innerWidth;
const vh = window.innerHeight;
const scale = Math.max(vw / GLOBE_VIEW.imgW, vh / GLOBE_VIEW.imgH);
const drawW = GLOBE_VIEW.imgW * scale;
const drawH = GLOBE_VIEW.imgH * scale;
const offX = (vw - drawW) / 2;
const offY = (vh - drawH) / 2;
const left = offX + (GLOBE_VIEW.cx - GLOBE_VIEW.r) * scale;
const top = offY + (GLOBE_VIEW.cy - GLOBE_VIEW.r) * scale;
const size = GLOBE_VIEW.r * 2 * scale;
return {
left,
top,
width: size,
height: size,
scale,
cx: GLOBE_VIEW.cx * scale + offX,
cy: GLOBE_VIEW.cy * scale + offY,
r: GLOBE_VIEW.r * scale,
rim: GLOBE_VIEW.rim * scale,
};
}
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('image load failed: ' + url));
img.src = url;
});
}
function rasterizeImage(img, w, h, scale) {
const s = scale || 1;
const rw = Math.max(1, Math.round(w * s));
const rh = Math.max(1, Math.round(h * s));
const c = document.createElement('canvas');
c.width = rw;
c.height = rh;
const ctx = c.getContext('2d', { willReadFrequently: true });
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, rw, rh);
return { data: ctx.getImageData(0, 0, rw, rh).data, w: rw, h: rh };
}
function sampleImageDataBilinearWrapU(imgData, w, h, fx, fy) {
const y = Math.max(0, Math.min(h - 1.001, fy));
let x = fx % w;
if (x < 0) {
x += w;
}
const x0 = x | 0;
const tx = x - x0;
const x1 = (x0 + 1) % w;
const y0 = y | 0;
const y1 = Math.min(h - 1, y0 + 1);
const ty = y - y0;
const i00 = (y0 * w + x0) * 4;
const i10 = (y0 * w + x1) * 4;
const i01 = (y1 * w + x0) * 4;
const i11 = (y1 * w + x1) * 4;
const out = [0, 0, 0, 0];
for (let c = 0; c < 4; c++) {
out[c] = Math.round(
imgData[i00 + c] * (1 - tx) * (1 - ty) +
imgData[i10 + c] * tx * (1 - ty) +
imgData[i01 + c] * (1 - tx) * ty +
imgData[i11 + c] * tx * ty
);
}
return out;
}
function sampleImageDataBilinear(imgData, w, h, fx, fy) {
const x = Math.max(0, Math.min(w - 1.001, fx));
const y = Math.max(0, Math.min(h - 1.001, fy));
const x0 = x | 0;
const y0 = y | 0;
const x1 = Math.min(w - 1, x0 + 1);
const y1 = Math.min(h - 1, y0 + 1);
const tx = x - x0;
const ty = y - y0;
const i00 = (y0 * w + x0) * 4;
const i10 = (y0 * w + x1) * 4;
const i01 = (y1 * w + x0) * 4;
const i11 = (y1 * w + x1) * 4;
const out = [0, 0, 0, 0];
for (let c = 0; c < 4; c++) {
out[c] = Math.round(
imgData[i00 + c] * (1 - tx) * (1 - ty) +
imgData[i10 + c] * tx * (1 - ty) +
imgData[i01 + c] * (1 - tx) * ty +
imgData[i11 + c] * tx * ty
);
}
return out;
}
function inverseRotateY(nx, ny, nz, rotY) {
const cosR = Math.cos(rotY);
const sinR = Math.sin(rotY);
return {
x: nx * cosR - nz * sinR,
y: ny,
z: nx * sinR + nz * cosR,
};
}
function bodyToTex(body) {
return { x: body.x, y: -body.y, z: body.z };
}
function bodyToView(body, rotY) {
const cosR = Math.cos(rotY);
const sinR = Math.sin(rotY);
return {
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 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++) {
const a = (i / steps) * Math.PI * 2;
out.push(
hubPixelToBody(ellipse.cx + ellipse.rx * Math.cos(a), ellipse.cy + ellipse.ry * Math.sin(a))
);
}
return out;
}
function parallelStyle(bright, scale, z, limbZ) {
const zCut = limbZ != null ? limbZ : FRONT_Z;
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
if (t <= 0) {
return null;
}
const alpha = bright ? 0.55 + 0.45 * t : 0.35 + 0.5 * t;
const rgb = bright ? '219,231,246' : '231,236,243';
return {
color: 'rgba(' + rgb + ',' + alpha.toFixed(3) + ')',
width: Math.max(bright ? 3 : 2, scale * (bright ? 0.009 : 0.006)) * (0.5 + 0.5 * t),
};
}
function meridianStyle(prime, scale, z, limbZ) {
const zCut = limbZ != null ? limbZ : FRONT_Z;
const t = Math.max(0, Math.min(1, (z - zCut) / (0.95 - zCut)));
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, limbZ) {
let run = [];
const zCut = limbZ != null ? limbZ : FRONT_Z;
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++) {
if (!bodies[i]) {
flush();
continue;
}
const view = bodyToView(bodies[i], rotY);
if (view.z <= zCut) {
flush();
continue;
}
run.push({
sx: cx + view.x * r,
sy: cy - view.y * r,
z: view.z,
});
}
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;
}
function parallelBandHalfWidth(par) {
const hubRy = par.bright ? HUB_EW_PARALLELS[0].ry : HUB_EW_PARALLELS[1].ry;
return hubRy / GLOBE_VIEW.r;
}
function parallelBandStyle(par, z, bandPx, layer) {
const t = Math.max(0, Math.min(1, (z - LIMB_Z) / (0.95 - LIMB_Z)));
if (t <= 0) {
return null;
}
const rgb = par.bright ? '219,231,246' : '231,236,243';
if (layer === 'halo') {
return {
color: 'rgba(' + rgb + ',' + (0.1 + 0.2 * t).toFixed(3) + ')',
width: bandPx * 1.08,
};
}
return {
color: 'rgba(' + rgb + ',' + (par.bright ? 0.4 + 0.52 * t : 0.24 + 0.4 * t).toFixed(3) + ')',
width: bandPx * (0.58 + 0.42 * t),
};
}
/** One filled ribbon per visible arc — no per-longitude quads (avoids comb artifacts). */
function fillParallelBandPath(ctx, cx, cy, r, rotY, par) {
const lat = par.lat;
const halfW = parallelBandHalfWidth(par);
const latLo = Math.max(-Math.PI / 2 + 0.02, lat - halfW);
const latHi = Math.min(Math.PI / 2 - 0.02, lat + halfW);
const outer = parallelBodyRingAtLat(latHi, SPHERE_AXIS_STEPS);
const inner = parallelBodyRingAtLat(latLo, SPHERE_AXIS_STEPS);
const n = Math.min(outer.length, inner.length);
const rgb = par.bright ? '219,231,246' : '231,236,243';
let runO = [];
let runI = [];
function flushFill() {
if (runO.length < 2) {
runO = [];
runI = [];
return;
}
let zSum = 0;
for (let k = 0; k < runO.length; k++) {
zSum += runO[k].z;
}
const zAvg = zSum / runO.length;
const t = Math.max(0, Math.min(1, (zAvg - LIMB_Z) / (0.95 - LIMB_Z)));
if (t <= 0) {
runO = [];
runI = [];
return;
}
const alpha = (par.bright ? 0.18 + 0.52 * t : 0.1 + 0.36 * t).toFixed(3);
ctx.beginPath();
ctx.moveTo(runO[0].sx, runO[0].sy);
for (let k = 1; k < runO.length; k++) {
ctx.lineTo(runO[k].sx, runO[k].sy);
}
for (let k = runI.length - 1; k >= 0; k--) {
ctx.lineTo(runI[k].sx, runI[k].sy);
}
ctx.closePath();
ctx.fillStyle = 'rgba(' + rgb + ',' + alpha + ')';
ctx.fill();
runO = [];
runI = [];
}
for (let i = 0; i < n; i++) {
const vo = bodyToView(outer[i], rotY);
const vi = bodyToView(inner[i], rotY);
if (vo.z <= LIMB_Z || vi.z <= LIMB_Z) {
flushFill();
continue;
}
runO.push({ sx: cx + vo.x * r, sy: cy - vo.y * r, z: vo.z });
runI.push({ sx: cx + vi.x * r, sy: cy - vi.y * r, z: vi.z });
}
flushFill();
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
parallelBodyRingAtLat(lat, SPHERE_AXIS_STEPS),
function (z) {
const style = parallelStyle(par.bright, 1, z, LIMB_Z);
if (!style) {
return null;
}
return {
color: style.color,
width: Math.max(style.width, (par.bright ? 1.8 : 1.4)),
};
},
LIMB_Z
);
}
/** Wide dual stroke on centerline — globe3d=7. */
function strokeParallelBandWide(ctx, cx, cy, r, rotY, par, layoutScale) {
const halfW = parallelBandHalfWidth(par);
const bandPx = Math.max(par.bright ? 6 : 10, 2 * halfW * r);
const ring = parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS);
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
ring,
function (z) {
return parallelBandStyle(par, z, bandPx, 'halo');
},
LIMB_Z
);
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
ring,
function (z) {
return parallelBandStyle(par, z, bandPx, 'core');
},
LIMB_Z
);
}
/** globe3d=6 — v4 sphere model with volumetric EW bands. */
function drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly);
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
SPHERE_PARALLELS.forEach(function (par) {
fillParallelBandPath(ctx, cx, cy, r, rotY, par);
});
if (!parallelsOnly) {
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 ellipse centerline on sphere, wide stroke — stays elliptical under Y rotation (globe3d=8). */
function strokeHubEllipseParallel(ctx, cx, cy, r, rotY, ellipse, layoutScale) {
const scale = r / GLOBE_VIEW.r;
const bandPx = Math.max(ellipse.bright ? 5 : 8, 2 * ellipse.ry * scale);
const par = { bright: ellipse.bright };
const ring = ellipseBodyRing(ellipse, AXIS_STEPS);
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
ring,
function (z) {
return parallelBandStyle(par, z, bandPx, 'halo');
},
LIMB_Z
);
strokeBodyRing(
ctx,
cx,
cy,
r,
rotY,
ring,
function (z) {
return parallelBandStyle(par, z, bandPx, 'core');
},
LIMB_Z
);
}
/** globe3d=8 — hub EW ellipses on sphere; v4 NS meridians. */
function drawHubEllipseGridRotated(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) {
strokeHubEllipseParallel(ctx, cx, cy, r, rotY, ellipse, layoutScale);
});
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();
}
function hubRyForParallel(par) {
if (par.bright) {
return HUB_EW_PARALLELS[0].ry;
}
return par.lat > 0 ? HUB_EW_PARALLELS[1].ry : HUB_EW_PARALLELS[2].ry;
}
/** v4 visible arc bounds → 2D ellipse (curved ends via disk clip). */
function drawParallelScreenEllipse(ctx, cx, cy, r, rotY, par) {
let xMin = Infinity;
let xMax = -Infinity;
let zSum = 0;
let n = 0;
const ring = parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS);
for (let i = 0; i < ring.length; i++) {
const view = bodyToView(ring[i], rotY);
if (view.z <= LIMB_Z) {
continue;
}
xMin = Math.min(xMin, view.x);
xMax = Math.max(xMax, view.x);
zSum += view.z;
n++;
}
if (n < 2 || xMax <= xMin) {
return;
}
const t = Math.max(0, Math.min(1, (zSum / n - LIMB_Z) / (0.95 - LIMB_Z)));
const rx = ((xMax - xMin) / 2) * r;
const ry = hubRyForParallel(par) * (r / GLOBE_VIEW.r);
if (rx < 0.5 || ry < 0.5) {
return;
}
const ecx = cx + ((xMin + xMax) / 2) * r;
const ecy = cy - Math.sin(par.lat) * r;
const rgb = par.bright ? '219,231,246' : '231,236,243';
const alpha = par.bright ? 0.55 + 0.45 * t : 0.35 + 0.5 * t;
const layoutScale = r / GLOBE_VIEW.r;
const lineW = Math.max(par.bright ? 2.5 : 2, layoutScale * 0.0075) * (0.55 + 0.45 * t);
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
ctx.beginPath();
ctx.ellipse(ecx, ecy, rx, ry, 0, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(' + rgb + ',' + alpha.toFixed(3) + ')';
ctx.lineWidth = lineW;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
ctx.restore();
}
/** @deprecated screen-Y strips — flat rectangles; use drawParallelScreenEllipse. */
function fillParallelScreenStrip(ctx, cx, cy, r, rotY, par) {
const halfPx = (par.bright ? HUB_EW_PARALLELS[0].ry : HUB_EW_PARALLELS[1].ry) * (r / GLOBE_VIEW.r);
const ring = parallelBodyRingAtLat(par.lat, SPHERE_AXIS_STEPS);
const rgb = par.bright ? '219,231,246' : '231,236,243';
let run = [];
function flushFill() {
if (run.length < 2) {
run = [];
return;
}
let zSum = 0;
for (let k = 0; k < run.length; k++) {
zSum += run[k].z;
}
const zAvg = zSum / run.length;
const t = Math.max(0, Math.min(1, (zAvg - LIMB_Z) / (0.95 - LIMB_Z)));
if (t <= 0) {
run = [];
return;
}
const alpha = (par.bright ? 0.16 + 0.48 * t : 0.09 + 0.34 * t).toFixed(3);
ctx.beginPath();
for (let k = 0; k < run.length; k++) {
ctx.lineTo(run[k].sx, run[k].sy - halfPx);
}
for (let k = run.length - 1; k >= 0; k--) {
ctx.lineTo(run[k].sx, run[k].sy + halfPx);
}
ctx.closePath();
ctx.fillStyle = 'rgba(' + rgb + ',' + alpha + ')';
ctx.fill();
run = [];
}
for (let i = 0; i < ring.length; i++) {
const view = bodyToView(ring[i], rotY);
if (view.z <= LIMB_Z) {
flushFill();
continue;
}
run.push({
sx: cx + view.x * r,
sy: cy - view.y * r,
z: view.z,
});
}
flushFill();
}
/** globe3d=10 — v4 EW screen strips + v4 meridians. */
function drawSphereGridScreenStrip(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) {
drawParallelScreenEllipse(ctx, cx, cy, r, rotY, par);
});
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();
}
/** globe3d=7 — v4 sphere model, wide stroke bands (no path fill). */
function drawSphereGridWideStroke(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly);
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
SPHERE_PARALLELS.forEach(function (par) {
strokeParallelBandWide(ctx, cx, cy, r, rotY, par, layoutScale);
});
if (!parallelsOnly) {
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();
}
/** Full-sphere grid rings (globe3d>=4). */
function drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly);
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
);
});
if (!parallelsOnly) {
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 ellipses traced on sphere (globe3d=5) — matches static art at yaw 0. */
function drawHubParallelGrid(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) {
strokeBodyRing(ctx, cx, cy, r, rotY, ellipseBodyRing(ellipse, AXIS_STEPS), function (z) {
return parallelStyle(ellipse.bright, layoutScale, z, LIMB_Z);
}, LIMB_Z);
});
ctx.restore();
}
/** Hub SVG grid: EW ellipses + NS meridian paths (globe3d=1). */
function drawHubGrid(ctx, cx, cy, r, rotY, layoutScale, opts) {
const parallelsOnly = !!(opts && opts.parallelsOnly);
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
HUB_EW_PARALLELS.forEach(function (ellipse) {
strokeBodyRing(ctx, cx, cy, r, rotY, ellipseBodyRing(ellipse, AXIS_STEPS), function (z) {
return parallelStyle(ellipse.bright, layoutScale, z);
});
});
if (!parallelsOnly) {
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();
}
function drawGlobeGrid(ctx, cx, cy, r, rotY, layoutScale, renderVersion) {
if (renderVersion === 3) {
drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: true });
return;
}
if (renderVersion === 4) {
drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion === 5) {
drawHubParallelGrid(ctx, cx, cy, r, rotY, layoutScale);
return;
}
if (renderVersion === 6) {
drawSphereGridBanded(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion === 7) {
drawSphereGridWideStroke(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion === 8) {
drawHubEllipseGridRotated(ctx, cx, cy, r, rotY, layoutScale);
return;
}
if (renderVersion === 9) {
drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
if (renderVersion === 10 || renderVersion === 11) {
drawSphereGridScreenStrip(ctx, cx, cy, r, rotY, layoutScale);
return;
}
if (renderVersion >= 2) {
drawSphereGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
return;
}
drawHubGrid(ctx, cx, cy, r, rotY, layoutScale, { parallelsOnly: false });
}
function drawOceanRim(ctx, globeImg, cx, cy, r, rim, layoutScale, renderVersion) {
if (renderVersion >= 3) {
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, rim, 0, Math.PI * 2);
ctx.arc(cx, cy, r, 0, Math.PI * 2, true);
ctx.clip();
const g = ctx.createRadialGradient(cx, cy, r, cx, cy, rim);
g.addColorStop(0, 'rgba(28,48,74,0.98)');
g.addColorStop(1, 'rgba(14,24,38,0.92)');
ctx.fillStyle = g;
ctx.fill();
ctx.restore();
return;
}
drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale);
}
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 texLonLat(tex) {
return {
lon: Math.atan2(tex.x, tex.z),
lat: Math.asin(Math.max(-1, Math.min(1, tex.y))),
};
}
function wrapLon(lon) {
let l = lon;
while (l > Math.PI) {
l -= Math.PI * 2;
}
while (l < -Math.PI) {
l += Math.PI * 2;
}
return l;
}
/** Inverse Mercator row — matches extract_red_polygons.py mercator_y_to_lat. */
function latToMercatorRow(lat, h) {
const clamped = Math.max(-1.52, Math.min(1.52, lat));
const t = 0.5 + Math.asinh(Math.tan(clamped)) / (2 * Math.PI);
return Math.max(0, Math.min(h - 1, (1 - t) * (h - 1)));
}
function sampleMercFlat(mercFlat, lon, lat) {
const u = ((wrapLon(lon) + Math.PI) / (2 * Math.PI)) * (mercFlat.w - 1);
const v = latToMercatorRow(lat, mercFlat.h);
return sampleImageDataBilinearWrapU(mercFlat.data, mercFlat.w, mercFlat.h, u, v);
}
/** Inverse hub-disk → flat plate (same affine as globe SVG), X wraps 360°. */
function sampleFlatPlateFromHub(mercFlat, hpX, hpY) {
const fx = (hpX - CONTINENT_INNER_TX) / CONTINENT_INNER_SX;
const fy = (hpY - CONTINENT_INNER_TY) / CONTINENT_INNER_SY;
if (fy < 0 || fy > CONTINENT_FLAT_H - 1) {
return null;
}
const sx = (fx / CONTINENT_FLAT_W) * mercFlat.w;
const sy = (fy / CONTINENT_FLAT_H) * mercFlat.h;
return sampleImageDataBilinearWrapU(mercFlat.data, mercFlat.w, mercFlat.h, sx, sy);
}
/** Sphere tex → mercator-flat plate (lon center + inverse Mercator Y). */
function sampleMercFlatFromTex(mercFlat, tex, landZoom) {
const ll = texLonLat(tex);
const mapped = landZoomLonLat(ll.lon, ll.lat, landZoom);
return sampleMercFlat(mercFlat, mapped.lon, mapped.lat);
}
function isLandPixel(r, g, b, a) {
return a > 10 && r > 100 && g < 140 && b < 140;
}
/** hub-logo-continents-mercator-{globe,flat}.svg grey-blue fills (not red JPG extract). */
function isHubContinentPixel(r, g, b, a) {
if (a < 8) {
return false;
}
if (r > 168 && g > 198 && b > 218) {
return false;
}
if (r < 48 && g < 58 && b < 82) {
return false;
}
const lum = r * 0.299 + g * 0.587 + b * 0.114;
if (lum < 92 || lum > 238) {
return false;
}
return b >= 100 && g >= 95 && r >= 68;
}
function hubContinentRgba(sample) {
return [sample[0], sample[1], sample[2], 255];
}
/** Solid fill from rasterized globe SVG — skip gradient fringe / drop-shadow. */
function isSolidContinentPixel(r, g, b, a) {
if (a < 72) {
return false;
}
if (r > 168 && g > 198 && b > 218) {
return false;
}
if (r < 58 && g < 68 && b < 95) {
return false;
}
const lum = r * 0.299 + g * 0.587 + b * 0.114;
if (lum < 102 || lum > 218) {
return false;
}
return b >= 105 && g >= 100 && r >= 72;
}
/** 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;
}
const OCEAN_SOLID = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
function sampleOceanSolid() {
return OCEAN_SOLID;
}
function sampleOceanClean(globe, tex) {
if (tex.z <= FRONT_Z) {
return OCEAN_SOLID;
}
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_SOLID;
}
function sampleLandRgbaLegacy(continents, mercFlat, tex) {
if (tex.z > FRONT_Z) {
const ortho = sampleHub(continents, 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);
if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) {
return [flat[0], flat[1], flat[2], Math.min(255, flat[3])];
}
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;
}
/** globe3d=11 — geographic mercator wrap (360°); z > LIMB_Z only. */
function sampleLandVisibleHemisphere(mercFlat, tex, landZoom) {
if (tex.z <= LIMB_Z) {
return null;
}
const flat = sampleMercFlatFromTex(mercFlat, tex, landZoom);
if (isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) {
return hubContinentRgba(flat);
}
return null;
}
function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
const r2 = r * r;
const x0 = Math.max(0, Math.floor(cx - r));
const y0 = Math.max(0, Math.floor(cy - r));
const x1 = Math.min(ctx.canvas.width - 1, Math.ceil(cx + r));
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;
}
const out = ctx.createImageData(w, h);
const px = out.data;
let p = 0;
for (let dy = y0; dy <= y1; dy++) {
for (let dx = x0; dx <= x1; dx++) {
const ox = dx - cx;
const oy = dy - cy;
if (ox * ox + oy * oy > r2) {
px[p++] = 0;
px[p++] = 0;
px[p++] = 0;
px[p++] = 0;
continue;
}
const nx = ox / r;
const ny = oy / r;
const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));
const tex = bodyToTex(inverseRotateY(nx, ny, nz, rotY));
const rgba = fn(tex);
px[p++] = rgba[0];
px[p++] = rgba[1];
px[p++] = rgba[2];
px[p++] = rgba[3] !== undefined ? rgba[3] : 255;
}
}
return { out, x0, y0 };
}
function blitLayer(ctx, cx, cy, r, layer) {
if (!layer) {
return;
}
const { out, x0, y0 } = layer;
const w = out.width;
const h = out.height;
const src = out.data;
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.clip();
const existing = ctx.getImageData(x0, y0, w, h);
const dst = existing.data;
for (let i = 0; i < src.length; i += 4) {
const sa = src[i + 3];
if (sa === 0) {
continue;
}
if (sa === 255) {
dst[i] = src[i];
dst[i + 1] = src[i + 1];
dst[i + 2] = src[i + 2];
dst[i + 3] = 255;
continue;
}
const a = sa / 255;
dst[i] = Math.round(src[i] * a + dst[i] * (1 - a));
dst[i + 1] = Math.round(src[i + 1] * a + dst[i + 1] * (1 - a));
dst[i + 2] = Math.round(src[i + 2] * a + dst[i + 2] * (1 - a));
dst[i + 3] = 255;
}
ctx.putImageData(existing, x0, y0);
ctx.restore();
}
function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) {
const drawW = GLOBE_VIEW.imgW * layoutScale;
const drawH = GLOBE_VIEW.imgH * layoutScale;
const dx = cx - GLOBE_VIEW.cx * layoutScale;
const dy = cy - GLOBE_VIEW.cy * layoutScale;
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, rim, 0, Math.PI * 2);
ctx.arc(cx, cy, r, 0, Math.PI * 2, true);
ctx.clip();
ctx.drawImage(globeImg, 0, 0, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, dx, dy, drawW, drawH);
ctx.restore();
}
export async function mountHubGlobe(stage, opts) {
const build = opts.build || 'earth3d';
const reducedMotion = !!opts.reducedMotion;
const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1);
const landZoom = resolveLandZoom(opts.landZoom);
const canvas = document.createElement('canvas');
canvas.className = 'hub-globe-canvas';
canvas.setAttribute('aria-hidden', 'true');
stage.appendChild(canvas);
const urls = {
globe: hubAsset('assets/hub-logo-fragment-globe.svg', build),
continents: hubAsset('assets/hub-logo-continents-mercator-globe.svg', build),
mercatorFlat: hubAsset('assets/hub-logo-continents-mercator-flat.svg', build),
};
let globeImg;
let continentsImg;
let mercatorFlatImg;
try {
[globeImg, continentsImg, mercatorFlatImg] = await Promise.all([
loadImage(urls.globe),
loadImage(urls.continents),
loadImage(urls.mercatorFlat),
]);
} catch (e) {
console.warn('HubGlobe: asset load failed', e);
canvas.remove();
return null;
}
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 mercW = (mercatorFlatImg.naturalWidth || 800) * 2;
const mercH = (mercatorFlatImg.naturalHeight || 519) * 2;
const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1);
let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0;
let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S;
let dragging = false;
let lastPointerX = 0;
let lastPointerTime = 0;
let raf = 0;
let lastFrame = performance.now();
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const resize = () => {
const rect = globeCoverRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const px = Math.max(64, Math.floor(rect.width * dpr));
canvas.style.left = rect.left + 'px';
canvas.style.top = rect.top + 'px';
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
canvas.width = px;
canvas.height = px;
return { rect, dpr, px };
};
let layout = resize();
window.addEventListener('resize', () => {
layout = resize();
});
const render = () => {
const { rect, dpr, px } = layout;
const cx = (rect.cx - rect.left) * dpr;
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);
blitLayer(
ctx,
cx,
cy,
r,
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
if (renderVersion >= 3) {
return sampleOceanSolid();
}
return sampleOceanClean(globe, tex);
})
);
if (renderVersion === 1) {
blitLayer(
ctx,
cx,
cy,
r,
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
const l = sampleLandRgbaLegacy(continents, mercFlat, tex);
return l || [0, 0, 0, 0];
})
);
} else if (renderVersion === 2) {
blitLayer(
ctx,
cx,
cy,
r,
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
const l = sampleLandRgbaMercator(mercFlat, tex);
return l || [0, 0, 0, 0];
})
);
} else if (renderVersion >= 11) {
blitLayer(
ctx,
cx,
cy,
r,
fillDiskRegion(ctx, cx, cy, r, rotation, function (tex) {
const l = sampleLandVisibleHemisphere(mercFlat, tex, landZoom);
return l || [0, 0, 0, 0];
})
);
}
drawGlobeGrid(ctx, cx, cy, r, rotation, layoutScale, renderVersion);
drawOceanRim(ctx, globeImg, cx, cy, r, rim, layoutScale, renderVersion);
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, layoutScale * 0.006);
ctx.stroke();
ctx.restore();
};
const onPointerDown = (ev) => {
if (ev.button !== 0) return;
dragging = true;
lastPointerX = ev.clientX;
lastPointerTime = performance.now();
canvas.setPointerCapture(ev.pointerId);
canvas.classList.add('is-dragging');
ev.preventDefault();
};
const onPointerMove = (ev) => {
if (!dragging) return;
const now = performance.now();
const dx = ev.clientX - lastPointerX;
rotation += dx * DRAG_ROTATION_PER_PX;
if (now > lastPointerTime) {
const inst = (dx * DRAG_ROTATION_PER_PX) / ((now - lastPointerTime) / 1000);
angularVelocity = Math.max(-2.5, Math.min(2.5, inst));
}
lastPointerX = ev.clientX;
lastPointerTime = now;
ev.preventDefault();
};
const endDrag = (ev) => {
if (!dragging) return;
dragging = false;
canvas.classList.remove('is-dragging');
try {
canvas.releasePointerCapture(ev.pointerId);
} catch (ignored) {}
};
canvas.addEventListener('pointerdown', onPointerDown);
canvas.addEventListener('pointermove', onPointerMove);
canvas.addEventListener('pointerup', endDrag);
canvas.addEventListener('pointercancel', endDrag);
const tick = (now) => {
raf = requestAnimationFrame(tick);
const dt = Math.min(0.05, (now - lastFrame) / 1000);
lastFrame = now;
if (!dragging) {
if (!reducedMotion) {
const target = AUTO_SPIN_RAD_S;
angularVelocity += (target - angularVelocity) * 1.2 * dt;
if (Math.abs(angularVelocity - target) < 0.004) angularVelocity = target;
} else {
angularVelocity *= Math.exp(-4 * dt);
}
rotation += angularVelocity * dt;
}
render();
};
raf = requestAnimationFrame(tick);
stage.dataset.globe3d = String(renderVersion);
if (renderVersion >= 11) {
stage.dataset.globeLatf = String(landZoom.latf);
stage.dataset.globeLngf = String(landZoom.lngf);
stage.dataset.globeZy = String(landZoom.zy);
stage.dataset.globeZx = String(landZoom.zx);
}
stage.dataset.globeAxis =
renderVersion === 11
? 'ew-sphere-ellipse-land'
: renderVersion === 10
? 'ew-sphere-ellipse'
: renderVersion === 9
? 'ew-ns-sphere-v4'
: renderVersion >= 8
? 'ew-hub-ellipse-rot'
: renderVersion >= 7
? 'ew-sphere-stroke'
: renderVersion >= 6
? 'ew-sphere-band'
: renderVersion >= 5
? 'ew-hub-ellipse'
: renderVersion >= 3
? 'ew-sphere'
: renderVersion >= 2
? 'ew-ns-sphere'
: 'ew-ns-grid';
return {
destroy() {
cancelAnimationFrame(raf);
window.removeEventListener('resize', resize);
canvas.remove();
delete stage.dataset.globe3d;
delete stage.dataset.globeAxis;
delete stage.dataset.globeLatf;
delete stage.dataset.globeLngf;
delete stage.dataset.globeZy;
delete stage.dataset.globeZx;
},
};
}