<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<script src="https://unpkg.com/lenis@1.3.15/dist/lenis.min.js"></script>
<script>
const lenis = new Lenis({ duration: 1.4 });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
</script>// =========================================================
// 1. TEXT SCRAMBLE ON HOVER
// =========================================================
function scrambleText(el, finalText, options = {}) {
const chars = options.chars || '01<>[]{}#$@';
const duration = options.duration || 0.5;
const revealPad = options.revealDelay ?? 0.15;
const length = finalText.length;
const state = { progress: 0 };
return gsap.to(state, {
progress: 1,
duration: duration,
ease: 'none',
onUpdate: () => {
const revealCount = Math.floor(state.progress * (1 + revealPad) * length);
let output = '';
for (let i = 0; i < length; i++) {
if (finalText[i] === ' ') {
output += ' ';
} else if (i < revealCount) {
output += finalText[i];
} else {
output += chars[Math.floor(Math.random() * chars.length)];
}
}
el.textContent = output;
},
onComplete: () => {
el.textContent = finalText;
if (options.onComplete) options.onComplete();
},
});
}
const menus = document.querySelectorAll('.menu-navbar, .button, .link-navbar, .item-work, .card-blog');
menus.forEach((menu) => {
const text = menu.querySelector('.scramble');
if (!text) return;
const originalText = text.textContent.trim();
const originalColor = getComputedStyle(text).color;
let active = false;
menu.addEventListener('mouseenter', () => {
if (active) return;
active = true;
gsap.to(text, { color: '#22d3ee', duration: 0.2 });
scrambleText(text, originalText, {
chars: '01<>[]{}#$@',
duration: 0.5,
revealDelay: 0.15,
onComplete: () => {
gsap.to(text, {
color: originalColor,
duration: 0.4,
});
active = false;
},
});
});
menu.addEventListener('mouseleave', () => {
// re-scramble halus saat mouse keluar, tanpa mengunci state "active"
scrambleText(text, originalText, {
chars: '01<>[]{}#$@',
duration: 0.5,
revealDelay: 0,
});
});
});// =========================================================
// 2. RIPPLE BAR (scroll-linked)
// =========================================================
const wrapper = document.querySelector('.wrapper-ripple');
if (wrapper) {
const template = wrapper.querySelector('.ripple-bar');
if (template) {
const gap = 2;
const influence = 8;
function buildRipple() {
wrapper.querySelectorAll('.ripple-bar:not(:first-child)').forEach((el) => el.remove());
const wrapperHeight = wrapper.offsetHeight;
const barHeight = template.offsetHeight;
const total = Math.floor(wrapperHeight / (barHeight + gap));
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
wrapper.style.alignItems = 'center';
wrapper.style.gap = `${gap}px`;
for (let i = 1; i < total; i++) {
const clone = template.cloneNode(true);
clone.classList.add('ripple-clone');
wrapper.appendChild(clone);
}
updateRipple(0);
}
function updateRipple(progress) {
const bars = wrapper.querySelectorAll('.ripple-bar');
const active = progress * (bars.length - 1);
bars.forEach((bar, index) => {
const distance = Math.abs(index - active);
const strength = Math.exp(-(distance * distance) / (2 * influence * influence));
gsap.set(bar, {
scaleX: 0.2 + strength * 0.8,
opacity: 0.2 + strength * 0.8,
transformOrigin: 'center center',
});
});
}
buildRipple();
window.addEventListener('resize', buildRipple);
ScrollTrigger.create({
trigger: '.track-service',
start: 'center-=15% bottom',
end: 'bottom-=10% top',
scrub: true,
onUpdate: (self) => updateRipple(self.progress),
});
}
}// =========================================================
// 3. NAVBAR HIDE ON SCROLL
// =========================================================
if (window.innerWidth >= 992) {
const navbar = document.querySelector('.navbar');
const logo = document.querySelector('.link-logo-nav');
if (navbar && logo) {
let lastScroll = window.pageYOffset;
let isHidden = false;
window.addEventListener('scroll', () => {
const currentScroll = window.pageYOffset;
if (currentScroll > lastScroll && currentScroll > 50) {
if (!isHidden) {
isHidden = true;
gsap.to(logo, { yPercent: -100, opacity: 0, duration: 0.6, ease: 'power3.inOut' });
gsap.to(navbar, {
xPercent: 100,
opacity: 0,
duration: 0.6,
ease: 'power3.inOut',
onComplete: () => {
gsap.set([navbar, logo], { display: 'none' });
},
});
}
} else if (currentScroll < lastScroll) {
if (isHidden) {
isHidden = false;
gsap.set([navbar, logo], { display: '' });
gsap.set(navbar, { xPercent: 100, opacity: 0 });
gsap.set(logo, { yPercent: -100, opacity: 0 });
gsap.to(navbar, { xPercent: 0, opacity: 1, duration: 0.6, ease: 'power3.out' });
gsap.to(logo, { yPercent: 0, opacity: 1, duration: 0.6, ease: 'power3.out' });
}
}
lastScroll = currentScroll;
});
}
}<script>
document.addEventListener('DOMContentLoaded', function () {
gsap.registerPlugin(ScrollTrigger, SplitText);
var CONFIG = {
fromColor: 'rgba(255, 255, 255, 0.15)',
glitchColor: '#22d3ee',
toColor: 'rgba(255, 255, 255, 1)',
flickerCount: 4,
flickerSpeed: 0.045,
settleDuration: 0.3,
settleEase: 'power2.out',
splitType: 'words',
stagger: 0.06,
start: 'top 85%',
end: 'bottom 60%',
toggleActions: 'play none play reverse',
};
function buildGlitchTimeline(target) {
var tl = gsap.timeline();
for (var i = 0; i < CONFIG.flickerCount; i++) {
var flickerToGlitch = i % 2 === 0;
tl.set(
target,
{
color: flickerToGlitch ? CONFIG.glitchColor : CONFIG.fromColor,
},
i === 0 ? 0 : '+=' + CONFIG.flickerSpeed,
);
}
tl.to(
target,
{
color: CONFIG.toColor,
duration: CONFIG.settleDuration,
ease: CONFIG.settleEase,
},
'+=' + CONFIG.flickerSpeed,
);
return tl;
}
var elements = document.querySelectorAll('.reveal-animation');
elements.forEach(function (el) {
gsap.set(el, { visibility: 'hidden' });
var split = new SplitText(el, {
type: CONFIG.splitType,
wordsClass: 'reveal-word',
linesClass: 'reveal-line',
});
var targets = split.words && split.words.length ? split.words : split.lines;
gsap.set(targets, {
display: 'inline-block',
willChange: 'color',
color: CONFIG.fromColor,
});
gsap.set(el, { visibility: 'visible' });
var master = gsap.timeline({
scrollTrigger: {
trigger: el,
start: CONFIG.start,
end: CONFIG.end,
toggleActions: CONFIG.toggleActions,
},
});
targets.forEach(function (word, i) {
master.add(buildGlitchTimeline(word), i * CONFIG.stagger);
});
});
window.addEventListener('load', function () {
ScrollTrigger.refresh();
});
});
</script>