/* Devskin v4 — PRO effects library --------------------------------------------------------- Canvas particle network, typed cycling text, magnetic cursor trails, parallax, character-splitter, scroll pipelines, animated stats bars, comparison slider, testimonials carousel, code-editor typewriter. All vanilla JS, GPU-friendly. --------------------------------------------------------- */ (function () { 'use strict'; var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; var isMobile = window.innerWidth < 768 || ('ontouchstart' in window); /* ============================================================ 1) Canvas particle network — connecting dots ============================================================ */ function initParticleNetwork() { var canvas = document.getElementById('particleNet'); if (!canvas) return; if (isMobile || reduced) { canvas.remove(); return; } var ctx = canvas.getContext('2d'); var dpr = Math.min(window.devicePixelRatio || 1, 2); var w, h; var stars = []; var count = 75; var mouse = { x: -9999, y: -9999 }; function resize() { w = canvas.clientWidth; h = canvas.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function Star() { this.x = Math.random() * w; this.y = Math.random() * h; this.vx = (Math.random() - 0.5) * 0.14; this.vy = (Math.random() - 0.5) * 0.14; this.r = Math.random() * 3.5 + 1.2; this.alpha = Math.random() * 0.35 + 0.15; this.pulseSpeed = Math.random() * 0.012 + 0.004; this.pulseDir = Math.random() > 0.5 ? 1 : -1; } Star.prototype.step = function () { this.x += this.vx; this.y += this.vy; if (this.x < 0) this.x = w; if (this.x > w) this.x = 0; if (this.y < 0) this.y = h; if (this.y > h) this.y = 0; this.alpha += this.pulseSpeed * this.pulseDir; if (this.alpha > 0.55 || this.alpha < 0.12) { this.pulseDir *= -1; } }; Star.prototype.draw = function () { var dx = this.x - mouse.x; var dy = this.y - mouse.y; var dist = Math.sqrt(dx * dx + dy * dy); var currentAlpha = this.alpha; var currentRadius = this.r; if (dist < 220) { var factor = 1 - dist / 220; currentAlpha = Math.min(0.95, this.alpha + factor * 0.65); currentRadius = this.r + factor * 3.2; // Desenha uma linha de conexão suave para o mouse ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo(mouse.x, mouse.y); ctx.strokeStyle = 'rgba(255, 255, 255, ' + (factor * 0.35) + ')'; ctx.lineWidth = factor * 1.5; ctx.stroke(); } var grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, currentRadius); grad.addColorStop(0, 'rgba(255, 255, 255, ' + currentAlpha + ')'); grad.addColorStop(0.3, 'rgba(212, 212, 216, ' + (currentAlpha * 0.45) + ')'); grad.addColorStop(1, 'rgba(24, 24, 27, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(this.x, this.y, currentRadius, 0, Math.PI * 2); ctx.fill(); }; function loop() { ctx.clearRect(0, 0, w, h); for (var i = 0; i < stars.length; i++) { var s = stars[i]; s.step(); s.draw(); } if (!stopped) requestAnimationFrame(loop); } var stopped = false; resize(); for (var i = 0; i < count; i++) stars.push(new Star()); window.addEventListener('resize', resize); canvas.addEventListener('mousemove', function (e) { var r = canvas.getBoundingClientRect(); mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top; }); canvas.addEventListener('mouseleave', function () { mouse.x = -9999; mouse.y = -9999; }); if (!reduced) loop(); } /* ============================================================ 2) Typed cycling text ============================================================ */ function initTyped() { document.querySelectorAll('[data-typed]').forEach(function (el) { var words = (el.getAttribute('data-typed') || '').split('|').map(function (s) { return s.trim(); }).filter(Boolean); if (!words.length) return; var idx = 0, char = 0, deleting = false; function tick() { var word = words[idx]; if (!deleting) { char++; el.textContent = word.slice(0, char); if (char === word.length) { deleting = true; setTimeout(tick, 1700); return; } } else { char--; el.textContent = word.slice(0, char); if (char === 0) { deleting = false; idx = (idx + 1) % words.length; } } setTimeout(tick, deleting ? 35 : 80); } tick(); }); } /* ============================================================ 3) Magnetic buttons — hover follow ============================================================ */ function initMagnetic() { if (reduced || isMobile) return; document.querySelectorAll('.magnetic').forEach(function (el) { var strength = parseFloat(el.getAttribute('data-magnet') || 0.3); el.addEventListener('mousemove', function (e) { var r = el.getBoundingClientRect(); var x = e.clientX - r.left - r.width / 2; var y = e.clientY - r.top - r.height / 2; el.style.transform = 'translate(' + (x * strength) + 'px,' + (y * strength) + 'px)'; }); el.addEventListener('mouseleave', function () { el.style.transform = ''; }); }); } /* Cursor glow removed — caused a stray white orb over dark footer */ function initCursorGlow() { /* noop */ } /* ============================================================ 5) Parallax on scroll (data-parallax="0.3") ============================================================ */ function initParallax() { if (reduced) return; var els = [].slice.call(document.querySelectorAll('[data-parallax]')); if (!els.length) return; function apply() { var vh = window.innerHeight; els.forEach(function (el) { var r = el.getBoundingClientRect(); if (r.bottom < 0 || r.top > vh) return; var k = parseFloat(el.getAttribute('data-parallax') || 0.2); var offset = (r.top - vh / 2) * k * -1; el.style.transform = 'translate3d(0,' + offset.toFixed(1) + 'px,0)'; }); } window.addEventListener('scroll', apply, { passive: true }); window.addEventListener('resize', apply); apply(); } /* ============================================================ 6) Char-split reveal — each character animates in ============================================================ */ /* Word-split reveal DISABLED — it rewrote h2/h1 into inline-block spans on boot, which changed word-wrap breakpoints and consequently the height of headings. That height delta cascaded through the page, causing visible scroll shift as the user was scrolling. We replace with a simple fade-in handled by the regular `.reveal` observer — no DOM mutation, no layout recalc, no scroll jump. */ function initCharSplit() { document.querySelectorAll('[data-split]').forEach(function (el) { el.classList.add('in'); // reveal target already visible el.removeAttribute('data-split'); // stop future processing }); } /* ============================================================ 7) Stats bars — progress bar fills on scroll ============================================================ */ function initStatsBars() { var bars = document.querySelectorAll('.stats-bar'); if (!bars.length) return; if (!('IntersectionObserver' in window) || reduced) { bars.forEach(function (b) { b.style.setProperty('--val', (parseInt(b.getAttribute('data-value') || 0, 10)) + '%'); b.classList.add('in'); }); return; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (!e.isIntersecting) return; var b = e.target; b.style.setProperty('--val', (parseInt(b.getAttribute('data-value') || 0, 10)) + '%'); b.classList.add('in'); io.unobserve(b); }); }, { threshold: 0.4 }); bars.forEach(function (b) { io.observe(b); }); } /* ============================================================ 8) Comparison slider (before / after) ============================================================ */ function initComparison() { document.querySelectorAll('.compare').forEach(function (c) { var slider = c.querySelector('.compare-slider'); var handle = c.querySelector('.compare-handle'); var after = c.querySelector('.compare-after'); if (!slider || !handle || !after) return; function setPos(percent) { percent = Math.max(0, Math.min(100, percent)); after.style.clipPath = 'inset(0 0 0 ' + percent + '%)'; handle.style.left = percent + '%'; } setPos(50); var dragging = false; function onMove(e) { if (!dragging) return; var r = c.getBoundingClientRect(); var x = (e.touches ? e.touches[0].clientX : e.clientX) - r.left; setPos((x / r.width) * 100); } handle.addEventListener('mousedown', function () { dragging = true; }); handle.addEventListener('touchstart', function () { dragging = true; }, { passive: true }); window.addEventListener('mouseup', function () { dragging = false; }); window.addEventListener('touchend', function () { dragging = false; }); window.addEventListener('mousemove', onMove); window.addEventListener('touchmove', onMove, { passive: true }); c.addEventListener('click', function (e) { var r = c.getBoundingClientRect(); setPos(((e.clientX - r.left) / r.width) * 100); }); }); } /* ============================================================ 9) Testimonials carousel (simple horizontal slider) ============================================================ */ function initTestimonials() { document.querySelectorAll('.testi-carousel').forEach(function (c) { var track = c.querySelector('.testi-track'); if (!track) return; var slides = [].slice.call(track.children); if (!slides.length) return; var idx = 0; function go(i) { idx = (i + slides.length) % slides.length; track.style.transform = 'translateX(' + (-idx * 100) + '%)'; var dots = c.querySelectorAll('.testi-dot'); dots.forEach(function (d, di) { d.classList.toggle('active', di === idx); }); } var prev = c.querySelector('.testi-prev'); var next = c.querySelector('.testi-next'); if (prev) prev.addEventListener('click', function () { go(idx - 1); }); if (next) next.addEventListener('click', function () { go(idx + 1); }); var dotsWrap = c.querySelector('.testi-dots'); if (dotsWrap) { slides.forEach(function (_, i) { var d = document.createElement('button'); d.type = 'button'; d.className = 'testi-dot' + (i === 0 ? ' active' : ''); d.addEventListener('click', function () { go(i); }); dotsWrap.appendChild(d); }); } var auto = setInterval(function () { go(idx + 1); }, 6000); c.addEventListener('mouseenter', function () { clearInterval(auto); }); }); } /* ============================================================ 10) Code editor typewriter ============================================================ */ function initCodeEditor() { // Skip typewriter on mobile — long-running setTimeout chain eats CPU + battery // and adds nothing for users on small screens (code is hard to read anyway). if (isMobile) return; document.querySelectorAll('.code-editor[data-lines]').forEach(function (ed) { var lines = JSON.parse(ed.getAttribute('data-lines') || '[]'); var pre = ed.querySelector('pre'); if (!pre) return; var li = 0, ci = 0; function step() { if (li >= lines.length) { // Loop after pause setTimeout(function () { pre.innerHTML = ''; li = 0; ci = 0; step(); }, 4000); return; } var line = lines[li]; if (ci <= line.length) { pre.innerHTML = lines.slice(0, li).map(function (l) { return colorize(l); }).join('\n') + '\n' + colorize(line.slice(0, ci)) + '|'; ci++; setTimeout(step, 22); } else { li++; ci = 0; setTimeout(step, 120); } } function colorize(s) { return s .replace(/(\/\/.*$)/g, '$1') .replace(/\b(const|let|var|function|return|async|await|import|from|export|if|else|new|class)\b/g, '$1') .replace(/\b(true|false|null|undefined)\b/g, '$1') .replace(/('[^&]*'|"[^"]*")/g, '$1'); } if ('IntersectionObserver' in window && !reduced) { var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting) { step(); io.unobserve(e.target); } }); }, { threshold: 0.3 }); io.observe(ed); } else { step(); } }); } /* ============================================================ 11) FAQ accordion ============================================================ */ function initFaq() { document.querySelectorAll('.faq-item').forEach(function (q) { var btn = q.querySelector('.faq-q'); if (!btn) return; btn.addEventListener('click', function () { q.classList.toggle('open'); btn.setAttribute('aria-expanded', q.classList.contains('open')); }); }); } /* ============================================================ 12) 3D tech orb — CSS-based rotating sphere with logos ============================================================ */ function initTechOrb() { var orbs = document.querySelectorAll('.tech-orb'); orbs.forEach(function (orb) { var items = orb.querySelectorAll('.tech-orb-item'); var n = items.length; if (!n) return; items.forEach(function (el, i) { var phi = Math.acos(-1 + (2 * i) / n); var theta = Math.sqrt(n * Math.PI) * phi; el.style.setProperty('--phi', phi); el.style.setProperty('--theta', theta); }); }); } /* ============================================================ Boot ============================================================ */ function boot() { var pnFired = false; function startPN(){ if (pnFired) return; pnFired = true; initParticleNetwork(); } ['scroll','pointerdown','keydown','touchstart'].forEach(function(ev){ window.addEventListener(ev, startPN, { once: true, passive: true }); }); setTimeout(startPN, 1200); initTyped(); initMagnetic(); initCursorGlow(); initParallax(); initCharSplit(); initStatsBars(); initComparison(); initTestimonials(); initCodeEditor(); initFaq(); initTechOrb(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(boot, 30); } else { document.addEventListener('DOMContentLoaded', boot); } })();