├── .babelrc ├── .gitignore ├── .htmlnanorc ├── .screenshot.png ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets └── favicon.png ├── docs ├── clear-pipes.b3d7541c.js ├── clear-pipes.b3d7541c.js.map ├── index.html ├── main.84e2604c.css └── main.84e2604c.css.map ├── index.html ├── index.tsx ├── main.css ├── package-lock.json ├── package.json ├── src ├── physics.ts ├── process.ts ├── render.tsx ├── state.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | "@babel/preset-react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.htmlnanorc: -------------------------------------------------------------------------------- 1 | { 2 | "minifySvg": false 3 | } -------------------------------------------------------------------------------- /.screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxBittker/clear-pipes/ad287915fe0940652e2f500cfbfa03c2de5f6ee4/.screenshot.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabledLanguageIds": [ 3 | "asciidoc", 4 | "c", 5 | "cpp", 6 | "csharp", 7 | "css", 8 | "git-commit", 9 | "go", 10 | "handlebars", 11 | "haskell", 12 | "html", 13 | "jade", 14 | "java", 15 | "javascript", 16 | "javascriptreact", 17 | "json", 18 | "jsonc", 19 | "latex", 20 | "less", 21 | "markdown", 22 | "php", 23 | "plaintext", 24 | "pug", 25 | "restructuredtext", 26 | "rust", 27 | "scala", 28 | "scss", 29 | "text", 30 | "typescriptreact", 31 | "yaml", 32 | "yml" 33 | ] 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Max Bittker 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 | # clear-pipes 2 | visualize data flow 3 | 4 | ![](http://john1701a.com/prius/animations/Prius-Animation_750x443_BatteryDrive.gif) 5 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxBittker/clear-pipes/ad287915fe0940652e2f500cfbfa03c2de5f6ee4/assets/favicon.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | @nyt_first_said
t
made by max bittker

speed:

-------------------------------------------------------------------------------- /docs/main.84e2604c.css: -------------------------------------------------------------------------------- 1 | body{font-family:Libre Franklin,sans-serif;background-size:40px 40px;background-image:radial-gradient(circle,#999 1px,transparent 0)}*{color:#333;margin:0}text{fill:#000}.pipetext{font-size:20px}svg{width:100%;height:100%}.tube{stroke-width:30px;stroke:rgba(0,0,200,.05)}.trash{stroke:rgba(150,0,30,.1)}textPath{letter-spacing:.1em}h1{font-size:25px}#speedbox{position:fixed;left:5;z-index:500;bottom:5;display:flex}#article-link{font-size:15px;font-weight:100}#article-link,#info{font-family:Libre Franklin}#info{font-size:25px}.credit{position:fixed;right:2px;bottom:0;z-index:500;font-weight:100;text-shadow:#fff 0 1px}:root{--spin-duration:2000ms}.pinwheel{animation-name:spin;animation-duration:var(--spin-duration);animation-iteration-count:infinite;animation-timing-function:linear;fill:#fff;stroke:#000}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}foreignObject>div{margin-bottom:10px;height:100%}.boxText{padding:5px}.infoBox{font-size:18px}.infoBox p{margin-bottom:9px}.word-bank{padding:10px 0 10px 10px;margin-right:10px;font-size:18px;margin-top:1px;height:100%}.word-span{border-radius:3px;padding:0 2px;animation:fadeInAnimation 1s ease;animation-iteration-count:1;animation-fill-mode:forwards}@keyframes fadeInAnimation{0%{opacity:0}to{opacity:1}}#scratch{visibility:hidden} 2 | /*# sourceMappingURL=/clear-pipes/main.84e2604c.css.map */ -------------------------------------------------------------------------------- /docs/main.84e2604c.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["main.css"],"names":[],"mappings":"AAAA,KACE,qCAAyC,CACzC,yBAA0B,CAC1B,+DACF,CACA,EACE,UAAW,CACX,QAEF,CAEA,KACE,SAIF,CACA,UACE,cACF,CACA,IACE,UAAW,CACX,WACF,CAKA,MACE,iBAAkB,CAClB,wBAEF,CACA,OACE,wBACF,CACA,SAEE,mBACF,CACA,GACE,cAEF,CAIA,UACE,cAAe,CACf,MAAO,CACP,WAAY,CACZ,QAAS,CACT,YACF,CACA,cACE,cAAe,CACf,eAEF,CACA,oBAFE,0BAOF,CALA,MAEE,cAGF,CACA,QACE,cAAe,CACf,SAAU,CACV,QAAS,CACT,WAAY,CACZ,eAAgB,CAEhB,sBACF,CACA,MACE,sBACF,CACA,UACE,mBAAoB,CACpB,uCAAwC,CACxC,kCAAmC,CACnC,gCAAiC,CACjC,SAAW,CACX,WAEF,CAEA,gBACE,GACE,sBACF,CACA,GACE,uBACF,CACF,CACA,kBACE,kBAAmB,CACnB,WAGF,CACA,SACE,WACF,CACA,SACE,cACF,CACA,WACE,iBACF,CACA,WAEE,wBAA2B,CAC3B,iBAAkB,CAClB,cAAe,CAEf,cAAe,CAMf,WAEF,CACA,WAIE,iBAAkB,CAClB,aAAgB,CAChB,iCAAkC,CAClC,2BAA4B,CAC5B,4BACF,CACA,2BACE,GACE,SACF,CACA,GACE,SACF,CACF,CACA,SACE,iBACF","file":"main.84e2604c.css","sourceRoot":"..","sourcesContent":["body {\n font-family: \"Libre Franklin\", sans-serif;\n background-size: 40px 40px;\n background-image: radial-gradient(circle, #999 1px, rgba(0, 0, 0, 0) 1px);\n}\n* {\n color: #333;\n margin: 0;\n /* border: 1px red solid; */\n}\n\ntext {\n fill: black;\n /* stroke: rgb(195, 195, 240); */\n\n /* stroke-width: 1px; */\n}\n.pipetext {\n font-size: 20px;\n}\nsvg {\n width: 100%;\n height: 100%;\n}\npath {\n /* stroke-width: 40px; */\n /* stroke:red; */\n}\n.tube {\n stroke-width: 30px;\n stroke: rgba(0, 0, 200, 0.05);\n /* stroke-dasharray: 20px 10px; */\n}\n.trash {\n stroke: rgba(150, 0, 30, 0.1);\n}\ntextPath {\n /* font-size: 0.4em; */\n letter-spacing: 0.1em;\n}\nh1 {\n font-size: 25px;\n /* text-align: center; */\n}\n\n@media only screen and (max-width: 700px) {\n}\n#speedbox {\n position: fixed;\n left: 5;\n z-index: 500;\n bottom: 5;\n display: flex;\n}\n#article-link {\n font-size: 15px;\n font-weight: 100;\n font-family: \"Libre Franklin\";\n}\n#info {\n font-family: \"Libre Franklin\";\n font-size: 25px;\n\n /* font-style: italic; */\n}\n.credit {\n position: fixed;\n right: 2px;\n bottom: 0;\n z-index: 500;\n font-weight: 100;\n\n text-shadow: white 0px 1px;\n}\n:root {\n --spin-duration: 2000ms;\n}\n.pinwheel {\n animation-name: spin;\n animation-duration: var(--spin-duration);\n animation-iteration-count: infinite;\n animation-timing-function: linear;\n fill: white;\n stroke: black;\n /* stroke-linecap: round; */\n}\n\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\nforeignObject > div {\n margin-bottom: 10px;\n height: 100%;\n /* padding: 10px; */\n /* margin: 10px; */\n}\n.boxText {\n padding: 5px;\n}\n.infoBox {\n font-size: 18px;\n}\n.infoBox p {\n margin-bottom: 9px;\n}\n.word-bank {\n /* text-align: justify; */\n padding: 10px 0px 10px 10px;\n margin-right: 10px;\n font-size: 18px;\n /* word-wrap: no-wrap; */\n margin-top: 1px;\n\n /* display: flex; */\n /* justify-content: space-between; */\n /* align-items: center; */\n /* flex-wrap: wrap; */\n height: 100%;\n /* overflow-y: scroll; */\n}\n.word-span {\n /* position: absolute; */\n /* background-color: rgba(200, 0, 200, 0.05); */\n /* border: 1px solid rgba(200, 0, 200, 0.5); */\n border-radius: 3px;\n padding: 0px 2px;\n animation: fadeInAnimation ease 1s;\n animation-iteration-count: 1;\n animation-fill-mode: forwards;\n}\n@keyframes fadeInAnimation {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n#scratch {\n visibility: hidden;\n}\n"]} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @nyt_first_said 5 | 10 | 11 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | t 22 | 23 |
24 | made by max bittker 25 |
26 | 27 | 28 | 33 | 38 | 39 |

speed:

40 | 41 | 49 |
50 | 51 | 52 | 55 | 56 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | import Two from "two.js"; 2 | import * as React from "react"; 3 | import * as ReactDOMServer from "react-dom/server"; 4 | import { 5 | makeConnector, 6 | makeBox, 7 | makeInfoBox, 8 | makeGradient, 9 | makeHopper, 10 | } from "./src/render"; 11 | import { startPhysics } from "./src/physics"; 12 | import { processTDV } from "./src/process"; 13 | import { Vector } from "matter-js"; 14 | import { subtract } from "./src/utils"; 15 | 16 | let elem = document.getElementById("draw-animation"); 17 | let width = window.innerWidth; 18 | let height = window.innerHeight; 19 | window.width = width; 20 | window.height = height; 21 | 22 | let two = new Two({ fullscreen: true, autostart: true }).appendTo(elem); 23 | two.renderer.domElement.setAttribute("viewBox", "-0 -0 1000 1050"); 24 | 25 | window.two = two; 26 | 27 | let pHopper = new Two.Vector(200, 220); 28 | let pClean = new Two.Vector(200, 30 + 750); 29 | let pRule = new Two.Vector(500, 30 + 750); 30 | // let pCache = new Two.Vector(600, 750); 31 | let pCheck = new Two.Vector(800, 30 + 750); 32 | let pDestination = new Two.Vector(950, 350); 33 | let pTrash = new Two.Vector(0, 30 + 870); 34 | let pTrash2 = new Two.Vector(0, 30 + 905); 35 | let pTrash3 = new Two.Vector(0, 30 + 940); 36 | // let pTrash4 = new Two.Vector(0, 975); 37 | let pGradient = new Two.Vector(0, 30 + 905); 38 | 39 | let c1 = makeConnector( 40 | new Two.Vector(200, 420), 41 | pClean, 42 | "1", 43 | true, 44 | "wiggle", 45 | true 46 | ); 47 | let c2 = makeConnector(pClean, pRule, "2", false, "l"); 48 | // let c3 = makeConnector(pRule, pCache, "3", false, "l"); 49 | let c3 = makeConnector(pRule, pCheck, "3", false, "loop"); 50 | let c4 = makeConnector(pCheck, pDestination, "4"); 51 | 52 | let cTrash1 = makeConnector(pClean, pTrash, "t1", true); 53 | let cTrash2 = makeConnector(pRule, pTrash2, "t2", true); 54 | // let cTrash3 = makeConnector(pCache, pTrash3, "t3", true); 55 | let cTrash3 = makeConnector(pCheck, pTrash3, "t4", true); 56 | let trashGradient = makeGradient(pGradient.x, pGradient.y, 200); 57 | // let c7 = makeConnector(pCheck, pTrash, "5", true); 58 | 59 | let boxHopper = makeHopper(pHopper, 350, "a"); 60 | let boxClean = makeBox(pClean, 100, "b"); 61 | boxClean.setText("Trim Punctuation"); 62 | let boxRule = makeBox(pRule, 100, "c"); 63 | boxRule.setText("Capitalized?"); 64 | 65 | // let boxCache = makeBox(pCache, 100, "d"); 66 | // boxCache.setText("Not Seen by us?"); 67 | 68 | let boxCheck = makeBox(pCheck, 100, "d"); 69 | boxCheck.setText("Exists in NYT Archives?"); 70 | 71 | let boxDestination = makeBox(new Two.Vector(900, 350), 150, "f"); 72 | 73 | function formatWords(words: Array) { 74 | return `

${words.map((w) => `${w} `).join(" ")}

`; 75 | } 76 | 77 | let destinationWords: Array = ["Tweeted:"]; 78 | boxDestination.setText(formatWords(destinationWords)); 79 | 80 | let { addWord, removeWord, setGravity } = startPhysics(boxHopper); 81 | 82 | let text1 = new Two.Text("", 410, 110, { 83 | size: 105, 84 | weight: 100, 85 | family: "Libre Franklin", 86 | alignment: "left", 87 | }); 88 | 89 | let text2 = new Two.Text(`Loading... `, 415, 190, { 90 | size: 60, 91 | weight: 100, 92 | family: "Libre Franklin", 93 | alignment: "left", 94 | }); 95 | let text3 = new Two.Text(``, 415, 240, { 96 | size: 30, 97 | weight: 100, 98 | family: "Libre Franklin", 99 | alignment: "left", 100 | }); 101 | 102 | let infoBox = makeInfoBox(new Two.Vector(615, 470), 410, "info"); 103 | infoBox.setText(` 104 |

This is a visualization of the process behind @nyt_first_said.

105 | 106 |

Each day, a script scrapes new articles from nytimes.com. That text is tokenized, or split into words based on whitespace and punctuation.

107 | 108 |

Each word then must pass several criteria. Containing a number or special character is criteria for disqualification. To avoid proper nouns, all capitalized words are filtered.

109 | 110 |

The most important check is against the New York Time's archive search service. The archive goes back to 1851 and contains more than 13 million articles.

111 | 112 |

The paper publishes many thousands of words each day, but only a very few are firsts.

113 | 114 | more information 115 | `); 116 | 117 | let group = two.makeGroup(text1, text2, text3); 118 | let group2 = two.makeGroup(); 119 | two.update(); 120 | let svgElem = group2._renderer.elem; 121 | svgElem.innerHTML += ` 122 | 123 | Loading... 124 | 125 | `; 126 | 127 | function updateArticleLink(url) { 128 | let articleLink = document.getElementById("article-link"); 129 | articleLink.remove(); 130 | svgElem.innerHTML += ` 131 | 132 | ${url} 133 | 134 | `; 135 | } 136 | 137 | function startUp(setback) { 138 | let d = new Date(); 139 | var offset = new Date().getTimezoneOffset(); // getting offset to make time in gmt+0 zone (UTC) (for gmt+5 offset comes as -300 minutes) 140 | d.setMinutes(d.getMinutes() + offset); // date now in UTC time 141 | var easternTimeOffset = -240; //for dayLight saving, Eastern time become 4 hours behind UTC thats why its offset is -4x60 = -240 minutes. So when Day light is not active the offset will be -300 142 | d.setMinutes(d.getMinutes() + easternTimeOffset + setback * 60); 143 | 144 | const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d); 145 | const mo = new Intl.DateTimeFormat("en", { month: "long" }).format(d); 146 | const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d); 147 | 148 | let date = `${mo}-${da}-${ye}`; 149 | text1.value = `${mo} ${da}`; 150 | 151 | let wordList: string[] = []; 152 | let articles: string[][] = []; 153 | let article_i = 0; 154 | let wordCount = 0; 155 | let wordTotal = 0; 156 | function moveWord(): Promise { 157 | text2.value = `article ${article_i + 1}/${article_i + 1 + articles.length}`; 158 | text3.value = `word ${wordCount.toLocaleString()} / ${wordTotal.toLocaleString()}`; 159 | two.update(); 160 | wordCount++; 161 | if (wordList.length == 0) { 162 | wordList = articles.shift(); 163 | updateArticleLink(wordList.url); 164 | article_i++; 165 | console.log("processing article " + articles); 166 | } 167 | let word = wordList.shift(); 168 | 169 | addWord(word); 170 | 171 | let [initial, cleaned, passed, count, api_checked] = removeWord(); 172 | 173 | // boxHopper.setText(formatWords(words)); 174 | let speed = parseInt(document.getElementById("speed").value, 10); 175 | return c1 176 | .sendWord(initial, () => 177 | setTimeout(moveWord, (30 + word.length * 12) / speed) 178 | ) 179 | .then(() => { 180 | // moveWord(); 181 | if (initial.match(`[\@\/\#\_\-]`)) { 182 | cTrash1.sendWord(initial); 183 | throw "done"; 184 | } 185 | cTrash1.sendWord(subtract(initial, cleaned)); 186 | return c2.sendWord(cleaned); 187 | }) 188 | .then(() => { 189 | if (passed) { 190 | return c3.sendWord(passed); 191 | } else { 192 | cTrash2.sendWord(cleaned); 193 | throw "done"; 194 | } 195 | }) 196 | .then(() => { 197 | if (api_checked) { 198 | return c4.sendWord(api_checked).then(() => { 199 | destinationWords.push(api_checked); 200 | boxDestination.setText(formatWords(destinationWords)); 201 | }); 202 | } else { 203 | return cTrash3.sendWord(passed); 204 | } 205 | }); 206 | } 207 | 208 | // debugger; 209 | 210 | two.update(); 211 | let url = `https://api.shaderbooth.com:3002/static/records/${date}.txt`; 212 | fetch(url) 213 | .then((response) => response.text()) 214 | .then((blob) => { 215 | if (blob.length == 0) { 216 | startUp(-12); 217 | return; 218 | } 219 | articles = processTDV(blob); 220 | articles.shift(); 221 | articles = articles.reverse(); 222 | wordTotal = articles.reduce((acc, b) => b.length + acc, 0); 223 | 224 | wordList = articles.shift(); 225 | updateArticleLink(wordList.url); 226 | // articles = articles.sort((a, b) => a.length - b.length); 227 | console.log(articles); 228 | 229 | for (let i = 0; i < 25; i++) { 230 | let word = wordList.shift(); 231 | addWord(word); 232 | } 233 | 234 | window.setTimeout(moveWord, 1400); 235 | }) 236 | .catch((e) => { 237 | startUp(-12); 238 | return; 239 | }); 240 | 241 | two.bind("update", function () {}).play(); // Finally, start the animation loop 242 | } 243 | startUp(0); 244 | 245 | document.getElementById("speed").addEventListener("change", (e) => { 246 | let n = parseInt(e.target.value, 10); 247 | 248 | console.log(n); 249 | setGravity(1 + n / 20); 250 | 251 | document.documentElement.style.setProperty( 252 | "--spin-duration", 253 | 5000 / n + "ms" 254 | ); 255 | }); 256 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Libre Franklin", sans-serif; 3 | background-size: 40px 40px; 4 | background-image: radial-gradient(circle, #999 1px, rgba(0, 0, 0, 0) 1px); 5 | } 6 | * { 7 | color: #333; 8 | margin: 0; 9 | /* border: 1px red solid; */ 10 | } 11 | 12 | text { 13 | fill: black; 14 | /* stroke: rgb(195, 195, 240); */ 15 | 16 | /* stroke-width: 1px; */ 17 | } 18 | .pipetext { 19 | font-size: 20px; 20 | } 21 | svg { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | path { 26 | /* stroke-width: 40px; */ 27 | /* stroke:red; */ 28 | } 29 | .tube { 30 | stroke-width: 30px; 31 | stroke: rgb(244 244 252); 32 | /* stroke-dasharray: 20px 10px; */ 33 | } 34 | .trash { 35 | stroke: rgb(245 232 235); 36 | } 37 | textPath { 38 | /* font-size: 0.4em; */ 39 | letter-spacing: 0.1em; 40 | } 41 | h1 { 42 | font-size: 25px; 43 | /* text-align: center; */ 44 | } 45 | 46 | @media only screen and (max-width: 700px) { 47 | } 48 | #speedbox { 49 | position: fixed; 50 | left: 5; 51 | z-index: 500; 52 | bottom: 5; 53 | display: flex; 54 | } 55 | #article-link { 56 | font-size: 15px; 57 | font-weight: 100; 58 | font-family: "Libre Franklin"; 59 | } 60 | #info { 61 | font-family: "Libre Franklin"; 62 | font-size: 25px; 63 | 64 | /* font-style: italic; */ 65 | } 66 | .credit { 67 | position: fixed; 68 | right: 2px; 69 | bottom: 0; 70 | z-index: 500; 71 | font-weight: 100; 72 | 73 | text-shadow: white 0px 1px; 74 | } 75 | :root { 76 | --spin-duration: 2000ms; 77 | } 78 | .pinwheel { 79 | animation-name: spin; 80 | animation-duration: var(--spin-duration); 81 | animation-iteration-count: infinite; 82 | animation-timing-function: linear; 83 | fill: white; 84 | stroke: black; 85 | /* stroke-linecap: round; */ 86 | } 87 | 88 | @keyframes spin { 89 | from { 90 | transform: rotate(0deg); 91 | } 92 | to { 93 | transform: rotate(360deg); 94 | } 95 | } 96 | foreignObject > div { 97 | margin-bottom: 10px; 98 | height: 100%; 99 | /* padding: 10px; */ 100 | /* margin: 10px; */ 101 | } 102 | .boxText { 103 | padding: 5px; 104 | } 105 | .infoBox { 106 | font-size: 18px; 107 | } 108 | .infoBox p { 109 | margin-bottom: 9px; 110 | } 111 | .word-bank { 112 | /* text-align: justify; */ 113 | padding: 10px 0px 10px 10px; 114 | margin-right: 10px; 115 | font-size: 18px; 116 | /* word-wrap: no-wrap; */ 117 | margin-top: 1px; 118 | 119 | /* display: flex; */ 120 | /* justify-content: space-between; */ 121 | /* align-items: center; */ 122 | /* flex-wrap: wrap; */ 123 | height: 100%; 124 | /* overflow-y: scroll; */ 125 | } 126 | .word-span { 127 | /* position: absolute; */ 128 | /* background-color: rgba(200, 0, 200, 0.05); */ 129 | /* border: 1px solid rgba(200, 0, 200, 0.5); */ 130 | border-radius: 3px; 131 | padding: 0px 2px; 132 | animation: fadeInAnimation ease 1s; 133 | animation-iteration-count: 1; 134 | animation-fill-mode: forwards; 135 | } 136 | @keyframes fadeInAnimation { 137 | 0% { 138 | opacity: 0; 139 | } 140 | 100% { 141 | opacity: 1; 142 | } 143 | } 144 | #scratch { 145 | visibility: hidden; 146 | } 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clear-pipes", 3 | "version": "1.0.0", 4 | "description": "visualize dataa flow demo", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@types/matter-js": "^0.14.4", 8 | "@types/react": "^16.9.41", 9 | "@types/react-dom": "^16.9.8", 10 | "@types/two.js": "^0.7.2", 11 | "matter-js": "^0.14.2", 12 | "morphdom": "^2.6.1", 13 | "parcel": "^1.12.4", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "two.js": "^0.7.0-stable.1" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.10.4", 20 | "@babel/preset-env": "^7.10.4", 21 | "@babel/preset-react": "^7.10.4", 22 | "@babel/preset-typescript": "^7.10.4", 23 | "babel-core": "^6.26.3", 24 | "babel-preset-env": "^1.7.0", 25 | "babel-preset-react": "^6.24.1", 26 | "parcel-bundler": "^1.12.4", 27 | "typescript": "^3.8.2" 28 | }, 29 | "scripts": { 30 | "start": "parcel develop index.html", 31 | "build": "parcel build index.html -d docs --public-url /clear-pipes/" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/MaxBittker/clear-pipes.git" 36 | }, 37 | "author": "", 38 | "license": "ISC", 39 | "bugs": {} 40 | } 41 | -------------------------------------------------------------------------------- /src/physics.ts: -------------------------------------------------------------------------------- 1 | import * as Matter from "matter-js"; 2 | 3 | // module aliases 4 | let Engine = Matter.Engine, 5 | World = Matter.World, 6 | Vector = Matter.Vector, 7 | Bodies = Matter.Bodies; 8 | 9 | // create an engine 10 | let engine = Engine.create({ 11 | positionIterations: 5, 12 | constraintIterations: 5 13 | // enableSleeping: true 14 | }); 15 | 16 | let scratchSvg = document.getElementById("scratch"); 17 | const textStyle = `font-size: 18px; alignment-baseline: middle; text-anchor: middle;`; 18 | function renderedTextSize(string: string) { 19 | scratchSvg.innerHTML = `${string}`; 20 | let scratchText = document.getElementById("scratchText"); 21 | var bBox = scratchText.getBBox(); 22 | return { 23 | width: bBox.width, 24 | height: bBox.height 25 | }; 26 | } 27 | function closestBody(bodies: [], point: Matter.Vector) { 28 | if (bodies.length == 0) { 29 | return; 30 | } 31 | let smallest_d = Infinity; 32 | let smallest = bodies[0]; 33 | bodies.forEach(body => { 34 | let corners = body.vertices; 35 | 36 | corners.forEach(c => { 37 | let d = Vector.magnitude(Vector.sub(c, point)); 38 | // console.log(body); 39 | if (d < smallest_d && !body.pulse) { 40 | smallest = body; 41 | smallest_d = d; 42 | } 43 | }); 44 | }); 45 | if (smallest_d > 100) { 46 | // return false; 47 | } 48 | return smallest; 49 | } 50 | 51 | function startPhysics(box) { 52 | let boxes = []; 53 | 54 | let ground = Bodies.rectangle(200, 350 * 1.25, 400, 6, { isStatic: true }); 55 | let leftWall = Bodies.rectangle(0, 200, 6, 400, { isStatic: true }); 56 | let rightWall = Bodies.rectangle(350, 200, 6, 400, { isStatic: true }); 57 | let leftRamp = Bodies.rectangle(50, 5 + 350 * 1.0625, 300, 20, { 58 | isStatic: true, 59 | angle: Math.PI * 0.15 60 | }); 61 | let rightRamp = Bodies.rectangle(300, 5 + 350 * 1.0625, 300, 20, { 62 | isStatic: true, 63 | angle: Math.PI * 0.85 64 | }); 65 | 66 | // add all of the bodies to the world 67 | World.add(engine.world, [ground, leftWall, rightWall, leftRamp, rightRamp]); 68 | // World.add(engine.world, [ground, leftWall, rightWall]); //, leftRamp, rightRamp]); 69 | 70 | // run the engine 71 | Engine.run(engine); 72 | 73 | let radToDeg = r => r * (180 / Math.PI); 74 | Matter.Events.on(engine, "afterUpdate", () => { 75 | const paths = boxes.map((body, index) => { 76 | // const paths = engine.world.bodies.map((body, index) => { 77 | const { vertices, position, angle } = body; 78 | const pathData = `M ${body._width * -0.5} ${body._height * -0.5} l ${ 79 | body._width 80 | } 0 l 0 ${body._height} l ${-body._width} 0 z`; 81 | 82 | let fillColor = !body.pulse ? "white" : "rgba(255,230,230)"; 83 | const style = `fill: ${fillColor}; fill-opacity: 1; stroke: grey; stroke-width: 1px; stroke-opacity: 0.5`; 84 | const degrees = radToDeg(angle); 85 | const transform = `translate(${position.x}, ${position.y}) rotate(${degrees})`; 86 | let path = null; 87 | path = ``; 88 | return ` 89 | 90 | ${path} 91 | ${body.label} 92 | 93 | `; 94 | }); 95 | const style = ` shape-rendering: geometricPrecision;`; 96 | box.setText(` 97 | ${paths} 98 | `); 99 | }); 100 | return { 101 | setGravity: (n: number) => { 102 | engine.world.gravity.y = n; 103 | }, 104 | addWord: (data: string) => { 105 | if (!data) return; 106 | let word = data[0]; 107 | let { width, height } = renderedTextSize(word); 108 | width += 10; 109 | height += 5; 110 | let body = Bodies.rectangle( 111 | 100 + Math.random() * 100, 112 | -Math.random() * 200, 113 | width, 114 | height 115 | ); 116 | body._width = width; 117 | body._height = height; 118 | // body.frictionAir = 0.01; 119 | body.label = word; 120 | body.torque = Math.random() - 0.5; 121 | body.force = { x: 0.01, y: 0.0 }; 122 | body.data = data; 123 | boxes.push(body); 124 | World.add(engine.world, body); 125 | return body; 126 | }, 127 | removeWord: () => { 128 | let box = closestBody(boxes, { x: 175, y: 350 * 1.25 }); 129 | if (!box) return; 130 | // box.isSleeping = false; 131 | Matter.Body.setVelocity(box, { x: 0, y: -5 }); 132 | box.pulse = true; 133 | // console.log("a"); 134 | window.setTimeout(() => { 135 | // console.log("c"); 136 | World.remove(engine.world, box); 137 | boxes = boxes.filter(b => b != box); 138 | }, 200); 139 | 140 | return box.data; 141 | } 142 | }; 143 | } 144 | export { startPhysics }; 145 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | function processLine(str) { 2 | let toks = str.split("~"); 3 | return toks; 4 | } 5 | function processArticle(str) { 6 | let lines = str.split("\n"); 7 | let url = lines.shift(); 8 | // console.log(lines); 9 | let rest = lines.filter(l => !l.startsWith("Advertisement")).map(processLine); 10 | rest.url = url; 11 | 12 | return rest; 13 | } 14 | 15 | function processTDV(str) { 16 | let articles = str.split("ARTICLE:"); 17 | return articles.map(processArticle); 18 | // console.log(articles); 19 | } 20 | 21 | export { processTDV }; 22 | -------------------------------------------------------------------------------- /src/render.tsx: -------------------------------------------------------------------------------- 1 | import Two from "two.js"; 2 | import * as React from "react"; 3 | import * as ReactDOMServer from "react-dom/server"; 4 | import * as morphdom from "morphdom"; 5 | let morph = morphdom.default; 6 | 7 | function makeHopper(pos, size: number, id: string) { 8 | let two = window.two; 9 | let verts = [ 10 | new Two.Anchor(0, 0), 11 | new Two.Anchor(size, 0), 12 | new Two.Anchor(size, size), 13 | new Two.Anchor(size / 2 + 15, size * 1.23), 14 | new Two.Anchor(size / 2 - 15, size * 1.23), 15 | new Two.Anchor(0, size), 16 | new Two.Anchor(0, 0), 17 | ]; 18 | let verts2 = [ 19 | new Two.Anchor(size, 0), 20 | 21 | new Two.Anchor(size + 10, 0 + 10), 22 | new Two.Anchor(size + 10, size + 10), 23 | new Two.Anchor(size / 2 + 10, size * 1.25 + 10), 24 | new Two.Anchor(10, size + 10), 25 | new Two.Anchor(0, size), 26 | ]; 27 | let body1 = two.makePath(verts, true); 28 | let body2 = two.makePath(verts2, true); 29 | // let line = two.makeLine( 30 | // size / 2, 31 | // size * 1.25, 32 | // size / 2 + 10, 33 | // size * 1.25 + 10 34 | // ); 35 | let line2 = two.makeLine(size, size, size + 10, size + 10); 36 | 37 | body1.fill = "white"; 38 | body2.fill = "lavender"; 39 | 40 | // body1.fill = "transparent"; 41 | // body2.fill = "transparent"; 42 | let groupBackground = two.makeGroup(body2); 43 | let group = two.makeGroup(body1, line2); 44 | 45 | two.update(); 46 | 47 | let r = 30; 48 | let contentId = "content" + id; 49 | const htmlStringBody = ReactDOMServer.renderToStaticMarkup( 50 | 51 | 52 |

{/*

{id}

*/}
53 | 54 | 55 | ); 56 | const htmlStringSpinner = ReactDOMServer.renderToStaticMarkup( 57 | 58 | 67 | 74 | 75 | 76 | 83 | 90 | 97 | 104 | 105 | 106 | ); 107 | 108 | let svgElem = group._renderer.elem; 109 | let svgElemBackground = groupBackground._renderer.elem; 110 | 111 | svgElem.innerHTML += htmlStringBody; 112 | svgElemBackground.innerHTML += htmlStringSpinner; 113 | 114 | group.translation.set(pos.x - size / 2, pos.y - size / 2); 115 | groupBackground.translation.set(pos.x - size / 2, pos.y - size / 2); 116 | // let htmlContent = document.getElementById(contentId); 117 | return { 118 | setText(word) { 119 | let newHTML = ` 120 | 121 |
122 | ${word} 123 |
124 | `; 125 | let htmlContent = document.getElementById(contentId); 126 | 127 | // console.log(word); 128 | 129 | morph(htmlContent, newHTML); 130 | // htmlContent.innerHTML = word; 131 | }, 132 | }; 133 | } 134 | 135 | function makeBox(pos, size: number, id: string) { 136 | let two = window.two; 137 | let rect1 = two.makeRectangle(size / 2, size / 2, size, size); 138 | let verts = [ 139 | new Two.Anchor(size, 0), 140 | new Two.Anchor(size + 10, 10), 141 | new Two.Anchor(size + 10, size + 10), 142 | new Two.Anchor(10, size + 10), 143 | new Two.Anchor(0, size), 144 | ]; 145 | let path = two.makePath(verts, true); 146 | let line = two.makeLine(size, size, size + 10, size + 10); 147 | 148 | path.fill = "lavender"; 149 | rect1.fill = "white"; 150 | // rect1.fill = "transparent"; 151 | // path.fill = "transparent"; 152 | let group2 = two.makeGroup(path); 153 | 154 | let group = two.makeGroup(rect1, line); 155 | two.update(); 156 | 157 | let r = 25; 158 | if (id == "e") { 159 | r = 0; 160 | } 161 | 162 | let contentId = "content" + id; 163 | const htmlString = ReactDOMServer.renderToStaticMarkup( 164 | 165 | 174 | 181 | 182 | 183 | 190 | 197 | 204 | 211 | 212 | 213 | ); 214 | const htmlString2 = ReactDOMServer.renderToStaticMarkup( 215 | 216 | 225 | {/* */} 233 | {/* */} 234 | {/* */} 235 | {/* */} 236 | {/* */} 237 | 238 | 239 | 240 |
{/*

{id}

*/}
241 |
242 |
243 | ); 244 | let svgElem = group._renderer.elem; 245 | let svgElem2 = group2._renderer.elem; 246 | // svgElem.innerHTML += htmlString; 247 | svgElem.innerHTML += htmlString2; 248 | svgElem2.innerHTML += htmlString; 249 | 250 | group.translation.set(pos.x - size / 2, pos.y - size / 2); 251 | group2.translation.set(pos.x - size / 2, pos.y - size / 2); 252 | // let htmlContent = document.getElementById(contentId); 253 | return { 254 | setText(word) { 255 | let newHTML = ` 256 | 257 |
258 | ${word} 259 |
260 | `; 261 | let htmlContent = document.getElementById(contentId); 262 | 263 | // console.log(word); 264 | 265 | morph(htmlContent, newHTML); 266 | // htmlContent.innerHTML = word; 267 | }, 268 | }; 269 | } 270 | 271 | function makePath(a, b) { 272 | let mid = new Two.Vector(a.x, b.y); 273 | two.makeLine(a.x, a.y, mid.x, mid.y); 274 | two.makeLine(mid.x, mid.y, b.x, b.y); 275 | } 276 | 277 | function makeConnector( 278 | p1, 279 | p2, 280 | id, 281 | flip = false, 282 | flourish = "", 283 | proportional_space = false 284 | ) { 285 | let two = window.two; 286 | // let path = makePath(p1, p2); 287 | 288 | let group = two.makeGroup(); 289 | two.update(); 290 | let rx = 45; 291 | let ry = 45; 292 | let sweep = 0; 293 | let rotate = 0; 294 | if (p2.x < p1.x) { 295 | rx *= -1; 296 | sweep = 1 - sweep; 297 | rotate = 180; 298 | } 299 | if (p2.y > p1.y) { 300 | ry *= -1; 301 | sweep = 1 - sweep; 302 | } 303 | // let length = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y); 304 | 305 | let pM = new Two.Vector(p2.x, p1.y); 306 | let xFirst = 1; 307 | let yFirst = 0; 308 | if (flip) { 309 | pM = new Two.Vector(p1.x, p2.y); 310 | rx *= -1; 311 | ry *= -1; 312 | xFirst = 0; 313 | yFirst = 1; 314 | sweep = 1 - sweep; 315 | } 316 | 317 | let flourishPath = ""; 318 | if (flourish == "wiggle") { 319 | flourishPath = ` 320 | l 0 125 321 | 322 | a 25 25, 0, 0, 1, -25, 25 323 | l -75 0 324 | a 25 25, 0, 0, 0, -25, 25 325 | l 0 0 326 | a 25 25, 0, 0, 0, 25, 25 327 | l 200 0 328 | a 25 25, 0, 0, 1, 25, 25 329 | l 0 0 330 | a 25 25, 0, 0, 1, -25, 25 331 | l -75 0 332 | a 25 25, 0, 0, 0, -25, 25 333 | `; 334 | } else if (flourish == "loop") { 335 | flourishPath = ` 336 | l 150 0 337 | a 40 40, 0, 0, 0, 40, -40 338 | a 40 40, 0, 0, 0, -40, -40 339 | a 40 40, 0, 0, 0, -40, 40 340 | a 40 40, 0, 0, 0, 40, 40 341 | `; 342 | } 343 | console.log(id); 344 | let className = id[0] == "t" ? "tube trash" : "tube"; 345 | 346 | let pathIds = Array.from( 347 | { length: 10 }, 348 | (x, i) => "textpath" + id + i.toString() 349 | ); 350 | 351 | let A = `A 45, 45, 0, 0, ${sweep}, ${pM.x - rx * yFirst} ${ 352 | pM.y - ry * xFirst 353 | }`; 354 | if (flourish != "") { 355 | A = ""; 356 | } 357 | const htmlString = ReactDOMServer.renderToStaticMarkup( 358 | 359 | 370 | 371 | {pathIds.map((pid) => { 372 | return ( 373 | 374 | 382 | 383 | ); 384 | })} 385 | 386 | ); 387 | let svgElem = group._renderer.elem; 388 | 389 | svgElem.innerHTML += htmlString; 390 | 391 | let path: SVGPathElement = document.getElementById(id); 392 | let pathLength = path.getTotalLength(); 393 | 394 | let textPaths = pathIds.map((pid) => document.getElementById(pid)); 395 | 396 | let p_i = 0; 397 | return { 398 | sendWord(word: string, clear?: Function): Promise { 399 | var isSafari = 400 | /Safari/.test(navigator.userAgent) && 401 | /Apple Computer/.test(navigator.vendor); 402 | 403 | if (rotate > 0 && !isSafari) { 404 | word = word.split("").reverse().join(""); 405 | } 406 | let textPath = textPaths[p_i]; 407 | 408 | p_i = (p_i + 1) % textPaths.length; 409 | textPath.textContent = word; 410 | 411 | if (!textPath) { 412 | throw new Error("no path"); 413 | } 414 | let cb = clear; 415 | const animationProgress = new Promise((resolve, reject) => { 416 | let space = proportional_space ? word.length * 12 : 80; 417 | 418 | let offset = -space; 419 | 420 | let updateOffset = () => { 421 | if (!textPath) { 422 | throw new Error("no path"); 423 | } 424 | 425 | if (cb && offset > space) { 426 | cb(); 427 | cb = null; 428 | } 429 | offset += parseInt(document.getElementById("speed").value, 10); 430 | textPath.setAttribute("startOffset", `${offset}px`); 431 | if (offset < pathLength) { 432 | window.requestAnimationFrame(updateOffset); 433 | } else { 434 | // console.log("done :)"); 435 | resolve(); 436 | } 437 | }; 438 | updateOffset(); 439 | }); 440 | 441 | return animationProgress; 442 | }, 443 | }; 444 | } 445 | function makeGradient(x, y, size) { 446 | let two = window.two; 447 | var linearGradient = two.makeLinearGradient( 448 | -size / 2, 449 | 0, 450 | size / 2, 451 | 0, 452 | new Two.Stop(0.5, "rgba(255,255,255,255)"), 453 | new Two.Stop(1, "rgba(255,255,255,0)") 454 | 455 | // new Two.Stop(0.5, "blue") 456 | // new Two.Stop(1, ) 457 | ); 458 | 459 | var rectangle = two.makeRectangle(x, y, size, size); 460 | // var rectangle2 = two.makeRectangle(x, y, size, size); 461 | 462 | rectangle.noStroke(); 463 | 464 | rectangle.fill = linearGradient; 465 | } 466 | 467 | function makeInfoBox(pos, size: number, id: string) { 468 | let two = window.two; 469 | let rect1 = two.makeRectangle(size / 2, size / 2.25, size, size); 470 | 471 | rect1.stroke = "rgba(0,0,0,0.0)"; 472 | rect1.fill = "white"; 473 | rect1.fill = "rgba(255,255,255,.6)"; 474 | 475 | let group = two.makeGroup(rect1); 476 | two.update(); 477 | 478 | let contentId = "content" + id; 479 | const htmlString2 = ReactDOMServer.renderToStaticMarkup( 480 | 481 | 490 | 491 | 492 |
{/*

{id}

*/}
493 |
494 |
495 | ); 496 | let svgElem = group._renderer.elem; 497 | svgElem.innerHTML += htmlString2; 498 | 499 | group.translation.set(pos.x - size / 2, pos.y - size / 2); 500 | return { 501 | setText(word) { 502 | let newHTML = ` 503 | 504 |
505 | ${word} 506 |
507 | `; 508 | let htmlContent = document.getElementById(contentId); 509 | 510 | morph(htmlContent, newHTML); 511 | }, 512 | }; 513 | } 514 | 515 | export { makeConnector, makeBox, makeHopper, makeInfoBox, makeGradient }; 516 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | var tileSize = 7; 2 | var editorRatio = 16; 3 | 4 | export {tileSize, editorRatio}; 5 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import {} from "./state"; 2 | function remove_first_occurrence(str, searchstr) { 3 | let index = str.indexOf(searchstr); 4 | if (index === -1) { 5 | return str; 6 | } 7 | return str.slice(0, index) + str.slice(index + searchstr.length); 8 | } 9 | 10 | function subtract(a, b) { 11 | let letters = b.split(""); 12 | let out = a; 13 | 14 | letters.forEach(l => { 15 | out = remove_first_occurrence(out, l); 16 | }); 17 | return out; 18 | } 19 | export { subtract }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "strict": true, 6 | "jsx": "react", 7 | "rootDir": "./src", 8 | "incremental": true, 9 | "experimentalDecorators": true, 10 | "lib": ["dom", "es2015"] 11 | } 12 | } 13 | --------------------------------------------------------------------------------