/* andrii.dk home v2 — interactive layer
* - mouse-tracked hero glow
* - 3D tilt on .tilt cards (also updates --mx/--my for radial highlight)
* - scroll reveal (Intersection Observer)
* - marquee duplication for seamless loop
* - inset-shadow on click handled by CSS :active
*/
(function () {
"use strict";
function ready(fn) {
if (document.readyState !== "loading") fn();
else document.addEventListener("DOMContentLoaded", fn);
}
ready(function () {
injectGoogleFont();
forceLayouts();
initReveal();
initTilt();
initHeroGlow();
initMarquee();
initPortraitImage();
initContactForm();
initTypewriter();
initChatBubble();
initAutoQuarter();
// wow-pack
initGradientMesh();
initScrollProgress();
initGrainOverlay();
initLiveStatTicker();
initParticleField();
init3DObject();
initLiveTerminal();
initCustomCursor();
initMagneticButtons();
initCardSpotlight();
initClickRipple();
initCountUp();
initH1Glitch();
initHeroParallax();
});
// ---------- auto-quarter (next quarter; if Q4 → Q1 of next year) ----------
function initAutoQuarter() {
var el = document.getElementById("anQuarter");
if (!el) return;
var now = new Date();
var month = now.getMonth(); // 0..11
var year = now.getFullYear();
var currentQ = Math.floor(month / 3) + 1; // 1..4
var nextQ = currentQ === 4 ? 1 : currentQ + 1;
var nextY = currentQ === 4 ? year + 1 : year;
el.textContent = "Q" + nextQ + " " + nextY;
}
// ---------- inject Google Font (WP strips @import in post_content) ----------
function injectGoogleFont() {
if (document.querySelector('link[data-an-font]')) return;
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700;800;900&display=swap';
link.setAttribute('data-an-font', '1');
document.head.appendChild(link);
// also force the style on stats nums after load
setTimeout(function () {
document.querySelectorAll('.an .stat__num, .an .ach__title').forEach(function (el) {
el.style.setProperty('font-family', "'Playfair Display', Georgia, serif", 'important');
});
}, 300);
}
// ---------- typewriter on hero subtitle ----------
function initTypewriter() {
var el = document.getElementById("anTw");
if (!el) return;
var raw = (el.dataset.words || "").split(",").map(function (s) { return s.trim(); }).filter(Boolean);
if (!raw.length) return;
var i = 0, j = 0, deleting = false;
function tick() {
var word = raw[i % raw.length];
if (!deleting) {
j++;
el.textContent = word.slice(0, j);
if (j >= word.length) { deleting = true; setTimeout(tick, 1800); return; }
setTimeout(tick, 80);
} else {
j--;
el.textContent = word.slice(0, j);
if (j <= 0) { deleting = false; i++; setTimeout(tick, 350); return; }
setTimeout(tick, 40);
}
}
setTimeout(tick, 1200);
}
// ---------- floating chat bubble → scroll to form ----------
function initChatBubble() {
var btn = document.getElementById("anChatBtn");
if (!btn) return;
btn.addEventListener("click", function () {
var form = document.getElementById("anContactForm");
if (!form) { window.location.href = "mailto:andrii@best-choice.dk"; return; }
form.scrollIntoView({ behavior: "smooth", block: "start" });
setTimeout(function () { var f = form.querySelector("input[name=name]"); if (f) f.focus(); }, 600);
});
}
// ---------- cookie banner ----------
function initCookieBanner() {
var KEY = "an_cookie_choice_v1";
var bar = document.getElementById("anCookie");
if (!bar) return;
try {
if (localStorage.getItem(KEY)) return; // already chosen
} catch (e) {}
setTimeout(function () { bar.classList.add("is-on"); }, 1200);
function close(choice) {
try { localStorage.setItem(KEY, choice); } catch (e) {}
bar.classList.remove("is-on");
}
var ok = document.getElementById("anCookieAccept");
var no = document.getElementById("anCookieReject");
if (ok) ok.addEventListener("click", function () { close("accept"); });
if (no) no.addEventListener("click", function () { close("reject"); });
}
// ---------- theme toggle (button only — visual delight, future: real switch) ----------
function initThemeToggle() {
var btn = document.getElementById("anThemeToggle");
if (!btn) return;
var isDark = true;
btn.addEventListener("click", function () {
isDark = !isDark;
btn.textContent = isDark ? "🌙" : "☀️";
// simple inversion attempt: this is dark theme by design, light mode is a "wow" preview
if (!isDark) {
document.documentElement.style.setProperty("--an-theme-flash", "1");
btn.style.background = "linear-gradient(135deg,#f5c542,#e0457b)";
} else {
document.documentElement.style.removeProperty("--an-theme-flash");
btn.style.background = "";
}
});
}
// ---------- contact form (formsubmit.co AJAX) ----------
function initContactForm() {
var form = document.getElementById("anContactForm");
if (!form) return;
var msgEl = document.getElementById("anContactMsg");
var btn = form.querySelector("button[type=submit]");
function showMsg(text, ok) {
if (!msgEl) return;
msgEl.style.display = "block";
msgEl.style.background = ok ? "rgba(124, 219, 138, 0.1)" : "rgba(224, 69, 123, 0.1)";
msgEl.style.color = ok ? "#7cdb8a" : "#f47fa3";
msgEl.style.border = ok ? "1px solid rgba(124,219,138,.3)" : "1px solid rgba(224,69,123,.3)";
msgEl.textContent = text;
}
form.addEventListener("submit", function (ev) {
ev.preventDefault();
var raw = Object.fromEntries(new FormData(form).entries());
// Map "name" → "first_name" для нашого WP endpoint
var data = Object.assign({}, raw);
if (raw.name && !raw.first_name) data.first_name = raw.name;
btn.disabled = true;
btn.textContent = "Надсилаю…";
function send(url) {
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify(data),
}).then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json().catch(function () { return {}; });
});
}
function ok() {
form.reset();
showMsg("✓ Дякую! Повідомлення надіслано — відповім протягом доби.", true);
btn.disabled = false; btn.textContent = "Надіслано";
setTimeout(function () {
btn.innerHTML = ' Надіслати';
}, 4000);
}
function fail() {
btn.disabled = false; btn.textContent = "Надіслати";
showMsg("Не вдалося надіслати. Напиши напряму на andrii@best-choice.dk", false);
}
send(form.action).then(ok).catch(function () {
var fb = form.dataset.fallback;
if (fb) send(fb).then(ok).catch(fail);
else fail();
});
});
// input focus highlight
form.querySelectorAll("input, textarea, select").forEach(function (el) {
el.addEventListener("focus", function () { el.style.borderColor = "var(--magenta)"; });
el.addEventListener("blur", function () { el.style.borderColor = "var(--line)"; });
});
}
// ---------- force grid/flex layouts (theme overrides display:block) ----------
function forceLayouts() {
var rules = [
[".an .topbar", { display: "flex", justifyContent: "flex-end", gap: "8px" }],
[".an .lang-pills", { display: "flex", flexWrap: "wrap", gap: "8px" }],
[".an .ach", { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "20px" }],
[".an .stats", { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "24px" }],
[".an .areas", { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "24px" }],
[".an .bento", { display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gridAutoRows: "minmax(220px, auto)", gap: "20px" }],
[".an .about", { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "60px" }],
[".an .speak-list", { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "14px" }],
[".an .serv", { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "20px" }],
[".an .pers", { display: "grid", gridTemplateColumns: "1fr 1.6fr", gap: "60px", alignItems: "start" }],
[".an .pers-mos", { display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: "14px" }],
[".an .pers-facts", { display: "flex", gap: "24px" }],
[".an .pers-fact", { display: "flex", flexDirection: "column", gap: "2px" }],
[".an .pers-mos__cell", { display: "flex", alignItems: "center", justifyContent: "center" }],
[".an .ctas", { display: "flex", gap: "14px", flexWrap: "wrap" }],
[".an .chips", { display: "flex", gap: "10px", flexWrap: "wrap" }],
[".an .skills", { display: "flex", gap: "8px", flexWrap: "wrap" }],
[".an .marq__track", { display: "flex", gap: "14px" }],
[".an .area", { display: "flex", flexDirection: "column", gap: "18px" }],
[".an .area__tags", { display: "flex", flexWrap: "wrap", gap: "6px" }],
[".an .proj", { display: "flex", flexDirection: "column", gap: "14px" }],
[".an .proj__head", { display: "flex", alignItems: "center", gap: "14px" }],
[".an .serv__card", { display: "flex", flexDirection: "column", gap: "14px" }],
[".an .speak-feat__meta", { display: "flex", gap: "22px", flexWrap: "wrap" }],
[".an .contact__cta", { display: "flex", justifyContent: "center", gap: "14px", flexWrap: "wrap" }],
[".an .contact__cvrs", { display: "flex", justifyContent: "center", gap: "28px", flexWrap: "wrap" }],
[".an .live", { display: "inline-flex", alignItems: "center", gap: "10px" }],
[".an .live__dot", { display: "inline-block" }],
[".an .portrait__badge", { display: "flex", alignItems: "center", gap: "12px" }],
[".an .portrait__chip", { display: "inline-flex", alignItems: "center", gap: "6px" }],
];
var w = window.innerWidth;
rules.forEach(function (rule) {
var sel = rule[0]; var st = rule[1];
document.querySelectorAll(sel).forEach(function (el) {
Object.keys(st).forEach(function (k) {
el.style.setProperty(toKebab(k), st[k], "important");
});
});
});
// bento spans
document.querySelectorAll(".an .proj--lg").forEach(function(el){ el.style.setProperty("grid-column","span 3","important");el.style.setProperty("grid-row","span 2","important"); });
document.querySelectorAll(".an .proj--md").forEach(function(el){ el.style.setProperty("grid-column","span 3","important"); });
document.querySelectorAll(".an .proj--sm").forEach(function(el){ el.style.setProperty("grid-column","span 2","important"); });
// hero inner already inline-styled, but ensure
var hi = document.querySelector(".an .hero__inner");
if (hi) {
hi.style.setProperty("display","grid","important");
hi.style.setProperty("grid-template-columns","1.3fr 1fr","important");
hi.style.setProperty("gap","80px","important");
hi.style.setProperty("align-items","center","important");
}
// Apply mobile breakpoints
if (w < 900) applyMobile();
window.addEventListener("resize", function(){ if (window.innerWidth < 900) applyMobile(); else applyDesktop(); });
}
function toKebab(s){ return s.replace(/[A-Z]/g, function(m){ return "-"+m.toLowerCase(); }); }
function applyMobile(){
[".an .hero__inner",".an .areas",".an .bento",".an .about",".an .speak-list",".an .serv",".an .pers",".an .stats"].forEach(function(sel){
document.querySelectorAll(sel).forEach(function(el){
if (sel === ".an .stats") el.style.setProperty("grid-template-columns","repeat(2,1fr)","important");
else el.style.setProperty("grid-template-columns","1fr","important");
});
});
document.querySelectorAll(".an .proj--lg, .an .proj--md, .an .proj--sm").forEach(function(el){
el.style.setProperty("grid-column","span 1","important");
el.style.setProperty("grid-row","auto","important");
});
}
function applyDesktop(){
var hi = document.querySelector(".an .hero__inner");
if (hi) hi.style.setProperty("grid-template-columns","1.3fr 1fr","important");
document.querySelectorAll(".an .stats").forEach(function(el){ el.style.setProperty("grid-template-columns","repeat(4,1fr)","important"); });
document.querySelectorAll(".an .areas").forEach(function(el){ el.style.setProperty("grid-template-columns","repeat(3,1fr)","important"); });
document.querySelectorAll(".an .bento").forEach(function(el){ el.style.setProperty("grid-template-columns","repeat(6,1fr)","important"); });
document.querySelectorAll(".an .about, .an .speak-list, .an .serv").forEach(function(el){ el.style.setProperty("grid-template-columns","repeat(2,1fr)","important"); });
document.querySelectorAll(".an .pers").forEach(function(el){ el.style.setProperty("grid-template-columns","1fr 1.6fr","important"); });
document.querySelectorAll(".an .proj--lg").forEach(function(el){ el.style.setProperty("grid-column","span 3","important"); el.style.setProperty("grid-row","span 2","important"); });
document.querySelectorAll(".an .proj--md").forEach(function(el){ el.style.setProperty("grid-column","span 3","important"); el.style.setProperty("grid-row","auto","important"); });
document.querySelectorAll(".an .proj--sm").forEach(function(el){ el.style.setProperty("grid-column","span 2","important"); el.style.setProperty("grid-row","auto","important"); });
}
// ---------- scroll reveal ----------
function initReveal() {
var nodes = document.querySelectorAll(".an .rev, .an .rev-stagger");
if (!nodes.length || !("IntersectionObserver" in window)) {
nodes.forEach(function (n) { n.classList.add("is-in"); });
return;
}
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
e.target.classList.add("is-in");
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
nodes.forEach(function (n) { io.observe(n); });
}
// ---------- 3D tilt cards ----------
function initTilt() {
var cards = document.querySelectorAll(".an .tilt");
cards.forEach(function (card) {
var raf = null;
function onMove(ev) {
var r = card.getBoundingClientRect();
var x = (ev.clientX - r.left) / r.width;
var y = (ev.clientY - r.top) / r.height;
var rotY = (x - 0.5) * 8;
var rotX = (0.5 - y) * 8;
if (raf) cancelAnimationFrame(raf);
raf = requestAnimationFrame(function () {
card.style.transform = "translateY(-6px) rotateX(" + rotX.toFixed(2) + "deg) rotateY(" + rotY.toFixed(2) + "deg)";
card.style.setProperty("--mx", (x * 100) + "%");
card.style.setProperty("--my", (y * 100) + "%");
});
}
function reset() {
if (raf) cancelAnimationFrame(raf);
card.style.transform = "";
}
card.addEventListener("mousemove", onMove);
card.addEventListener("mouseleave", reset);
});
}
// ---------- hero glow follows mouse ----------
function initHeroGlow() {
var hero = document.querySelector(".an .hero");
var glow = document.getElementById("anHeroGlow1");
if (!hero || !glow) return;
function onMove(ev) {
var r = hero.getBoundingClientRect();
var x = ev.clientX - r.left;
var y = ev.clientY - r.top;
glow.style.left = x + "px";
glow.style.top = y + "px";
glow.style.transform = "translate(-50%, -50%)";
}
hero.addEventListener("mousemove", onMove);
}
// ---------- marquee: duplicate inner for seamless loop ----------
function initMarquee() {
var track = document.getElementById("anMarq");
if (!track) return;
track.innerHTML += track.innerHTML;
}
// ============================================================
// ==================== WOW PACK (v3) ========================
// ============================================================
// ---------- 1. HERO PARTICLE FIELD (canvas 2D, neuron network) ----------
function initParticleField() {
var c = document.getElementById("anHeroParticles");
if (!c) return;
var ctx = c.getContext("2d");
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var W = 0, H = 0, pts = [], mx = -9999, my = -9999;
function resize() {
var p = c.parentElement;
if (!p) return;
var r = p.getBoundingClientRect();
W = c.width = Math.max(1, Math.floor(r.width * dpr));
H = c.height = Math.max(1, Math.floor(r.height * dpr));
c.style.width = r.width + "px";
c.style.height = r.height + "px";
}
resize();
window.addEventListener("resize", resize);
var N = window.innerWidth < 768 ? 36 : 78;
for (var i = 0; i < N; i++) {
pts.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.35 * dpr,
vy: (Math.random() - 0.5) * 0.35 * dpr,
r: Math.random() * 1.4 + 0.6
});
}
var hero = c.parentElement;
hero.addEventListener("mousemove", function (e) {
var r = c.getBoundingClientRect();
mx = (e.clientX - r.left) * dpr;
my = (e.clientY - r.top) * dpr;
});
hero.addEventListener("mouseleave", function () { mx = -9999; my = -9999; });
var maxDist = 150 * dpr;
var pushR = 130 * dpr;
function frame() {
ctx.clearRect(0, 0, W, H);
for (var i = 0; i < pts.length; i++) {
var p = pts[i];
var dxm = p.x - mx, dym = p.y - my;
var dm = Math.sqrt(dxm * dxm + dym * dym);
if (dm < pushR && dm > 0) {
var push = (pushR - dm) / pushR * 1.2;
p.x += dxm / dm * push;
p.y += dym / dm * push;
}
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > W) p.vx *= -1;
if (p.y < 0 || p.y > H) p.vy *= -1;
p.x = Math.max(0, Math.min(W, p.x));
p.y = Math.max(0, Math.min(H, p.y));
ctx.fillStyle = "rgba(255, 200, 150, 0.95)";
ctx.shadowColor = "rgba(224, 69, 123, 0.7)";
ctx.shadowBlur = 8 * dpr;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * dpr * 1.4, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
for (var a = 0; a < pts.length; a++) {
for (var b = a + 1; b < pts.length; b++) {
var dx = pts[a].x - pts[b].x;
var dy = pts[a].y - pts[b].y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < maxDist) {
var alpha = (1 - d / maxDist) * 0.55;
ctx.strokeStyle = "rgba(224, 69, 123, " + alpha.toFixed(3) + ")";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pts[a].x, pts[a].y);
ctx.lineTo(pts[b].x, pts[b].y);
ctx.stroke();
}
}
}
requestAnimationFrame(frame);
}
frame();
}
// ---------- 2. THREE.JS ICOSAHEDRON (3D wireframe orb) ----------
function init3DObject() {
var box = document.getElementById("anHero3D");
if (!box) return;
if (matchMedia("(max-width: 900px)").matches) return; // skip on mobile to save battery
loadThree(function (THREE) {
var w = box.clientWidth || 280;
var h = box.clientHeight || 280;
var scene = new THREE.Scene();
var cam = new THREE.PerspectiveCamera(45, w / h, 0.1, 100);
cam.position.z = 4;
var renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(w, h);
renderer.setClearColor(0x000000, 0);
box.appendChild(renderer.domElement);
var outerGeo = new THREE.IcosahedronGeometry(1.55, 1);
var outerMat = new THREE.MeshBasicMaterial({ color: 0xe0457b, wireframe: true, transparent: true, opacity: 0.85 });
var outer = new THREE.Mesh(outerGeo, outerMat);
scene.add(outer);
var innerGeo = new THREE.IcosahedronGeometry(0.85, 0);
var innerMat = new THREE.MeshBasicMaterial({ color: 0xd4a373, wireframe: true, transparent: true, opacity: 0.65 });
var inner = new THREE.Mesh(innerGeo, innerMat);
scene.add(inner);
var coreGeo = new THREE.IcosahedronGeometry(0.35, 0);
var coreMat = new THREE.MeshBasicMaterial({ color: 0xf5c542, wireframe: true, transparent: true, opacity: 0.9 });
var core = new THREE.Mesh(coreGeo, coreMat);
scene.add(core);
var mxN = 0, myN = 0;
window.addEventListener("mousemove", function (e) {
mxN = (e.clientX / window.innerWidth - 0.5) * 2;
myN = (e.clientY / window.innerHeight - 0.5) * 2;
});
function loop() {
outer.rotation.x += 0.0035 + myN * 0.002;
outer.rotation.y += 0.0055 + mxN * 0.002;
inner.rotation.x -= 0.006;
inner.rotation.y -= 0.009;
core.rotation.x += 0.012;
core.rotation.y += 0.014;
renderer.render(scene, cam);
requestAnimationFrame(loop);
}
loop();
window.addEventListener("resize", function () {
var nw = box.clientWidth, nh = box.clientHeight;
if (nw && nh) {
cam.aspect = nw / nh;
cam.updateProjectionMatrix();
renderer.setSize(nw, nh);
}
});
});
}
function loadThree(cb) {
if (window.THREE) { cb(window.THREE); return; }
if (window.__anThreeLoading) { window.__anThreeQueue.push(cb); return; }
window.__anThreeLoading = true;
window.__anThreeQueue = [cb];
function actuallyLoad() {
var s = document.createElement("script");
s.src = "https://unpkg.com/three@0.160.0/build/three.min.js";
s.crossOrigin = "anonymous";
s.async = true;
s.onload = function () {
window.__anThreeQueue.forEach(function (fn) { try { fn(window.THREE); } catch (e) {} });
window.__anThreeQueue = [];
};
s.onerror = function () { console.warn("three.js failed to load"); };
document.head.appendChild(s);
}
// defer until after page load + 1.2s of idle, so it never blocks LCP/FCP
function startWhenIdle() {
if (window.requestIdleCallback) {
requestIdleCallback(actuallyLoad, { timeout: 2500 });
} else {
setTimeout(actuallyLoad, 1200);
}
}
if (document.readyState === "complete") startWhenIdle();
else window.addEventListener("load", function () { setTimeout(startWhenIdle, 200); });
}
// ---------- 3. CUSTOM CURSOR (neon, no blend) + TRAIL + MAGNETIC BUTTONS ----------
function initCustomCursor() {
if (matchMedia("(pointer: coarse)").matches) return;
document.body.classList.add("an-has-cursor");
var dot = document.createElement("div");
dot.className = "an-cursor an-cursor--dot";
document.body.appendChild(dot);
var ring = document.createElement("div");
ring.className = "an-cursor an-cursor--ring";
document.body.appendChild(ring);
// trail dots
var TRAIL_N = 8;
var trail = [];
for (var i = 0; i < TRAIL_N; i++) {
var t = document.createElement("div");
t.className = "an-cursor an-cursor--trail";
t.style.opacity = (1 - i / TRAIL_N) * 0.55;
t.style.transform = "scale(" + (1 - i / (TRAIL_N * 1.5)) + ")";
document.body.appendChild(t);
trail.push({ el: t, x: 0, y: 0 });
}
var x = window.innerWidth / 2, y = window.innerHeight / 2;
var rx = x, ry = y;
document.addEventListener("mousemove", function (e) {
x = e.clientX; y = e.clientY;
dot.style.transform = "translate3d(" + (x - 5) + "px," + (y - 5) + "px,0)";
});
function loop() {
rx += (x - rx) * 0.20;
ry += (y - ry) * 0.20;
ring.style.transform = "translate3d(" + (rx - 22) + "px," + (ry - 22) + "px,0)";
// trail with progressive lag
var px = x, py = y;
for (var i = 0; i < trail.length; i++) {
var lag = 0.18 + i * 0.04;
trail[i].x += (px - trail[i].x) * lag;
trail[i].y += (py - trail[i].y) * lag;
trail[i].el.style.transform = "translate3d(" + (trail[i].x - 6) + "px," + (trail[i].y - 6) + "px,0) scale(" + (1 - i / 12) + ")";
px = trail[i].x; py = trail[i].y;
}
requestAnimationFrame(loop);
}
loop();
var hoverSel = "a, button, .btn, .an-chat, .ach__card, .area, .proj, .serv__card, .tl-i, .skill, .chip, input, textarea, select, .term, h1, h2, h3";
document.addEventListener("mouseover", function (e) {
if (e.target.closest && e.target.closest(hoverSel)) ring.classList.add("is-hover");
});
document.addEventListener("mouseout", function (e) {
if (e.target.closest && e.target.closest(hoverSel)) ring.classList.remove("is-hover");
});
document.addEventListener("mousedown", function () { ring.classList.add("is-down"); });
document.addEventListener("mouseup", function () { ring.classList.remove("is-down"); });
}
// ---------- 6. ANIMATED GRADIENT MESH (full-page background) ----------
function initGradientMesh() {
if (document.getElementById("anMesh")) return;
var mesh = document.createElement("div");
mesh.id = "anMesh";
mesh.setAttribute("aria-hidden", "true");
mesh.innerHTML = '';
document.body.appendChild(mesh);
}
// ---------- 7. CARD SPOTLIGHT (radial glow follows cursor on every card) ----------
function initCardSpotlight() {
var sel = ".an .ach__card, .an .area, .an .proj, .an .serv__card, .an .tl-i__card, .an .speak-list li, .an .chip, .an .term, .an .speak-feat";
document.querySelectorAll(sel).forEach(function (el) {
el.classList.add("an-spot");
el.addEventListener("mousemove", function (e) {
var r = el.getBoundingClientRect();
var x = ((e.clientX - r.left) / r.width) * 100;
var y = ((e.clientY - r.top) / r.height) * 100;
el.style.setProperty("--sx", x + "%");
el.style.setProperty("--sy", y + "%");
});
});
}
// ---------- 8. CLICK RIPPLE (magenta wave on every click) ----------
function initClickRipple() {
if (matchMedia("(pointer: coarse)").matches) return;
document.addEventListener("click", function (e) {
// skip clicks on form fields (annoying)
if (e.target.closest("input, textarea, select")) return;
var r = document.createElement("span");
r.className = "an-ripple";
r.style.left = e.clientX + "px";
r.style.top = e.clientY + "px";
document.body.appendChild(r);
setTimeout(function () { r.remove(); }, 800);
});
}
// ---------- 9. STATS COUNT-UP on scroll into view ----------
function initCountUp() {
var els = document.querySelectorAll(".an .stat__num, .an .ach__title");
if (!els.length || !("IntersectionObserver" in window)) return;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
var el = e.target;
if (el.dataset.counted) return;
var raw = el.textContent.trim();
var m = raw.match(/^([+\-]?)(\d[\d\s]*)(.*)$/);
if (!m) return;
var prefix = m[1] || "";
var num = parseInt(m[2].replace(/\s/g, ""), 10);
var suffix = m[3] || "";
if (isNaN(num) || num < 2) return;
el.dataset.counted = "1";
var dur = 1400;
var start = performance.now();
function tick(now) {
var p = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = Math.round(num * eased);
el.textContent = prefix + val + suffix;
if (p < 1) requestAnimationFrame(tick);
else el.textContent = prefix + num + suffix;
}
requestAnimationFrame(tick);
io.unobserve(el);
});
}, { threshold: 0.4 });
els.forEach(function (el) { io.observe(el); });
}
// ---------- 10. H1 PERIODIC GLITCH ----------
function initH1Glitch() {
var h1 = document.querySelector(".an .h1");
if (!h1) return;
h1.classList.add("an-glitchable");
function trigger() {
h1.classList.add("is-glitching");
setTimeout(function () { h1.classList.remove("is-glitching"); }, 320);
setTimeout(trigger, 5000 + Math.random() * 6000);
}
setTimeout(trigger, 4000);
}
// ---------- 11. HERO PARALLAX (subtle scroll-driven motion) ----------
function initHeroParallax() {
var hero = document.querySelector(".an .hero");
if (!hero) return;
var portrait = document.querySelector(".an .portrait");
var heading = document.querySelector(".an .hero .h1");
var threeBox = document.getElementById("anHero3D");
function update() {
var y = window.scrollY;
if (y > window.innerHeight) return;
if (portrait) portrait.style.transform = "translateY(" + (y * 0.15) + "px)";
if (heading) heading.style.transform = "translateY(" + (y * -0.08) + "px)";
if (threeBox) threeBox.style.transform = "translate3d(0," + (y * 0.25) + "px,0)";
}
window.addEventListener("scroll", update, { passive: true });
}
function initMagneticButtons() {
if (matchMedia("(pointer: coarse)").matches) return;
document.querySelectorAll(".an .btn, .an-chat").forEach(function (btn) {
btn.addEventListener("mousemove", function (e) {
var r = btn.getBoundingClientRect();
var x = e.clientX - r.left - r.width / 2;
var y = e.clientY - r.top - r.height / 2;
var force = 0.28;
btn.style.setProperty("--mag-x", (x * force).toFixed(2) + "px");
btn.style.setProperty("--mag-y", (y * force).toFixed(2) + "px");
btn.classList.add("is-mag");
});
btn.addEventListener("mouseleave", function () {
btn.style.setProperty("--mag-x", "0px");
btn.style.setProperty("--mag-y", "0px");
btn.classList.remove("is-mag");
});
});
}
// ---------- 4. LIVE TERMINAL (auto-typing fake console) ----------
function initLiveTerminal() {
var term = document.getElementById("anTermBody");
if (!term) return;
var lines = [
{ c: "python3 deploy.py andrii.dk", t: "cmd" },
{ c: "✓ uploading 18 assets...", t: "ok", d: 200 },
{ c: "✓ pushing home_uk.js to /uploads", t: "ok", d: 150 },
{ c: "✓ rest_api: page 128 updated", t: "ok", d: 150 },
{ c: "", t: "gap", d: 350 },
{ c: "ai-agent run --task \"resume today's leads\"", t: "cmd" },
{ c: "3 нових запити: 1 на сайт-під-ключ, 2 на консультацію", t: "out", d: 700 },
{ c: "", t: "gap", d: 350 },
{ c: "git log --oneline -3", t: "cmd" },
{ c: "4a2bf1c add real photos to home v2", t: "log", d: 130 },
{ c: "8c9d22a humanize tech proof bar", t: "log", d: 130 },
{ c: "1ee44a3 fix mobile speaker grid", t: "log", d: 130 },
{ c: "", t: "gap", d: 400 },
{ c: "curl -X POST /wp-json/an/v1/lead", t: "cmd" },
{ c: "200 OK · email sent to andrii@best-choice.dk", t: "ok", d: 600 },
{ c: "", t: "gap", d: 600 },
{ c: "ssh deploy@cars-export.lynbro.dk", t: "cmd" },
{ c: "✓ DMR XML parsed: 5 832 042 records · 3.4s", t: "ok", d: 500 },
{ c: "", t: "gap", d: 700 }
];
var lineIdx = 0;
var running = false;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting && !running) { running = true; tick(); }
});
}, { threshold: 0.2 });
io.observe(term);
function appendLine(text, type) {
var div = document.createElement("div");
div.className = "term-l term-l--" + type;
var prompt = "";
if (type === "cmd") prompt = '$ ';
else if (type === "out") prompt = '› ';
else if (type === "log") prompt = '· ';
else if (type === "ok") prompt = '✓ ';
div.innerHTML = prompt + '';
term.appendChild(div);
// keep buffer small
while (term.children.length > 20) term.removeChild(term.firstChild);
return div.querySelector(".term-c");
}
function typeText(target, text, speed, done) {
var i = 0;
function step() {
target.textContent = text.slice(0, ++i);
if (i < text.length) setTimeout(step, speed);
else setTimeout(done, 50);
}
step();
}
function tick() {
var ln = lines[lineIdx % lines.length];
lineIdx++;
if (ln.t === "gap") { setTimeout(tick, ln.d || 400); return; }
var span = appendLine("", ln.t);
var speed = ln.t === "cmd" ? 32 : 8;
typeText(span, ln.c, speed, function () {
setTimeout(tick, ln.d || 250);
term.scrollTop = term.scrollHeight;
});
}
}
// ---------- 5a. GRAIN OVERLAY (animated noise) ----------
function initGrainOverlay() {
var c = document.createElement("canvas");
c.id = "anGrain";
c.setAttribute("aria-hidden", "true");
document.body.appendChild(c);
var ctx = c.getContext("2d");
var size = 140;
c.width = size; c.height = size;
function regen() {
var img = ctx.createImageData(size, size);
var d = img.data;
for (var i = 0; i < d.length; i += 4) {
var v = (Math.random() * 255) | 0;
d[i] = d[i + 1] = d[i + 2] = v;
d[i + 3] = 22; // very light
}
ctx.putImageData(img, 0, 0);
}
regen();
setInterval(regen, 120);
}
// ---------- 5b. SCROLL PROGRESS BAR ----------
function initScrollProgress() {
var bar = document.getElementById("anScrollProgress");
if (!bar) return;
function update() {
var h = document.documentElement;
var max = h.scrollHeight - h.clientHeight;
var p = max > 0 ? (h.scrollTop / max) * 100 : 0;
bar.style.width = p.toFixed(2) + "%";
}
update();
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
}
// ---------- 5c. LIVE STAT TICKER (uptime + counters in hero) ----------
function initLiveStatTicker() {
var t = document.getElementById("anLiveStats");
if (!t) return;
// randomized but stable starting values
var base = Date.now();
var seed = Math.floor((base / (24 * 3600 * 1000)) % 999);
var leads = 120 + (seed % 60); // ~120-180
var deployMin = 7 + (seed % 23); // last deploy 7-30 min ago
// uptime since 2022-04-01 (Денмаrk start)
var start = new Date("2022-04-01T00:00:00Z").getTime();
function fmt() {
var diff = Date.now() - start;
var days = Math.floor(diff / (24 * 3600 * 1000));
var ml = 'LIVE · ';
ml += '' + days + ' днів у Данії · ';
ml += '' + leads + ' лідів за квартал · ';
ml += 'last deploy ' + deployMin + ' хв тому';
t.innerHTML = ml;
}
fmt();
setInterval(function () {
// tiny jitter for realism
if (Math.random() < 0.3) deployMin++;
if (Math.random() < 0.05) leads++;
fmt();
}, 12000);
}
// ---------- swap portrait if image url available via data attribute ----------
function initPortraitImage() {
var img = document.getElementById("anPortraitImg");
if (!img) return;
var src = img.dataset.src || img.getAttribute("src");
if (!src || src.indexOf("__") === 0) return;
var test = new Image();
test.onload = function () {
img.src = src;
img.classList.add("is-loaded");
// Hide AL placeholder when real photo loaded
var placeholder = document.querySelector(".an .portrait__placeholder");
if (placeholder) placeholder.style.display = "none";
};
test.src = src;
}
})();