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