Skip to content

Instantly share code, notes, and snippets.

@lezhangxyz
Created March 3, 2025 14:43
Show Gist options
  • Save lezhangxyz/fb73ab2de5a44db2ce82bac8a6f73c1b to your computer and use it in GitHub Desktop.
Save lezhangxyz/fb73ab2de5a44db2ce82bac8a6f73c1b to your computer and use it in GitHub Desktop.
Text scrambler effect
// Scrambled text effect from cultura.xyz
// Can be looped, applied to other elements
class TextScramble {
constructor(el) {
this.el = el;
this.chars = "!<>-_\\/[]{}—=+*^?#________";
this.update = this.update.bind(this);
}
setText(newText) {
const oldText = this.el.innerText;
this.el.style.minHeight = `${this.el.offsetHeight}px`; // Preserve height
this.el.style.display = "inline-block"; // Prevent layout shift
const length = Math.max(oldText.length, newText.length);
const promise = new Promise((resolve) => (this.resolve = resolve));
this.queue = [];
for (let i = 0; i < length; i++) {
const from = oldText[i] || "";
const to = newText[i] || "";
const start = Math.floor(Math.random() * 40); // Increase randomness start
const end = start + Math.floor(Math.random() * 40); // Increase duration
this.queue.push({ from, to, start, end });
}
cancelAnimationFrame(this.frameRequest);
this.frame = 0;
this.update();
return promise;
}
update() {
let output = "";
let complete = 0;
for (let i = 0, n = this.queue.length; i < n; i++) {
let { from, to, start, end, char } = this.queue[i];
if (this.frame >= end) {
complete++;
output += to;
} else if (this.frame >= start) {
if (!char || Math.random() < 0.28) {
char = this.randomChar();
this.queue[i].char = char;
}
output += `<span style=\"opacity: 0.7; display: inline-block; width: 1ch;\">${char}</span>`; // Keep space reserved
} else {
output += `<span style=\"display: inline-block; width: 1ch;\">${from}</span>`;
}
}
this.el.innerHTML = output;
if (complete === this.queue.length) {
this.resolve();
} else {
this.frameRequest = requestAnimationFrame(this.update);
this.frame++;
}
}
randomChar() {
return this.chars[Math.floor(Math.random() * this.chars.length)];
}
}
function initTextScrambleForHeaders() {
const headers = document.querySelectorAll("em");
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const header = entry.target;
const text = header.innerText;
const fx = new TextScramble(header);
fx.setText(text);
observer.unobserve(header);
}
});
}, { threshold: 0.5 });
headers.forEach(header => observer.observe(header));
}
document.addEventListener("DOMContentLoaded", () => {
initTextScrambleForHeaders();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment