├── .gitignore ├── Makefile ├── screencast ├── demo.gif ├── demo2.gif ├── demo3.gif └── play.png ├── src ├── icons │ ├── logo16.png │ ├── logo48.png │ ├── logo128.png │ └── logo256.png ├── manifest.json ├── style.css └── index.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | release 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pack: 2 | @zip release/game-of-life.zip -r src/* 3 | -------------------------------------------------------------------------------- /screencast/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/screencast/demo.gif -------------------------------------------------------------------------------- /screencast/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/screencast/demo2.gif -------------------------------------------------------------------------------- /screencast/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/screencast/demo3.gif -------------------------------------------------------------------------------- /screencast/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/screencast/play.png -------------------------------------------------------------------------------- /src/icons/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/src/icons/logo16.png -------------------------------------------------------------------------------- /src/icons/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/src/icons/logo48.png -------------------------------------------------------------------------------- /src/icons/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/src/icons/logo128.png -------------------------------------------------------------------------------- /src/icons/logo256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanchuan/game-of-life/HEAD/src/icons/logo256.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Game-of-Life", 3 | "version" : "1.3.7", 4 | "description" : "Play Conway's Game of life on GitHub contribution board", 5 | "icons": { 6 | "16": "icons/logo48.png", 7 | "48": "icons/logo128.png", 8 | "128": "icons/logo256.png", 9 | "256": "icons/logo256.png" 10 | }, 11 | "content_scripts": [{ 12 | "matches": [ 13 | "https://github.com/*" 14 | ], 15 | "exclude_matches": [ 16 | "https://github.com/new/*", 17 | "https://github.com/settings/*", 18 | "https://github.com/pricing/*", 19 | "https://github.com/blog/*", 20 | "https://github.com/contact/*", 21 | "https://github.com/site/*", 22 | "https://github.com/about/*" 23 | ], 24 | "css": ["style.css"], 25 | "js": ["index.js"], 26 | "run_at": "document_end" 27 | }], 28 | "manifest_version": 2 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Yuan Chuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Game of Life 2 | > A browser extension to play [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) on GitHub contribution board. 3 | 4 | ![alt](screencast/demo3.gif) 5 | 6 | ## How to play 7 | 8 | 1. Install the extension from [Chrome Web Store](https://chrome.google.com/webstore/detail/game-of-life/bhhddgpklpjchoemcgggncekimleaaok) 9 | 2. Click `Play` button from anyone's GitHub profile page: 10 | 11 | ![alt](screencast/play.png) 12 | 13 | ## Patterns 14 | 15 | * [Gosper glider gun](http://www.conwaylife.com/wiki/Gosper_glider_gun) -- Bill Gosper 1970 16 | * [Queen bee shuttle](http://www.conwaylife.com/wiki/Queen_bee_shuttle) -- Bill Gosper 1970 17 | * [Lightweight spaceship](http://www.conwaylife.com/wiki/Lightweight_spaceship) -- John Conway 1970 18 | * [Washerwoman](http://www.conwaylife.com/wiki/Washerwoman) -- Earl Abbe 1971 19 | * [Ants](http://www.conwaylife.com/wiki/Ants) -- Unknown 20 | * [Bi-clock](http://www.conwaylife.com/wiki/Bi-clock) -- Dale Edwin Cole 1971 21 | * [4-8-12 diamond](http://www.conwaylife.com/wiki/4-8-12_diamond) --- Honeywell group 1971 22 | * [Pinwheel](http://www.conwaylife.com/wiki/Pinwheel) -- Simon Norton 1970 23 | * [Radial pseudo-barberpole](http://www.conwaylife.com/wiki/Pseudo-barberpole)-- Gabriel Nivasch 1994 24 | * [Tumbler](http://www.conwaylife.com/wiki/Tumbler) -- George Collins 1970 25 | * [Turning toads](http://www.conwaylife.com/wiki/Turning_toads) -- Dean Hickerson 1989 26 | * [38P7.2](http://www.conwaylife.com/wiki/38P7.2) -- Nicolay Beluchenko 2009 27 | * [Blonker](http://www.conwaylife.com/wiki/Blonker) -- Nicolay Beluchenko 2004 28 | * [Octagon 2](http://www.conwaylife.com/wiki/Octagon_2) -- Arthur Taber 1971 29 | * [Star](http://www.conwaylife.com/wiki/Star) -- Hartmut Holzwart 1993 30 | * [Pentadecathlon](http://www.conwaylife.com/wiki/Pentadecathlon) -- John Conway 1970 31 | * [Pentapole](http://www.conwaylife.com/wiki/Pentapole) -- Unknown 1970 32 | * [Cow](http://www.conwaylife.com/wiki/Cow) -- Unknown 33 | * [Ellison p4 HW emulator](http://www.conwaylife.com/wiki/Ellison_P4_HW_emulator) -- Scot Ellison 2010 34 | * [Swine](http://www.conwaylife.com/wiki/Swine) -- Scot Ellison 2011 35 | * [Caterer on figure eight](http://www.conwaylife.com/wiki/Caterer_on_figure_eight) -- Unknown 36 | * [Almosymmetric](http://www.conwaylife.com/wiki/Almosymmetric) -- Unknown 1971 37 | * [Circle of fire](http://www.conwaylife.com/wiki/Circle_of_fire) -- Unknown 38 | * [Carnival shuttle](http://www.conwaylife.com/wiki/Carnival_shuttle) -- Robert Wainwright 1984 39 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | [data-gol-container] { 2 | position: relative; 3 | } 4 | .border[data-gol-container] { 5 | border-color: transparent !important; 6 | } 7 | [data-gol-container] .flash-notice { 8 | display: none; 9 | } 10 | [data-gol-container="running"] li[style$="238);"] { 11 | opacity: .8; 12 | } 13 | [data-gol-container]:not([data-gol-container="running"]) 14 | #gol-contribution-board li:hover { 15 | z-index: 2; 16 | outline: 1px solid rgba(30, 104, 35, .55); 17 | } 18 | 19 | 20 | #gol-contribution-board { 21 | position: absolute; 22 | top: -14px; 23 | left: 13px; 24 | width: calc(100% + 8px); 25 | height: 100%; 26 | background: #fff; 27 | display: flex; 28 | } 29 | #gol-contribution-board:after { 30 | content: ''; 31 | clear: both; 32 | display: block; 33 | visibility: hidden; 34 | } 35 | #gol-contribution-board li { 36 | display: block; 37 | font-size: 0; 38 | width: 14px; 39 | height: 14px; 40 | background-color: #eee; 41 | transition: background-color .2s ease; 42 | position: relative; 43 | z-index: 1; 44 | box-sizing: border-box; 45 | border: 1px solid #fff; 46 | margin-left: 1px; 47 | margin-bottom: 1px; 48 | } 49 | 50 | [gol-layout-overview] #gol-contribution-board { 51 | height: calc(100% + 62px); 52 | z-index: 10; 53 | top: -8px; 54 | left: -5px; 55 | } 56 | 57 | [gol-layout-overview] #gol-contribution-board:before { 58 | content: ''; 59 | position: absolute; 60 | width: calc(100% - 8px); 61 | margin: 0 auto; 62 | bottom: 0; 63 | height: 5px; 64 | border-radius: 3px 3px 0 0; 65 | border: 1px solid #d1d5da; 66 | border-bottom: none; 67 | left: 4px; 68 | } 69 | [gol-layout-overview] #gol-contribution-board li { 70 | width: 12px; 71 | height: 12px; 72 | } 73 | 74 | #gol-controls { 75 | position: absolute; 76 | text-align: center; 77 | width: 100%; 78 | bottom: -32px; 79 | padding-left: 40px; 80 | z-index: 11; 81 | background: transparent; 82 | } 83 | [gol-layout-overview] #gol-controls { 84 | background: #fff; 85 | } 86 | #gol-controls a { 87 | display: inline-block; 88 | position: relative; 89 | width: 5em; 90 | padding: 2px 0; 91 | margin: 0 2px; 92 | transition: all .2s ease; 93 | font-size: .8em; 94 | text-decoration: none; 95 | color: var(--color, #8cc665); 96 | box-shadow: currentColor 0px 0px 0px 1px; 97 | border-radius: 2px; 98 | cursor: pointer; 99 | } 100 | #gol-controls a:after { 101 | content: attr(data-action); 102 | } 103 | 104 | @keyframes rotating { 105 | from { transform: rotate(45deg); } 106 | to { transform: rotate(0) } 107 | } 108 | #gol-controls a[data-action="close"] { 109 | position: relative; 110 | width: 20px; 111 | height: 20px; 112 | line-height: 0; 113 | margin-left: 10px; 114 | vertical-align: text-top; 115 | border:none; 116 | box-shadow: none; 117 | background: none; 118 | color: var(--color, #8cc665); 119 | opacity: .7; 120 | font-size: 0; 121 | -webkit-animation: rotating .2s ease; 122 | animation: rotating .2s ease; 123 | -webkit-transfom-origin: 50% 50%; 124 | transfom-origin: 50% 50%; 125 | } 126 | #gol-controls a[data-action="close"]:before, 127 | #gol-controls a[data-action="close"]:after { 128 | content: ''; 129 | position: absolute; 130 | left: 50%; 131 | top: 50%; 132 | width: 61.8%; 133 | height: 1px; 134 | background: currentColor; 135 | -webkit-transfom-origin: 50% 50%; 136 | transfom-origin: 50% 50%; 137 | } 138 | #gol-controls a[data-action="close"]:before { 139 | -webkit-transform: translate(-50%, 0) rotate(-45deg); 140 | transform: translate(-50%, 0) rotate(-45deg); 141 | } 142 | #gol-controls a[data-action="close"]:after { 143 | -webkit-transform: translate(-50%, 0) rotate(45deg); 144 | transform: translate(-50%, 0) rotate(45deg); 145 | } 146 | #gol-controls a[data-action="close"]:hover { 147 | opacity: 1; 148 | } 149 | 150 | #gol-controls a[data-action="run"] { 151 | position: relative; 152 | } 153 | #gol-controls a[data-action="pause"] { 154 | filter: hue-rotate(-20deg); 155 | } 156 | #gol-controls a[data-action="run"]:before, 157 | #gol-controls a[data-action="pause"]:before { 158 | content: ''; 159 | position: absolute; 160 | z-index: -1; 161 | top: 0; bottom: 0; left: 0; right: 0; 162 | background: currentColor; 163 | opacity: .1; 164 | } 165 | #gol-controls a[data-action="pause"]:before { 166 | opacity: .15; 167 | } 168 | #gol-controls a[disabled] { 169 | background: transparent; 170 | color: #e0e0e0; 171 | border-color: currentColor; 172 | cursor: default; 173 | } 174 | #gol-controls a[data-action="run"]:not([disabled]):hover:before { 175 | opacity: .15; 176 | } 177 | #gol-controls a[data-action="next"]:not([disabled]), 178 | #gol-controls a[data-action="clear"]:not([disabled]), 179 | #gol-controls a[data-action="reset"]:not([disabled]) { 180 | opacity: .7; 181 | } 182 | #gol-controls a[data-action="next"]:not([disabled]):hover, 183 | #gol-controls a[data-action="clear"]:not([disabled]):hover, 184 | #gol-controls a[data-action="reset"]:not([disabled]):hover { 185 | opacity: 1; 186 | } 187 | 188 | #gol-controls a[data-action="reset"] { 189 | width: 2.5em; 190 | background: #fff; 191 | will-change: opacity; 192 | } 193 | #gol-controls a[data-action="reset"][disabled] { 194 | filter: grayscale(1); 195 | opacity: .5; 196 | } 197 | #gol-controls a[data-action="reset"]:after { 198 | content: '_'; 199 | visibility: hidden; 200 | z-index: -1; 201 | } 202 | #gol-controls a[title]:not([title=""])[data-action="reset"]:before { 203 | content: attr(title); 204 | position: absolute; 205 | white-space: nowrap; 206 | left: 50%; 207 | bottom: 100%; 208 | transform: translate(-50%, -2px); 209 | min-width: 8em; 210 | margin-bottom: 4px; 211 | border-radius: 2px; 212 | padding: 4px 8px; 213 | text-algin: center; 214 | background: rgba(0, 0, 0, .5); 215 | color: #fff; 216 | display: none; 217 | } 218 | [data-gol-container=""] 219 | #gol-controls a[title]:not([title=""])[data-action="reset"]:hover:before { 220 | display: block; 221 | transform: translate(-50%, 0); 222 | z-index: 9; 223 | } 224 | 225 | 226 | #gol-pattern-shadow { 227 | position: absolute; 228 | top: 0; 229 | left: 0; 230 | width: 3px; 231 | height: 3px; 232 | margin-left: 1px; 233 | margin-top: 2px; 234 | } 235 | 236 | #gol-generation { 237 | position: absolute; 238 | left: 16px; 239 | top: -10px; 240 | font-size: 12px; 241 | color: #b6b6b6; 242 | } 243 | [gol-layout-overview] #gol-generation { 244 | left: 0; 245 | } 246 | #gol-generation:before { 247 | content: 'Generation: '; 248 | margin-right: 2px; 249 | } 250 | 251 | 252 | #gol-button-play { 253 | float: left; 254 | margin-right: 1em; 255 | padding: 1px 5px 1px 9px; 256 | vertical-align: middle; 257 | transition: all .2s ease; 258 | color: var(--color, #8cc665); 259 | box-shadow: currentColor 0px 0px 0px .65px; 260 | border-radius: 2px; 261 | cursor: pointer; 262 | } 263 | #gol-button-play:hover { 264 | box-shadow: currentColor 0px 0px 0px 1px; 265 | text-decoration: none; 266 | } 267 | #gol-button-play:before { 268 | content: ''; 269 | display: inline-block; 270 | width: 4px; 271 | height: 4px; 272 | margin: -2px 8px 0 0; 273 | vertical-align: middle; 274 | color: currentColor; 275 | box-shadow: 276 | 0 -4px currentColor, 277 | 4px 0 currentColor, 278 | -4px 4px currentColor, 279 | 0 4px currentColor, 280 | 4px 4px currentColor; 281 | } 282 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var ROW = 13 4 | var COL = 60 5 | var TOTAL = ROW * COL 6 | var COLOR_DEAD = '#ebedf0' 7 | var COLOR_ALIVE = '#8cc665' 8 | var CONTAINER = 'data-gol-container' 9 | var EMPTY_ROW = repeat(COL, 0).split('').map(function(n) { return +n }) 10 | 11 | /** 12 | * Based on the `Run Length Encoded` format 13 | * http://conwaylife.com/wiki/RLE 14 | */ 15 | var PATTERNS = { 16 | "Gosper glider gun -- Bill Gosper 1970": rle( 17 | 36, 9, '24bo11b$22bobo11b$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o14b$2o8b\ 18 | o3bob2o4bobo11b$10bo5bo7bo11b$11bo3bo20b$12b2o!' 19 | ), 20 | "Queen bee shuttle -- Bill Gosper 1970": rle( 21 | 22, 7, '9bo12b$7bobo12b$6bobo13b$2o3bo2bo11b2o$2o4bobo11b2o$7bobo12b$9bo!' 22 | ), 23 | "Lightweight spaceship -- John Conway 1970": rle( 24 | 5, 4, 'bo2bo$o4b$o3bo$4o!' 25 | ), 26 | "Washerwoman -- Earl Abbe 1971": rle( 27 | 56, 5, 'o55b$2o4bo5bo5bo5bo5bo5bo5bo5bo5bob$3o2bobo3bobo3bobo3bobo3bobo3bobo3b\ 28 | obo3bobo3bobo$2o4bo5bo5bo5bo5bo5bo5bo5bo5bob$o!' 29 | ), 30 | "Ants -- Unknown": rle( 31 | 44, 4, '2o3b2o3b2o3b2o3b2o3b2o3b2o3b2o3b2o2b$2b2o3b2o3b2o3b2o3b2o3b2o3b2o3b2o\ 32 | 3b2o$2b2o3b2o3b2o3b2o3b2o3b2o3b2o3b2o3b2o$2o3b2o3b2o3b2o3b2o3b2o3b2o3b\ 33 | 2o3b2o!' 34 | ), 35 | "Bi-clock -- Dale Edwin Cole 1971": rle( 36 | 7, 7, '2bo4b$2o5b$2b2o3b$bo3bob$3b2o2b$5b2o$4bo!' 37 | ), 38 | "4-8-12 diamond -- Honeywell group 1971": rle( 39 | 12, 9, '4b4o4b2$2b8o2b2$12o2$2b8o2b2$4b4o!' 40 | ), 41 | "Pinwheel -- Simon Norton 1970": rle( 42 | 12, 12, '6b2o4b$6b2o4b2$4b4o4b$2obo4bo3b$2obo2bobo3b$3bo3b2ob2o$3bobo2bob2o$4b\ 43 | 4o4b2$4b2o6b$4b2o!' 44 | ), 45 | "Radial pseudo-barberpole -- Gabriel Nivasch 1994": rle( 46 | 13, 13, '10b2ob$2o9bob$o8bo3b$2b2o3bobo3b2$3bobobo5b2$5bobobo3b2$3bobo3b2o2b$3b\ 47 | o8bo$bo9b2o$b2o!' 48 | ), 49 | "Tumbler -- George Collins 1970": rle( 50 | 9, 5, 'bo5bob$obo3bobo$o2bobo2bo$2bo3bo2b$2b2ob2o!' 51 | ), 52 | "Turning toads -- Dean Hickerson 1989": rle( 53 | 37, 8, '15bo6bo14b$14b2o5b2o6b2o6b$6b3obobob2obobob2obobo10b$2b2obo6bobo4bobo\ 54 | 4bobo2bob2o2b$o2bobo3bo18b4obo2bo$2obobo27bob2o$3bo29bo3b$3b2o27b2o!' 55 | ), 56 | "38P7.2 -- Nicolay Beluchenko 2009": rle( 57 | 13, 11, '4bo3bo4b$o2bobobobo2bo$obo2bobo2bobo$bo2b2ob2o2bob$5bobo5b$2b2o5b2o2b$\ 58 | 2bo7bo2b$4bo3bo4b2$bo2bo3bo2bob$2b2o5b2o!' 59 | ), 60 | "Blonker -- Nicolay Beluchenko 2004": rle( 61 | 12, 8, 'o2b2o4bo$2o2bob2obo$4bobo$5b2o$7bo$7bo3bo$9bobo$10bo!' 62 | ), 63 | "Octagon 2 -- Arthur Taber 1971": rle( 64 | 8, 8, '3b2o3b$2bo2bo2b$bo4bob$o6bo$o6bo$bo4bob$2bo2bo2b$3b2o!' 65 | ), 66 | "Star -- Hartmut Holzwart 1993": rle( 67 | 11, 11, '4b3o4b2$2bobobobo2b2$obo5bobo$o9bo$obo5bobo2$2bobobobo2b2$4b3o!' 68 | ), 69 | "Pentadecathlon -- John Conway 1970": rle( 70 | 10, 3, '2bo4bo2b$2ob4ob2o$2bo4bo!' 71 | ), 72 | "Pentapole -- Unknown 1970": rle( 73 | 8, 8, '2o6b$obo5b2$2bobo3b2$4bobob$7bo$6b2o!' 74 | ), 75 | "Cow -- Unknown": rle( 76 | 40, 7, '2o7b2o2b2o2b2o2b2o2b2o2b2o2b2o5b$2o4bob3o2b2o2b2o2b2o2b2o2b2o2b2o3b2o$\ 77 | 4b2obo29bobo$4b2o3b29o2b$4b2obo30bob$2o4bob3o2b2o2b2o2b2o2b2o2b2o2b2o\ 78 | 2b2ob$2o7b2o2b2o2b2o2b2o2b2o2b2o2b2o!' 79 | ), 80 | "Ellison p4 HW emulator -- Scot Ellison 2010": rle( 81 | 24, 9, '11b2o11b$4bo3bo6bo3bo4b$3bobo12bobo3b$3bobo12bobo3b$2obobob10obobob2o$\ 82 | 2obo16bob2o$3bo16bo3b$3bobo4b2obo4bobo3b$4b2o4bob2o4b2o!' 83 | ), 84 | "Swine -- Scot Ellison 2011": rle( 85 | 37, 10, '33bo$9bo2b2o7bo10bobo$o2b2o2bobobo5bo3bo7bo2b2o$o10bo2bobo3bo4bo2bobob\ 86 | 2ob2o$o3bo4bo4b2obobo2bob2obo4b2o$3b2o4bob2obo2bobob2o4bo4bo3bo$2ob2ob\ 87 | obo2bo4bo3bobo2bo10bo$3b2o2bo7bo3bo5bobobo2b2o2bo$2bobo10bo7b2o2bo$3bo!' 88 | ), 89 | "Caterer on figure eight -- Unknown": rle( 90 | 18, 6, '4b2o6bo5b$2bob2o4bo3b4o$bo8bo3bo3b$4bo5bo7b$2obo9bo4b$2o9b2o!' 91 | ), 92 | "Almosymmetric -- Unknown 1971": rle( 93 | 9, 8, '4bo4b$2o2bobo2b$obo6b$7b2o$bo7b$o6bob$2obobo3b$5bo!' 94 | ), 95 | "Circle of fire -- Unknown": rle( 96 | 11, 11, '4bobo$2bo2bo2bo$3bobobo$b3obob3o$5bo$5ob5o$5bo$b3obob3o$3bobobo$2bo2\ 97 | bo2bo$4bobo!' 98 | ), 99 | "Carnival shuttle -- Robert Wainwright 1984": rle( 100 | 38, 7, '33bo3bo$2o3b2o26b5o$bobobo3bo2bo6b2o3bo2bo7bo2b$b2ob2o2b2o3b2o4b2o2b2o\ 101 | 3b2o4bobob$bobobo3bo2bo6b2o3bo2bo7bo2b$2o3b2o26b5o$33bo3bo!' 102 | ) 103 | } 104 | 105 | function repeat(times, str) { 106 | if (str.repeat) return str.repeat(+times) 107 | return new Array(+times + 1).join(str) 108 | } 109 | 110 | function ignoreHeadArg(fn) { 111 | return function() { 112 | return fn.apply(null, [].slice.call(arguments, 1)) 113 | } 114 | } 115 | 116 | /** 117 | * Transform RLE format into a 2-dimensional array of 0 and 1s 118 | */ 119 | function rle(x, y, pattern) { 120 | return { 121 | row: y, 122 | col: x, 123 | mapping: pattern 124 | .replace(/(\d+)(\$)/g, ignoreHeadArg(repeat)) 125 | .split('$') 126 | .map(function(line) { 127 | var pat = line 128 | .replace(/\!|\s+/g, '') 129 | .replace(/(\d+)([bo])/g, ignoreHeadArg(repeat)) 130 | pat += repeat(x - pat.length, 'b') 131 | return pat.split('').map(function(c) { 132 | return c === 'b' ? 0 : 1 133 | }) 134 | }) 135 | } 136 | } 137 | 138 | function forEachIndex(row, col, fn) { 139 | if (typeof row === 'function') { 140 | (fn = row, row = ROW, col = COL) 141 | } 142 | for (var x = 0; x < row; ++x) { 143 | for (var y = 0; y < col; ++y) { 144 | fn(x, y) 145 | } 146 | } 147 | } 148 | 149 | function forEachList(list, childSelector, fn) { 150 | if (typeof childSelector === 'function') { 151 | fn = childSelector 152 | for (var i = 0; i < list.length; ++i) { 153 | fn(i, list[i]) 154 | } 155 | } else { 156 | for (var x = 0; x < list.length; ++x) { 157 | var sublist = list[x].querySelectorAll(childSelector) 158 | for (var y = 0; y < sublist.length; ++y) { 159 | fn(y, x, sublist[y]) 160 | } 161 | } 162 | } 163 | } 164 | 165 | function createStatusBoard(fn) { 166 | var board = [] 167 | for (var i = 0; i < ROW; ++i) { 168 | board.push(EMPTY_ROW.slice(0)) 169 | } 170 | forEachIndex(function(x, y) { 171 | board[x][y] = fn ? fn(x, y) : 0 172 | }) 173 | return board 174 | } 175 | 176 | function createBoardByPattern(pattern) { 177 | var board = createStatusBoard() 178 | var row = pattern.row 179 | var col = pattern.col 180 | var startx = Math.ceil((ROW - row) / 2) 181 | var starty = Math.ceil((COL - col) / 2) 182 | forEachIndex(row, col, function(x, y) { 183 | board[startx + x][starty + y] = pattern.mapping[x][y] 184 | }) 185 | return board 186 | } 187 | 188 | function countNeighbors(board, x, y) { 189 | var x0 = (x > 0) 190 | var xr = (x < ROW - 1) 191 | var y0 = (y > 0) 192 | var yr = (y < COL - 1) 193 | return ( 194 | ((x0 && y0) ? board[x - 1][y - 1] : 0) + 195 | (y0 ? board[x][y - 1] : 0) + 196 | ((xr && y0) ? board[x + 1][y - 1] : 0) + 197 | (x0 ? board[x - 1][y] : 0) + 198 | (xr ? board[x + 1][y] : 0) + 199 | ((x0 && yr) ? board[x - 1][y + 1] : 0) + 200 | (yr ? board[x][y + 1] : 0) + 201 | ((xr && yr) ? board[x + 1][y + 1] : 0) 202 | ) 203 | } 204 | 205 | function next(board) { 206 | return createStatusBoard(function(x, y) { 207 | var neighbors = countNeighbors(board, x, y) 208 | var status = board[x][y] 209 | switch (status) { 210 | case 0: if (neighbors === 3) status = 1; break 211 | case 1: if (neighbors <= 1 || neighbors >= 4) status = 0 212 | } 213 | return status 214 | }) 215 | } 216 | 217 | function randomBoxshadows() { 218 | var pallette = [COLOR_DEAD, COLOR_ALIVE] 219 | var length = pallette.length 220 | var shadows = [] 221 | forEachIndex(5, 3, function(x, y) { 222 | shadows.push([ 223 | ((x + 1) << 2) + 'px', 224 | ((y + 1) << 2) + 'px', 225 | 0, 0, pallette[Math.floor(Math.random() * length)] 226 | ].join(' ')) 227 | }) 228 | return shadows.join(',') 229 | } 230 | 231 | function isTagName(name, el) { 232 | if (el) { 233 | return el.tagName.toLowerCase() === name 234 | } 235 | } 236 | 237 | function setTransitonDelay(el, delay) { 238 | if (el) { 239 | el.style.transitionDelay = el.style.webkitTransitionDelay = delay 240 | } 241 | } 242 | 243 | function fillColor(cell, color) { 244 | if (cell) { 245 | if (isTagName('rect', cell)) { 246 | return cell.getAttribute('fill') 247 | } 248 | return color 249 | ? (cell.style.backgroundColor = color) 250 | : cell.style.backgroundColor 251 | } 252 | } 253 | 254 | function getKey(x, y) { 255 | return x + '-' + y 256 | } 257 | 258 | var getPosition = function() { 259 | var position = {} 260 | forEachIndex(function(x, y) { 261 | position[getKey(x, y)] = { x: x, y: y } 262 | }) 263 | return function(key) { 264 | return position[key] || {} 265 | } 266 | }() 267 | 268 | var Canvas = function() { 269 | var id = 'gol-contribution-board' 270 | var cells = {} 271 | var canvas = null 272 | var animating = false 273 | var emptyCells = repeat(COL, 274 | '' 275 | ) 276 | 277 | function targetCell(fn) { 278 | return function(e) { 279 | e.stopPropagation() 280 | var cell = e.target 281 | if (isTagName('li', cell)) { 282 | fn(cell, e) 283 | } 284 | } 285 | } 286 | var toggle = targetCell(function(cell) { 287 | if (Canvas.activeDrawing) { 288 | Canvas.ontoggle && Canvas.ontoggle(cell) 289 | } 290 | }) 291 | var activeDrawing = targetCell(function(cell, e) { 292 | Canvas.activeDrawing = true 293 | toggle(e) 294 | }) 295 | var inActiveDrawing = targetCell(function() { 296 | Canvas.activeDrawing = false 297 | }) 298 | 299 | return { 300 | build: function() { 301 | if (canvas) return canvas 302 | canvas = document.createElement('div') 303 | canvas.id = id 304 | canvas.innerHTML = emptyCells 305 | canvas.addEventListener('mouseover', toggle) 306 | canvas.addEventListener('mousedown', activeDrawing) 307 | canvas.addEventListener('mouseup', inActiveDrawing) 308 | 309 | forEachList(canvas.querySelectorAll('ul'), 'li', function(x, y, elem) { 310 | var key = getKey(x, y) 311 | cells[key] = elem 312 | elem.setAttribute('data-key', key) 313 | }) 314 | return canvas 315 | }, 316 | render: function(board, pallette) { 317 | forEachIndex(function(x, y) { 318 | var key = getKey(x, y) 319 | var status = board[x][y] 320 | fillColor(cells[key], status 321 | ? (pallette ? pallette[key] : COLOR_ALIVE) 322 | : COLOR_DEAD 323 | ) 324 | }) 325 | }, 326 | isEmpty: function() { 327 | return TOTAL === canvas.querySelectorAll('li[style$="240);"]').length 328 | }, 329 | animating: function() { 330 | return animating 331 | }, 332 | animateBackground: function() { 333 | animating = true 334 | var cells = canvas.querySelectorAll('li:not([style$="240);"])') 335 | forEachList(cells, function(_, cell) { 336 | setTransitonDelay(cell, (600 * Math.random()) + 'ms') 337 | }) 338 | setTimeout(function() { 339 | forEachList(cells, function(_, cell) { 340 | cell.style.backgroundColor = COLOR_ALIVE 341 | }) 342 | }) 343 | setTimeout(function() { 344 | forEachList(cells, function(_, cell) { 345 | setTransitonDelay(cell, '') 346 | }) 347 | animating = false 348 | }, 600) 349 | }, 350 | fixAlignment: function() { 351 | var offset = function(el) { 352 | return el && el.getBoundingClientRect().left 353 | } 354 | var head = document.querySelector('.js-calendar-graph-svg > g > g:first-child') 355 | var mirror = canvas.querySelector('ul:nth-child(6)') 356 | 357 | if (offset(head) - offset(mirror) == 0.5) { 358 | canvas.style.marginLeft = '-.5px' 359 | } 360 | }, 361 | remove: function() { 362 | cells = {} 363 | if (canvas) { 364 | canvas.removeEventListener('mouseover', toggle) 365 | canvas.removeEventListener('mousedown', activeDrawing) 366 | canvas.removeEventListener('mouseup', inActiveDrawing) 367 | canvas.parentNode.removeChild(canvas) 368 | canvas = null 369 | } 370 | } 371 | } 372 | }() 373 | 374 | var Controls = function() { 375 | var id = 'gol-controls' 376 | var controls = null 377 | var generation = null 378 | var buttons = {} 379 | var actions = ['reset', 'run', 'pause', 'next', 'clear', 'close'] 380 | var content = '0' + ( 381 | actions.map(function(name) { 382 | var content = (name === 'reset' ? '' : '') 383 | return '' + content + '' 384 | }).join('') 385 | ) 386 | function action(e) { 387 | var btn = e.target 388 | if (btn.hasAttribute('data-action') && !btn.hasAttribute('disabled')) { 389 | Controls.onclick && Controls.onclick(btn) 390 | } 391 | return false 392 | } 393 | return { 394 | build: function() { 395 | if (controls) return controls 396 | controls = document.createElement('div') 397 | controls.id = id 398 | controls.innerHTML = content 399 | controls.style.setProperty('--color', COLOR_ALIVE) 400 | actions.forEach(function(name) { 401 | buttons[name] = controls.querySelector('[data-action="' + name + '"]') 402 | }) 403 | controls.addEventListener('click', action) 404 | return controls 405 | }, 406 | apply: function(operation, names, value) { 407 | if (!Array.isArray(names)) names = [names] 408 | names.forEach(function(name) { 409 | var btn = buttons[name] 410 | switch (operation) { 411 | case 'disable': btn.setAttribute('disabled', true); break 412 | case 'enable': btn.removeAttribute('disabled'); break 413 | case 'show': btn.style.display = ''; break 414 | case 'hide': btn.style.display = 'none'; break 415 | case 'setTitle': btn.title = value; break 416 | case 'setShadow': btn.firstChild.style.boxShadow = value 417 | } 418 | }) 419 | return this 420 | }, 421 | generation: function(count) { 422 | if (!generation) { 423 | generation = document.getElementById('gol-generation') 424 | } 425 | if (generation) { 426 | generation.innerHTML = count 427 | } 428 | }, 429 | remove: function() { 430 | buttons = {} 431 | if (controls) { 432 | controls.removeEventListener('click', action) 433 | controls.parentNode.removeChild(controls) 434 | generation = null 435 | controls = null 436 | } 437 | } 438 | } 439 | }() 440 | 441 | var Game = { 442 | generation: 0, 443 | running: false, 444 | timer: null, 445 | container: null, 446 | board: null 447 | } 448 | 449 | Game._getIntialStatus = function() { 450 | var board = createStatusBoard() 451 | var gs = Game.container.querySelectorAll('.js-calendar-graph-svg > g > g') 452 | var pallette = {} 453 | forEachList(gs, 'rect', function(x, y, rect) { 454 | var color = fillColor(rect) 455 | var dx = x + 3, dy = y + 4 456 | board[dx][dy] = (color === COLOR_DEAD) ? 0 : 1 457 | pallette[getKey(dx, dy)] = color 458 | }) 459 | return { board: board, pallette: pallette } 460 | } 461 | 462 | Game._getNextPattern = function() { 463 | var names = Object.keys(PATTERNS) 464 | var max = names.length 465 | var index = 0 466 | return function() { 467 | if (index >= max) { 468 | index = 0 469 | } 470 | var name = names[index++] 471 | return { 472 | name: name, 473 | pattern: PATTERNS[name] 474 | } 475 | } 476 | }() 477 | 478 | Game._updateControls = function() { 479 | Game.running 480 | ? Controls 481 | .apply('disable', ['next', 'clear', 'reset']) 482 | .apply('show', 'pause') 483 | .apply('hide', 'run') 484 | : Controls 485 | .apply('enable', 'reset') 486 | .apply('show', 'run') 487 | .apply('hide', 'pause') 488 | .apply( 489 | (Canvas.isEmpty() ? 'disable' : 'enable'), 490 | ['run', 'next', 'clear'] 491 | ) 492 | } 493 | 494 | Game._updateGeneration = function(count) { 495 | Controls.generation(Game.generation = count) 496 | } 497 | 498 | Game._ontoggle = function(cell) { 499 | if (!Game.running) { 500 | var p = getPosition(cell.getAttribute('data-key')) 501 | var x = p.x, y = p.y 502 | var status = Game.board[x][y] 503 | fillColor(cell, (status ? COLOR_DEAD : COLOR_ALIVE)) 504 | Game.board[x][y] = (status ? 0 : 1) 505 | Game._updateGeneration(0) 506 | Game._updateControls() 507 | } 508 | } 509 | 510 | Game._loop = function() { 511 | if (Game.running) { 512 | Game.next() 513 | Game.timer = setTimeout(function() { 514 | Game._loop() 515 | }, 80) 516 | } 517 | } 518 | 519 | Game.init = function() { 520 | var graph = document.querySelector('.js-yearly-contributions') 521 | if (hasActiveOverview()) { 522 | graph.setAttribute('gol-layout-overview', true) 523 | } else { 524 | let cal = document.querySelector('.graph-before-activity-overview') 525 | if (cal) { 526 | cal.style.transform = 'translateY(10px)' 527 | } 528 | } 529 | 530 | var id = 'gol-button-play' 531 | if (graph && !document.getElementById(id)) { 532 | var play = document.createElement('a') 533 | var legend = document.querySelector('.contrib-legend') 534 | COLOR_ALIVE = getComputedStyle(legend.querySelector('.legend li:nth-child(3)')).backgroundColor 535 | play.style.setProperty('--color', COLOR_ALIVE) 536 | play.id = id 537 | play.title = "Play Conway's Game of Life" 538 | play.innerHTML = 'Play' 539 | play.addEventListener('click', function(e) { 540 | Game.play() 541 | return false 542 | }) 543 | if (legend) { 544 | legend.insertBefore(play, legend.firstChild) 545 | } 546 | } 547 | } 548 | 549 | Game.play = function() { 550 | var gc = Game.container = 551 | document.querySelector('.js-calendar-graph').parentNode 552 | gc.setAttribute(CONTAINER, '') 553 | gc.appendChild(Canvas.build()) 554 | gc.appendChild(Controls.build()) 555 | 556 | Game.reset(true) 557 | Canvas.fixAlignment() 558 | Controls 559 | .apply('hide', 'pause') 560 | .apply('setTitle', 'reset', 'select pattern') 561 | .onclick = function(btn) { 562 | var action = Game[btn.getAttribute('data-action')] 563 | action && action() 564 | } 565 | } 566 | 567 | Game.run = function() { 568 | Game.running = true 569 | Game.container.setAttribute(CONTAINER, 'running') 570 | Game._loop() 571 | Game._updateControls() 572 | } 573 | 574 | Game.next = function() { 575 | Canvas.render(Game.board = next(Game.board)) 576 | Game._updateGeneration(++Game.generation) 577 | if (Canvas.isEmpty()) { 578 | Game._updateControls() 579 | return Game.pause() 580 | } 581 | } 582 | 583 | Game.pause = function() { 584 | clearTimeout(Game.timer) 585 | Game.running = false 586 | if (Game.container) { 587 | Game.container.setAttribute(CONTAINER, '') 588 | Game._updateControls() 589 | } 590 | } 591 | 592 | Game.clear = function() { 593 | Canvas.render(Game.board = createStatusBoard()) 594 | Game._updateGeneration(0) 595 | Game._updateControls() 596 | Controls.apply('setTitle', 'reset', 'select pattern') 597 | } 598 | 599 | Game.reset = function(begin) { 600 | if (Canvas.animating()) return false 601 | Game.pause() 602 | 603 | if (begin) { 604 | var initial = Game._getIntialStatus() 605 | Canvas.render( 606 | Game.board = initial.board, 607 | initial.pallette 608 | ) 609 | } else { 610 | var nextPattern = Game._getNextPattern() 611 | var board = createBoardByPattern(nextPattern.pattern) 612 | Controls.apply('setTitle', 'reset', nextPattern.name) 613 | Canvas.render(Game.board = board) 614 | } 615 | 616 | Controls.apply('setShadow', 'reset', randomBoxshadows()) 617 | Canvas.ontoggle = Game._ontoggle 618 | Canvas.animateBackground() 619 | Game._updateGeneration(0) 620 | Game._updateControls() 621 | } 622 | 623 | Game.close = function() { 624 | if (Game.container) { 625 | Game.pause() 626 | Game.container.removeAttribute(CONTAINER) 627 | Canvas.remove() 628 | Controls.remove() 629 | Game.container = null 630 | } 631 | } 632 | 633 | Game.init() 634 | 635 | var container = document.getElementById('js-pjax-container') 636 | var loaderBar = document.getElementById('js-pjax-loader-bar') 637 | var maxAttempt = 100 638 | var isDetecting = false 639 | 640 | if (!container) return false 641 | container.addEventListener('click', function(e) { 642 | if (!isDetecting) { 643 | var link = e.target 644 | if (isTagName('span', link)) { 645 | link = e.target.parentNode 646 | } 647 | if (isTagName('a', link) 648 | && /underline\-nav\-item|js\-year\-link/.test(link.className)) { 649 | Game.close() 650 | detectPjaxEnd(function() { 651 | Game.init() 652 | }, maxAttempt) 653 | } 654 | } 655 | }) 656 | 657 | function hasActiveOverview() { 658 | return document.querySelector('.js-activity-overview-graph-container') 659 | } 660 | 661 | function detectPjaxEnd(fn, attempt) { 662 | if (!attempt) { 663 | return isDetecting = false 664 | } 665 | isDetecting = true 666 | setTimeout(function() { 667 | if (/is\-loading/.test(loaderBar.className) 668 | || document.getElementById('gol-button-play')) { 669 | detectPjaxEnd(fn, --attempt) 670 | } else { 671 | setTimeout(function() { 672 | isDetecting = false 673 | fn() 674 | }, 500) 675 | } 676 | }, 500) 677 | } 678 | 679 | }()) 680 | --------------------------------------------------------------------------------