Earlier today, Brad Woods Digital Garden made it to the front page of HN. I had never heard of this website before, so I clicked on the link and explored it. As it turns out, it’s the web development blog of Brad Woods. It’s also one of the most detailed and high-effort webpages I’ve seen in quite a while, and the blog content is high-quality and very interesting. But what really stood out to me was how most of the text on the page initially appears scrambled, and then gets “solved” in a quirky scramble animation.
This effect is awesome, and I wanted to see how it would look on my website. I initially planned to just steal Woods’ animation by nabbing the code from my Firefox cache, but he’s minified and obfuscated it in a way that would take more effort to unravel than I’m willing to expend.
My Implementation
So instead I just reverse engineered it. I wanted it to be a CSS keyframe animation that I could call within a class, but this kind of content manipulation isn’t really doable in CSS. Instead I used the “animationstart” event in JavaScript. Here’s the full JavaScript function I came up with. It’s un-obfuscated because that’s how normal developers publish code.
1(function () {
2 document.addEventListener('animationstart', function (e) {
3 if (e.animationName === 'scramble-text' && e.target instanceof HTMLElement) {
4 const element = e.target;
5 const originalText = element.textContent;
6 if (!originalText) return;
7
8 const SCRAMBLE_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';
9 const REVEAL_DELAY = 10;
10 const SOLVE_DELAY = 60;
11 const SCRAMBLE_SPEED = 50;
12 const HIDDEN_CHAR = '\u2007';
13
14 let charStates = Array.from(originalText).map(char => ({
15 final: char,
16 state: 'hidden',
17 }));
18
19 element.textContent = Array(originalText.length + 1).join(HIDDEN_CHAR);
20
21 const totalRevealTime = (charStates.length - 1) * REVEAL_DELAY;
22 const totalAnimationDuration = totalRevealTime + ((charStates.length - 1) * SOLVE_DELAY);
23 const startTime = performance.now();
24
25 charStates.forEach((charState, i) => {
26 setTimeout(() => {
27 charState.state = (charState.final.trim() === '') ? 'solved' : 'scrambled';
28 }, i * REVEAL_DELAY);
29 setTimeout(() => {
30 charState.state = 'solved';
31 }, totalRevealTime + (i * SOLVE_DELAY));
32 });
33
34 let lastScrambleUpdate = 0;
35 let prevOutput = [];
36
37 const update = (currentTime) => {
38 const elapsed = currentTime - startTime;
39
40 if (elapsed >= totalAnimationDuration + SOLVE_DELAY) {
41 element.textContent = originalText;
42 return;
43 }
44
45 const scrambleIntervalPassed = (currentTime - lastScrambleUpdate) >= SCRAMBLE_SPEED;
46
47 let currentOutput = [];
48 for (let i = 0; i < charStates.length; i++) {
49 const charState = charStates[i];
50 switch (charState.state) {
51 case 'hidden':
52 currentOutput[i] = HIDDEN_CHAR;
53 break;
54 case 'scrambled':
55 currentOutput[i] = scrambleIntervalPassed
56 ? SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)]
57 : prevOutput[i];
58 break;
59 case 'solved':
60 currentOutput[i] = charState.final;
61 break;
62 }
63 }
64
65 if (scrambleIntervalPassed) {
66 lastScrambleUpdate = currentTime;
67 }
68
69 element.textContent = currentOutput.join('');
70 prevOutput = currentOutput;
71
72 requestAnimationFrame(update);
73 };
74
75 requestAnimationFrame(update);
76 }
77 }, { passive: true });
78})();
You use it by defining a CSS keyframe named scramble-text that doesn’t do anything substantial, and the function will hijack that keyframe and add the text scramble animation. Here’s an example implementation in CSS.
1.scramble-me {
2 animation: scramble-text 1s;
3}
4
5@keyframes scramble-text {
6 from {
7 opacity: 0.99;
8 }
9
10 to {
11 opacity: 1;
12 }
13}
Demo
I realize that you, dear reader, probably aren’t invested enough to actually test out my code, so I’ve made a little demo with CodePen.
Conclusion
I love this effect. I added it to the “Ahoy!” heading on my home page, and I think it looks really cool. Feel free to use my code however you like.
~Ethan