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:
@@ -13,6 +13,7 @@
|
||||
* globe3d=10: v4 visible-arc → screen ellipse (hub ry), disk-clipped.
|
||||
* 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.
|
||||
* ?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 };
|
||||
|
||||
@@ -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):
|
||||
* 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];
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
function isSolidContinentPixel(r, g, b, a) {
|
||||
if (a < 72) {
|
||||
@@ -1107,17 +1137,44 @@ function sampleLandRgbaMercator(mercFlat, tex) {
|
||||
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) {
|
||||
if (tex.z <= LIMB_Z) {
|
||||
return null;
|
||||
}
|
||||
const flat = sampleMercFlatFromTex(mercFlat, tex, landZoom);
|
||||
if (isHubContinentPixel(flat[0], flat[1], flat[2], flat[3])) {
|
||||
return hubContinentRgba(flat);
|
||||
}
|
||||
let kind = classifyHubContinentPixel(flat[0], flat[1], flat[2], flat[3]);
|
||||
if (!kind) {
|
||||
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) {
|
||||
const r2 = r * r;
|
||||
@@ -1219,6 +1276,8 @@ export async function mountHubGlobe(stage, opts) {
|
||||
const reducedMotion = !!opts.reducedMotion;
|
||||
const renderVersion = Math.max(1, parseInt(opts.renderVersion, 10) || 1);
|
||||
const landZoom = resolveLandZoom(opts.landZoom);
|
||||
const spinFactor = resolveSpinFactor(opts.spinFactor);
|
||||
const spinTarget = AUTO_SPIN_RAD_S * spinFactor;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'hub-globe-canvas';
|
||||
@@ -1253,7 +1312,7 @@ export async function mountHubGlobe(stage, opts) {
|
||||
const mercFlat = rasterizeImage(mercatorFlatImg, mercW, mercH, 1);
|
||||
|
||||
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 lastPointerX = 0;
|
||||
let lastPointerTime = 0;
|
||||
@@ -1391,7 +1450,7 @@ export async function mountHubGlobe(stage, opts) {
|
||||
|
||||
if (!dragging) {
|
||||
if (!reducedMotion) {
|
||||
const target = AUTO_SPIN_RAD_S;
|
||||
const target = spinTarget;
|
||||
angularVelocity += (target - angularVelocity) * 1.2 * dt;
|
||||
if (Math.abs(angularVelocity - target) < 0.004) angularVelocity = target;
|
||||
} else {
|
||||
@@ -1405,6 +1464,9 @@ export async function mountHubGlobe(stage, opts) {
|
||||
|
||||
raf = requestAnimationFrame(tick);
|
||||
stage.dataset.globe3d = String(renderVersion);
|
||||
if (spinFactor !== 1) {
|
||||
stage.dataset.globeAx = String(spinFactor);
|
||||
}
|
||||
if (renderVersion >= 11) {
|
||||
stage.dataset.globeLatf = String(landZoom.latf);
|
||||
stage.dataset.globeLngf = String(landZoom.lngf);
|
||||
@@ -1441,6 +1503,7 @@ export async function mountHubGlobe(stage, opts) {
|
||||
canvas.remove();
|
||||
delete stage.dataset.globe3d;
|
||||
delete stage.dataset.globeAxis;
|
||||
delete stage.dataset.globeAx;
|
||||
delete stage.dataset.globeLatf;
|
||||
delete stage.dataset.globeLngf;
|
||||
delete stage.dataset.globeZy;
|
||||
|
||||
11
index.php
11
index.php
@@ -187,7 +187,7 @@ function hub_h(string $s): string {
|
||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
||||
|
||||
var logoBuild = '20260704globe29';
|
||||
var logoBuild = '20260704globe31';
|
||||
|
||||
function hubAsset(rel) {
|
||||
var link = document.querySelector('link[href*="hub.css"]');
|
||||
@@ -268,6 +268,12 @@ function hub_h(string $s): string {
|
||||
var fLegacy = parseFloat(globeLatfLegacy);
|
||||
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 globe3dLayers = { globe: true, continents: true, gridOverlay: true };
|
||||
|
||||
@@ -315,6 +321,9 @@ function hub_h(string $s): string {
|
||||
if (Object.keys(globeLandZoom).length > 0) {
|
||||
globeOpts.landZoom = globeLandZoom;
|
||||
}
|
||||
if (globeSpinFactor !== 1) {
|
||||
globeOpts.spinFactor = globeSpinFactor;
|
||||
}
|
||||
var globeModuleUrl = new URL('assets/hub-globe.js', window.location.href);
|
||||
globeModuleUrl.searchParams.set('v', logoBuild);
|
||||
globeModuleUrl.searchParams.set('globe3d', String(globe3dVersion));
|
||||
|
||||
Reference in New Issue
Block a user