//*html*//
/* Hero Section - Horizontal Scrolling Text */ //*.hero2-scrolling { height: 120vh; display: flex; align-items: center overflow: hidden; position: relative; } .scrolling-rail { display: flex; white-space: nowrap; } .scrolling-rail h1 { font-size: clamp(50px, 15vw, 100px); font-weight: 250; margin-right: 150px; text-transform: uppercase; letter-spacing: 0.02em; } .scrolling-rail h1 span { display: inline-block; transition: transform 2s; } .scrolling-rail h1 span.shake { animation: shake 3s ease-in-out; } @keyframes shake { 0%, 100% { transform: translateX(0) translateY(0); } 25% { transform: translateX(-3px) translateY(-2px) rotate(360deg); } 50% { transform: translateX(3px) translateY(2px) rotate(360deg); } 75% { transform: translateX(-2px) translateY(1px) rotate(360deg); } }

FILIPE ABREU DA FONSECA

DIGITAL CV

// Split Hero Text with Shake Effect function splitHeroText() { const heroHeadings = document.querySelectorAll('.scrolling-rail h1'); heroHeadings.forEach(h1 => { const text = h1.textContent; h1.innerHTML = ''; text.split('').forEach(char => { const span = document.createElement('span'); span.textContent = char === ' ' ? '\u00A0' : char; span.addEventListener('mouseenter', () => { span.classList.add('shake'); setTimeout(() => span.classList.remove('shake'), 500); }); h1.appendChild(span); }); }); } splitHeroText(); const heroTl = horizontalLoop('.scrolling-rail h1', { repeat: -1, paddingRight: 350, speed: 2 }); heroTl.timeScale(0); Observer.create({ target: '.hero2-scrolling', type: 'wheel,touch', onChangeY(self) { let factor = 2; if (self.deltaY < 0) factor *= -1; gsap.timeline({ defaults: { ease: "none" }}) .to(heroTl, { timeScale: factor * 2.5, duration: 0.2, overwrite: true }) .to(heroTl, { timeScale: 0, duration: 1 }, "+=0.3"); } }); function horizontalLoop(items, config) { items = gsap.utils.toArray(items); config = config || {}; let tl = gsap.timeline({ repeat: config.repeat, paused: config.paused, defaults: {ease: "none"}, onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100) }); let length = items.length; let startX = items[0].offsetLeft; let times = []; let widths = []; let xPercents = []; let pixelsPerSecond = (config.speed || 1) * 100; let snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1); let totalWidth, curX, distanceToStart, distanceToLoop, item, i; gsap.set(items, { xPercent: (i, el) => { let w = widths[i] = parseFloat(gsap.getProperty(el, "width", "px")); xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / w * 100 + gsap.getProperty(el, "xPercent")); return xPercents[i]; } }); gsap.set(items, {x: 0}); totalWidth = items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX") + (parseFloat(config.paddingRight) || 0); for (i = 0; i < length; i++) { item = items[i]; curX = xPercents[i] / 100 * widths[i]; distanceToStart = item.offsetLeft + curX - startX; distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX"); tl.to(item, {xPercent: snap((curX - distanceToLoop) / widths[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0) .fromTo(item, {xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)}, {xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond) .add("label" + i, distanceToStart / pixelsPerSecond); times[i] = distanceToStart / pixelsPerSecond; } tl.times = times; tl.progress(1, true).progress(0, true); if (config.reversed) { tl.vars.onReverseComplete(); tl.reverse(); } return tl; }