/* 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; } })();