Ethan Marks

Text Scramble Animation

· 3 min read

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