mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:57:50 +03:00
initiaal 3d
This commit is contained in:
@@ -13,6 +13,10 @@ Review one layer: `?logoPart=space|globe|full`
|
||||
|
||||
Compare Earth grids: `?logoPart=globe&grid=3` (default) or `?logoPart=full&grid=4`
|
||||
|
||||
**3D Earth (feature/3d_earth):** Canvas orthographic globe baked from the same SVG layers as the static hub (`globe` + `continents-mercator-globe` + `gridOverlay`). Idle spin is slow (~0.035 rad/s); drag horizontally to rotate. Disable with `?globe3d=0`. Deploy `assets/hub-globe.js` with `index.html`.
|
||||
|
||||
**3D globe background** (default): WebGL sphere with Mercator continents, meridian/equator grid, slow Y-axis spin; drag horizontally to spin (speed follows pointer). Disable: `?globe3d=0` (flat SVG layers). Default yaw: `0.42` rad (`?globeYawDeg=24` to override). Requires deploying `assets/hub-globe.js` + `hub-logo-layers.css` + updated `index.html` (see `DEPLOY.md`).
|
||||
|
||||
**BE only.** FE `apps.f0xx.org` sends `location /` → `http://artc0.intra.raptor.org:80` (not :8089).
|
||||
|
||||
Deploy copies/symlink under `.../androidcast_project/` on the VM. Background watermark is **3% opacity** (`hub.css`).
|
||||
|
||||
404
examples/app_hub/assets/hub-globe.js
Normal file
404
examples/app_hub/assets/hub-globe.js
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* Hub landing globe — orthographic Canvas2D (matches static SVG stack at rotation 0).
|
||||
* Bakes globe + continents-mercator-globe + grid overlay into an equirectangular map,
|
||||
* then scrolls it for Y-axis rotation (same visual language as the flat layers).
|
||||
*/
|
||||
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
|
||||
|
||||
/** Idle spin: ~4× slower than the first Three.js attempt (0.14 → 0.035 rad/s). */
|
||||
const AUTO_SPIN_RAD_S = 0.035;
|
||||
const DRAG_ROTATION_PER_PX = 0.004;
|
||||
const EQ_WIDTH = 1024;
|
||||
const EQ_HEIGHT = 512;
|
||||
|
||||
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
|
||||
|
||||
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 compositeStaticLayers(globeImg, continentsImg, gridImg) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = GLOBE_VIEW.imgW;
|
||||
c.height = GLOBE_VIEW.imgH;
|
||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||
ctx.drawImage(globeImg, 0, 0, c.width, c.height);
|
||||
ctx.drawImage(continentsImg, 0, 0, c.width, c.height);
|
||||
ctx.drawImage(gridImg, 0, 0, c.width, c.height);
|
||||
return { canvas: c, ctx, data: ctx.getImageData(0, 0, c.width, c.height) };
|
||||
}
|
||||
|
||||
function sampleComposite(data, sx, sy) {
|
||||
const w = data.width;
|
||||
const h = data.height;
|
||||
const x = Math.max(0, Math.min(w - 1, Math.round(sx)));
|
||||
const y = Math.max(0, Math.min(h - 1, Math.round(sy)));
|
||||
const i = (y * w + x) * 4;
|
||||
return [data.data[i], data.data[i + 1], data.data[i + 2], data.data[i + 3]];
|
||||
}
|
||||
|
||||
function sampleMercatorFlat(ctx, lon, lat) {
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
const u = (lon + Math.PI) / (2 * Math.PI);
|
||||
const v = 0.5 - lat / Math.PI;
|
||||
const x = Math.max(0, Math.min(w - 1, Math.round(u * (w - 1))));
|
||||
const y = Math.max(0, Math.min(h - 1, Math.round(v * (h - 1))));
|
||||
const d = ctx.getImageData(x, y, 1, 1).data;
|
||||
return [d[0], d[1], d[2], d[3]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build equirectangular texture from the static orthographic hub artwork.
|
||||
*/
|
||||
function bakeEquirectangular(compositeData, mercatorFlatImg) {
|
||||
const merc = document.createElement('canvas');
|
||||
merc.width = mercatorFlatImg.naturalWidth || 800;
|
||||
merc.height = mercatorFlatImg.naturalHeight || 519;
|
||||
const mercCtx = merc.getContext('2d', { willReadFrequently: true });
|
||||
mercCtx.drawImage(mercatorFlatImg, 0, 0);
|
||||
|
||||
const eq = document.createElement('canvas');
|
||||
eq.width = EQ_WIDTH;
|
||||
eq.height = EQ_HEIGHT;
|
||||
const out = eq.getContext('2d').createImageData(EQ_WIDTH, EQ_HEIGHT);
|
||||
const { cx, cy, r } = GLOBE_VIEW;
|
||||
|
||||
for (let j = 0; j < EQ_HEIGHT; j++) {
|
||||
for (let i = 0; i < EQ_WIDTH; i++) {
|
||||
const lon = (i / EQ_WIDTH) * 2 * Math.PI - Math.PI;
|
||||
const lat = Math.PI * 0.5 - (j / EQ_HEIGHT) * Math.PI;
|
||||
const cosLat = Math.cos(lat);
|
||||
const x3 = cosLat * Math.sin(lon);
|
||||
const y3 = Math.sin(lat);
|
||||
const z3 = cosLat * Math.cos(lon);
|
||||
|
||||
let rgba;
|
||||
if (z3 > 0.02) {
|
||||
const sx = cx + x3 * r;
|
||||
const sy = cy - y3 * r;
|
||||
rgba = sampleComposite(compositeData, sx, sy);
|
||||
if (rgba[3] < 8) {
|
||||
rgba = sampleMercatorFlat(mercCtx, lon, lat);
|
||||
}
|
||||
} else {
|
||||
rgba = sampleMercatorFlat(mercCtx, lon, lat);
|
||||
if (rgba[3] < 16) {
|
||||
rgba = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
|
||||
}
|
||||
}
|
||||
|
||||
const p = (j * EQ_WIDTH + i) * 4;
|
||||
out.data[p] = rgba[0];
|
||||
out.data[p + 1] = rgba[1];
|
||||
out.data[p + 2] = rgba[2];
|
||||
out.data[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
eq.getContext('2d').putImageData(out, 0, 0);
|
||||
return eq;
|
||||
}
|
||||
|
||||
function drawEquirectSlice(ctx, eqCanvas, rotation, dx, dy, diam) {
|
||||
const w = eqCanvas.width;
|
||||
const h = eqCanvas.height;
|
||||
const offsetPx = ((rotation / (2 * Math.PI)) % 1) * w;
|
||||
const ox = ((offsetPx % w) + w) % w;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(dx, dy, diam * 0.5, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
ctx.drawImage(eqCanvas, -ox, 0, w, h, dx - diam * 0.5, dy - diam * 0.5, diam, diam);
|
||||
ctx.drawImage(eqCanvas, w - ox, 0, w, h, dx - diam * 0.5 + diam, dy - diam * 0.5, diam, diam);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function strokeMeridian(ctx, cx, cy, r, lonRad, rotY, strokeStyle, lineWidth) {
|
||||
const pts = [];
|
||||
for (let i = 0; i <= 64; i++) {
|
||||
const lat = -Math.PI / 2 + (i / 64) * Math.PI;
|
||||
const cosLat = Math.cos(lat);
|
||||
let x = cosLat * Math.sin(lonRad);
|
||||
let y = Math.sin(lat);
|
||||
let z = cosLat * Math.cos(lonRad);
|
||||
const x2 = x * Math.cos(rotY) + z * Math.sin(rotY);
|
||||
const z2 = -x * Math.sin(rotY) + z * Math.cos(rotY);
|
||||
if (z2 > 0.04) {
|
||||
pts.push({ x: cx + x2 * r, y: cy - y * r });
|
||||
}
|
||||
}
|
||||
if (pts.length < 2) return;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pts[0].x, pts[0].y);
|
||||
for (let k = 1; k < pts.length; k++) ctx.lineTo(pts[k].x, pts[k].y);
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawGridOverlay(ctx, rect, rotY) {
|
||||
const cx = rect.cx;
|
||||
const cy = rect.cy;
|
||||
const r = rect.r;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
|
||||
const equatorGrad = ctx.createLinearGradient(cx - r, cy, cx + r, cy);
|
||||
equatorGrad.addColorStop(0, 'rgba(39,54,75,0.74)');
|
||||
equatorGrad.addColorStop(0.5, 'rgba(219,231,246,0.88)');
|
||||
equatorGrad.addColorStop(1, 'rgba(31,44,64,0.74)');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, r * 0.99, r * 0.034, 0, 0, 2 * Math.PI);
|
||||
ctx.strokeStyle = equatorGrad;
|
||||
ctx.lineWidth = Math.max(2, rect.scale * 0.007);
|
||||
ctx.stroke();
|
||||
|
||||
const poleRy = r * 0.12;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy - r * 0.52, r * 0.82, poleRy, 0, 0, 2 * Math.PI);
|
||||
ctx.strokeStyle = 'rgba(231,236,243,0.72)';
|
||||
ctx.lineWidth = Math.max(1.5, rect.scale * 0.0055);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy + r * 0.52, r * 0.82, poleRy, 0, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
|
||||
const meridianStroke = 'rgba(238,245,255,0.92)';
|
||||
const primeStroke = 'rgba(238,245,255,0.98)';
|
||||
const lw = Math.max(2.5, rect.scale * 0.008);
|
||||
const lwPrime = Math.max(3, rect.scale * 0.0095);
|
||||
|
||||
for (let lonDeg = -150; lonDeg <= 150; lonDeg += 30) {
|
||||
const lon = (lonDeg * Math.PI) / 180;
|
||||
const isPrime = lonDeg === 0;
|
||||
strokeMeridian(ctx, cx, cy, r, lon, rotY, isPrime ? primeStroke : meridianStroke, isPrime ? lwPrime : lw);
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#5eead4';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#0a0e14';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawRim(ctx, compositeCanvas, cx, cy, r, rim, scale) {
|
||||
const drawW = GLOBE_VIEW.imgW * scale;
|
||||
const drawH = GLOBE_VIEW.imgH * scale;
|
||||
const dx = cx - GLOBE_VIEW.cx * scale;
|
||||
const dy = cy - GLOBE_VIEW.cy * scale;
|
||||
|
||||
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(compositeCanvas, 0, 0, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, dx, dy, drawW, drawH);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} stage
|
||||
* @param {{ build: string, reducedMotion?: boolean }} opts
|
||||
*/
|
||||
export async function mountHubGlobe(stage, opts) {
|
||||
const build = opts.build || 'earth3d';
|
||||
const reducedMotion = !!opts.reducedMotion;
|
||||
|
||||
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),
|
||||
grid: 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 gridImg;
|
||||
let mercatorFlatImg;
|
||||
try {
|
||||
[globeImg, continentsImg, gridImg, mercatorFlatImg] = await Promise.all([
|
||||
loadImage(urls.globe),
|
||||
loadImage(urls.continents),
|
||||
loadImage(urls.grid),
|
||||
loadImage(urls.mercatorFlat),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn('HubGlobe: asset load failed', e);
|
||||
canvas.remove();
|
||||
return null;
|
||||
}
|
||||
|
||||
const composite = compositeStaticLayers(globeImg, continentsImg, gridImg);
|
||||
const eqCanvas = bakeEquirectangular(composite.data, mercatorFlatImg);
|
||||
|
||||
let rotation = 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');
|
||||
|
||||
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 drawRect = {
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
rim,
|
||||
scale: rect.scale * dpr,
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
|
||||
ctx.clearRect(0, 0, px, px);
|
||||
drawEquirectSlice(ctx, eqCanvas, rotation, cx, cy, r * 2);
|
||||
drawGridOverlay(ctx, drawRect, rotation);
|
||||
drawRim(ctx, composite.canvas, cx, cy, r, rim, rect.scale * dpr);
|
||||
};
|
||||
|
||||
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 = 'active';
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener('resize', resize);
|
||||
canvas.remove();
|
||||
delete stage.dataset.globe3d;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -26,20 +26,34 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hub-logo-layer--globe {
|
||||
z-index: 2;
|
||||
/* Interactive globe canvas replaces globe + continents + gridOverlay when globe3d=1 */
|
||||
.hub-globe-canvas {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.hub-logo-layer--continents {
|
||||
z-index: 3;
|
||||
.hub-globe-canvas.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.hub-logo-layer--globe {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hub-logo-layer--gridOverlay {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.hub-logo-layer--compass {
|
||||
.hub-logo-layer--continents {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.hub-logo-layer--compass {
|
||||
z-index: 6;
|
||||
left: 54.7%;
|
||||
top: 75.9%;
|
||||
width: 11.5%;
|
||||
@@ -69,3 +83,9 @@
|
||||
[data-theme="light"] .hub-logo-stage {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hub-globe-canvas {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
||||
<link rel="stylesheet" href="hub.css?v=20260530t">
|
||||
<link rel="stylesheet" href="hub-logo-layers.css">
|
||||
<link rel="stylesheet" href="hub.css?v=20260601earth3d">
|
||||
<link rel="stylesheet" href="hub-logo-layers.css?v=20260601earth3d">
|
||||
<script src="assets/analytics.config.js"></script>
|
||||
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
||||
</head>
|
||||
@@ -158,7 +158,7 @@
|
||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
||||
|
||||
var logoBuild = '20260530t';
|
||||
var logoBuild = '20260601earth3d';
|
||||
|
||||
function hubAsset(rel) {
|
||||
var link = document.querySelector('link[href*="hub.css"]');
|
||||
@@ -224,8 +224,12 @@
|
||||
return base + '?v=' + logoBuild;
|
||||
}
|
||||
|
||||
if (logoEnabled) {
|
||||
var useGlobe3d = logoEnabled && (params.get('globe3d') || '1') !== '0';
|
||||
var globe3dSkip = { globe: true, continents: true, gridOverlay: true };
|
||||
|
||||
function appendLogoLayers() {
|
||||
list.forEach(function (name) {
|
||||
if (useGlobe3d && globe3dSkip[name]) return;
|
||||
var img = document.createElement('img');
|
||||
img.className = 'hub-logo-layer hub-logo-layer--' + name;
|
||||
img.src = fragmentSrc(name);
|
||||
@@ -240,6 +244,10 @@
|
||||
}
|
||||
stage.appendChild(img);
|
||||
});
|
||||
}
|
||||
|
||||
if (logoEnabled) {
|
||||
appendLogoLayers();
|
||||
|
||||
var logoOpacity = params.get('logoOpacity');
|
||||
if (logoOpacity) {
|
||||
@@ -248,6 +256,25 @@
|
||||
|
||||
stage.hidden = false;
|
||||
stage.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (useGlobe3d) {
|
||||
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var globeModuleUrl = new URL('assets/hub-globe.js', window.location.href);
|
||||
globeModuleUrl.searchParams.set('v', logoBuild);
|
||||
import(globeModuleUrl.href).then(function (mod) {
|
||||
return mod.mountHubGlobe(stage, { build: logoBuild, reducedMotion: reducedMotion });
|
||||
}).catch(function (err) {
|
||||
console.warn('HubGlobe: falling back to static layers', err);
|
||||
['globe', 'continents', 'gridOverlay'].forEach(function (name) {
|
||||
if (list.indexOf(name) === -1) return;
|
||||
var img = document.createElement('img');
|
||||
img.className = 'hub-logo-layer hub-logo-layer--' + name;
|
||||
img.src = fragmentSrc(name);
|
||||
img.alt = '';
|
||||
stage.appendChild(img);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* Nav dock overlaps globe left hemisphere; stack top → bottom. */
|
||||
|
||||
Reference in New Issue
Block a user