219 lines
9.0 KiB
JavaScript
219 lines
9.0 KiB
JavaScript
/* AnwarPi — WebGL light-beam background + scroll reveal.
|
|
Loads deferred on every page; no-ops gracefully if WebGL is unavailable. */
|
|
(function () {
|
|
"use strict";
|
|
var reduced = false;
|
|
try { reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; } catch (e) {}
|
|
var staticMode = false;
|
|
try { staticMode = new URLSearchParams(location.search).has("static"); } catch (e) {}
|
|
|
|
/* ================= WebGL beam field ================= */
|
|
function initWebGL() {
|
|
var canvas = document.createElement("canvas");
|
|
canvas.id = "anwarpi-bg";
|
|
canvas.setAttribute("aria-hidden", "true");
|
|
var first = document.body.firstChild;
|
|
if (first) document.body.insertBefore(canvas, first);
|
|
else document.body.appendChild(canvas);
|
|
|
|
var gl = canvas.getContext("webgl", { antialias: false, alpha: false, depth: false, stencil: false })
|
|
|| canvas.getContext("experimental-webgl");
|
|
if (!gl) { canvas.parentNode.removeChild(canvas); return; }
|
|
|
|
var vertSrc = "attribute vec2 p;void main(){gl_Position=vec4(p,0.0,1.0);}";
|
|
var fragSrc = [
|
|
"precision highp float;",
|
|
"uniform float uTime;",
|
|
"uniform vec2 uRes;",
|
|
"uniform vec2 uMouse;",
|
|
"float hash(float n){return fract(sin(n*127.1)*43758.5453);}",
|
|
"void main(){",
|
|
" vec2 uv = gl_FragCoord.xy / uRes;", /* y: 0 bottom -> 1 top */
|
|
" float t = uTime * 0.06;",
|
|
" float v = 0.0;",
|
|
/* vertical light beams — the AnwarPi "A" ray motif */
|
|
" for(int i = 0; i < 7; i++){",
|
|
" float fi = float(i);",
|
|
" float x = 0.08 + 0.84 * hash(fi + 1.0) + 0.05 * sin(t * 0.7 + fi * 2.4);",
|
|
" x += (uMouse.x - x) * 0.16;",
|
|
" float w = 0.016 + 0.05 * hash(fi + 7.0);",
|
|
" float beam = exp(-pow((uv.x - x) / w, 2.0));",
|
|
" float flick = 0.55 + 0.45 * sin(t * 1.1 + fi * 1.7);",
|
|
" flick *= 1.0 + 0.55 * exp(-pow((uv.x - uMouse.x) / 0.24, 2.0));",
|
|
" float fade = smoothstep(1.0, 0.12, uv.y);", /* brighter toward the bottom, like the mark */
|
|
" v += beam * flick * fade * (0.10 + 0.17 * hash(fi + 13.0));",
|
|
" }",
|
|
/* cursor halo — the light follows the visitor */
|
|
" v += 0.11 * exp(-length((uv - uMouse) * vec2(1.0, 0.9)) * 4.5);",
|
|
/* broad floor glow + soft vignette */
|
|
" v += 0.15 * exp(-uv.y * 2.4);",
|
|
" v *= smoothstep(1.3, 0.55, uv.y);",
|
|
" v = clamp(v, 0.0, 1.0);",
|
|
/* deep blue-black base, aligned with the shared --bg token */
|
|
" vec3 base = vec3(0.0005, 0.0618, 0.0896);",
|
|
" gl_FragColor = vec4(base + vec3(v), 1.0);",
|
|
"}"
|
|
].join("\n");
|
|
|
|
function compile(type, src) {
|
|
var s = gl.createShader(type);
|
|
gl.shaderSource(s, src);
|
|
gl.compileShader(s);
|
|
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { gl.deleteShader(s); return null; }
|
|
return s;
|
|
}
|
|
var vs = compile(gl.VERTEX_SHADER, vertSrc);
|
|
var fs = compile(gl.FRAGMENT_SHADER, fragSrc);
|
|
if (!vs || !fs) { canvas.parentNode.removeChild(canvas); return; }
|
|
var prog = gl.createProgram();
|
|
gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog);
|
|
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { canvas.parentNode.removeChild(canvas); return; }
|
|
gl.useProgram(prog);
|
|
|
|
var buf = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
var loc = gl.getAttribLocation(prog, "p");
|
|
gl.enableVertexAttribArray(loc);
|
|
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
var uTime = gl.getUniformLocation(prog, "uTime");
|
|
var uRes = gl.getUniformLocation(prog, "uRes");
|
|
var uMouse = gl.getUniformLocation(prog, "uMouse");
|
|
|
|
/* ---- pointer reaction: beams lean toward the cursor, halo follows ---- */
|
|
var mouseTX = 0.5, mouseTY = 0.55, mouseX = 0.5, mouseY = 0.55;
|
|
if (!reduced && !staticMode) {
|
|
window.addEventListener("pointermove", function (e) {
|
|
if (e.pointerType === "touch") return;
|
|
mouseTX = e.clientX / window.innerWidth;
|
|
mouseTY = 1 - e.clientY / window.innerHeight;
|
|
}, { passive: true });
|
|
document.addEventListener("pointerleave", function () {
|
|
mouseTX = 0.5; mouseTY = 0.55;
|
|
});
|
|
}
|
|
gl.uniform2f(uMouse, mouseX, mouseY);
|
|
|
|
var DPR_CAP = 1.5;
|
|
function resize() {
|
|
var dpr = Math.min(window.devicePixelRatio || 1, DPR_CAP);
|
|
var w = Math.max(1, Math.floor(window.innerWidth * dpr));
|
|
var h = Math.max(1, Math.floor(window.innerHeight * dpr));
|
|
if (canvas.width !== w || canvas.height !== h) {
|
|
canvas.width = w; canvas.height = h;
|
|
gl.viewport(0, 0, w, h);
|
|
}
|
|
gl.uniform2f(uRes, canvas.width, canvas.height);
|
|
}
|
|
|
|
var start = (window.performance && performance.now()) || 0;
|
|
function draw(now) {
|
|
mouseX += (mouseTX - mouseX) * 0.07;
|
|
mouseY += (mouseTY - mouseY) * 0.07;
|
|
gl.uniform1f(uTime, (now - start) / 1000);
|
|
gl.uniform2f(uMouse, mouseX, mouseY);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
}
|
|
|
|
resize();
|
|
draw(start); /* paint one frame synchronously so screenshots capture the field */
|
|
|
|
/* ---- article reading mode: fade the field down once past the header ---- */
|
|
if (document.body.hasAttribute("data-anw-dim")) {
|
|
var first = document.querySelector("main") && document.querySelector("main").firstElementChild;
|
|
var dimFrom = first ? first.offsetHeight : 420;
|
|
var onScroll = function () {
|
|
canvas.style.opacity = (window.scrollY || 0) > dimFrom ? "0.22" : "1";
|
|
};
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
onScroll();
|
|
}
|
|
|
|
if (reduced || staticMode) {
|
|
window.addEventListener("resize", function () { resize(); draw(start); });
|
|
return;
|
|
}
|
|
var running = true;
|
|
function loop(now) {
|
|
if (running) { draw(now); }
|
|
window.requestAnimationFrame(loop);
|
|
}
|
|
window.requestAnimationFrame(loop);
|
|
document.addEventListener("visibilitychange", function () {
|
|
running = !document.hidden;
|
|
if (running) start = (window.performance && performance.now()) - 0; /* keep phase simple */
|
|
});
|
|
}
|
|
|
|
/* ================= Scroll reveal ================= */
|
|
function initReveal() {
|
|
if (reduced || staticMode) return;
|
|
var vh = window.innerHeight || 800;
|
|
var candidates = document.querySelectorAll(
|
|
"main section > .container > *, main > .container > *, main .container > .grid > *, main .grid > *"
|
|
);
|
|
var seen = {};
|
|
var targets = [];
|
|
for (var i = 0; i < candidates.length; i++) {
|
|
var el = candidates[i];
|
|
if (seen[el.anwId]) continue;
|
|
if (el.closest(".menu-panel, .modal, dialog")) continue;
|
|
var r = el.getBoundingClientRect();
|
|
if (r.top < vh * 0.92) continue; /* above the fold: show instantly, never hide */
|
|
el.anwId = "anw" + i;
|
|
seen[el.anwId] = 1;
|
|
el.setAttribute("data-anw", "");
|
|
el.style.transitionDelay = ((i % 3) * 70) + "ms";
|
|
targets.push(el);
|
|
}
|
|
if (!targets.length) return;
|
|
document.documentElement.classList.add("anw-anim");
|
|
/* Force reveal after a safety window so nothing can stay hidden */
|
|
window.setTimeout(function () {
|
|
targets.forEach(function (el) { el.classList.add("anw-in"); });
|
|
}, 3000);
|
|
if (!("IntersectionObserver" in window)) return;
|
|
var io = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add("anw-in");
|
|
io.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.1, rootMargin: "0px 0px -8% 0px" });
|
|
targets.forEach(function (el) { io.observe(el); });
|
|
}
|
|
|
|
/* ================= Current page in mobile navigation ================= */
|
|
function initActiveNav() {
|
|
var topnav = document.querySelector(".topnav");
|
|
if (!topnav) return;
|
|
|
|
var page = (location.pathname.split("/").pop() || "index.html").toLowerCase();
|
|
var selector = {
|
|
"index.html": ".logo",
|
|
"arabic-ai-learning-catalog.html": "nav a[href^='#catalog']",
|
|
"arabic-ai-learning-catalog-v2.html": "nav a[href^='#catalog']",
|
|
"article-agent-guide.html": "nav a[href='article-agent-guide.html']",
|
|
"post-locked.html": "nav a[href='article-agent-guide.html']",
|
|
"post-unlocked.html": "nav a[href='article-agent-guide.html']",
|
|
"articles.html": "nav a[href='articles.html']",
|
|
"subscription.html": "nav a[href='#exclusive-guides']",
|
|
"vip-onboarding.html": "nav a[href='subscription.html']",
|
|
"account-settings.html": ".nav-account",
|
|
"login.html": ".logo",
|
|
"signup.html": ".logo",
|
|
"forgot-password.html": ".logo",
|
|
"privacy.html": "nav a[href='privacy.html']",
|
|
"about.html": "nav a[href='about.html']"
|
|
}[page];
|
|
|
|
var active = selector && topnav.querySelector(selector);
|
|
if (active) active.setAttribute("aria-current", "page");
|
|
}
|
|
|
|
function boot() { initActiveNav(); initWebGL(); initReveal(); }
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
else boot();
|
|
})();
|