/* ============================================================ OPZEKER IT — MODERN JS - Three.js animated WebGL hero (particle network / torus knot) - Typed text rotator - Scroll reveal (IntersectionObserver) - Spotlight hover on service cards - Magnetic nav, sticky header, scrollspy, smooth scroll - Counter up, skill bars animation ============================================================ */ (function () { 'use strict'; /* ---------- Helpers ---------- */ const $ = (s, r = document) => r.querySelector(s); const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ============================================================ 1. THREE.JS WEBGL HERO BACKGROUND Dynamic particle network + glowing torus knot ============================================================ */ function initWebGLHero() { const container = document.getElementById('mx-webgl'); if (!container || typeof THREE === 'undefined') return; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 55, container.clientWidth / container.clientHeight, 0.1, 1000 ); camera.position.z = 18; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setClearColor(0x000000, 0); container.appendChild(renderer.domElement); /* ----- Glowing torus knot (center hero object) ----- */ const knotGeo = new THREE.TorusKnotGeometry(3.2, 0.55, 220, 28, 2, 3); const knotMat = new THREE.MeshBasicMaterial({ color: 0x9600f4, wireframe: true, transparent: true, opacity: 0.28 }); const knot = new THREE.Mesh(knotGeo, knotMat); scene.add(knot); // Second ghostly knot for parallax const knot2Mat = new THREE.MeshBasicMaterial({ color: 0x00f2ff, wireframe: true, transparent: true, opacity: 0.18 }); const knot2 = new THREE.Mesh(knotGeo, knot2Mat); knot2.scale.setScalar(1.15); scene.add(knot2); /* ----- Particle starfield ----- */ const particleCount = prefersReducedMotion ? 400 : 1400; const positions = new Float32Array(particleCount * 3); const colors = new Float32Array(particleCount * 3); const colorA = new THREE.Color(0x00f2ff); const colorB = new THREE.Color(0xd42bff); for (let i = 0; i < particleCount; i++) { const i3 = i * 3; // distribute in a large sphere shell const r = 10 + Math.random() * 30; const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i3] = r * Math.sin(phi) * Math.cos(theta); positions[i3 + 1] = r * Math.sin(phi) * Math.sin(theta); positions[i3 + 2] = r * Math.cos(phi); const c = colorA.clone().lerp(colorB, Math.random()); colors[i3] = c.r; colors[i3 + 1] = c.g; colors[i3 + 2] = c.b; } const pGeo = new THREE.BufferGeometry(); pGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); pGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); const pMat = new THREE.PointsMaterial({ size: 0.08, vertexColors: true, transparent: true, opacity: 0.9, sizeAttenuation: true, blending: THREE.AdditiveBlending, depthWrite: false }); const points = new THREE.Points(pGeo, pMat); scene.add(points); /* ----- Interaction: mouse parallax ----- */ const target = { x: 0, y: 0 }; const current = { x: 0, y: 0 }; container.parentElement.addEventListener('mousemove', (e) => { const rect = container.getBoundingClientRect(); target.x = ((e.clientX - rect.left) / rect.width - 0.5) * 2; target.y = ((e.clientY - rect.top) / rect.height - 0.5) * 2; }); /* ----- Resize ----- */ function onResize() { const w = container.clientWidth; const h = container.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); } window.addEventListener('resize', onResize); /* ----- Animate ----- */ let rafId; const clock = new THREE.Clock(); function animate() { const t = clock.getElapsedTime(); // Smooth mouse lerp current.x += (target.x - current.x) * 0.05; current.y += (target.y - current.y) * 0.05; // Rotate knots knot.rotation.x = t * 0.15 + current.y * 0.4; knot.rotation.y = t * 0.22 + current.x * 0.4; knot2.rotation.x = -t * 0.10 + current.y * 0.25; knot2.rotation.y = -t * 0.17 + current.x * 0.25; // Rotate particle field slowly points.rotation.y = t * 0.03 + current.x * 0.15; points.rotation.x = current.y * 0.15; // Subtle camera breathing camera.position.z = 18 + Math.sin(t * 0.3) * 0.4; camera.lookAt(0, 0, 0); renderer.render(scene, camera); rafId = requestAnimationFrame(animate); } animate(); // Pause when tab hidden document.addEventListener('visibilitychange', () => { if (document.hidden) cancelAnimationFrame(rafId); else animate(); }); } /* ============================================================ 2. TYPED ROTATOR ============================================================ */ function initTyped() { const el = $('#mx-typed'); if (!el) return; const words = JSON.parse(el.dataset.words || '[]'); if (!words.length) return; const cursor = document.createElement('span'); cursor.className = 'typed-cursor'; cursor.textContent = '|'; const textSpan = document.createElement('span'); el.appendChild(textSpan); el.appendChild(cursor); let i = 0, j = 0, deleting = false; function tick() { const word = words[i]; if (!deleting) { textSpan.textContent = word.slice(0, ++j); if (j === word.length) { deleting = true; return setTimeout(tick, 1600); } } else { textSpan.textContent = word.slice(0, --j); if (j === 0) { deleting = false; i = (i + 1) % words.length; } } setTimeout(tick, deleting ? 40 : 70); } tick(); } /* ============================================================ 3. HEADER SHRINK + SCROLLSPY + SMOOTH SCROLL + BURGER ============================================================ */ function initHeader() { const header = $('.mx-header'); const navLinks = $$('.mx-nav-links a[href^="#"]'); const sections = navLinks .map(a => document.getElementById(a.getAttribute('href').slice(1))) .filter(Boolean); function onScroll() { if (window.scrollY > 30) header.classList.add('scrolled'); else header.classList.remove('scrolled'); // scrollspy const scroll = window.scrollY + 120; let activeId = null; sections.forEach(sec => { if (sec.offsetTop <= scroll) activeId = sec.id; }); navLinks.forEach(a => { a.classList.toggle('active', a.getAttribute('href') === '#' + activeId); }); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); // Smooth scroll for internal links $$('a[href^="#"]').forEach(a => { a.addEventListener('click', (e) => { const id = a.getAttribute('href'); if (id === '#' || id.length < 2) return; const target = document.querySelector(id); if (!target) return; e.preventDefault(); const y = target.getBoundingClientRect().top + window.scrollY - 70; window.scrollTo({ top: y, behavior: 'smooth' }); // close mobile nav const nav = $('.mx-nav-links'); const burger = $('.mx-burger'); if (nav && nav.classList.contains('mobile-open')) { nav.classList.remove('mobile-open'); burger && burger.classList.remove('open'); } }); }); // Burger const burger = $('.mx-burger'); const nav = $('.mx-nav-links'); if (burger && nav) { burger.addEventListener('click', () => { burger.classList.toggle('open'); nav.classList.toggle('mobile-open'); }); } } /* ============================================================ 4. SCROLL REVEAL ============================================================ */ function initReveal() { const els = $$('.reveal'); if (!('IntersectionObserver' in window)) { els.forEach(el => el.classList.add('is-visible')); return; } const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); io.unobserve(entry.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -50px 0px' }); els.forEach(el => io.observe(el)); } /* ============================================================ 5. SPOTLIGHT HOVER (service cards) ============================================================ */ function initSpotlight() { $$('.mx-service-card').forEach(card => { card.addEventListener('mousemove', (e) => { const r = card.getBoundingClientRect(); card.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%'); card.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%'); }); }); } /* ============================================================ 6. TILT on cards (subtle 3D) ============================================================ */ function initTilt() { if (prefersReducedMotion) return; $$('[data-tilt]').forEach(el => { const maxTilt = parseFloat(el.dataset.tilt) || 6; el.style.transformStyle = 'preserve-3d'; el.addEventListener('mousemove', (e) => { const r = el.getBoundingClientRect(); const px = (e.clientX - r.left) / r.width - 0.5; const py = (e.clientY - r.top) / r.height - 0.5; el.style.transform = `perspective(900px) rotateY(${px * maxTilt}deg) rotateX(${-py * maxTilt}deg) translateY(-6px)`; }); el.addEventListener('mouseleave', () => { el.style.transform = ''; }); }); } /* ============================================================ 7. MAGNETIC buttons ============================================================ */ function initMagnetic() { if (prefersReducedMotion) return; $$('[data-magnetic]').forEach(el => { const strength = parseFloat(el.dataset.magnetic) || 0.3; el.addEventListener('mousemove', (e) => { const r = el.getBoundingClientRect(); const mx = e.clientX - (r.left + r.width / 2); const my = e.clientY - (r.top + r.height / 2); el.style.transform = `translate(${mx * strength}px, ${my * strength}px)`; }); el.addEventListener('mouseleave', () => { el.style.transform = ''; }); }); } /* ============================================================ 8. COUNTER UP + SKILL BARS (on reveal) ============================================================ */ function initCounters() { if (!('IntersectionObserver' in window)) return; // Numbers $$('[data-count]').forEach(el => { const target = parseFloat(el.dataset.count); const duration = parseInt(el.dataset.duration || '1800', 10); const suffix = el.dataset.suffix || ''; const prefix = el.dataset.prefix || ''; let started = false; const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !started) { started = true; const start = performance.now(); function step(now) { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); const val = target * eased; const display = Number.isInteger(target) ? Math.round(val) : val.toFixed(1); el.textContent = prefix + display + suffix; if (t < 1) requestAnimationFrame(step); } requestAnimationFrame(step); io.unobserve(el); } }); }, { threshold: 0.5 }); io.observe(el); }); // Skill bars $$('.mx-skill-bar').forEach(bar => { const pct = parseFloat(bar.dataset.value || '0'); const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { bar.style.width = pct + '%'; io.unobserve(bar); } }); }, { threshold: 0.3 }); io.observe(bar); }); } /* ============================================================ 9. MARQUEE DUPLICATION (seamless loop) ============================================================ */ function initMarquee() { $$('.mx-marquee-track').forEach(track => { const clone = track.innerHTML; track.innerHTML = clone + clone; }); } /* ============================================================ BOOT ============================================================ */ document.addEventListener('DOMContentLoaded', () => { initHeader(); initReveal(); initSpotlight(); initTilt(); initMagnetic(); initCounters(); initMarquee(); initTyped(); initWebGLHero(); }); })();