2.0
This commit is contained in:
parent
d371c44145
commit
e347292cac
3 changed files with 1906 additions and 380 deletions
409
assets/js/custom/modern.js
Normal file
409
assets/js/custom/modern.js
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
/* ============================================================
|
||||
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();
|
||||
});
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue