Jump to…
snowfeat: given a new redesign and emulated terminal homepagerxkspqmsoknz1mo
1/**
2 * Terminal typewriter animation for the homepage example session.
3 *
4 * Each line in the terminal body is either a command (typed out character by
5 * character with a blinking cursor) or output (revealed instantly after the
6 * command finishes "typing"). Blank lines pause briefly before continuing.
7 *
8 * The animation respects `prefers-reduced-motion`: when the user prefers
9 * reduced motion, all lines are shown immediately with no animation.
10 *
11 * The animation starts when the terminal scrolls into view (IntersectionObserver)
12 * and only plays once.
13 */
14(function () {
15 "use strict";
16
17 var CHAR_DELAY = 32; // ms between typed characters
18 var LINE_PAUSE = 80; // ms pause after a full line before next
19 var CMD_PAUSE = 400; // ms pause after a command finishes before showing output
20 var BLANK_PAUSE = 250; // ms pause for blank lines
21 var INITIAL_DELAY = 600; // ms before animation starts after becoming visible
22
23 function initTerminalAnimation() {
24 var body = document.querySelector("[data-term-animate]");
25 if (!body) return;
26
27 // Respect prefers-reduced-motion
28 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
29 body.classList.add("term-ready");
30 return;
31 }
32
33 var lines = body.querySelectorAll("[data-term-line]");
34 if (!lines.length) return;
35
36 // Hide all lines initially. Toggled via a class, not inline styles: the
37 // site's CSP has no `style-src 'unsafe-inline'`, so `el.style.x = y` is
38 // silently blocked.
39 body.classList.add("term-animating");
40
41 var played = false;
42
43 // Start when visible
44 var observer = new IntersectionObserver(function (entries) {
45 if (played) return;
46 for (var j = 0; j < entries.length; j++) {
47 if (entries[j].isIntersecting) {
48 played = true;
49 observer.disconnect();
50 setTimeout(function () { playAnimation(body, lines); }, INITIAL_DELAY);
51 break;
52 }
53 }
54 }, { threshold: 0.3 });
55
56 observer.observe(body);
57 }
58
59 function playAnimation(body, lines) {
60 var idx = 0;
61
62 function next() {
63 if (idx >= lines.length) {
64 body.classList.remove("term-animating");
65 body.classList.add("term-ready");
66 return;
67 }
68
69 var line = lines[idx];
70 idx++;
71
72 line.classList.add("term-line-visible");
73
74 var isCmd = line.hasAttribute("data-term-cmd");
75 var isBlank = line.hasAttribute("data-term-blank");
76
77 if (isBlank) {
78 setTimeout(next, BLANK_PAUSE);
79 } else if (isCmd) {
80 typeCommand(line, function () {
81 setTimeout(next, CMD_PAUSE);
82 });
83 } else {
84 // Output lines: reveal instantly
85 setTimeout(next, LINE_PAUSE);
86 }
87 }
88
89 next();
90 }
91
92 function typeCommand(el, callback) {
93 var text = el.getAttribute("data-term-text") || el.textContent;
94 var original = el.textContent;
95 el.textContent = "";
96 el.classList.add("term-typing");
97
98 var charIdx = 0;
99
100 function typeChar() {
101 if (charIdx >= text.length) {
102 el.textContent = original;
103 el.classList.remove("term-typing");
104 callback();
105 return;
106 }
107
108 el.textContent = text.substring(0, charIdx + 1);
109 charIdx++;
110 setTimeout(typeChar, CHAR_DELAY);
111 }
112
113 typeChar();
114 }
115
116 // Init on DOMContentLoaded or immediately if already loaded
117 if (document.readyState === "loading") {
118 document.addEventListener("DOMContentLoaded", initTerminalAnimation);
119 } else {
120 initTerminalAnimation();
121 }
122})();

122 lines · JavaScript