mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:38:53 +03:00
sync globe
This commit is contained in:
@@ -13,9 +13,7 @@ Review one layer: `?logoPart=space|globe|full`
|
|||||||
|
|
||||||
Compare Earth grids: `?logoPart=globe&grid=3` (default) or `?logoPart=full&grid=4`
|
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): Canvas2D orthographic sphere — ocean from `hub-logo-fragment-globe.svg`, continents from `hub-logo-continents-mercator-flat.svg` (front hemisphere only), Mercator grid projected in 3D with depth-weighted lines. Slow Y-axis spin (~0.035 rad/s); drag horizontally to rotate. Disable: `?globe3d=0` (static SVG stack). Yaw override: `?globeYawDeg=24`. Deploy `assets/hub-globe.js` with `index.php` (see `DEPLOY.md`).
|
||||||
|
|
||||||
**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).
|
**BE only.** FE `apps.f0xx.org` sends `location /` → `http://artc0.intra.raptor.org:80` (not :8089).
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Hub landing globe — orthographic Canvas2D (matches static SVG stack at rotation 0).
|
* Hub landing globe — orthographic Canvas2D sphere.
|
||||||
* Bakes globe + continents-mercator-globe + grid overlay into an equirectangular map,
|
* Ocean shading from hub-logo-fragment-globe.svg; continents from mercator-flat.svg.
|
||||||
* then scrolls it for Y-axis rotation (same visual language as the flat layers).
|
* Y-axis spin with front-hemisphere-only land; grid lines are 3D-projected with depth weighting.
|
||||||
*/
|
*/
|
||||||
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 };
|
||||||
|
|
||||||
/** Idle spin: ~4× slower than the first Three.js attempt (0.14 → 0.035 rad/s). */
|
|
||||||
const AUTO_SPIN_RAD_S = 0.035;
|
const AUTO_SPIN_RAD_S = 0.035;
|
||||||
const DRAG_ROTATION_PER_PX = 0.004;
|
const DRAG_ROTATION_PER_PX = 0.004;
|
||||||
const EQ_WIDTH = 1024;
|
|
||||||
const EQ_HEIGHT = 512;
|
|
||||||
|
|
||||||
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
|
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
|
||||||
|
const FRONT_Z = 0.02;
|
||||||
|
|
||||||
function hubAsset(rel, build) {
|
function hubAsset(rel, build) {
|
||||||
const link = document.querySelector('link[href*="hub.css"]');
|
const link = document.querySelector('link[href*="hub.css"]');
|
||||||
@@ -53,105 +50,57 @@ function loadImage(url) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function compositeStaticLayers(globeImg, continentsImg, gridImg) {
|
function rasterizeImage(img, w, h) {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
c.width = GLOBE_VIEW.imgW;
|
c.width = w;
|
||||||
c.height = GLOBE_VIEW.imgH;
|
c.height = h;
|
||||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||||
ctx.drawImage(globeImg, 0, 0, c.width, c.height);
|
ctx.drawImage(img, 0, 0, w, h);
|
||||||
ctx.drawImage(continentsImg, 0, 0, c.width, c.height);
|
return ctx.getImageData(0, 0, w, h);
|
||||||
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) {
|
function sampleImageData(imgData, w, h, x, y) {
|
||||||
const w = data.width;
|
const ix = Math.max(0, Math.min(w - 1, x | 0));
|
||||||
const h = data.height;
|
const iy = Math.max(0, Math.min(h - 1, y | 0));
|
||||||
const x = Math.max(0, Math.min(w - 1, Math.round(sx)));
|
const i = (iy * w + ix) * 4;
|
||||||
const y = Math.max(0, Math.min(h - 1, Math.round(sy)));
|
return [
|
||||||
const i = (y * w + x) * 4;
|
imgData[i],
|
||||||
return [data.data[i], data.data[i + 1], data.data[i + 2], data.data[i + 3]];
|
imgData[i + 1],
|
||||||
|
imgData[i + 2],
|
||||||
|
imgData[i + 3],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function sampleMercatorFlat(ctx, lon, lat) {
|
function isLandPixel(r, g, b, a) {
|
||||||
const w = ctx.canvas.width;
|
return a > 8 && r > 120 && g < 130 && b < 130;
|
||||||
const h = ctx.canvas.height;
|
}
|
||||||
|
|
||||||
|
function sampleMercator(merc, lon, lat) {
|
||||||
const u = (lon + Math.PI) / (2 * Math.PI);
|
const u = (lon + Math.PI) / (2 * Math.PI);
|
||||||
const v = 0.5 - lat / Math.PI;
|
const v = 0.5 - lat / Math.PI;
|
||||||
const x = Math.max(0, Math.min(w - 1, Math.round(u * (w - 1))));
|
const x = Math.round(u * (merc.w - 1));
|
||||||
const y = Math.max(0, Math.min(h - 1, Math.round(v * (h - 1))));
|
const y = Math.round(v * (merc.h - 1));
|
||||||
const d = ctx.getImageData(x, y, 1, 1).data;
|
return sampleImageData(merc.data, merc.w, merc.h, x, y);
|
||||||
return [d[0], d[1], d[2], d[3]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function sampleGlobeOcean(globe, xw, yw) {
|
||||||
* Build equirectangular texture from the static orthographic hub artwork.
|
const sx = Math.round(GLOBE_VIEW.cx + xw * GLOBE_VIEW.r);
|
||||||
*/
|
const sy = Math.round(GLOBE_VIEW.cy - yw * GLOBE_VIEW.r);
|
||||||
function bakeEquirectangular(compositeData, mercatorFlatImg) {
|
const rgba = sampleImageData(globe.data, globe.w, globe.h, sx, sy);
|
||||||
const merc = document.createElement('canvas');
|
if (rgba[3] < 8) {
|
||||||
merc.width = mercatorFlatImg.naturalWidth || 800;
|
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
|
||||||
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 = [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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return rgba;
|
||||||
eq.getContext('2d').putImageData(out, 0, 0);
|
|
||||||
return eq;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawEquirectSlice(ctx, eqCanvas, rotation, dx, dy, diam) {
|
function inverseRotateY(nx, ny, nz, rotY) {
|
||||||
const w = eqCanvas.width;
|
const cosR = Math.cos(rotY);
|
||||||
const h = eqCanvas.height;
|
const sinR = Math.sin(rotY);
|
||||||
const ox = ((rotation / (2 * Math.PI)) * w % w + w) % w;
|
return {
|
||||||
const destX = dx - diam * 0.5;
|
x: nx * cosR - nz * sinR,
|
||||||
const destY = dy - diam * 0.5;
|
y: ny,
|
||||||
|
z: nx * sinR + nz * cosR,
|
||||||
ctx.save();
|
};
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(dx, dy, diam * 0.5, 0, Math.PI * 2);
|
|
||||||
ctx.clip();
|
|
||||||
|
|
||||||
const slice1w = w - ox;
|
|
||||||
const destSlice1 = diam * (slice1w / w);
|
|
||||||
ctx.drawImage(eqCanvas, ox, 0, slice1w, h, destX, destY, destSlice1, diam);
|
|
||||||
if (ox > 0.5) {
|
|
||||||
ctx.drawImage(eqCanvas, 0, 0, ox, h, destX + destSlice1, destY, diam - destSlice1, diam);
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotateY(x, y, z, rotY) {
|
function rotateY(x, y, z, rotY) {
|
||||||
@@ -162,80 +111,197 @@ function rotateY(x, y, z, rotY) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function strokeParallel(ctx, cx, cy, r, latRad, rotY, strokeStyle, lineWidth) {
|
/**
|
||||||
const pts = [];
|
* Orthographic disk → world lon/lat → mercator land + globe ocean (front face only).
|
||||||
for (let i = 0; i <= 64; i++) {
|
*/
|
||||||
const lon = -Math.PI + (i / 64) * 2 * Math.PI;
|
function drawGlobeDisk(ctx, cx, cy, r, rotY, merc, globe) {
|
||||||
const cosLat = Math.cos(latRad);
|
const r2 = r * r;
|
||||||
const p = rotateY(cosLat * Math.sin(lon), Math.sin(latRad), cosLat * Math.cos(lon), rotY);
|
const x0 = Math.max(0, Math.floor(cx - r));
|
||||||
if (p.z > 0.04) {
|
const y0 = Math.max(0, Math.floor(cy - r));
|
||||||
pts.push({ x: cx + p.x * r, y: cy - p.y * 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
const d2 = ox * ox + oy * oy;
|
||||||
|
if (d2 > 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 wpos = inverseRotateY(nx, ny, nz, rotY);
|
||||||
|
const lon = Math.atan2(wpos.x, wpos.z);
|
||||||
|
const lat = Math.asin(Math.max(-1, Math.min(1, wpos.y)));
|
||||||
|
|
||||||
|
let r8;
|
||||||
|
let g8;
|
||||||
|
let b8;
|
||||||
|
if (wpos.z > FRONT_Z) {
|
||||||
|
const land = sampleMercator(merc, lon, lat);
|
||||||
|
if (isLandPixel(land[0], land[1], land[2], land[3])) {
|
||||||
|
r8 = land[0];
|
||||||
|
g8 = land[1];
|
||||||
|
b8 = land[2];
|
||||||
|
} else {
|
||||||
|
const ocean = sampleGlobeOcean(globe, wpos.x, wpos.y);
|
||||||
|
r8 = ocean[0];
|
||||||
|
g8 = ocean[1];
|
||||||
|
b8 = ocean[2];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r8 = OCEAN_FALLBACK.r;
|
||||||
|
g8 = OCEAN_FALLBACK.g;
|
||||||
|
b8 = OCEAN_FALLBACK.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
px[p++] = r8;
|
||||||
|
px[p++] = g8;
|
||||||
|
px[p++] = b8;
|
||||||
|
px[p++] = 255;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pts.length < 2) return;
|
|
||||||
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(pts[0].x, pts[0].y);
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
for (let k = 1; k < pts.length; k++) ctx.lineTo(pts[k].x, pts[k].y);
|
ctx.clip();
|
||||||
ctx.strokeStyle = strokeStyle;
|
ctx.putImageData(out, x0, y0);
|
||||||
ctx.lineWidth = lineWidth;
|
ctx.restore();
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function strokeMeridian(ctx, cx, cy, r, lonRad, rotY, strokeStyle, lineWidth) {
|
function strokeCurve3D(ctx, cx, cy, r, rotY, pointAt, styleAt) {
|
||||||
const pts = [];
|
let prev = null;
|
||||||
for (let i = 0; i <= 64; i++) {
|
const steps = 72;
|
||||||
const lat = -Math.PI / 2 + (i / 64) * Math.PI;
|
for (let i = 0; i <= steps; i++) {
|
||||||
const cosLat = Math.cos(lat);
|
const raw = pointAt(i / steps);
|
||||||
const p = rotateY(cosLat * Math.sin(lonRad), Math.sin(lat), cosLat * Math.cos(lonRad), rotY);
|
const p = rotateY(raw.x, raw.y, raw.z, rotY);
|
||||||
if (p.z > 0.04) {
|
if (p.z <= FRONT_Z) {
|
||||||
pts.push({ x: cx + p.x * r, y: cy - p.y * r });
|
prev = null;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
const sx = cx + p.x * r;
|
||||||
|
const sy = cy - p.y * r;
|
||||||
|
if (prev) {
|
||||||
|
const zMid = (prev.z + p.z) * 0.5;
|
||||||
|
const style = styleAt(zMid, i, steps);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(prev.sx, prev.sy);
|
||||||
|
ctx.lineTo(sx, sy);
|
||||||
|
ctx.strokeStyle = style.color;
|
||||||
|
ctx.lineWidth = style.width;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
prev = { sx, sy, z: p.z };
|
||||||
}
|
}
|
||||||
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) {
|
function drawGridOverlay(ctx, rect, rotY) {
|
||||||
const cx = rect.cx;
|
const cx = rect.cx;
|
||||||
const cy = rect.cy;
|
const cy = rect.cy;
|
||||||
const r = rect.r;
|
const r = rect.r;
|
||||||
|
const scale = rect.scale;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
|
||||||
const equatorGrad = ctx.createLinearGradient(cx - r, cy, cx + r, cy);
|
const lwBase = Math.max(2.5, scale * 0.008);
|
||||||
equatorGrad.addColorStop(0, 'rgba(39,54,75,0.74)');
|
const lwPrime = Math.max(3, scale * 0.0095);
|
||||||
equatorGrad.addColorStop(0.5, 'rgba(219,231,246,0.88)');
|
const lwThin = Math.max(1.5, scale * 0.0055);
|
||||||
equatorGrad.addColorStop(1, 'rgba(31,44,64,0.74)');
|
|
||||||
|
|
||||||
const meridianStroke = 'rgba(238,245,255,0.92)';
|
function depthStyle(baseWidth, centerAlpha, edgeAlpha) {
|
||||||
const primeStroke = 'rgba(238,245,255,0.98)';
|
return function (z) {
|
||||||
const parallelStroke = 'rgba(231,236,243,0.72)';
|
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (1 - FRONT_Z)));
|
||||||
const lw = Math.max(2.5, rect.scale * 0.008);
|
const alpha = edgeAlpha + (centerAlpha - edgeAlpha) * t;
|
||||||
const lwPrime = Math.max(3, rect.scale * 0.0095);
|
return {
|
||||||
const lwThin = Math.max(1.5, rect.scale * 0.0055);
|
width: baseWidth * (0.35 + 0.65 * t),
|
||||||
|
color: 'rgba(238,245,255,' + alpha.toFixed(3) + ')',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
strokeParallel(ctx, cx, cy, r, 0, rotY, equatorGrad, Math.max(2, rect.scale * 0.007));
|
strokeCurve3D(
|
||||||
strokeParallel(ctx, cx, cy, r, Math.PI * 0.25, rotY, parallelStroke, lwThin);
|
ctx,
|
||||||
strokeParallel(ctx, cx, cy, r, -Math.PI * 0.25, rotY, parallelStroke, lwThin);
|
cx,
|
||||||
|
cy,
|
||||||
|
r,
|
||||||
|
rotY,
|
||||||
|
function (t) {
|
||||||
|
const lon = -Math.PI + t * 2 * Math.PI;
|
||||||
|
return { x: Math.sin(lon), y: 0, z: Math.cos(lon) };
|
||||||
|
},
|
||||||
|
function (z) {
|
||||||
|
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (1 - FRONT_Z)));
|
||||||
|
const alpha = 0.45 + 0.43 * t;
|
||||||
|
return {
|
||||||
|
width: Math.max(2, scale * 0.007) * (0.4 + 0.6 * t),
|
||||||
|
color: 'rgba(219,231,246,' + alpha.toFixed(3) + ')',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
[-0.25, 0.25].forEach(function (latFrac) {
|
||||||
|
const latRad = latFrac * Math.PI;
|
||||||
|
const cosLat = Math.cos(latRad);
|
||||||
|
const sinLat = Math.sin(latRad);
|
||||||
|
const styleFn = depthStyle(lwThin, 0.55, 0.2);
|
||||||
|
strokeCurve3D(
|
||||||
|
ctx,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
r,
|
||||||
|
rotY,
|
||||||
|
function (t) {
|
||||||
|
const lon = -Math.PI + t * 2 * Math.PI;
|
||||||
|
return { x: cosLat * Math.sin(lon), y: sinLat, z: cosLat * Math.cos(lon) };
|
||||||
|
},
|
||||||
|
function (z) {
|
||||||
|
const s = styleFn(z);
|
||||||
|
return { width: s.width, color: 'rgba(231,236,243,' + (0.2 + 0.52 * Math.max(0, (z - FRONT_Z) / (1 - FRONT_Z))).toFixed(3) + ')' };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
for (let lonDeg = -150; lonDeg <= 150; lonDeg += 30) {
|
for (let lonDeg = -150; lonDeg <= 150; lonDeg += 30) {
|
||||||
const lon = (lonDeg * Math.PI) / 180;
|
const lonRad = (lonDeg * Math.PI) / 180;
|
||||||
const isPrime = lonDeg === 0;
|
const isPrime = lonDeg === 0;
|
||||||
strokeMeridian(ctx, cx, cy, r, lon, rotY, isPrime ? primeStroke : meridianStroke, isPrime ? lwPrime : lw);
|
const styleFn = depthStyle(isPrime ? lwPrime : lwBase, isPrime ? 0.98 : 0.78, 0.18);
|
||||||
|
strokeCurve3D(
|
||||||
|
ctx,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
r,
|
||||||
|
rotY,
|
||||||
|
function (t) {
|
||||||
|
const lat = -Math.PI / 2 + t * Math.PI;
|
||||||
|
const cosLat = Math.cos(lat);
|
||||||
|
return { x: cosLat * Math.sin(lonRad), y: Math.sin(lat), z: cosLat * Math.cos(lonRad) };
|
||||||
|
},
|
||||||
|
function (z) {
|
||||||
|
const s = styleFn(z);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
|
ctx.arc(cx, cy, Math.max(3, scale * 0.01), 0, Math.PI * 2);
|
||||||
ctx.fillStyle = '#5eead4';
|
ctx.fillStyle = '#5eead4';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.strokeStyle = '#0a0e14';
|
ctx.strokeStyle = '#0a0e14';
|
||||||
@@ -245,24 +311,24 @@ function drawGridOverlay(ctx, rect, rotY) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRim(ctx, compositeCanvas, cx, cy, r, rim, scale) {
|
function drawRim(ctx, globeImg, cx, cy, r, rim, layoutScale) {
|
||||||
const drawW = GLOBE_VIEW.imgW * scale;
|
const drawW = GLOBE_VIEW.imgW * layoutScale;
|
||||||
const drawH = GLOBE_VIEW.imgH * scale;
|
const drawH = GLOBE_VIEW.imgH * layoutScale;
|
||||||
const dx = cx - GLOBE_VIEW.cx * scale;
|
const dx = cx - GLOBE_VIEW.cx * layoutScale;
|
||||||
const dy = cy - GLOBE_VIEW.cy * scale;
|
const dy = cy - GLOBE_VIEW.cy * layoutScale;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, rim, 0, Math.PI * 2);
|
ctx.arc(cx, cy, rim, 0, Math.PI * 2);
|
||||||
ctx.arc(cx, cy, r, 0, Math.PI * 2, true);
|
ctx.arc(cx, cy, r, 0, Math.PI * 2, true);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
ctx.drawImage(compositeCanvas, 0, 0, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, dx, dy, drawW, drawH);
|
ctx.drawImage(globeImg, 0, 0, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH, dx, dy, drawW, drawH);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} stage
|
* @param {HTMLElement} stage
|
||||||
* @param {{ build: string, reducedMotion?: boolean }} opts
|
* @param {{ build: string, reducedMotion?: boolean, initialRotationY?: number }} opts
|
||||||
*/
|
*/
|
||||||
export async function mountHubGlobe(stage, opts) {
|
export async function mountHubGlobe(stage, opts) {
|
||||||
const build = opts.build || 'earth3d';
|
const build = opts.build || 'earth3d';
|
||||||
@@ -275,20 +341,14 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
|
|
||||||
const urls = {
|
const urls = {
|
||||||
globe: hubAsset('assets/hub-logo-fragment-globe.svg', build),
|
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),
|
mercatorFlat: hubAsset('assets/hub-logo-continents-mercator-flat.svg', build),
|
||||||
};
|
};
|
||||||
|
|
||||||
let globeImg;
|
let globeImg;
|
||||||
let continentsImg;
|
|
||||||
let gridImg;
|
|
||||||
let mercatorFlatImg;
|
let mercatorFlatImg;
|
||||||
try {
|
try {
|
||||||
[globeImg, continentsImg, gridImg, mercatorFlatImg] = await Promise.all([
|
[globeImg, mercatorFlatImg] = await Promise.all([
|
||||||
loadImage(urls.globe),
|
loadImage(urls.globe),
|
||||||
loadImage(urls.continents),
|
|
||||||
loadImage(urls.grid),
|
|
||||||
loadImage(urls.mercatorFlat),
|
loadImage(urls.mercatorFlat),
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -297,10 +357,18 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const composite = compositeStaticLayers(globeImg, continentsImg, gridImg);
|
const globe = {
|
||||||
const eqCanvas = bakeEquirectangular(composite.data, mercatorFlatImg);
|
w: GLOBE_VIEW.imgW,
|
||||||
|
h: GLOBE_VIEW.imgH,
|
||||||
|
data: rasterizeImage(globeImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH).data,
|
||||||
|
};
|
||||||
|
const merc = {
|
||||||
|
w: mercatorFlatImg.naturalWidth || 800,
|
||||||
|
h: mercatorFlatImg.naturalHeight || 519,
|
||||||
|
data: rasterizeImage(mercatorFlatImg, mercatorFlatImg.naturalWidth || 800, mercatorFlatImg.naturalHeight || 519).data,
|
||||||
|
};
|
||||||
|
|
||||||
let rotation = 0;
|
let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0;
|
||||||
let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S;
|
let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
let lastPointerX = 0;
|
let lastPointerX = 0;
|
||||||
@@ -308,7 +376,7 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
let raf = 0;
|
let raf = 0;
|
||||||
let lastFrame = performance.now();
|
let lastFrame = performance.now();
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
const rect = globeCoverRect();
|
const rect = globeCoverRect();
|
||||||
@@ -340,12 +408,11 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
r,
|
r,
|
||||||
rim,
|
rim,
|
||||||
scale: rect.scale * dpr,
|
scale: rect.scale * dpr,
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.clearRect(0, 0, px, px);
|
ctx.clearRect(0, 0, px, px);
|
||||||
drawEquirectSlice(ctx, eqCanvas, rotation, cx, cy, r * 2);
|
drawGlobeDisk(ctx, cx, cy, r, rotation, merc, globe);
|
||||||
|
drawRim(ctx, globeImg, cx, cy, r, rim, rect.scale * dpr);
|
||||||
drawGridOverlay(ctx, drawRect, rotation);
|
drawGridOverlay(ctx, drawRect, rotation);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ unset($__platformDir);
|
|||||||
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 = '20260606landing';
|
var logoBuild = '20260606globe';
|
||||||
|
|
||||||
function hubAsset(rel) {
|
function hubAsset(rel) {
|
||||||
var link = document.querySelector('link[href*="hub.css"]');
|
var link = document.querySelector('link[href*="hub.css"]');
|
||||||
|
|||||||
Reference in New Issue
Block a user