mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
sync globe
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* Hub landing globe — orthographic Canvas2D sphere.
|
||||
* Matches globe3d=0 reference: ocean from hub-logo-fragment-globe.svg,
|
||||
* continents from hub-logo-continents-mercator-globe.svg (body-fixed orthographic disk).
|
||||
* Y-axis spin; land only on front body face (z > 0); grid projected in 3D with depth weighting.
|
||||
* Reference stack (globe3d=0): hub-logo-fragment-globe + continents-mercator-globe.
|
||||
* Body frame uses 180° flip around equator (X) so N/S matches the static hub.
|
||||
*/
|
||||
export const GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 322, rim: 330 };
|
||||
|
||||
const AUTO_SPIN_RAD_S = 0.035;
|
||||
const DRAG_ROTATION_PER_PX = 0.004;
|
||||
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
|
||||
const FRONT_Z = 0.02;
|
||||
const VIEW_Z_EPS = 0.001;
|
||||
const BODY_FRONT_Z = 0.001;
|
||||
|
||||
function hubAsset(rel, build) {
|
||||
const link = document.querySelector('link[href*="hub.css"]');
|
||||
@@ -67,18 +67,22 @@ function sampleImageData(imgData, w, h, x, y) {
|
||||
return [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];
|
||||
}
|
||||
|
||||
/** View unit sphere (nx,ny,nz) → body-fixed unit sphere before Y spin. */
|
||||
/** 180° about equator (X): swap S↔N to match globe3d=0 hub orientation. */
|
||||
function flipSouthNorth(p) {
|
||||
return { x: p.x, y: -p.y, z: -p.z };
|
||||
}
|
||||
|
||||
function inverseRotateY(nx, ny, nz, rotY) {
|
||||
const cosR = Math.cos(rotY);
|
||||
const sinR = Math.sin(rotY);
|
||||
return {
|
||||
return flipSouthNorth({
|
||||
x: nx * cosR - nz * sinR,
|
||||
y: ny,
|
||||
z: nx * sinR + nz * cosR,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function rotateY(x, y, z, rotY) {
|
||||
function rotateYRaw(x, y, z, rotY) {
|
||||
return {
|
||||
x: x * Math.cos(rotY) + z * Math.sin(rotY),
|
||||
y,
|
||||
@@ -86,7 +90,15 @@ function rotateY(x, y, z, rotY) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Orthographic hub disk coords for body point on unit sphere (same as static SVG stack). */
|
||||
function sphereLatLon(lon, lat) {
|
||||
const cosLat = Math.cos(lat);
|
||||
return flipSouthNorth({
|
||||
x: cosLat * Math.sin(lon),
|
||||
y: Math.sin(lat),
|
||||
z: cosLat * Math.cos(lon),
|
||||
});
|
||||
}
|
||||
|
||||
function bodyToHubPixel(xb, yb) {
|
||||
return {
|
||||
x: Math.round(GLOBE_VIEW.cx + xb * GLOBE_VIEW.r),
|
||||
@@ -94,30 +106,55 @@ function bodyToHubPixel(xb, yb) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample globe3d=0 continent + ocean stack in body-fixed frame.
|
||||
* Continents layer is authoritative where opaque; else globe ocean shading.
|
||||
*/
|
||||
function sampleBodySurface(continents, globe, xb, yb, zb) {
|
||||
if (zb <= FRONT_Z) {
|
||||
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b];
|
||||
function sampleMercatorFlat(merc, lon, lat) {
|
||||
const u = (lon + Math.PI) / (2 * Math.PI);
|
||||
const v = 0.5 - lat / Math.PI;
|
||||
const x = Math.round(u * (merc.w - 1));
|
||||
const y = Math.round(v * (merc.h - 1));
|
||||
return sampleImageData(merc.data, merc.w, merc.h, x, y);
|
||||
}
|
||||
const p = bodyToHubPixel(xb, yb);
|
||||
const land = sampleImageData(continents.data, continents.w, continents.h, p.x, p.y);
|
||||
if (land[3] > 24) {
|
||||
return [land[0], land[1], land[2]];
|
||||
|
||||
function isLandPixel(r, g, b, a) {
|
||||
return a > 12 && r > 100 && g < 140 && b < 140;
|
||||
}
|
||||
const ocean = sampleImageData(globe.data, globe.w, globe.h, p.x, p.y);
|
||||
if (ocean[3] < 8) {
|
||||
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b];
|
||||
}
|
||||
return [ocean[0], ocean[1], ocean[2]];
|
||||
|
||||
function bodyLonLat(xb, yb, zb) {
|
||||
return {
|
||||
lon: Math.atan2(xb, zb),
|
||||
lat: Math.asin(Math.max(-1, Math.min(1, yb))),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible orthographic disk → inverse Y-rotation → body-fixed texture (globe3d=0 reference).
|
||||
* globe3d=0 orthographic disk (primary) + mercator-flat lon/lat fill during rotation.
|
||||
*/
|
||||
function drawGlobeDisk(ctx, cx, cy, r, rotY, continents, globe) {
|
||||
function sampleBodySurface(continents, globe, mercFlat, xb, yb, zb) {
|
||||
const p = bodyToHubPixel(xb, yb);
|
||||
const ocean = sampleImageData(globe.data, globe.w, globe.h, p.x, p.y);
|
||||
const oceanRgb =
|
||||
ocean[3] < 8
|
||||
? [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b]
|
||||
: [ocean[0], ocean[1], ocean[2]];
|
||||
|
||||
if (zb <= BODY_FRONT_Z) {
|
||||
return oceanRgb;
|
||||
}
|
||||
|
||||
const ortho = sampleImageData(continents.data, continents.w, continents.h, p.x, p.y);
|
||||
if (ortho[3] > 20) {
|
||||
return [ortho[0], ortho[1], ortho[2]];
|
||||
}
|
||||
|
||||
const ll = bodyLonLat(xb, yb, zb);
|
||||
const flat = sampleMercatorFlat(mercFlat, ll.lon, ll.lat);
|
||||
if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) {
|
||||
return [flat[0], flat[1], flat[2]];
|
||||
}
|
||||
|
||||
return oceanRgb;
|
||||
}
|
||||
|
||||
function drawGlobeDisk(ctx, cx, cy, r, rotY, continents, globe, mercFlat) {
|
||||
const r2 = r * r;
|
||||
const x0 = Math.max(0, Math.floor(cx - r));
|
||||
const y0 = Math.max(0, Math.floor(cy - r));
|
||||
@@ -147,7 +184,7 @@ function drawGlobeDisk(ctx, cx, cy, r, rotY, continents, globe) {
|
||||
const ny = oy / r;
|
||||
const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));
|
||||
const body = inverseRotateY(nx, ny, nz, rotY);
|
||||
const rgb = sampleBodySurface(continents, globe, body.x, body.y, body.z);
|
||||
const rgb = sampleBodySurface(continents, globe, mercFlat, body.x, body.y, body.z);
|
||||
|
||||
px[p++] = rgb[0];
|
||||
px[p++] = rgb[1];
|
||||
@@ -164,31 +201,61 @@ function drawGlobeDisk(ctx, cx, cy, r, rotY, continents, globe) {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function strokeCurve3D(ctx, cx, cy, r, rotY, pointAt, styleAt) {
|
||||
let prev = null;
|
||||
const steps = 72;
|
||||
/** Draw each contiguous visible arc of a 3D curve (latitude circles stay on the surface). */
|
||||
function strokeVisibleArcs3D(ctx, cx, cy, r, rotY, pointAt, styleAt, steps) {
|
||||
const samples = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const raw = pointAt(i / steps);
|
||||
const p = rotateY(raw.x, raw.y, raw.z, rotY);
|
||||
if (p.z <= FRONT_Z) {
|
||||
prev = null;
|
||||
const p = rotateYRaw(raw.x, raw.y, raw.z, rotY);
|
||||
if (p.z <= VIEW_Z_EPS) {
|
||||
samples.push(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);
|
||||
samples.push({
|
||||
sx: cx + p.x * r,
|
||||
sy: cy - p.y * r,
|
||||
z: p.z,
|
||||
});
|
||||
}
|
||||
|
||||
let run = [];
|
||||
function flushRun() {
|
||||
if (run.length < 2) {
|
||||
run = [];
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i < run.length; i++) {
|
||||
const a = run[i - 1];
|
||||
const b = run[i];
|
||||
const style = styleAt((a.z + b.z) * 0.5);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(prev.sx, prev.sy);
|
||||
ctx.lineTo(sx, sy);
|
||||
ctx.moveTo(a.sx, a.sy);
|
||||
ctx.lineTo(b.sx, b.sy);
|
||||
ctx.strokeStyle = style.color;
|
||||
ctx.lineWidth = style.width;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.stroke();
|
||||
}
|
||||
prev = { sx, sy, z: p.z };
|
||||
run = [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
if (!samples[i]) {
|
||||
flushRun();
|
||||
} else {
|
||||
run.push(samples[i]);
|
||||
}
|
||||
}
|
||||
flushRun();
|
||||
}
|
||||
|
||||
function depthStyle(baseWidth, centerAlpha, edgeAlpha, z) {
|
||||
const t = Math.max(0, Math.min(1, z / 0.95));
|
||||
return {
|
||||
width: baseWidth * (0.3 + 0.7 * t),
|
||||
color: 'rgba(238,245,255,' + (edgeAlpha + (centerAlpha - edgeAlpha) * t).toFixed(3) + ')',
|
||||
};
|
||||
}
|
||||
|
||||
function drawGridOverlay(ctx, rect, rotY) {
|
||||
@@ -196,6 +263,7 @@ function drawGridOverlay(ctx, rect, rotY) {
|
||||
const cy = rect.cy;
|
||||
const r = rect.r;
|
||||
const scale = rect.scale;
|
||||
const steps = 128;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
@@ -205,19 +273,9 @@ function drawGridOverlay(ctx, rect, rotY) {
|
||||
const lwBase = Math.max(2.5, scale * 0.008);
|
||||
const lwPrime = Math.max(3, scale * 0.0095);
|
||||
const lwThin = Math.max(1.5, scale * 0.0055);
|
||||
const lwEq = Math.max(2, scale * 0.007);
|
||||
|
||||
function depthStyle(baseWidth, centerAlpha, edgeAlpha) {
|
||||
return function (z) {
|
||||
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (1 - FRONT_Z)));
|
||||
const alpha = edgeAlpha + (centerAlpha - edgeAlpha) * t;
|
||||
return {
|
||||
width: baseWidth * (0.35 + 0.65 * t),
|
||||
color: 'rgba(238,245,255,' + alpha.toFixed(3) + ')',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
strokeCurve3D(
|
||||
strokeVisibleArcs3D(
|
||||
ctx,
|
||||
cx,
|
||||
cy,
|
||||
@@ -225,23 +283,21 @@ function drawGridOverlay(ctx, rect, rotY) {
|
||||
rotY,
|
||||
function (t) {
|
||||
const lon = -Math.PI + t * 2 * Math.PI;
|
||||
return { x: Math.sin(lon), y: 0, z: Math.cos(lon) };
|
||||
return sphereLatLon(lon, 0);
|
||||
},
|
||||
function (z) {
|
||||
const t = Math.max(0, Math.min(1, (z - FRONT_Z) / (1 - FRONT_Z)));
|
||||
const alpha = 0.45 + 0.43 * t;
|
||||
const t = Math.max(0, Math.min(1, z / 0.95));
|
||||
return {
|
||||
width: Math.max(2, scale * 0.007) * (0.4 + 0.6 * t),
|
||||
color: 'rgba(219,231,246,' + alpha.toFixed(3) + ')',
|
||||
width: lwEq * (0.35 + 0.65 * t),
|
||||
color: 'rgba(219,231,246,' + (0.42 + 0.46 * t).toFixed(3) + ')',
|
||||
};
|
||||
}
|
||||
},
|
||||
steps
|
||||
);
|
||||
|
||||
[-0.25, 0.25].forEach(function (latFrac) {
|
||||
const latRad = latFrac * Math.PI;
|
||||
const cosLat = Math.cos(latRad);
|
||||
const sinLat = Math.sin(latRad);
|
||||
strokeCurve3D(
|
||||
const lat = latFrac * Math.PI;
|
||||
strokeVisibleArcs3D(
|
||||
ctx,
|
||||
cx,
|
||||
cy,
|
||||
@@ -249,22 +305,25 @@ function drawGridOverlay(ctx, rect, rotY) {
|
||||
rotY,
|
||||
function (t) {
|
||||
const lon = -Math.PI + t * 2 * Math.PI;
|
||||
return { x: cosLat * Math.sin(lon), y: sinLat, z: cosLat * Math.cos(lon) };
|
||||
return sphereLatLon(lon, lat);
|
||||
},
|
||||
function (z) {
|
||||
const t = Math.max(0, Math.min(1, z / 0.95));
|
||||
return {
|
||||
width: lwThin * (0.35 + 0.65 * Math.max(0, (z - FRONT_Z) / (1 - FRONT_Z))),
|
||||
color: 'rgba(231,236,243,' + (0.2 + 0.52 * Math.max(0, (z - FRONT_Z) / (1 - FRONT_Z))).toFixed(3) + ')',
|
||||
width: lwThin * (0.35 + 0.65 * t),
|
||||
color: 'rgba(231,236,243,' + (0.18 + 0.54 * t).toFixed(3) + ')',
|
||||
};
|
||||
}
|
||||
},
|
||||
steps
|
||||
);
|
||||
});
|
||||
|
||||
for (let lonDeg = -150; lonDeg <= 150; lonDeg += 30) {
|
||||
const lonRad = (lonDeg * Math.PI) / 180;
|
||||
const lon = (lonDeg * Math.PI) / 180;
|
||||
const isPrime = lonDeg === 0;
|
||||
const styleFn = depthStyle(isPrime ? lwPrime : lwBase, isPrime ? 0.98 : 0.78, 0.18);
|
||||
strokeCurve3D(
|
||||
const bw = isPrime ? lwPrime : lwBase;
|
||||
const ca = isPrime ? 0.98 : 0.78;
|
||||
strokeVisibleArcs3D(
|
||||
ctx,
|
||||
cx,
|
||||
cy,
|
||||
@@ -272,12 +331,12 @@ function drawGridOverlay(ctx, rect, rotY) {
|
||||
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) };
|
||||
return sphereLatLon(lon, lat);
|
||||
},
|
||||
function (z) {
|
||||
return styleFn(z);
|
||||
}
|
||||
return depthStyle(bw, ca, 0.15, z);
|
||||
},
|
||||
96
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,14 +382,17 @@ export async function mountHubGlobe(stage, opts) {
|
||||
const urls = {
|
||||
globe: hubAsset('assets/hub-logo-fragment-globe.svg', build),
|
||||
continents: hubAsset('assets/hub-logo-continents-mercator-globe.svg', build),
|
||||
mercatorFlat: hubAsset('assets/hub-logo-continents-mercator-flat.svg', build),
|
||||
};
|
||||
|
||||
let globeImg;
|
||||
let continentsImg;
|
||||
let mercatorFlatImg;
|
||||
try {
|
||||
[globeImg, continentsImg] = await Promise.all([
|
||||
[globeImg, continentsImg, mercatorFlatImg] = await Promise.all([
|
||||
loadImage(urls.globe),
|
||||
loadImage(urls.continents),
|
||||
loadImage(urls.mercatorFlat),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn('HubGlobe: asset load failed', e);
|
||||
@@ -348,6 +410,15 @@ export async function mountHubGlobe(stage, opts) {
|
||||
h: GLOBE_VIEW.imgH,
|
||||
data: rasterizeImage(continentsImg, GLOBE_VIEW.imgW, GLOBE_VIEW.imgH).data,
|
||||
};
|
||||
const mercFlat = {
|
||||
w: mercatorFlatImg.naturalWidth || 800,
|
||||
h: mercatorFlatImg.naturalHeight || 519,
|
||||
data: rasterizeImage(
|
||||
mercatorFlatImg,
|
||||
mercatorFlatImg.naturalWidth || 800,
|
||||
mercatorFlatImg.naturalHeight || 519
|
||||
).data,
|
||||
};
|
||||
|
||||
let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0;
|
||||
let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S;
|
||||
@@ -386,7 +457,7 @@ export async function mountHubGlobe(stage, opts) {
|
||||
const drawRect = { cx, cy, r, rim, scale: rect.scale * dpr };
|
||||
|
||||
ctx.clearRect(0, 0, px, px);
|
||||
drawGlobeDisk(ctx, cx, cy, r, rotation, continents, globe);
|
||||
drawGlobeDisk(ctx, cx, cy, r, rotation, continents, globe, mercFlat);
|
||||
drawRim(ctx, globeImg, cx, cy, r, rim, rect.scale * dpr);
|
||||
drawGridOverlay(ctx, drawRect, rotation);
|
||||
ctx.save();
|
||||
|
||||
@@ -180,7 +180,7 @@ unset($__platformDir);
|
||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
||||
|
||||
var logoBuild = '20260606globe2';
|
||||
var logoBuild = '20260606globe3';
|
||||
|
||||
function hubAsset(rel) {
|
||||
var link = document.querySelector('link[href*="hub.css"]');
|
||||
|
||||
Reference in New Issue
Block a user