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

Hub globe v2: mercator wrap, GPU path, landing layout updates.

Refine 3D globe rendering, layer CSS, landing page wiring and TOC UX.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-08-07 22:15:46 +02:00
parent 78d5911e48
commit c54919189c
7 changed files with 1404 additions and 199 deletions

225
assets/hub-globe-gpu.js Normal file
View File

@@ -0,0 +1,225 @@
/**
* WebGL land disk (globe3d=11) — GPU orthographic mercator sample; ocean/sun stay Canvas2D.
*/
const VS = `
attribute vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const FS = `
precision highp float;
uniform vec3 uDisk;
uniform float uRotY;
uniform sampler2D uMerc;
uniform float uLatf;
uniform float uLngf;
uniform float uZy;
uniform float uZx;
uniform float uPp;
uniform float uPe;
uniform float uLonCenter;
uniform float uLimbZ;
const float PI = 3.141592653589793;
const float TAU = 6.283185307179586;
float wrapLon(float lon) {
lon = mod(lon + PI, TAU);
if (lon < 0.0) lon += TAU;
return lon - PI;
}
float latToMercatorV(float lat) {
float clamped = clamp(lat, -1.52, 1.52);
float t = 0.5 + asinh(tan(clamped)) / TAU;
return clamp(1.0 - t, 0.0, 1.0);
}
vec2 landZoomLonLat(float lon, float lat, vec3 tex) {
float latT = min(1.0, abs(lat) / (PI * 0.5));
float lonT = min(1.0, abs(lon) / PI);
float rimT = min(1.0, length(tex.xz));
float latMul = uLatf * (uZy * (1.0 - latT * latT) + latT * latT);
float lonMul = uLngf * (uZx * (1.0 - lonT * lonT) + lonT * lonT);
float poleLens = 1.0 - uPp * latT * latT;
float edgeLens = 1.0 - uPe * rimT * rimT;
return vec2(
wrapLon(lon * lonMul * edgeLens + uLonCenter),
clamp(lat * latMul * poleLens, -1.52, 1.52)
);
}
void main() {
vec2 p = (gl_FragCoord.xy - uDisk.xy) / uDisk.z;
float rr = dot(p, p);
if (rr > 1.0) {
discard;
}
float nz = sqrt(max(0.0, 1.0 - rr));
if (nz <= uLimbZ) {
discard;
}
float cosR = cos(uRotY);
float sinR = sin(uRotY);
float bx = p.x * cosR - nz * sinR;
float by = p.y;
float bz = p.x * sinR + nz * cosR;
vec3 tex = vec3(bx, by, bz);
float lon = atan(bx, bz);
float lat = asin(clamp(by, -1.0, 1.0));
vec2 mapped = landZoomLonLat(lon, lat, tex);
float u = fract((mapped.x + PI) / TAU);
float v = latToMercatorV(mapped.y);
vec4 merc = texture2D(uMerc, vec2(u, v));
float alpha = merc.a / 255.0;
if (alpha <= 0.004) {
discard;
}
gl_FragColor = vec4(merc.rgb / 255.0, alpha);
}
`;
function compileShader(gl, type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
console.warn('Globe GPU shader:', gl.getShaderInfoLog(sh));
gl.deleteShader(sh);
return null;
}
return sh;
}
function buildMercTexture(gl, cache, cs) {
const w = cache.w;
const h = cache.h;
const pixels = new Uint8Array(w * h * 4);
for (let i = 0; i < w * h; i++) {
const k = cache.kind[i];
if (!k) {
continue;
}
let r = cache.rgb[i * 3];
let g = cache.rgb[i * 3 + 1];
let b = cache.rgb[i * 3 + 2];
let a = cs.fillAlpha;
if (k === 2 || k === 3) {
r = Math.min(255, r + cs.borderBoostR);
g = Math.min(255, g + cs.borderBoostG);
b = Math.min(255, b + cs.borderBoostB);
a = cs.borderAlpha;
}
const o = i * 4;
pixels[o] = r;
pixels[o + 1] = g;
pixels[o + 2] = b;
pixels[o + 3] = a;
}
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
gl.bindTexture(gl.TEXTURE_2D, null);
return tex;
}
/**
* @param {HTMLCanvasElement} canvas
* @param {{ cache: object, continentStyle: object, landZoom: object, lonCenter: number }} opts
*/
export function createGpuGlobeRenderer(canvas, opts) {
const gl = canvas.getContext('webgl', {
alpha: true,
antialias: false,
depth: false,
stencil: false,
preserveDrawingBuffer: false,
powerPreference: 'high-performance',
});
if (!gl) {
return null;
}
const vs = compileShader(gl, gl.VERTEX_SHADER, VS);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, FS);
if (!vs || !fs) {
return null;
}
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.warn('Globe GPU link:', gl.getProgramInfoLog(prog));
return null;
}
gl.deleteShader(vs);
gl.deleteShader(fs);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
const cs = opts.continentStyle;
const mercTex = buildMercTexture(gl, opts.cache, cs);
const lz = opts.landZoom;
const loc = {
aPos: gl.getAttribLocation(prog, 'aPos'),
uDisk: gl.getUniformLocation(prog, 'uDisk'),
uRotY: gl.getUniformLocation(prog, 'uRotY'),
uMerc: gl.getUniformLocation(prog, 'uMerc'),
uLatf: gl.getUniformLocation(prog, 'uLatf'),
uLngf: gl.getUniformLocation(prog, 'uLngf'),
uZy: gl.getUniformLocation(prog, 'uZy'),
uZx: gl.getUniformLocation(prog, 'uZx'),
uPp: gl.getUniformLocation(prog, 'uPp'),
uPe: gl.getUniformLocation(prog, 'uPe'),
uLonCenter: gl.getUniformLocation(prog, 'uLonCenter'),
uLimbZ: gl.getUniformLocation(prog, 'uLimbZ'),
};
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
return {
draw(cx, cy, r, rotY, width, height) {
gl.viewport(0, 0, width, height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(prog);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.enableVertexAttribArray(loc.aPos);
gl.vertexAttribPointer(loc.aPos, 2, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, mercTex);
gl.uniform1i(loc.uMerc, 0);
gl.uniform3f(loc.uDisk, cx, cy, r);
gl.uniform1f(loc.uRotY, rotY);
gl.uniform1f(loc.uLatf, lz.latf);
gl.uniform1f(loc.uLngf, lz.lngf);
gl.uniform1f(loc.uZy, lz.zy);
gl.uniform1f(loc.uZx, lz.zx);
gl.uniform1f(loc.uPp, lz.pp);
gl.uniform1f(loc.uPe, lz.pe);
gl.uniform1f(loc.uLonCenter, opts.lonCenter);
gl.uniform1f(loc.uLimbZ, 0.002);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
},
destroy() {
gl.deleteTexture(mercTex);
gl.deleteBuffer(buf);
gl.deleteProgram(prog);
},
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -108,7 +108,7 @@
document.body.addEventListener('click', function (ev) {
var t = ev.target.closest('[data-goto-page]');
if (!t) return;
if (t.tagName === 'A' && t.getAttribute('href') === '#') {
if (t.tagName === 'A') {
ev.preventDefault();
}
setActivePage(Number(t.getAttribute('data-goto-page')));