104 lines
3.6 KiB
JavaScript
104 lines
3.6 KiB
JavaScript
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 `<span class="hero-rotator__char"> </span>`;
|
|
return `<span class="hero-rotator__char" style="color:${randomScrambleColor(colorBase)}">${ch}</span>`;
|
|
})
|
|
.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);
|
|
}
|