import { clampNum } from "../shared/math.js"; import { prefersReducedMotion } from "../shared/media.js"; export function initHeroWordSlot() { const wordEl = document.querySelector("[data-hero-rotator-word]"); if (!wordEl) return; const words = ["web", "mobile", "desktop"]; if (!words.length) return; if (prefersReducedMotion()) { wordEl.textContent = words[0]; return; } const alphabet = "abcdefghijklmnopqrstuvwxyz"; const slotStepMs = 45; const letterStopDelayMs = 80; const pauseBetweenWordsMs = 1700; let currentWordIndex = 0; let runToken = 0; const padWord = (word, len) => word.padEnd(len, " "); const randomLetter = () => alphabet[Math.floor(Math.random() * alphabet.length)]; function readThemeColorBase() { const root = getComputedStyle(document.documentElement); const h = clampNum(root.getPropertyValue("--theme-h"), 0, 360, 210); const s = clampNum(root.getPropertyValue("--theme-s"), 30, 100, 90); const l = clampNum(root.getPropertyValue("--theme-l"), 20, 70, 40); return { h, s, l }; } function randomScrambleColor(base) { const hVar = (base.h + (Math.random() * 84 - 42) + 360) % 360; const sVar = Math.round(clampNum(base.s + (Math.random() * 26 - 13), 35, 100, base.s)); const lVar = Math.round(clampNum(base.l + (Math.random() * 24 - 12), 22, 76, base.l)); return `hsl(${Math.round(hVar)} ${sVar}% ${lVar}%)`; } function renderChars(chars, scramble = false, base = null) { if (!scramble) { wordEl.textContent = chars.join(""); return; } const colorBase = base || readThemeColorBase(); const html = chars .map((ch) => { if (ch === " ") return ` `; return `${ch}`; }) .join(""); wordEl.innerHTML = html; } function animateToWord(nextWord) { runToken += 1; const token = runToken; const fromWord = words[currentWordIndex]; const maxLen = Math.max(fromWord.length, nextWord.length); const targetChars = padWord(nextWord, maxLen).split(""); const liveChars = padWord(fromWord, maxLen).split(""); const finalCharTime = (maxLen - 1) * letterStopDelayMs + 380; const spacingResetTime = Math.max(finalCharTime - 240, 0); const start = performance.now(); wordEl.style.setProperty("--hero-slot-spacing", "0.085em"); const themeColorBase = readThemeColorBase(); function tick(now) { if (token !== runToken) return; const elapsed = now - start; if (elapsed >= spacingResetTime) wordEl.style.setProperty("--hero-slot-spacing", "0"); for (let i = 0; i < maxLen; i++) { const settleAt = i * letterStopDelayMs; if (elapsed < settleAt) continue; const settleProgress = elapsed - settleAt; if (settleProgress >= 340) liveChars[i] = targetChars[i]; else if (targetChars[i] === " ") liveChars[i] = " "; else liveChars[i] = randomLetter(); } renderChars(liveChars, true, themeColorBase); if (elapsed >= finalCharTime) { renderChars(targetChars); wordEl.style.setProperty("--hero-slot-spacing", "0"); currentWordIndex = words.indexOf(nextWord); window.setTimeout(nextCycle, pauseBetweenWordsMs); return; } window.setTimeout(() => requestAnimationFrame(tick), slotStepMs); } requestAnimationFrame(tick); } function nextCycle() { const nextIndex = (currentWordIndex + 1) % words.length; animateToWord(words[nextIndex]); } wordEl.textContent = words[0]; window.setTimeout(nextCycle, pauseBetweenWordsMs); }