1
0
mirror of git://f0xx.org/ac/ac-be-hub synced 2026-08-09 20:59:23 +03:00

feat(globe3d): ?ax spin factor and v11 continent transparency

Expose URL ?ax=N as auto-spin speed multiplier for all interactive globe
versions. On globe3d=11, render semi-transparent continent fill with
brighter coastline/border pixels matching the static SVG art.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-08-05 22:37:20 +02:00
parent 2d31fa4f46
commit 9cfe5ff5bd
2 changed files with 79 additions and 7 deletions

View File

@@ -13,6 +13,7 @@
* globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped. * globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped.
* globe3d=11: v10 grid + geographic mercator-flat land (360° U-wrap). * globe3d=11: v10 grid + geographic mercator-flat land (360° U-wrap).
* ?latf=1.25&lngf=1&zy=1&zx=1&pp=0&pe=0 — land plate zoom + pole/limb lens pinch. * ?latf=1.25&lngf=1&zy=1&zx=1&pp=0&pe=0 — land plate zoom + pole/limb lens pinch.
* ?ax=2 — auto-spin speed factor (1 = default, 2 = double, …).
*/ */
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 };
@@ -52,6 +53,14 @@ function resolveLandZoom(raw) {
}; };
} }
/** URL ?ax=N — auto-spin speed multiplier (default 1). */
function resolveSpinFactor(raw) {
if (typeof raw !== 'number' || !isFinite(raw)) {
return 1;
}
return Math.max(0.05, Math.min(8, raw));
}
/** /**
* Land plate sample remap (globe3d=11 URL tunables): * Land plate sample remap (globe3d=11 URL tunables):
* latf/lngf — linear zoom; zy/zx — equator/prime-meridian mag (1 = uniform). * latf/lngf — linear zoom; zy/zx — equator/prime-meridian mag (1 = uniform).
@@ -1025,6 +1034,27 @@ function hubContinentRgba(sample) {
return [sample[0], sample[1], sample[2], 255]; return [sample[0], sample[1], sample[2], 255];
} }
/** Fill vs stroke from hub-logo-continents-mercator-flat.svg raster. */
function classifyHubContinentPixel(r, g, b, a) {
if (a < 8) {
return null;
}
if (r > 168 && g > 198 && b > 218) {
return null;
}
const lum = r * 0.299 + g * 0.587 + b * 0.114;
if (lum < 92 || lum > 238) {
return null;
}
if (!(b >= 100 && g >= 95 && r >= 68)) {
return null;
}
if (lum >= 186 || (g > 198 && b > 212)) {
return 'border';
}
return 'fill';
}
/** Solid fill from rasterized globe SVG — skip gradient fringe / drop-shadow. */ /** Solid fill from rasterized globe SVG — skip gradient fringe / drop-shadow. */
function isSolidContinentPixel(r, g, b, a) { function isSolidContinentPixel(r, g, b, a) {
if (a < 72) { if (a < 72) {
@@ -1107,16 +1137,43 @@ function sampleLandRgbaMercator(mercFlat, tex) {
return null; return null;
} }
/** globe3d=11 — geographic mercator wrap (360°); z > LIMB_Z only. */ /** globe3d=11 — geographic mercator wrap; semi-transparent fill + contrast borders. */
function sampleLandVisibleHemisphere(mercFlat, tex, landZoom) { function sampleLandVisibleHemisphere(mercFlat, tex, landZoom) {
if (tex.z <= LIMB_Z) { if (tex.z <= LIMB_Z) {
return null; return null;
} }
const flat = sampleMercFlatFromTex(mercFlat, tex, landZoom); const flat = sampleMercFlatFromTex(mercFlat, tex, landZoom);
if (isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) { let kind = classifyHubContinentPixel(flat[0], flat[1], flat[2], flat[3]);
return hubContinentRgba(flat); if (!kind) {
return null;
} }
return null; if (kind === 'fill') {
const ll = texLonLat(tex);
const mapped = landZoomLonLat(ll.lon, ll.lat, landZoom, tex);
const eps = 0.0035;
const neighbors = [
sampleMercFlat(mercFlat, mapped.lon + eps, mapped.lat),
sampleMercFlat(mercFlat, mapped.lon - eps, mapped.lat),
sampleMercFlat(mercFlat, mapped.lon, mapped.lat + eps),
sampleMercFlat(mercFlat, mapped.lon, mapped.lat - eps),
];
for (let i = 0; i < neighbors.length; i++) {
const n = neighbors[i];
if (!classifyHubContinentPixel(n[0], n[1], n[2], n[3])) {
kind = 'border';
break;
}
}
}
if (kind === 'border') {
return [
Math.min(255, flat[0] + 18),
Math.min(255, flat[1] + 14),
Math.min(255, flat[2] + 8),
232,
];
}
return [flat[0], flat[1], flat[2], 148];
} }
function fillDiskRegion(ctx, cx, cy, r, rotY, fn) { function fillDiskRegion(ctx, cx, cy, r, rotY, fn) {
@@ -1219,6 +1276,8 @@ export async function mountHubGlobe(stage, opts) {
const reducedMotion = !!opts.reducedMotion; const reducedMotion = !!opts.reducedMotion;
const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1); const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1);
const landZoom = resolveLandZoom(opts.landZoom); const landZoom = resolveLandZoom(opts.landZoom);
const spinFactor = resolveSpinFactor(opts.spinFactor);
const spinTarget = AUTO_SPIN_RAD_S * spinFactor;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.className = 'hub-globe-canvas'; canvas.className = 'hub-globe-canvas';
@@ -1253,7 +1312,7 @@ export async function mountHubGlobe(stage, opts) {
const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1); const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1);
let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0; let rotation = typeof opts.initialRotationY === 'number' ? opts.initialRotationY : 0;
let angularVelocity = reducedMotion ? 0 : AUTO_SPIN_RAD_S; let angularVelocity = reducedMotion ? 0 : spinTarget;
let dragging = false; let dragging = false;
let lastPointerX = 0; let lastPointerX = 0;
let lastPointerTime = 0; let lastPointerTime = 0;
@@ -1391,7 +1450,7 @@ export async function mountHubGlobe(stage, opts) {
if (!dragging) { if (!dragging) {
if (!reducedMotion) { if (!reducedMotion) {
const target = AUTO_SPIN_RAD_S; const target = spinTarget;
angularVelocity += (target - angularVelocity) * 1.2 * dt; angularVelocity += (target - angularVelocity) * 1.2 * dt;
if (Math.abs(angularVelocity - target) < 0.004) angularVelocity = target; if (Math.abs(angularVelocity - target) < 0.004) angularVelocity = target;
} else { } else {
@@ -1405,6 +1464,9 @@ export async function mountHubGlobe(stage, opts) {
raf = requestAnimationFrame(tick); raf = requestAnimationFrame(tick);
stage.dataset.globe3d = String(renderVersion); stage.dataset.globe3d = String(renderVersion);
if (spinFactor !== 1) {
stage.dataset.globeAx = String(spinFactor);
}
if (renderVersion >= 11) { if (renderVersion >= 11) {
stage.dataset.globeLatf = String(landZoom.latf); stage.dataset.globeLatf = String(landZoom.latf);
stage.dataset.globeLngf = String(landZoom.lngf); stage.dataset.globeLngf = String(landZoom.lngf);
@@ -1441,6 +1503,7 @@ export async function mountHubGlobe(stage, opts) {
canvas.remove(); canvas.remove();
delete stage.dataset.globe3d; delete stage.dataset.globe3d;
delete stage.dataset.globeAxis; delete stage.dataset.globeAxis;
delete stage.dataset.globeAx;
delete stage.dataset.globeLatf; delete stage.dataset.globeLatf;
delete stage.dataset.globeLngf; delete stage.dataset.globeLngf;
delete stage.dataset.globeZy; delete stage.dataset.globeZy;

View File

@@ -187,7 +187,7 @@ function hub_h(string $s): string {
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 = '20260704globe29'; var logoBuild = '20260704globe31';
function hubAsset(rel) { function hubAsset(rel) {
var link = document.querySelector('link[href*="hub.css"]'); var link = document.querySelector('link[href*="hub.css"]');
@@ -268,6 +268,12 @@ function hub_h(string $s): string {
var fLegacy = parseFloat(globeLatfLegacy); var fLegacy = parseFloat(globeLatfLegacy);
if (isFinite(fLegacy)) globeLandZoom.latf = fLegacy; if (isFinite(fLegacy)) globeLandZoom.latf = fLegacy;
} }
var globeAxRaw = params.get('ax');
var globeSpinFactor = 1;
if (globeAxRaw !== null && globeAxRaw !== '') {
var axParsed = parseFloat(globeAxRaw);
if (isFinite(axParsed)) globeSpinFactor = axParsed;
}
var useGlobe3d = logoEnabled && globe3dVersion !== 0; var useGlobe3d = logoEnabled && globe3dVersion !== 0;
var globe3dLayers = { globe: true, continents: true, gridOverlay: true }; var globe3dLayers = { globe: true, continents: true, gridOverlay: true };
@@ -315,6 +321,9 @@ function hub_h(string $s): string {
if (Object.keys(globeLandZoom).length > 0) { if (Object.keys(globeLandZoom).length > 0) {
globeOpts.landZoom = globeLandZoom; globeOpts.landZoom = globeLandZoom;
} }
if (globeSpinFactor !== 1) {
globeOpts.spinFactor = globeSpinFactor;
}
var globeModuleUrl = new URL('assets/hub-globe.js', window.location.href); var globeModuleUrl = new URL('assets/hub-globe.js', window.location.href);
globeModuleUrl.searchParams.set('v', logoBuild); globeModuleUrl.searchParams.set('v', logoBuild);
globeModuleUrl.searchParams.set('globe3d', String(globe3dVersion)); globeModuleUrl.searchParams.set('globe3d', String(globe3dVersion));