1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 03:38:52 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-06 18:44:52 +02:00
parent 58c47722f9
commit a9cc29e3ae
2 changed files with 137 additions and 23 deletions

View File

@@ -10,6 +10,12 @@ const DRAG_ROTATION_PER_PX = 0.004;
const OCEAN_FALLBACK = { r: 22, g: 38, b: 60 };
const FRONT_Z = 0.02;
const RASTER_SCALE = 2;
const EQ_W = 1024;
const EQ_H = 512;
/** Hub ortho disk parallels ≈ sin(lat) offset from SVG pole ellipses. */
const GRID_PARALLELS = [0, 0.521, -0.521];
const GRID_MERIDIAN_SPACING = Math.PI / 6;
const GRID_LINE_RAD = 0.012;
function hubAsset(rel, build) {
const link = document.querySelector('link[href*="hub.css"]');
@@ -123,6 +129,92 @@ function isLandPixel(r, g, b, a) {
return a > 12 && r > 100 && g < 140 && b < 140;
}
function isGridPixel(r, g, b, a) {
return a > 10 && r + g + b > 280;
}
function bodyLonLat(tex) {
return {
lon: Math.atan2(tex.x, tex.z),
lat: Math.asin(Math.max(-1, Math.min(1, tex.y))),
};
}
function sampleEquator(map, lon, lat) {
const u = (lon + Math.PI) / (2 * Math.PI);
const v = 0.5 - lat / Math.PI;
return sampleImageDataBilinear(map.data, map.w, map.h, u * (map.w - 1), v * (map.h - 1));
}
/** Procedural Mercator grid on wrap-around hemisphere (matches hub 30° meridians + pole ellipses). */
function analyticGridRgba(lon, lat) {
const wrapped = ((lon + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
const cell = wrapped % GRID_MERIDIAN_SPACING;
const merDist = Math.min(cell, GRID_MERIDIAN_SPACING - cell);
const onMeridian = merDist < GRID_LINE_RAD;
let onParallel = false;
let bright = false;
for (let i = 0; i < GRID_PARALLELS.length; i++) {
const targetLat = Math.asin(Math.max(-1, Math.min(1, GRID_PARALLELS[i])));
if (Math.abs(lat - targetLat) < GRID_LINE_RAD) {
onParallel = true;
bright = i === 0;
}
}
if (!onMeridian && !onParallel) {
return [0, 0, 0, 0];
}
if (onMeridian) {
const prime = merDist < GRID_LINE_RAD && (Math.abs(lon) < GRID_LINE_RAD || Math.abs(Math.abs(lon) - Math.PI) < GRID_LINE_RAD);
return prime ? [238, 245, 255, 220] : [238, 245, 255, 170];
}
return bright ? [219, 231, 246, 190] : [231, 236, 243, 130];
}
/**
* Equirect grid: ortho hub artwork on front body face + analytic lines on back wrap.
*/
function bakeGridEquator(gridOverlay, globe) {
const out = new Uint8ClampedArray(EQ_W * EQ_H * 4);
for (let j = 0; j < EQ_H; j++) {
for (let i = 0; i < EQ_W; i++) {
const lon = (i / EQ_W) * 2 * Math.PI - Math.PI;
const lat = Math.PI * 0.5 - (j / EQ_H) * Math.PI;
const cosLat = Math.cos(lat);
const tex = toTextureBody({
x: cosLat * Math.sin(lon),
y: Math.sin(lat),
z: cosLat * Math.cos(lon),
});
const p = (j * EQ_W + i) * 4;
let rgba = [0, 0, 0, 0];
if (tex.z > FRONT_Z) {
const overlay = sampleOverlayRgba(gridOverlay, tex);
if (isGridPixel(overlay[0], overlay[1], overlay[2], overlay[3])) {
rgba = overlay;
} else {
const hp = hubPixelF(tex);
const sx = (hp.x / GLOBE_VIEW.imgW) * (globe.w - 1);
const sy = (hp.y / GLOBE_VIEW.imgH) * (globe.h - 1);
const base = sampleImageDataBilinear(globe.data, globe.w, globe.h, sx, sy);
if (isGridPixel(base[0], base[1], base[2], base[3])) {
rgba = base;
}
}
} else {
rgba = analyticGridRgba(lon, lat);
}
out[p] = rgba[0];
out[p + 1] = rgba[1];
out[p + 2] = rgba[2];
out[p + 3] = rgba[3];
}
}
return { data: out, w: EQ_W, h: EQ_H };
}
function sampleOceanRgba(globe, tex) {
const hp = hubPixelF(tex);
const sx = (hp.x / GLOBE_VIEW.imgW) * (globe.w - 1);
@@ -143,17 +235,18 @@ function sampleOverlayRgba(layer, tex) {
function sampleLandRgba(continents, mercFlat, body) {
const tex = toTextureBody(body);
const hp = hubPixelF(tex);
const sx = (hp.x / GLOBE_VIEW.imgW) * (continents.w - 1);
const sy = (hp.y / GLOBE_VIEW.imgH) * (continents.h - 1);
const ortho = sampleImageDataBilinear(continents.data, continents.w, continents.h, sx, sy);
if (ortho[3] > 6) {
return ortho;
if (tex.z > FRONT_Z) {
const hp = hubPixelF(tex);
const sx = (hp.x / GLOBE_VIEW.imgW) * (continents.w - 1);
const sy = (hp.y / GLOBE_VIEW.imgH) * (continents.h - 1);
const ortho = sampleImageDataBilinear(continents.data, continents.w, continents.h, sx, sy);
if (ortho[3] > 6) {
return ortho;
}
}
const lon = Math.atan2(tex.x, tex.z);
const lat = Math.asin(Math.max(-1, Math.min(1, tex.y)));
const uv = mercatorFlatUV(lon, lat, mercFlat.w, mercFlat.h);
const ll = bodyLonLat(tex);
const uv = mercatorFlatUV(ll.lon, ll.lat, mercFlat.w, mercFlat.h);
const flat = sampleImageDataBilinear(mercFlat.data, mercFlat.w, mercFlat.h, uv.x, uv.y);
if (isLandPixel(flat[0], flat[1], flat[2], flat[3])) {
return [flat[0], flat[1], flat[2], Math.min(255, flat[3])];
@@ -162,6 +255,31 @@ function sampleLandRgba(continents, mercFlat, body) {
return null;
}
function sampleGridRgba(body, gridEq, gridOverlay, globe) {
const tex = toTextureBody(body);
if (tex.z > FRONT_Z) {
const overlay = sampleOverlayRgba(gridOverlay, tex);
if (overlay[3] >= 10) {
return overlay;
}
const hp = hubPixelF(tex);
const sx = (hp.x / GLOBE_VIEW.imgW) * (globe.w - 1);
const sy = (hp.y / GLOBE_VIEW.imgH) * (globe.h - 1);
const base = sampleImageDataBilinear(globe.data, globe.w, globe.h, sx, sy);
if (isGridPixel(base[0], base[1], base[2], base[3])) {
return base;
}
return [0, 0, 0, 0];
}
const ll = bodyLonLat(tex);
const eq = sampleEquator(gridEq, ll.lon, ll.lat);
if (eq[3] < 8) {
return [0, 0, 0, 0];
}
return eq;
}
function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
const r2 = r * r;
const x0 = Math.max(0, Math.floor(cx - r));
@@ -214,21 +332,19 @@ function blitLayer(ctx, cx, cy, r, layer) {
function drawOceanLayer(ctx, cx, cy, r, rotY, globe) {
const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) {
if (body.z <= FRONT_Z) {
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
const tex = toTextureBody(body);
if (tex.z > FRONT_Z) {
return sampleOceanRgba(globe, tex);
}
return sampleOceanRgba(globe, toTextureBody(body));
return [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255];
});
blitLayer(ctx, cx, cy, r, layer);
}
function drawGridLayer(ctx, cx, cy, r, rotY, gridOverlay) {
function drawGridLayer(ctx, cx, cy, r, rotY, gridEq, gridOverlay, globe) {
const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) {
if (body.z <= FRONT_Z) {
return [0, 0, 0, 0];
}
const px = sampleOverlayRgba(gridOverlay, toTextureBody(body));
if (px[3] < 10) {
const px = sampleGridRgba(body, gridEq, gridOverlay, globe);
if (px[3] < 8) {
return [0, 0, 0, 0];
}
return px;
@@ -238,9 +354,6 @@ function drawGridLayer(ctx, cx, cy, r, rotY, gridOverlay) {
function drawLandLayer(ctx, cx, cy, r, rotY, continents, mercFlat) {
const layer = fillDiskRegion(ctx, cx, cy, r, rotY, function (body) {
if (body.z <= FRONT_Z) {
return [0, 0, 0, 0];
}
const rgba = sampleLandRgba(continents, mercFlat, body);
if (!rgba) {
return [0, 0, 0, 0];
@@ -304,6 +417,7 @@ export async function mountHubGlobe(stage, opts) {
const mercW = (mercatorFlatImg.naturalWidth || 800) * 2;
const mercH = (mercatorFlatImg.naturalHeight || 519) * 2;
const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1);
const gridEq = bakeGridEquator(gridOverlay, globe);
let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0;
let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S;
@@ -342,7 +456,7 @@ export async function mountHubGlobe(stage, opts) {
ctx.clearRect(0, 0, px, px);
drawOceanLayer(ctx, cx, cy, r, rotation, globe);
drawGridLayer(ctx, cx, cy, r, rotation, gridOverlay);
drawGridLayer(ctx, cx, cy, r, rotation, gridEq, gridOverlay, globe);
drawLandLayer(ctx, cx, cy, r, rotation, continents, mercFlat);
drawRim(ctx, globeImg, cx, cy, r, rim, rect.scale * dpr);
ctx.save();

View File

@@ -180,7 +180,7 @@ unset($__platformDir);
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
var logoBuild = '20260606globe6';
var logoBuild = '20260606globe7';
function hubAsset(rel) {
var link = document.querySelector('link[href*="hub.css"]');