├── .nojekyll ├── .gitignore ├── _redirects ├── demo.css ├── package.json ├── LICENSE ├── README.md ├── demo-pre.html ├── demo.html └── squirminal.js /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | / /demo.html 200! -------------------------------------------------------------------------------- /demo.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: -apple-system, system-ui, sans-serif; 4 | max-width: 60em; 5 | margin: 0 auto; 6 | background: #222; 7 | color: #fff; 8 | overflow-y: scroll; 9 | overflow-anchor: none; 10 | padding: 1em 1em 10em 1em; 11 | } 12 | pre, 13 | code { 14 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 15 | } 16 | pre { 17 | white-space: pre-wrap; 18 | } 19 | th, td { 20 | padding: .25em .5em; 21 | } 22 | a[href], 23 | a[href]:visited { 24 | color: inherit; 25 | } 26 | .help { 27 | white-space: pre; 28 | } 29 | squirm-inal { 30 | margin: .5em 0; 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zachleat/squirminal", 3 | "version": "3.0.1", 4 | "description": "Squirminal incrementally reveals text inside any arbitrary DOM element structure.", 5 | "main": "squirminal.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "start": "npx http-server ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/zachleat/squirminal.git" 15 | }, 16 | "author": { 17 | "name": "Zach Leatherman", 18 | "email": "zachleatherman@gmail.com", 19 | "url": "https://zachleat.com/" 20 | }, 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/zachleat/squirminal/issues" 24 | }, 25 | "homepage": "https://github.com/zachleat/squirminal#readme" 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Zach Leatherman @zachleat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squirminal 2 | 3 | The squirminal is a fake antique terminal web component 4 | 5 | * [Demo](https://squirminal.zachleat.dev/) 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install @zachleat/squirminal 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```html 16 | 17 | [2021-11-17T23:41:07.790Z] "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36" 18 | [2021-11-17T23:41:07.791Z] "GET /favicon.ico" Error (404): "Not found" 19 | [2021-11-17T23:41:41.895Z] "GET /demo.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 20 | 21 | 22 | ``` 23 | 24 | ## Features 25 | 26 | * Works with `prefers-reduced-motion` 27 | * Works without JavaScript (fallback to show content) 28 | * Blinking cursor via `` 29 | * Autoplay (only when visible) via `` 30 | * Works with text nodes inside any arbitrary HTML content. 31 | * Play/pause/reset button via `` 32 | * Use `` to reduce layout shift during animation. 33 | 34 | ## Changelog 35 | 36 | A list of breaking changes: 37 | 38 | - `v3.0.0` removed option to override tag name in `define()` function (it wasn’t supported in the rest of the code) 39 | - `v2.0.0` removed extensions `` and `` to simplify component maintenance 40 | 41 | ## Credits 42 | 43 | * [MIT](./LICENSE) 44 | -------------------------------------------------------------------------------- /demo-pre.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | squirm-inal Web Component Demo 8 | 9 | 10 | 11 | 12 |

squirminal Web Component Demo

13 |

Go back to the repository on GitHub.

14 | 15 |
16 |

Without <squirm-inal>:

17 | 18 |
class MyComponent extends HTMLElement {
19 | 	connectedCallback() {
20 | 		// web component stuff
21 | 		// web component stuff
22 | 		// web component stuff
23 | 		// web component stuff
24 | 		// web component stuff
25 | 		// web component stuff
26 | 		// web component stuff
27 | 		// web component stuff
28 | 	}
29 | }
30 | if("customElements" in window) {
31 | 	customElements.define("my-component", MyComponent);
32 | }
33 | 34 |
35 |

With <squirm-inal>:

36 | 37 | 38 |
class MyComponent extends HTMLElement {
39 | 	connectedCallback() {
40 | 		// web component stuff
41 | 		// web component stuff
42 | 		// web component stuff
43 | 		// web component stuff
44 | 		// web component stuff
45 | 		// web component stuff
46 | 		// web component stuff
47 | 		// web component stuff
48 | 	}
49 | }
50 | if("customElements" in window) {
51 | 	customElements.define("my-component", MyComponent);
52 | }
53 |
54 | 55 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | squirm-inal Web Component Demo 8 | 9 | 18 | 19 | 20 |

squirminal Web Component Demo

21 |

Go back to the repository on GitHub.

22 | 23 |

Plain

24 | 25 | Empty 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Accessible Text
33 |
 34 | [2021-11-17T23:41:07.790Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
 35 | [2021-11-17T23:41:07.791Z]  "GET /favicon.ico" Error (404): "Not found"
 36 | [2021-11-17T23:41:41.895Z]  "GET /demo.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 37 | [2021-11-17T23:41:41.944Z]  "GET /demo.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 38 | [2021-11-17T23:41:41.964Z]  "GET /squirminal.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 39 | [2021-11-17T23:41:41.964Z]  "GET /demo.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 40 | [2021-11-17T23:41:41.964Z]  "GET /squirminal.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 41 | [2021-11-17T23:41:41.979Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 42 | 			
43 | Empty 44 |
45 | 46 |

This one reveals characters with every keypress (mash your keyboard).

47 | 48 |
 49 | [2021-11-17T23:41:07.790Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
 50 | [2021-11-17T23:41:07.791Z]  "GET /favicon.ico" Error (404): "Not found"
 51 | [2021-11-17T23:41:41.895Z]  "GET /demo.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 52 | [2021-11-17T23:41:41.944Z]  "GET /demo.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 53 | [2021-11-17T23:41:41.964Z]  "GET /squirminal.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 54 | [2021-11-17T23:41:41.964Z]  "GET /demo.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 55 | [2021-11-17T23:41:41.964Z]  "GET /squirminal.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 56 | [2021-11-17T23:41:41.979Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 57 | 			
58 |
59 | 60 |

With Cursor

61 | 62 |
 63 | [2021-11-17T23:41:07.790Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
 64 | [2021-11-17T23:41:07.791Z]  "GET /favicon.ico" Error (404): "Not found"
 65 | [2021-11-17T23:41:41.895Z]  "GET /demo.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 66 | [2021-11-17T23:41:41.944Z]  "GET /demo.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 67 | [2021-11-17T23:41:41.964Z]  "GET /squirminal.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 68 | [2021-11-17T23:41:41.964Z]  "GET /demo.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 69 | [2021-11-17T23:41:41.964Z]  "GET /squirminal.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 70 | [2021-11-17T23:41:41.979Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
 71 | 			
72 |
73 | 74 |

Works with arbitrary HTML content

75 | 76 |

This one has a link

77 | 78 |
 79 | 

Test[2021-11-17T23:41:07.790Z] "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"

80 | [2021-11-17T23:41:07.791Z] "GET /favicon.ico" Error (404): "Not found" 81 | [2021-11-17T23:41:41.895Z] "GET /demo.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 82 | [2021-11-17T23:41:41.944Z] "GET /demo.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 83 | [2021-11-17T23:41:41.964Z] "GET /squirminal.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 84 | [2021-11-17T23:41:41.964Z] "GET /demo.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 85 | [2021-11-17T23:41:41.964Z] "GET /squirminal.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 86 | [2021-11-17T23:41:41.979Z] "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 87 |
88 |
89 | 90 |

This one has a table

91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |
Movie TitleRankYearRatingGross
Avatar1200983%$2.7B
Titanic2199788%$2.1B
The Avengers3201292%$1.5B
Harry Potter and the Deathly Hallows—Part 24201196%$1.3B
Frozen5201389%$1.2B
Iron Man 36201378%$1.2B
Transformers: Dark of the Moon7201136%$1.1B
The Lord of the Rings: The Return of the King8200395%$1.1B
Skyfall9201292%$1.1B
Transformers: Age of Extinction10201418%$1.0B
175 |
176 | 177 |

With Autoplay

178 | 179 |
180 | [2021-11-17T23:41:07.790Z]  "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"
181 | [2021-11-17T23:41:07.791Z]  "GET /favicon.ico" Error (404): "Not found"
182 | [2021-11-17T23:41:41.895Z]  "GET /demo.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
183 | [2021-11-17T23:41:41.944Z]  "GET /demo.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"
184 | 
185 |

[2021-11-17T23:41:41.964Z] "GET /squirminal.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0"

186 |
187 | [2021-11-17T23:41:41.964Z] "GET /demo.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 188 | [2021-11-17T23:41:41.964Z] "GET /squirminal.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 189 | [2021-11-17T23:41:41.979Z] "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0" 190 |
191 |
192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /squirminal.js: -------------------------------------------------------------------------------- 1 | class Squirminal extends HTMLElement { 2 | static define() { 3 | if(!("customElements" in window)) { 4 | return; 5 | } 6 | // tagName was removed (it didn’t work anyway) 7 | window.customElements.define(this.tagName, Squirminal); 8 | } 9 | 10 | static tagName = "squirm-inal"; 11 | 12 | static attr = { 13 | cursor: "cursor", 14 | autoplay: "autoplay", 15 | buttons: "buttons", 16 | global: "global", 17 | dimensions: "dimensions", 18 | speed: "speed", 19 | }; 20 | 21 | static classes = { 22 | showCursor: "show-cursor", 23 | emptyNode: "sq-empty", 24 | cursor: "sq-cursor", 25 | }; 26 | 27 | static css = ` 28 | ${Squirminal.tagName} { 29 | --sq-cursor: #30c8c9; 30 | display: block; 31 | } 32 | ${Squirminal.tagName} .${Squirminal.classes.emptyNode} { 33 | display: none; 34 | } 35 | ${Squirminal.tagName}.${Squirminal.classes.showCursor}.${Squirminal.classes.cursor}:after, 36 | ${Squirminal.tagName}.${Squirminal.classes.showCursor} .${Squirminal.classes.cursor}:after { 37 | content: ""; 38 | display: inline-block; 39 | width: 0.7em; 40 | height: 1.2em; 41 | margin-left: 0.2em; 42 | background-color: var(--sq-cursor); 43 | vertical-align: text-bottom; 44 | animation: squirminal-blink 1s infinite steps(2, start); 45 | } 46 | @keyframes squirminal-blink { 47 | 0% { 48 | background-color: var(--sq-cursor); 49 | } 50 | 100% { 51 | background-color: transparent; 52 | } 53 | }` 54 | 55 | static defaultSpeed = 2; // higher is faster, 10 is about the fastest it can go. 56 | 57 | static chunkSize = { 58 | min: 5, 59 | max: 30 60 | }; 61 | 62 | static flatDepth = 1000; 63 | 64 | static events = { 65 | start: "squirminal.start", 66 | end: "squirminal.end", 67 | frameAdded: "squirminal.frameadded", 68 | }; 69 | 70 | static _needsCss = true; 71 | 72 | _serializeContent(node, selector = [], shouldTrim = false) { 73 | if(node.nodeType === 3) { 74 | let text = node.nodeValue; 75 | if(shouldTrim) { 76 | text = text.trim(); 77 | } 78 | node.nodeValue = ""; 79 | 80 | // this represents characters that need to be added to the page. 81 | return { 82 | text: text.split(""), 83 | selector, 84 | }; 85 | } else if(node.nodeType === 1) { 86 | if(node.tagName.toLowerCase() !== Squirminal.tagName) { 87 | node.classList.add(Squirminal.classes.emptyNode); 88 | } 89 | if(!node.innerText) { 90 | return { 91 | text: false, 92 | selector, 93 | } 94 | } 95 | } 96 | 97 | let content = []; 98 | let j = 0; 99 | 100 | for(let child of Array.from(node.childNodes)) { 101 | content.push(this._serializeContent(child, [...selector, j], shouldTrim)); 102 | j++; 103 | } 104 | 105 | return content; 106 | } 107 | 108 | static getNode(target, selector) { 109 | for(let childIndex of selector) { 110 | target = target.childNodes[childIndex]; 111 | } 112 | return target; 113 | } 114 | 115 | static removeEmpty(node) { 116 | while(node) { 117 | if(node.classList) { 118 | node.classList.remove(this.classes.emptyNode); 119 | } 120 | if(node.parentNode?.tagName.toLowerCase() === this.tagName) { 121 | break; 122 | } 123 | node = node.parentNode; 124 | } 125 | } 126 | 127 | static removeAllEmptyChildren(node) { 128 | node.querySelectorAll(`:scope .${this.classes.emptyNode}`).forEach(el => el.classList.remove(this.classes.emptyNode)); 129 | } 130 | 131 | swapCursor(node) { 132 | if(!node || !node.classList) { 133 | return; 134 | } 135 | if(this._lastCursor) { 136 | this._lastCursor.classList.remove(Squirminal.classes.cursor); 137 | } 138 | node.classList.add(Squirminal.classes.cursor); 139 | this._lastCursor = node; 140 | } 141 | 142 | addCharacters(target, characterCount = 1) { 143 | for(let entry of this.serialized) { 144 | let str = []; 145 | while(entry.text && entry.text.length > 0 && characterCount-- > 0) { 146 | str.push(entry.text.shift()); 147 | } 148 | 149 | let targetNode = Squirminal.getNode(target, entry.selector); 150 | if(entry.text !== false) { 151 | targetNode.nodeValue += str.join(""); 152 | } 153 | if(entry.text && entry.text.length > 0) { 154 | this.swapCursor(targetNode.parentNode); 155 | } 156 | if(entry.text === false) { 157 | Squirminal.removeAllEmptyChildren(targetNode); 158 | } 159 | Squirminal.removeEmpty(targetNode); 160 | 161 | if(characterCount < 0) { 162 | break; 163 | } 164 | } 165 | } 166 | 167 | hasQueue() { 168 | for(let entry of this.serialized) { 169 | if(entry.text.length > 0) { 170 | return true; 171 | } 172 | } 173 | return false; 174 | } 175 | 176 | connectedCallback() { 177 | if (!("replaceSync" in CSSStyleSheet.prototype)) { 178 | return; 179 | } 180 | 181 | Squirminal._addCss(); 182 | 183 | if(this.hasAttribute(Squirminal.attr.dimensions)) { 184 | this.style.minHeight = `${this.offsetHeight}px`; 185 | } 186 | 187 | this.init(); 188 | 189 | // TODO this is not ideal because the intersectionRatio is based on the empty terminal, not the 190 | // final animated version. So it’s tiny when empty and when the IntersectionRatio is 1 it may 191 | // animate off the bottom of the viewport. 192 | if(this.hasAttribute(Squirminal.attr.autoplay)) { 193 | this._whenVisible(this, (isVisible) => { 194 | if(isVisible) { 195 | this.play(); 196 | } 197 | }); 198 | } 199 | 200 | if(this.hasAttribute(Squirminal.attr.cursor)) { 201 | // show until finished 202 | if(this.getAttribute(Squirminal.attr.cursor) === "manual") { 203 | this.classList.add(Squirminal.classes.showCursor); 204 | this.swapCursor(this); 205 | } 206 | 207 | this.addEventListener("squirminal.start", () => { 208 | this.classList.add(Squirminal.classes.showCursor); 209 | }); 210 | 211 | this.addEventListener("squirminal.end", () => { 212 | this.classList.remove(Squirminal.classes.showCursor); 213 | }); 214 | } 215 | 216 | 217 | let href = this.getAttribute("href"); 218 | if(href) { 219 | this.addEventListener("squirminal.end", () => { 220 | window.location.href = href; 221 | }); 222 | } 223 | } 224 | 225 | static _addCss() { 226 | if(!Squirminal._needsCss) { 227 | return; 228 | } 229 | 230 | Squirminal._needsCss = false; 231 | let sheet = new CSSStyleSheet(); 232 | sheet.replaceSync(Squirminal.css); 233 | document.adoptedStyleSheets.push(sheet); 234 | } 235 | 236 | init() { 237 | this.paused = true; 238 | this.originalContent = this.cloneNode(true); 239 | 240 | let isCursorManual = this.getAttribute(Squirminal.attr.cursor) === "manual"; 241 | this.serialized = this._serializeContent(this, [], isCursorManual).flat(Squirminal.flatDepth); 242 | 243 | // add non-text that have already been emptied by the serializer 244 | for(let child of Array.from(this.childNodes)) { 245 | this.appendChild(child); 246 | } 247 | 248 | // Play/pause button 249 | this.toggleButton = this.querySelector("button[data-sq-toggle]"); 250 | if(this.hasAttribute(Squirminal.attr.buttons) && !this.toggleButton) { 251 | let toggleBtn = document.createElement("button"); 252 | toggleBtn.innerText = "Play"; 253 | toggleBtn.setAttribute("data-sq-toggle", ""); 254 | toggleBtn.addEventListener("click", e => { 255 | this.toggle(); 256 | }) 257 | this.appendChild(toggleBtn); 258 | this.toggleButton = toggleBtn; 259 | } 260 | 261 | this.skipButton = this.querySelector("button[data-sq-skip]"); 262 | if(this.hasAttribute(Squirminal.attr.buttons) && !this.skipButton) { 263 | let skipBtn = document.createElement("button"); 264 | skipBtn.innerText = "Skip"; 265 | skipBtn.setAttribute("data-sq-skip", ""); 266 | skipBtn.addEventListener("click", e => { 267 | this.skip(); 268 | }) 269 | this.appendChild(skipBtn); 270 | this.skipButton = skipBtn; 271 | } 272 | } 273 | 274 | removeButtons() { 275 | this.toggleButton?.remove(); 276 | this.skipButton?.remove(); 277 | } 278 | 279 | onreveal(callback) { 280 | this.addEventListener(Squirminal.events.frameAdded, callback, { 281 | passive: true, 282 | }); 283 | this.addEventListener(Squirminal.events.end, () => { 284 | this.removeEventListener(Squirminal.events.frameAdded, callback); 285 | }, { 286 | passive: true, 287 | once: true, 288 | }); 289 | } 290 | 291 | onstart(callback) { 292 | this.addEventListener(Squirminal.events.start, callback, { 293 | passive: true, 294 | once: true, 295 | }); 296 | } 297 | 298 | onend(callback) { 299 | this.addEventListener(Squirminal.events.end, callback, { 300 | passive: true, 301 | once: true, 302 | }); 303 | } 304 | 305 | _whenVisible(el, callback) { 306 | if(!('IntersectionObserver' in window)) { 307 | // run by default without intersectionobserver 308 | callback(undefined); 309 | return; 310 | } 311 | 312 | return new IntersectionObserver(entries => { 313 | entries.forEach(entry => { 314 | callback(entry.isIntersecting) 315 | }); 316 | }, { 317 | threshold: 1 318 | }).observe(el); 319 | } 320 | 321 | toggle() { 322 | if(this.paused) { 323 | this.play(); 324 | } else { 325 | this.pause(); 326 | } 327 | } 328 | 329 | pause() { 330 | this.paused = true; 331 | } 332 | 333 | skip() { 334 | this.play({ 335 | chunkSize: this.originalContent.innerHTML.length, 336 | delay: 0 337 | }); 338 | } 339 | 340 | play(overrides = {}) { 341 | if(window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 342 | overrides.chunkSize = this.originalContent.innerHTML.length; 343 | overrides.delay = 0; 344 | } 345 | 346 | this.paused = false; 347 | if(this.hasQueue()) { 348 | this.dispatchEvent(new CustomEvent(Squirminal.events.start)); 349 | } 350 | 351 | this.removeButtons(); 352 | 353 | requestAnimationFrame(() => this.showMore(overrides, true)); 354 | } 355 | 356 | showMore(overrides = {}, continuePlaying = false) { 357 | if(this.paused && !overrides.force) { 358 | return; 359 | } 360 | 361 | if(!this.hasQueue()) { 362 | this.pause(); 363 | this.dispatchEvent(new CustomEvent(Squirminal.events.frameAdded)); 364 | this.dispatchEvent(new CustomEvent(Squirminal.events.end)); 365 | return; 366 | } 367 | 368 | // show a random chunk size between min/max 369 | let chunkSize = overrides.chunkSize || Math.round(Math.max(Squirminal.chunkSize.min, Math.random() * Squirminal.chunkSize.max + 1)); 370 | this.addCharacters(this, chunkSize); 371 | 372 | this.dispatchEvent(new CustomEvent(Squirminal.events.frameAdded)); 373 | 374 | if(continuePlaying) { 375 | this.animateNextFrame(chunkSize, overrides); 376 | } 377 | } 378 | 379 | animateNextFrame(chunkSize, overrides = {}) { 380 | let speed = parseFloat(this.getAttribute(Squirminal.attr.speed) || Squirminal.defaultSpeed); 381 | let normalizedSpeed = speed * .3; // convert from 0-10 to 0-3 382 | 383 | // the amount we wait is based on how many non-whitespace characters printed to the screen in this chunk 384 | let delay = overrides.delay > -1 ? overrides.delay : chunkSize * (1/normalizedSpeed); 385 | if(delay > 16) { 386 | setTimeout(() => { 387 | requestAnimationFrame(() => this.showMore(overrides, true)); 388 | }, delay); 389 | } else { 390 | requestAnimationFrame(() => this.showMore(overrides, true)); 391 | } 392 | } 393 | 394 | isGlobalCommand() { 395 | return this.hasAttribute(Squirminal.attr.global); 396 | } 397 | } 398 | 399 | Squirminal.define(); 400 | --------------------------------------------------------------------------------