├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── admin.ejs ├── arduino.cc ├── art ├── _base.js ├── automata.js ├── boxes.js ├── chess-pieces.js ├── chess.js ├── circle.js ├── collatz.js ├── combinators.js ├── divisions.js ├── element-markov.js ├── element.js ├── epicycles.js ├── fifteen.js ├── fraction.js ├── honeycomb.js ├── julia.js ├── knots.js ├── mario.js ├── nes.js ├── network.js ├── noise.js ├── pieces.js ├── quasiflake.js ├── rings.js ├── sandpiles.js ├── semicircle.js ├── stocks.js ├── three-body.js ├── turing.js ├── util.js ├── voronoi.js └── walk.js ├── package-lock.json ├── package.json ├── pages ├── [piece] │ ├── [seed].js │ └── art.module.css ├── _app.js ├── index.js └── style.css ├── public └── flat-eric.png ├── scripts ├── chess.js ├── find-element.js ├── generate-state.js ├── numerator.js └── ticker.js └── server.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | push_to_registry: 8 | name: Push Docker image to GitHub Packages 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@v2 13 | 14 | - name: Generate Docker tag 15 | id: meta 16 | uses: crazy-max/ghaction-docker-meta@v2 17 | with: 18 | images: | 19 | docker.pkg.github.com/jdan/hashart/hashart-srv 20 | tags: | 21 | type=sha,prefix= 22 | type=raw,value=latest 23 | 24 | - name: Log into GitHub Package registry 25 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin 26 | 27 | - name: Build and push to GitHub Packages 28 | uses: docker/build-push-action@v2 29 | with: 30 | push: true 31 | tags: ${{ steps.meta.outputs.tags }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | vendor/ 4 | db.json 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update -y 5 | # https://www.npmjs.com/package/canvas 6 | RUN apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev 7 | RUN apt-get install -y nodejs npm 8 | 9 | WORKDIR /app 10 | ADD . . 11 | RUN npm install --build-from-source 12 | 13 | CMD ["node", "server.js"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## hashart 2 | 3 | The code behind [hash.jordanscales.com](https://hash.jordanscales.com), an experiment in turning SHA-256 hashes into pixels. 4 | 5 | ![a screenshot of https://hash.jordanscales.com/circles/Hello,%20world!](https://user-images.githubusercontent.com/287268/114740072-810f3c00-9d17-11eb-9bad-e1d09f6521e2.png) 6 | 7 | ### running 8 | 9 | ``` 10 | npm i 11 | npm run dev 12 | ``` 13 | 14 | ### a small screenshot service 15 | 16 | This repository contains a small service for rendering art directly to PNGs using [canvas](https://www.npmjs.com/package/canvas). You can run it with: 17 | 18 | ``` 19 | node server.js 20 | ``` 21 | 22 | This service is also contained in a Docker image that I automatically publish to [GitHub packages](https://github.com/jdan/hashart/packages/728823): 23 | 24 | ``` 25 | docker run --rm -p "3000:3000" docker.pkg.github.com/jdan/hashart/hashart-srv:latest 26 | ``` 27 | 28 | To be able to use the "mario" piece: 29 | 30 | ``` 31 | docker run --rm -p "3000:3000" -v "/path/to/mariobros.nes:/app/vendor/roms/mariobros.nes" docker.pkg.github.com/jdan/hashart/hashart-srv:latest 32 | ``` 33 | 34 | ### rendering hashart on screens 35 | 36 | I uploaded [arduino.cc](/arduino.cc) to an [Inkplate 6](https://inkplate.io/) to display random 37 | hashart pieces in my apartment. I framed my inkplate using [Level Frames](https://www.levelframes.com/frames/new?width=5.25&height=4) (5 1/4" x 4" with 1 1/2" matting) and gave it a [2000mAh battery](https://www.adafruit.com/product/2011) which lasts quite a long time. 38 | 39 | ![A photo of two stuffed animals next to a wooden frame with a digital screen in the middle of it. The screen contains a piece of art consisting of semicircles stacked on top of each other tightly, almost resembling a Slinky, scattered around the canvas](https://user-images.githubusercontent.com/287268/119571180-0a864500-bd7f-11eb-8e04-f039b8b98c04.png) 40 | -------------------------------------------------------------------------------- /admin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin console 5 | 6 | 7 |

Enabled pieces

8 | 9 |
10 | <% pieces.forEach((piece) => { %> 11 |
12 | 21 |
22 | <% }) %> 23 | 24 |
25 | 29 |
30 | 31 | 32 | 33 |

34 | The random rotation is read from and written to $ROOT/db.json 35 |

36 |
37 | 38 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /arduino.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * A script to draw hashart images on an Inkplate (https://inkplate.io/) 3 | */ 4 | 5 | #include "Inkplate.h" 6 | #include 7 | #include 8 | 9 | Inkplate display(INKPLATE_1BIT); 10 | 11 | void setup() 12 | { 13 | display.begin(); 14 | display.clearDisplay(); 15 | display.clean(); 16 | 17 | display.setCursor(50, 290); 18 | display.setTextSize(3); 19 | display.print(F("Loading art")); 20 | display.display(); 21 | 22 | WiFi.begin("[your ssid here]", "[your password here]"); 23 | while (WiFi.status() != WL_CONNECTED) 24 | { 25 | delay(100); 26 | display.print("."); 27 | display.partialUpdate(); 28 | } 29 | 30 | display.println("Connected!"); 31 | display.partialUpdate(); 32 | } 33 | 34 | void loop() 35 | { 36 | // janky reconnect logic 37 | if (WiFi.status() != WL_CONNECTED) 38 | { 39 | WiFi.reconnect(); 40 | delay(5000); 41 | int cnt = 0; 42 | 43 | while (WiFi.status() != WL_CONNECTED) 44 | { 45 | delay(1000); 46 | cnt++; 47 | 48 | if (cnt == 10) 49 | { 50 | ESP.restart(); 51 | } 52 | } 53 | } 54 | 55 | display.clearDisplay(); 56 | // https://github.com/jdan/hashart#a-small-screenshot-service 57 | char *url = "https://FILLMEIN/random/800/600/random.png"; 58 | display.drawImage(url, 0, 0, true, 0); 59 | display.display(); 60 | 61 | // Sleep for 5 minutes 62 | esp_sleep_enable_timer_wakeup(1000L * 1000L * 60L * 5L); 63 | (void)esp_light_sleep_start(); 64 | } 65 | -------------------------------------------------------------------------------- /art/_base.js: -------------------------------------------------------------------------------- 1 | class Art { 2 | constructor(template) { 3 | if (!template) { 4 | throw "input template must be used"; 5 | } 6 | this.template = template; 7 | } 8 | 9 | description(buffer) { 10 | let obj = {}; 11 | let idx = 0; 12 | 13 | for (let [name, bytes] of Object.entries(this.template)) { 14 | const slice = buffer.slice(idx, idx + bytes); 15 | obj[name + "Buffer"] = slice; 16 | obj[name] = this.normalize(slice); 17 | 18 | idx += bytes; 19 | } 20 | 21 | return this.getDescription(obj); 22 | } 23 | 24 | getDescription() { 25 | return null; 26 | } 27 | 28 | templateEntries() { 29 | let usedBytes = Object.values(this.template).reduce((a, b) => a + b, 0); 30 | return Object.entries(this.template).concat( 31 | usedBytes < 32 ? [["unused", 32 - usedBytes]] : [] 32 | ); 33 | } 34 | 35 | explanation(buff) { 36 | let buffer = new Uint8Array(buff); 37 | let idx = 0; 38 | let segments = []; 39 | 40 | for (let [name, bytes] of this.templateEntries()) { 41 | let slice = buffer.slice(idx, idx + bytes); 42 | segments.push({ 43 | name, 44 | bytes: Array.prototype.map 45 | .call(slice, (x) => ("00" + x.toString(16)).slice(-2)) 46 | .join(""), 47 | normalized: this.normalize(slice), 48 | }); 49 | 50 | idx += bytes; 51 | } 52 | 53 | return segments; 54 | } 55 | 56 | normalize(buffer) { 57 | if (buffer.byteLength === 0) { 58 | return 0; 59 | } else { 60 | return buffer[0] / 0x100 + this.normalize(buffer.slice(1)) / 0x100; 61 | } 62 | } 63 | 64 | render(ctx, buffer, props) { 65 | let idx = 0; 66 | let obj = {}; 67 | 68 | for (let [name, bytes] of Object.entries(this.template)) { 69 | const slice = buffer.slice(idx, idx + bytes); 70 | obj[name + "Buffer"] = slice; 71 | obj[name] = this.normalize(slice); 72 | 73 | idx += bytes; 74 | } 75 | 76 | ctx.save(); 77 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 78 | ctx.fillStyle = "rgba(255, 255, 255, 1)"; 79 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 80 | this.draw(ctx, obj, props); 81 | ctx.restore(); 82 | } 83 | 84 | draw() { 85 | throw "Child class must implement draw()"; 86 | } 87 | } 88 | 89 | exports.Art = Art; 90 | -------------------------------------------------------------------------------- /art/automata.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | class Automata extends Art { 5 | constructor() { 6 | super({ 7 | rule: 1, 8 | seed: 4, 9 | size: 2, 10 | }); 11 | this.filename = "automata.js"; 12 | this.created = "26 Feb 2022"; 13 | } 14 | 15 | getDescription({ rule, seedBuffer }) { 16 | return ` 17 | Begin with an empty row of 0's and populate the center 18 | with a bitstring taken from seed. 19 | 20 | Build a table 21 | by converting rule to binary, and pairing each bit with a 22 | number starting from 7 and decreasing to 0. For our above seed, rule ${ 23 | rule * 256 24 | } will generate the following table: 25 | 26 | 27 | 28 | 29 | ${this.ruleToLookup(rule * 256) 30 | .map(([seq, _]) => ``) 31 | .join("\n")} 32 | 33 | 34 | 35 | ${this.ruleToLookup(rule * 256) 36 | .map(([_, val]) => ``) 37 | .join("\n")} 38 | 39 |
seq${seq}
val${val}
40 | 41 | For each bit in the row, generate a three-bit string using the bit to 42 | its left, the bit itself, and the bit to its right. Look up this value 43 | in the table above, and assign val as the new bit. 44 | 45 | Finally draw the resulting row on the next line of the canvas. 46 | `; 47 | } 48 | 49 | ruleToLookup(rule) { 50 | const bits = Math.floor(rule).toString(2).padStart(8, "0"); 51 | return ["111", "110", "101", "100", "011", "010", "001", "000"].map( 52 | (seq, idx) => [seq, bits[idx]] 53 | ); 54 | } 55 | 56 | nextRow(row, lookup) { 57 | const nextRow = row.slice(); 58 | for (let i = 0; i < row.length; i++) { 59 | const seq = [row[i - 1] || 0, row[i] || 0, row[i + 1] || 0].join(""); 60 | nextRow[i] = lookup[seq]; 61 | } 62 | return nextRow; 63 | } 64 | 65 | drawRow(ctx, row, bitSize, y) { 66 | const widthInBits = Math.floor(ctx.canvas.width / bitSize); 67 | const start = Math.floor(row.length / 2 - widthInBits / 2); 68 | const end = Math.floor(row.length / 2 + widthInBits / 2); 69 | 70 | for (let i = start; i < end; i++) { 71 | const x = (i - start) * bitSize; 72 | if (row[i] === "1") { 73 | ctx.fillStyle = `rgb(0, 0, 0)`; 74 | ctx.beginPath(); 75 | ctx.rect(x, y, bitSize, bitSize); 76 | ctx.fill(); 77 | } 78 | } 79 | } 80 | 81 | bufferToBinary(buffer) { 82 | let base10 = buffer.reduce((acc, d) => acc * 256 + d, 1); 83 | return base10 84 | .toString(2) 85 | .split("") 86 | .map((d) => parseInt(d)); 87 | } 88 | 89 | draw(ctx, { size, rule, seedBuffer }) { 90 | const w = ctx.canvas.width; 91 | const h = ctx.canvas.height; 92 | 93 | // 12px bits at 1200px looks decent 94 | const bitSize = _(project(size, 0, 1, 3, 12), w); 95 | 96 | // Initialize an array large enough to handle all possible growth 97 | let row = Array.from({ 98 | length: Math.floor((w + h + h) / bitSize), 99 | }).map((_) => 0); 100 | 101 | // Place `seed` on the tape 102 | const input = this.bufferToBinary(seedBuffer); 103 | for (let i = 0; i < input.length; i++) { 104 | const idx = Math.floor(row.length / 2 - input.length / 2) + i; 105 | row[idx] = input[i]; 106 | } 107 | 108 | const lookup = {}; 109 | this.ruleToLookup(rule * 256).forEach(([seq, val]) => { 110 | lookup[seq] = val; 111 | }); 112 | 113 | for (let i = 0; i < Math.floor(h / bitSize) + 1; i++) { 114 | row = this.nextRow(row, lookup); 115 | this.drawRow(ctx, row, bitSize, i * bitSize); 116 | } 117 | } 118 | } 119 | 120 | exports.Automata = Automata; 121 | -------------------------------------------------------------------------------- /art/boxes.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Boxes extends Art { 5 | constructor() { 6 | super({ 7 | box1: 6, 8 | box2: 6, 9 | box3: 6, 10 | box4: 6, 11 | }); 12 | 13 | this.filename = "boxes.js"; 14 | this.created = "29 Mar 2021"; 15 | } 16 | 17 | getDescription() { 18 | return ` 19 | From the template we gather dimensions for ${ 20 | Object.keys(this.template).length 21 | } boxes and place them 22 | next to each other on the ground. 23 | 24 | Each box is represented as six bytes, which is divided into three sections 25 | for width, depth, and height respectively. 26 | 27 | The boxes are rendered isometrically with a front-facing light source. 28 | `; 29 | } 30 | 31 | coordToIso(x, y, z) { 32 | return [x + 0.4 * z, y - 0.2 * z]; 33 | } 34 | 35 | drawBox(ctx, { x, y, w, d, h }) { 36 | const cols = { 37 | front: "rgb(240, 240, 240)", 38 | top: "rgb(180, 180, 180)", 39 | side: "rgb(120, 120, 120)", 40 | }; 41 | 42 | // front 43 | ctx.fillStyle = cols.front; 44 | ctx.beginPath(); 45 | ctx.moveTo(...this.coordToIso(x, y, -d)); 46 | ctx.lineTo(...this.coordToIso(x, y - h, -d)); 47 | ctx.lineTo(...this.coordToIso(x + w, y - h, -d)); 48 | ctx.lineTo(...this.coordToIso(x + w, y, -d)); 49 | ctx.lineTo(...this.coordToIso(x, y, -d)); 50 | ctx.fill(); 51 | ctx.stroke(); 52 | 53 | // top 54 | ctx.fillStyle = cols.top; 55 | ctx.beginPath(); 56 | ctx.moveTo(...this.coordToIso(x, y - h, -d)); 57 | ctx.lineTo(...this.coordToIso(x, y - h, 0)); 58 | ctx.lineTo(...this.coordToIso(x + w, y - h, 0)); 59 | ctx.lineTo(...this.coordToIso(x + w, y - h, -d)); 60 | ctx.lineTo(...this.coordToIso(x, y - h, -d)); 61 | ctx.fill(); 62 | ctx.stroke(); 63 | 64 | // side 65 | ctx.fillStyle = cols.side; 66 | ctx.beginPath(); 67 | ctx.moveTo(...this.coordToIso(x + w, y, -d)); 68 | ctx.lineTo(...this.coordToIso(x + w, y - h, -d)); 69 | ctx.lineTo(...this.coordToIso(x + w, y - h, 0)); 70 | ctx.lineTo(...this.coordToIso(x + w, y, 0)); 71 | ctx.lineTo(...this.coordToIso(x + w, y, -d)); 72 | ctx.fill(); 73 | ctx.stroke(); 74 | } 75 | 76 | draw(ctx, { box1Buffer, box2Buffer, box3Buffer, box4Buffer }) { 77 | const w = ctx.canvas.width; 78 | const h = ctx.canvas.height; 79 | const x = 0.25 * w; 80 | const y = 0.7 * h; 81 | 82 | const wMin = _(20, w); 83 | const dScale = _(400, w); 84 | const wScale = _(300, w); 85 | const hScale = _(600, h); 86 | 87 | const w1 = (256 * box1Buffer[0] + box1Buffer[1]) / 65536; 88 | const d1 = (256 * box1Buffer[2] + box1Buffer[3]) / 65536; 89 | const h1 = (256 * box1Buffer[4] + box1Buffer[5]) / 65536; 90 | 91 | const w2 = (256 * box2Buffer[0] + box2Buffer[1]) / 65536; 92 | const d2 = (256 * box2Buffer[2] + box2Buffer[3]) / 65536; 93 | const h2 = (256 * box2Buffer[4] + box2Buffer[5]) / 65536; 94 | 95 | const w3 = (256 * box3Buffer[0] + box3Buffer[1]) / 65536; 96 | const d3 = (256 * box3Buffer[2] + box3Buffer[3]) / 65536; 97 | const h3 = (256 * box3Buffer[4] + box3Buffer[5]) / 65536; 98 | 99 | const w4 = (256 * box4Buffer[0] + box4Buffer[1]) / 65536; 100 | const d4 = (256 * box4Buffer[2] + box4Buffer[3]) / 65536; 101 | const h4 = (256 * box4Buffer[4] + box4Buffer[5]) / 65536; 102 | 103 | this.drawBox(ctx, { 104 | x, 105 | y, 106 | d: d1 * dScale, 107 | w: w1 * wScale + wMin, 108 | h: h1 * hScale, 109 | }); 110 | 111 | this.drawBox(ctx, { 112 | x: x + w1 * wScale + wMin, 113 | y, 114 | d: d2 * dScale, 115 | w: w2 * wScale + wMin, 116 | h: h2 * hScale, 117 | }); 118 | 119 | this.drawBox(ctx, { 120 | x: x + w1 * wScale + wMin + w2 * wScale + wMin, 121 | y, 122 | d: d3 * dScale, 123 | w: w3 * wScale + wMin, 124 | h: h3 * hScale, 125 | }); 126 | 127 | this.drawBox(ctx, { 128 | x: x + w1 * wScale + wMin + w2 * wScale + wMin + w3 * wScale + wMin, 129 | y, 130 | d: d4 * dScale, 131 | w: w4 * wScale + wMin, 132 | h: h4 * hScale, 133 | }); 134 | } 135 | } 136 | 137 | exports.Boxes = Boxes; 138 | -------------------------------------------------------------------------------- /art/chess-pieces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was generated using 3 | * http://demo.qunee.com/svg2canvas/ 4 | * Using the SVG files found in 5 | * https://github.com/ornicar/lila/tree/master/public/piece/cburnett 6 | */ 7 | 8 | exports.chessPieces = { 9 | "bB.svg": { 10 | draw: function (ctx) { 11 | ctx.save(); 12 | ctx.strokeStyle = "rgba(0,0,0,0)"; 13 | ctx.miterLimit = 4; 14 | ctx.font = ""; 15 | ctx.font = " 10px sans-serif"; 16 | ctx.save(); 17 | ctx.fillStyle = "rgba(0,0,0,0)"; 18 | ctx.strokeStyle = "#000"; 19 | ctx.lineWidth = 1.5; 20 | ctx.lineCap = "round"; 21 | ctx.lineJoin = "round"; 22 | ctx.font = " 10px sans-serif"; 23 | ctx.save(); 24 | ctx.fillStyle = "#000"; 25 | ctx.strokeStyle = "#000"; 26 | ctx.lineCap = "butt"; 27 | ctx.font = " 10px sans-serif"; 28 | ctx.save(); 29 | ctx.fillStyle = "#000"; 30 | ctx.strokeStyle = "#000"; 31 | ctx.font = " 10px sans-serif"; 32 | ctx.beginPath(); 33 | ctx.moveTo(9, 36); 34 | ctx.bezierCurveTo(12.39, 35.03, 19.11, 36.43, 22.5, 34); 35 | ctx.bezierCurveTo(25.89, 36.43, 32.61, 35.03, 36, 36); 36 | ctx.bezierCurveTo(36, 36, 37.65, 36.54, 39, 38); 37 | ctx.bezierCurveTo(38.32, 38.97, 37.35, 38.99, 36, 38.5); 38 | ctx.bezierCurveTo(32.61, 37.53, 25.89, 38.96, 22.5, 37.5); 39 | ctx.bezierCurveTo(19.11, 38.96, 12.39, 37.53, 9, 38.5); 40 | ctx.bezierCurveTo(7.646, 38.99, 6.677, 38.97, 6, 38); 41 | ctx.bezierCurveTo(7.354, 36.06, 9, 36, 9, 36); 42 | ctx.closePath(); 43 | ctx.fill("evenodd"); 44 | ctx.stroke(); 45 | ctx.restore(); 46 | ctx.save(); 47 | ctx.fillStyle = "#000"; 48 | ctx.strokeStyle = "#000"; 49 | ctx.font = " 10px sans-serif"; 50 | ctx.beginPath(); 51 | ctx.moveTo(15, 32); 52 | ctx.bezierCurveTo(17.5, 34.5, 27.5, 34.5, 30, 32); 53 | ctx.bezierCurveTo(30.5, 30.5, 30, 30, 30, 30); 54 | ctx.bezierCurveTo(30, 27.5, 27.5, 26, 27.5, 26); 55 | ctx.bezierCurveTo(33, 24.5, 33.5, 14.5, 22.5, 10.5); 56 | ctx.bezierCurveTo(11.5, 14.5, 12, 24.5, 17.5, 26); 57 | ctx.bezierCurveTo(17.5, 26, 15, 27.5, 15, 30); 58 | ctx.bezierCurveTo(15, 30, 14.5, 30.5, 15, 32); 59 | ctx.closePath(); 60 | ctx.fill("evenodd"); 61 | ctx.stroke(); 62 | ctx.restore(); 63 | ctx.save(); 64 | ctx.fillStyle = "#000"; 65 | ctx.strokeStyle = "#000"; 66 | ctx.font = " 10px sans-serif"; 67 | ctx.beginPath(); 68 | ctx.moveTo(25, 8); 69 | ctx.translate(22.5, 8); 70 | ctx.rotate(0); 71 | ctx.arc(0, 0, 2.5, 0, 3.141592653589793, 0); 72 | ctx.rotate(0); 73 | ctx.translate(-22.5, -8); 74 | ctx.translate(22.5, 8); 75 | ctx.rotate(0); 76 | ctx.arc(0, 0, 2.5, 3.141592653589793, 6.283185307179586, 0); 77 | ctx.rotate(0); 78 | ctx.translate(-22.5, -8); 79 | ctx.fill("evenodd"); 80 | ctx.stroke(); 81 | ctx.restore(); 82 | ctx.restore(); 83 | ctx.save(); 84 | ctx.fillStyle = "rgba(0,0,0,0)"; 85 | ctx.strokeStyle = "#ececec"; 86 | ctx.lineJoin = "miter"; 87 | ctx.font = " 10px sans-serif"; 88 | ctx.beginPath(); 89 | ctx.moveTo(17.5, 26); 90 | ctx.lineTo(27.5, 26); 91 | ctx.moveTo(15, 30); 92 | ctx.lineTo(30, 30); 93 | ctx.moveTo(22.5, 15.5); 94 | ctx.lineTo(22.5, 20.5); 95 | ctx.moveTo(20, 18); 96 | ctx.lineTo(25, 18); 97 | ctx.fill("evenodd"); 98 | ctx.stroke(); 99 | ctx.restore(); 100 | ctx.restore(); 101 | ctx.restore(); 102 | }, 103 | }, 104 | "bK.svg": { 105 | draw: function (ctx) { 106 | ctx.save(); 107 | ctx.strokeStyle = "rgba(0,0,0,0)"; 108 | ctx.miterLimit = 4; 109 | ctx.font = ""; 110 | ctx.font = " 10px sans-serif"; 111 | ctx.save(); 112 | ctx.fillStyle = "rgba(0,0,0,0)"; 113 | ctx.strokeStyle = "#000"; 114 | ctx.lineWidth = 1.5; 115 | ctx.lineCap = "round"; 116 | ctx.lineJoin = "round"; 117 | ctx.font = " 10px sans-serif"; 118 | ctx.save(); 119 | ctx.fillStyle = "rgba(0,0,0,0)"; 120 | ctx.strokeStyle = "#000"; 121 | ctx.lineJoin = "miter"; 122 | ctx.font = " 10px sans-serif"; 123 | ctx.beginPath(); 124 | ctx.moveTo(22.5, 11.63); 125 | ctx.lineTo(22.5, 6); 126 | ctx.fill("evenodd"); 127 | ctx.stroke(); 128 | ctx.restore(); 129 | ctx.save(); 130 | ctx.fillStyle = "#000"; 131 | ctx.strokeStyle = "#000"; 132 | ctx.lineCap = "butt"; 133 | ctx.lineJoin = "miter"; 134 | ctx.font = " 10px sans-serif"; 135 | ctx.beginPath(); 136 | ctx.moveTo(22.5, 25); 137 | ctx.bezierCurveTo(22.5, 25, 27, 17.5, 25.5, 14.5); 138 | ctx.bezierCurveTo(25.5, 14.5, 24.5, 12, 22.5, 12); 139 | ctx.bezierCurveTo(20.5, 12, 19.5, 14.5, 19.5, 14.5); 140 | ctx.bezierCurveTo(18, 17.5, 22.5, 25, 22.5, 25); 141 | ctx.fill("evenodd"); 142 | ctx.stroke(); 143 | ctx.restore(); 144 | ctx.save(); 145 | ctx.fillStyle = "#000"; 146 | ctx.strokeStyle = "#000"; 147 | ctx.font = " 10px sans-serif"; 148 | ctx.beginPath(); 149 | ctx.moveTo(11.5, 37); 150 | ctx.bezierCurveTo(17, 40.5, 27, 40.5, 32.5, 37); 151 | ctx.lineTo(32.5, 30); 152 | ctx.bezierCurveTo(32.5, 30, 41.5, 25.5, 38.5, 19.5); 153 | ctx.bezierCurveTo(34.5, 13, 25, 16, 22.5, 23.5); 154 | ctx.lineTo(22.5, 27); 155 | ctx.lineTo(22.5, 23.5); 156 | ctx.bezierCurveTo(19, 16, 9.5, 13, 6.5, 19.5); 157 | ctx.bezierCurveTo(3.5, 25.5, 11.5, 29.5, 11.5, 29.5); 158 | ctx.lineTo(11.5, 37); 159 | ctx.closePath(); 160 | ctx.fill("evenodd"); 161 | ctx.stroke(); 162 | ctx.restore(); 163 | ctx.save(); 164 | ctx.fillStyle = "rgba(0,0,0,0)"; 165 | ctx.strokeStyle = "#000"; 166 | ctx.lineJoin = "miter"; 167 | ctx.font = " 10px sans-serif"; 168 | ctx.beginPath(); 169 | ctx.moveTo(20, 8); 170 | ctx.lineTo(25, 8); 171 | ctx.fill("evenodd"); 172 | ctx.stroke(); 173 | ctx.restore(); 174 | ctx.save(); 175 | ctx.fillStyle = "rgba(0,0,0,0)"; 176 | ctx.strokeStyle = "#ececec"; 177 | ctx.font = " 10px sans-serif"; 178 | ctx.beginPath(); 179 | ctx.moveTo(32, 29.5); 180 | ctx.bezierCurveTo(32, 29.5, 40.5, 25.5, 38.03, 19.85); 181 | ctx.bezierCurveTo(34.15, 14, 25, 18, 22.5, 24.5); 182 | ctx.lineTo(22.51, 26.6); 183 | ctx.lineTo(22.5, 24.5); 184 | ctx.bezierCurveTo(20, 18, 9.906, 14, 6.997, 19.85); 185 | ctx.bezierCurveTo(4.5, 25.5, 11.85, 28.85, 11.85, 28.85); 186 | ctx.fill("evenodd"); 187 | ctx.stroke(); 188 | ctx.restore(); 189 | ctx.save(); 190 | ctx.fillStyle = "rgba(0,0,0,0)"; 191 | ctx.strokeStyle = "#ececec"; 192 | ctx.font = " 10px sans-serif"; 193 | ctx.beginPath(); 194 | ctx.moveTo(11.5, 30); 195 | ctx.bezierCurveTo(17, 27, 27, 27, 32.5, 30); 196 | ctx.moveTo(11.5, 33.5); 197 | ctx.bezierCurveTo(17, 30.5, 27, 30.5, 32.5, 33.5); 198 | ctx.moveTo(11.5, 37); 199 | ctx.bezierCurveTo(17, 34, 27, 34, 32.5, 37); 200 | ctx.fill("evenodd"); 201 | ctx.stroke(); 202 | ctx.restore(); 203 | ctx.restore(); 204 | ctx.restore(); 205 | }, 206 | }, 207 | "bN.svg": { 208 | draw: function (ctx) { 209 | ctx.save(); 210 | ctx.strokeStyle = "rgba(0,0,0,0)"; 211 | ctx.miterLimit = 4; 212 | ctx.font = ""; 213 | ctx.font = " 10px sans-serif"; 214 | ctx.save(); 215 | ctx.fillStyle = "rgba(0,0,0,0)"; 216 | ctx.strokeStyle = "#000"; 217 | ctx.lineWidth = 1.5; 218 | ctx.lineCap = "round"; 219 | ctx.lineJoin = "round"; 220 | ctx.font = " 10px sans-serif"; 221 | ctx.save(); 222 | ctx.fillStyle = "#000"; 223 | ctx.strokeStyle = "#000"; 224 | ctx.font = " 10px sans-serif"; 225 | ctx.beginPath(); 226 | ctx.moveTo(22, 10); 227 | ctx.bezierCurveTo(32.5, 11, 38.5, 18, 38, 39); 228 | ctx.lineTo(15, 39); 229 | ctx.bezierCurveTo(15, 30, 25, 32.5, 23, 18); 230 | ctx.fill("evenodd"); 231 | ctx.stroke(); 232 | ctx.restore(); 233 | ctx.save(); 234 | ctx.fillStyle = "#000"; 235 | ctx.strokeStyle = "#000"; 236 | ctx.font = " 10px sans-serif"; 237 | ctx.beginPath(); 238 | ctx.moveTo(24, 18); 239 | ctx.bezierCurveTo(24.38, 20.91, 18.45, 25.37, 16, 27); 240 | ctx.bezierCurveTo(13, 29, 13.18, 31.34, 11, 31); 241 | ctx.bezierCurveTo(9.958, 30.06, 12.41, 27.96, 11, 28); 242 | ctx.bezierCurveTo(10, 28, 11.19, 29.23, 10, 30); 243 | ctx.bezierCurveTo(9, 30, 5.997, 31, 6, 26); 244 | ctx.bezierCurveTo(6, 24, 12, 14, 12, 14); 245 | ctx.bezierCurveTo(12, 14, 13.89, 12.1, 14, 10.5); 246 | ctx.bezierCurveTo(13.27, 9.506, 13.5, 8.5, 13.5, 7.5); 247 | ctx.bezierCurveTo(14.5, 6.5, 16.5, 10, 16.5, 10); 248 | ctx.lineTo(18.5, 10); 249 | ctx.bezierCurveTo(18.5, 10, 19.28, 8.008, 21, 7); 250 | ctx.bezierCurveTo(22, 7, 22, 10, 22, 10); 251 | ctx.fill("evenodd"); 252 | ctx.stroke(); 253 | ctx.restore(); 254 | ctx.save(); 255 | ctx.fillStyle = "#ececec"; 256 | ctx.strokeStyle = "#ececec"; 257 | ctx.font = " 10px sans-serif"; 258 | ctx.beginPath(); 259 | ctx.moveTo(9.5, 25.5); 260 | ctx.translate(9, 25.5); 261 | ctx.rotate(0); 262 | ctx.arc(0, 0, 0.5, 0, 3.141592653589793, 0); 263 | ctx.rotate(0); 264 | ctx.translate(-9, -25.5); 265 | ctx.translate(9, 25.5); 266 | ctx.rotate(0); 267 | ctx.arc(0, 0, 0.5, 3.141592653589793, 6.283185307179586, 0); 268 | ctx.rotate(0); 269 | ctx.translate(-9, -25.5); 270 | ctx.moveTo(14.933, 15.75); 271 | ctx.translate(14.495025042545517, 15.508616850991054); 272 | ctx.rotate(0.5235987755982988); 273 | ctx.scale(0.3333333333333333, 1); 274 | ctx.arc(0, 0, 1.5, -0.006629074978609724, -3.1349551104967315, 0); 275 | ctx.scale(3, 1); 276 | ctx.rotate(-0.5235987755982988); 277 | ctx.translate(-14.495025042545517, -15.508616850991054); 278 | ctx.translate(14.504974957454483, 15.491383149008946); 279 | ctx.rotate(0.5235987755982988); 280 | ctx.scale(0.3333333333333333, 1); 281 | ctx.arc(0, 0, 1.5, 3.1349635786111834, 0.0066375430930616375, 0); 282 | ctx.scale(3, 1); 283 | ctx.rotate(-0.5235987755982988); 284 | ctx.translate(-14.504974957454483, -15.491383149008946); 285 | ctx.closePath(); 286 | ctx.fill("evenodd"); 287 | ctx.stroke(); 288 | ctx.restore(); 289 | ctx.save(); 290 | ctx.fillStyle = "#ececec"; 291 | ctx.strokeStyle = "rgba(0,0,0,0)"; 292 | ctx.font = " 10px sans-serif"; 293 | ctx.beginPath(); 294 | ctx.moveTo(24.55, 10.4); 295 | ctx.lineTo(24.1, 11.85); 296 | ctx.lineTo(24.6, 12); 297 | ctx.bezierCurveTo(27.75, 13, 30.25, 14.49, 32.5, 18.75); 298 | ctx.bezierCurveTo(34.75, 23.009999999999998, 35.75, 29.06, 35.25, 39); 299 | ctx.lineTo(35.2, 39.5); 300 | ctx.lineTo(37.45, 39.5); 301 | ctx.lineTo(37.5, 39); 302 | ctx.bezierCurveTo(38, 28.939999999999998, 36.62, 22.15, 34.25, 17.66); 303 | ctx.bezierCurveTo(31.88, 13.17, 28.46, 11.02, 25.060000000000002, 10.5); 304 | ctx.lineTo(24.55, 10.4); 305 | ctx.closePath(); 306 | ctx.fill("evenodd"); 307 | ctx.stroke(); 308 | ctx.restore(); 309 | ctx.restore(); 310 | ctx.restore(); 311 | }, 312 | }, 313 | "bP.svg": { 314 | draw: function (ctx) { 315 | ctx.save(); 316 | ctx.strokeStyle = "rgba(0,0,0,0)"; 317 | ctx.miterLimit = 4; 318 | ctx.font = ""; 319 | ctx.font = " 10px sans-serif"; 320 | ctx.save(); 321 | ctx.strokeStyle = "#000"; 322 | ctx.lineWidth = 1.5; 323 | ctx.lineCap = "round"; 324 | ctx.font = " 10px sans-serif"; 325 | ctx.beginPath(); 326 | ctx.moveTo(22.5, 9); 327 | ctx.bezierCurveTo(20.29, 9, 18.5, 10.79, 18.5, 13); 328 | ctx.bezierCurveTo(18.5, 13.89, 18.79, 14.71, 19.28, 15.379999999999999); 329 | ctx.bezierCurveTo(17.33, 16.5, 16, 18.59, 16, 21); 330 | ctx.bezierCurveTo(16, 23.03, 16.94, 24.84, 18.41, 26.03); 331 | ctx.bezierCurveTo(15.41, 27.09, 11, 31.580000000000002, 11, 39.5); 332 | ctx.lineTo(34, 39.5); 333 | ctx.bezierCurveTo(34, 31.58, 29.59, 27.09, 26.59, 26.03); 334 | ctx.bezierCurveTo(28.06, 24.84, 29, 23.03, 29, 21); 335 | ctx.bezierCurveTo(29, 18.59, 27.67, 16.5, 25.72, 15.379999999999999); 336 | ctx.bezierCurveTo( 337 | 26.209999999999997, 338 | 14.709999999999999, 339 | 26.5, 340 | 13.889999999999999, 341 | 26.5, 342 | 13 343 | ); 344 | ctx.bezierCurveTo(26.5, 10.79, 24.71, 9, 22.5, 9); 345 | ctx.closePath(); 346 | ctx.fill(); 347 | ctx.stroke(); 348 | ctx.restore(); 349 | ctx.restore(); 350 | }, 351 | }, 352 | "bQ.svg": { 353 | draw: function (ctx) { 354 | ctx.save(); 355 | ctx.strokeStyle = "rgba(0,0,0,0)"; 356 | ctx.miterLimit = 4; 357 | ctx.font = ""; 358 | ctx.font = " 10px sans-serif"; 359 | ctx.save(); 360 | ctx.strokeStyle = "#000"; 361 | ctx.lineWidth = 1.5; 362 | ctx.lineCap = "round"; 363 | ctx.lineJoin = "round"; 364 | ctx.font = " 10px sans-serif"; 365 | ctx.save(); 366 | ctx.strokeStyle = "rgba(0,0,0,0)"; 367 | ctx.font = " 10px sans-serif"; 368 | ctx.save(); 369 | ctx.strokeStyle = "rgba(0,0,0,0)"; 370 | ctx.font = " 10px sans-serif"; 371 | ctx.beginPath(); 372 | ctx.arc(6, 12, 2.75, 0, 6.283185307179586, false); 373 | ctx.closePath(); 374 | ctx.fill("evenodd"); 375 | ctx.stroke(); 376 | ctx.restore(); 377 | ctx.save(); 378 | ctx.strokeStyle = "rgba(0,0,0,0)"; 379 | ctx.font = " 10px sans-serif"; 380 | ctx.beginPath(); 381 | ctx.arc(14, 9, 2.75, 0, 6.283185307179586, false); 382 | ctx.closePath(); 383 | ctx.fill("evenodd"); 384 | ctx.stroke(); 385 | ctx.restore(); 386 | ctx.save(); 387 | ctx.strokeStyle = "rgba(0,0,0,0)"; 388 | ctx.font = " 10px sans-serif"; 389 | ctx.beginPath(); 390 | ctx.arc(22.5, 8, 2.75, 0, 6.283185307179586, false); 391 | ctx.closePath(); 392 | ctx.fill("evenodd"); 393 | ctx.stroke(); 394 | ctx.restore(); 395 | ctx.save(); 396 | ctx.strokeStyle = "rgba(0,0,0,0)"; 397 | ctx.font = " 10px sans-serif"; 398 | ctx.beginPath(); 399 | ctx.arc(31, 9, 2.75, 0, 6.283185307179586, false); 400 | ctx.closePath(); 401 | ctx.fill("evenodd"); 402 | ctx.stroke(); 403 | ctx.restore(); 404 | ctx.save(); 405 | ctx.strokeStyle = "rgba(0,0,0,0)"; 406 | ctx.font = " 10px sans-serif"; 407 | ctx.beginPath(); 408 | ctx.arc(39, 12, 2.75, 0, 6.283185307179586, false); 409 | ctx.closePath(); 410 | ctx.fill("evenodd"); 411 | ctx.stroke(); 412 | ctx.restore(); 413 | ctx.restore(); 414 | ctx.save(); 415 | ctx.strokeStyle = "#000"; 416 | ctx.lineCap = "butt"; 417 | ctx.font = " 10px sans-serif"; 418 | ctx.beginPath(); 419 | ctx.moveTo(9, 26); 420 | ctx.bezierCurveTo(17.5, 24.5, 30, 24.5, 36, 26); 421 | ctx.lineTo(38.5, 13.5); 422 | ctx.lineTo(31, 25); 423 | ctx.lineTo(30.7, 10.9); 424 | ctx.lineTo(25.5, 24.5); 425 | ctx.lineTo(22.5, 10); 426 | ctx.lineTo(19.5, 24.5); 427 | ctx.lineTo(14.3, 10.9); 428 | ctx.lineTo(14, 25); 429 | ctx.lineTo(6.5, 13.5); 430 | ctx.lineTo(9, 26); 431 | ctx.closePath(); 432 | ctx.fill("evenodd"); 433 | ctx.stroke(); 434 | ctx.restore(); 435 | ctx.save(); 436 | ctx.strokeStyle = "#000"; 437 | ctx.lineCap = "butt"; 438 | ctx.font = " 10px sans-serif"; 439 | ctx.beginPath(); 440 | ctx.moveTo(9, 26); 441 | ctx.bezierCurveTo(9, 28, 10.5, 28, 11.5, 30); 442 | ctx.bezierCurveTo(12.5, 31.5, 12.5, 31, 12, 33.5); 443 | ctx.bezierCurveTo(10.5, 34.5, 10.5, 36, 10.5, 36); 444 | ctx.bezierCurveTo(9, 37.5, 11, 38.5, 11, 38.5); 445 | ctx.bezierCurveTo(17.5, 39.5, 27.5, 39.5, 34, 38.5); 446 | ctx.bezierCurveTo(34, 38.5, 35.5, 37.5, 34, 36); 447 | ctx.bezierCurveTo(34, 36, 34.5, 34.5, 33, 33.5); 448 | ctx.bezierCurveTo(32.5, 31, 32.5, 31.5, 33.5, 30); 449 | ctx.bezierCurveTo(34.5, 28, 36, 28, 36, 26); 450 | ctx.bezierCurveTo(27.5, 24.5, 17.5, 24.5, 9, 26); 451 | ctx.closePath(); 452 | ctx.fill("evenodd"); 453 | ctx.stroke(); 454 | ctx.restore(); 455 | ctx.save(); 456 | ctx.fillStyle = "rgba(0,0,0,0)"; 457 | ctx.strokeStyle = "#000"; 458 | ctx.lineCap = "butt"; 459 | ctx.font = " 10px sans-serif"; 460 | ctx.beginPath(); 461 | ctx.moveTo(11, 38.5); 462 | ctx.translate(22.5, 5.443230647868809); 463 | ctx.rotate(0.017453292519943295); 464 | ctx.arc(0, 0, 35, 1.888133661192342, 1.2185524073575649, 1); 465 | ctx.rotate(-0.017453292519943295); 466 | ctx.translate(-22.5, -5.443230647868809); 467 | ctx.fill("evenodd"); 468 | ctx.stroke(); 469 | ctx.restore(); 470 | ctx.save(); 471 | ctx.fillStyle = "rgba(0,0,0,0)"; 472 | ctx.strokeStyle = "#ececec"; 473 | ctx.font = " 10px sans-serif"; 474 | ctx.beginPath(); 475 | ctx.moveTo(11, 29); 476 | ctx.translate(22.5, 62.05676935213119); 477 | ctx.rotate(0.017453292519943295); 478 | ctx.arc(0, 0, 35, -1.9230402462322285, -1.2534589923974515, 0); 479 | ctx.rotate(-0.017453292519943295); 480 | ctx.translate(-22.5, -62.05676935213119); 481 | ctx.moveTo(12.5, 31.5); 482 | ctx.lineTo(32.5, 31.5); 483 | ctx.moveTo(11.5, 34.5); 484 | ctx.translate(22.5, 1.2735045483277005); 485 | ctx.rotate(0.017453292519943295); 486 | ctx.arc(0, 0, 35, 1.8730471829377007, 1.233638885612206, 1); 487 | ctx.rotate(-0.017453292519943295); 488 | ctx.translate(-22.5, -1.2735045483277005); 489 | ctx.moveTo(10.5, 37.5); 490 | ctx.translate(22.5, 4.621435554452809); 491 | ctx.rotate(0.017453292519943295); 492 | ctx.arc(0, 0, 35, 1.9032997455180158, 1.203386323031891, 1); 493 | ctx.rotate(-0.017453292519943295); 494 | ctx.translate(-22.5, -4.621435554452809); 495 | ctx.fill("evenodd"); 496 | ctx.stroke(); 497 | ctx.restore(); 498 | ctx.restore(); 499 | ctx.restore(); 500 | }, 501 | }, 502 | "bR.svg": { 503 | draw: function (ctx) { 504 | ctx.save(); 505 | ctx.strokeStyle = "rgba(0,0,0,0)"; 506 | ctx.miterLimit = 4; 507 | ctx.font = ""; 508 | ctx.font = " 10px sans-serif"; 509 | ctx.save(); 510 | ctx.strokeStyle = "#000"; 511 | ctx.lineWidth = 1.5; 512 | ctx.lineCap = "round"; 513 | ctx.lineJoin = "round"; 514 | ctx.font = " 10px sans-serif"; 515 | ctx.save(); 516 | ctx.strokeStyle = "#000"; 517 | ctx.lineCap = "butt"; 518 | ctx.font = " 10px sans-serif"; 519 | ctx.beginPath(); 520 | ctx.moveTo(9, 39); 521 | ctx.lineTo(36, 39); 522 | ctx.lineTo(36, 36); 523 | ctx.lineTo(9, 36); 524 | ctx.lineTo(9, 39); 525 | ctx.closePath(); 526 | ctx.moveTo(12.5, 32); 527 | ctx.lineTo(14, 29.5); 528 | ctx.lineTo(31, 29.5); 529 | ctx.lineTo(32.5, 32); 530 | ctx.lineTo(12.5, 32); 531 | ctx.closePath(); 532 | ctx.moveTo(12, 36); 533 | ctx.lineTo(12, 32); 534 | ctx.lineTo(33, 32); 535 | ctx.lineTo(33, 36); 536 | ctx.lineTo(12, 36); 537 | ctx.closePath(); 538 | ctx.fill("evenodd"); 539 | ctx.stroke(); 540 | ctx.restore(); 541 | ctx.save(); 542 | ctx.strokeStyle = "#000"; 543 | ctx.lineCap = "butt"; 544 | ctx.lineJoin = "miter"; 545 | ctx.font = " 10px sans-serif"; 546 | ctx.beginPath(); 547 | ctx.moveTo(14, 29.5); 548 | ctx.lineTo(14, 16.5); 549 | ctx.lineTo(31, 16.5); 550 | ctx.lineTo(31, 29.5); 551 | ctx.lineTo(14, 29.5); 552 | ctx.closePath(); 553 | ctx.fill("evenodd"); 554 | ctx.stroke(); 555 | ctx.restore(); 556 | ctx.save(); 557 | ctx.strokeStyle = "#000"; 558 | ctx.lineCap = "butt"; 559 | ctx.font = " 10px sans-serif"; 560 | ctx.beginPath(); 561 | ctx.moveTo(14, 16.5); 562 | ctx.lineTo(11, 14); 563 | ctx.lineTo(34, 14); 564 | ctx.lineTo(31, 16.5); 565 | ctx.lineTo(14, 16.5); 566 | ctx.closePath(); 567 | ctx.moveTo(11, 14); 568 | ctx.lineTo(11, 9); 569 | ctx.lineTo(15, 9); 570 | ctx.lineTo(15, 11); 571 | ctx.lineTo(20, 11); 572 | ctx.lineTo(20, 9); 573 | ctx.lineTo(25, 9); 574 | ctx.lineTo(25, 11); 575 | ctx.lineTo(30, 11); 576 | ctx.lineTo(30, 9); 577 | ctx.lineTo(34, 9); 578 | ctx.lineTo(34, 14); 579 | ctx.lineTo(11, 14); 580 | ctx.closePath(); 581 | ctx.fill("evenodd"); 582 | ctx.stroke(); 583 | ctx.restore(); 584 | ctx.save(); 585 | ctx.fillStyle = "rgba(0,0,0,0)"; 586 | ctx.strokeStyle = "#ececec"; 587 | ctx.lineWidth = 1; 588 | ctx.lineJoin = "miter"; 589 | ctx.font = " 10px sans-serif"; 590 | ctx.beginPath(); 591 | ctx.moveTo(12, 35.5); 592 | ctx.lineTo(33, 35.5); 593 | ctx.moveTo(13, 31.5); 594 | ctx.lineTo(32, 31.5); 595 | ctx.moveTo(14, 29.5); 596 | ctx.lineTo(31, 29.5); 597 | ctx.moveTo(14, 16.5); 598 | ctx.lineTo(31, 16.5); 599 | ctx.moveTo(11, 14); 600 | ctx.lineTo(34, 14); 601 | ctx.fill("evenodd"); 602 | ctx.stroke(); 603 | ctx.restore(); 604 | ctx.restore(); 605 | ctx.restore(); 606 | }, 607 | }, 608 | "wB.svg": { 609 | draw: function (ctx) { 610 | ctx.save(); 611 | ctx.strokeStyle = "rgba(0,0,0,0)"; 612 | ctx.miterLimit = 4; 613 | ctx.font = ""; 614 | ctx.font = " 10px sans-serif"; 615 | ctx.save(); 616 | ctx.fillStyle = "rgba(0,0,0,0)"; 617 | ctx.strokeStyle = "#000"; 618 | ctx.lineWidth = 1.5; 619 | ctx.lineCap = "round"; 620 | ctx.lineJoin = "round"; 621 | ctx.font = " 10px sans-serif"; 622 | ctx.save(); 623 | ctx.fillStyle = "#fff"; 624 | ctx.strokeStyle = "#000"; 625 | ctx.lineCap = "butt"; 626 | ctx.font = " 10px sans-serif"; 627 | ctx.save(); 628 | ctx.fillStyle = "#fff"; 629 | ctx.strokeStyle = "#000"; 630 | ctx.font = " 10px sans-serif"; 631 | ctx.beginPath(); 632 | ctx.moveTo(9, 36); 633 | ctx.bezierCurveTo(12.39, 35.03, 19.11, 36.43, 22.5, 34); 634 | ctx.bezierCurveTo(25.89, 36.43, 32.61, 35.03, 36, 36); 635 | ctx.bezierCurveTo(36, 36, 37.65, 36.54, 39, 38); 636 | ctx.bezierCurveTo(38.32, 38.97, 37.35, 38.99, 36, 38.5); 637 | ctx.bezierCurveTo(32.61, 37.53, 25.89, 38.96, 22.5, 37.5); 638 | ctx.bezierCurveTo(19.11, 38.96, 12.39, 37.53, 9, 38.5); 639 | ctx.bezierCurveTo(7.646, 38.99, 6.677, 38.97, 6, 38); 640 | ctx.bezierCurveTo(7.354, 36.06, 9, 36, 9, 36); 641 | ctx.closePath(); 642 | ctx.fill("evenodd"); 643 | ctx.stroke(); 644 | ctx.restore(); 645 | ctx.save(); 646 | ctx.fillStyle = "#fff"; 647 | ctx.strokeStyle = "#000"; 648 | ctx.font = " 10px sans-serif"; 649 | ctx.beginPath(); 650 | ctx.moveTo(15, 32); 651 | ctx.bezierCurveTo(17.5, 34.5, 27.5, 34.5, 30, 32); 652 | ctx.bezierCurveTo(30.5, 30.5, 30, 30, 30, 30); 653 | ctx.bezierCurveTo(30, 27.5, 27.5, 26, 27.5, 26); 654 | ctx.bezierCurveTo(33, 24.5, 33.5, 14.5, 22.5, 10.5); 655 | ctx.bezierCurveTo(11.5, 14.5, 12, 24.5, 17.5, 26); 656 | ctx.bezierCurveTo(17.5, 26, 15, 27.5, 15, 30); 657 | ctx.bezierCurveTo(15, 30, 14.5, 30.5, 15, 32); 658 | ctx.closePath(); 659 | ctx.fill("evenodd"); 660 | ctx.stroke(); 661 | ctx.restore(); 662 | ctx.save(); 663 | ctx.fillStyle = "#fff"; 664 | ctx.strokeStyle = "#000"; 665 | ctx.font = " 10px sans-serif"; 666 | ctx.beginPath(); 667 | ctx.moveTo(25, 8); 668 | ctx.translate(22.5, 8); 669 | ctx.rotate(0); 670 | ctx.arc(0, 0, 2.5, 0, 3.141592653589793, 0); 671 | ctx.rotate(0); 672 | ctx.translate(-22.5, -8); 673 | ctx.translate(22.5, 8); 674 | ctx.rotate(0); 675 | ctx.arc(0, 0, 2.5, 3.141592653589793, 6.283185307179586, 0); 676 | ctx.rotate(0); 677 | ctx.translate(-22.5, -8); 678 | ctx.fill("evenodd"); 679 | ctx.stroke(); 680 | ctx.restore(); 681 | ctx.restore(); 682 | ctx.save(); 683 | ctx.fillStyle = "rgba(0,0,0,0)"; 684 | ctx.strokeStyle = "#000"; 685 | ctx.lineJoin = "miter"; 686 | ctx.font = " 10px sans-serif"; 687 | ctx.beginPath(); 688 | ctx.moveTo(17.5, 26); 689 | ctx.lineTo(27.5, 26); 690 | ctx.moveTo(15, 30); 691 | ctx.lineTo(30, 30); 692 | ctx.moveTo(22.5, 15.5); 693 | ctx.lineTo(22.5, 20.5); 694 | ctx.moveTo(20, 18); 695 | ctx.lineTo(25, 18); 696 | ctx.fill("evenodd"); 697 | ctx.stroke(); 698 | ctx.restore(); 699 | ctx.restore(); 700 | ctx.restore(); 701 | }, 702 | }, 703 | "wK.svg": { 704 | draw: function (ctx) { 705 | ctx.save(); 706 | ctx.strokeStyle = "rgba(0,0,0,0)"; 707 | ctx.miterLimit = 4; 708 | ctx.font = ""; 709 | ctx.font = " 10px sans-serif"; 710 | ctx.save(); 711 | ctx.fillStyle = "rgba(0,0,0,0)"; 712 | ctx.strokeStyle = "#000"; 713 | ctx.lineWidth = 1.5; 714 | ctx.lineCap = "round"; 715 | ctx.lineJoin = "round"; 716 | ctx.font = " 10px sans-serif"; 717 | ctx.save(); 718 | ctx.fillStyle = "rgba(0,0,0,0)"; 719 | ctx.strokeStyle = "#000"; 720 | ctx.lineJoin = "miter"; 721 | ctx.font = " 10px sans-serif"; 722 | ctx.beginPath(); 723 | ctx.moveTo(22.5, 11.63); 724 | ctx.lineTo(22.5, 6); 725 | ctx.moveTo(20, 8); 726 | ctx.lineTo(25, 8); 727 | ctx.fill("evenodd"); 728 | ctx.stroke(); 729 | ctx.restore(); 730 | ctx.save(); 731 | ctx.fillStyle = "#fff"; 732 | ctx.strokeStyle = "#000"; 733 | ctx.lineCap = "butt"; 734 | ctx.lineJoin = "miter"; 735 | ctx.font = " 10px sans-serif"; 736 | ctx.beginPath(); 737 | ctx.moveTo(22.5, 25); 738 | ctx.bezierCurveTo(22.5, 25, 27, 17.5, 25.5, 14.5); 739 | ctx.bezierCurveTo(25.5, 14.5, 24.5, 12, 22.5, 12); 740 | ctx.bezierCurveTo(20.5, 12, 19.5, 14.5, 19.5, 14.5); 741 | ctx.bezierCurveTo(18, 17.5, 22.5, 25, 22.5, 25); 742 | ctx.fill("evenodd"); 743 | ctx.stroke(); 744 | ctx.restore(); 745 | ctx.save(); 746 | ctx.fillStyle = "#fff"; 747 | ctx.strokeStyle = "#000"; 748 | ctx.font = " 10px sans-serif"; 749 | ctx.beginPath(); 750 | ctx.moveTo(11.5, 37); 751 | ctx.bezierCurveTo(17, 40.5, 27, 40.5, 32.5, 37); 752 | ctx.lineTo(32.5, 30); 753 | ctx.bezierCurveTo(32.5, 30, 41.5, 25.5, 38.5, 19.5); 754 | ctx.bezierCurveTo(34.5, 13, 25, 16, 22.5, 23.5); 755 | ctx.lineTo(22.5, 27); 756 | ctx.lineTo(22.5, 23.5); 757 | ctx.bezierCurveTo(19, 16, 9.5, 13, 6.5, 19.5); 758 | ctx.bezierCurveTo(3.5, 25.5, 11.5, 29.5, 11.5, 29.5); 759 | ctx.lineTo(11.5, 37); 760 | ctx.closePath(); 761 | ctx.fill("evenodd"); 762 | ctx.stroke(); 763 | ctx.restore(); 764 | ctx.save(); 765 | ctx.fillStyle = "rgba(0,0,0,0)"; 766 | ctx.strokeStyle = "#000"; 767 | ctx.font = " 10px sans-serif"; 768 | ctx.beginPath(); 769 | ctx.moveTo(11.5, 30); 770 | ctx.bezierCurveTo(17, 27, 27, 27, 32.5, 30); 771 | ctx.moveTo(11.5, 33.5); 772 | ctx.bezierCurveTo(17, 30.5, 27, 30.5, 32.5, 33.5); 773 | ctx.moveTo(11.5, 37); 774 | ctx.bezierCurveTo(17, 34, 27, 34, 32.5, 37); 775 | ctx.fill("evenodd"); 776 | ctx.stroke(); 777 | ctx.restore(); 778 | ctx.restore(); 779 | ctx.restore(); 780 | }, 781 | }, 782 | "wN.svg": { 783 | draw: function (ctx) { 784 | ctx.save(); 785 | ctx.strokeStyle = "rgba(0,0,0,0)"; 786 | ctx.miterLimit = 4; 787 | ctx.font = ""; 788 | ctx.font = " 10px sans-serif"; 789 | ctx.save(); 790 | ctx.fillStyle = "rgba(0,0,0,0)"; 791 | ctx.strokeStyle = "#000"; 792 | ctx.lineWidth = 1.5; 793 | ctx.lineCap = "round"; 794 | ctx.lineJoin = "round"; 795 | ctx.font = " 10px sans-serif"; 796 | ctx.save(); 797 | ctx.fillStyle = "#fff"; 798 | ctx.strokeStyle = "#000"; 799 | ctx.font = " 10px sans-serif"; 800 | ctx.beginPath(); 801 | ctx.moveTo(22, 10); 802 | ctx.bezierCurveTo(32.5, 11, 38.5, 18, 38, 39); 803 | ctx.lineTo(15, 39); 804 | ctx.bezierCurveTo(15, 30, 25, 32.5, 23, 18); 805 | ctx.fill("evenodd"); 806 | ctx.stroke(); 807 | ctx.restore(); 808 | ctx.save(); 809 | ctx.fillStyle = "#fff"; 810 | ctx.strokeStyle = "#000"; 811 | ctx.font = " 10px sans-serif"; 812 | ctx.beginPath(); 813 | ctx.moveTo(24, 18); 814 | ctx.bezierCurveTo(24.38, 20.91, 18.45, 25.37, 16, 27); 815 | ctx.bezierCurveTo(13, 29, 13.18, 31.34, 11, 31); 816 | ctx.bezierCurveTo(9.958, 30.06, 12.41, 27.96, 11, 28); 817 | ctx.bezierCurveTo(10, 28, 11.19, 29.23, 10, 30); 818 | ctx.bezierCurveTo(9, 30, 5.997, 31, 6, 26); 819 | ctx.bezierCurveTo(6, 24, 12, 14, 12, 14); 820 | ctx.bezierCurveTo(12, 14, 13.89, 12.1, 14, 10.5); 821 | ctx.bezierCurveTo(13.27, 9.506, 13.5, 8.5, 13.5, 7.5); 822 | ctx.bezierCurveTo(14.5, 6.5, 16.5, 10, 16.5, 10); 823 | ctx.lineTo(18.5, 10); 824 | ctx.bezierCurveTo(18.5, 10, 19.28, 8.008, 21, 7); 825 | ctx.bezierCurveTo(22, 7, 22, 10, 22, 10); 826 | ctx.fill("evenodd"); 827 | ctx.stroke(); 828 | ctx.restore(); 829 | ctx.save(); 830 | ctx.fillStyle = "#000"; 831 | ctx.strokeStyle = "#000"; 832 | ctx.font = " 10px sans-serif"; 833 | ctx.beginPath(); 834 | ctx.moveTo(9.5, 25.5); 835 | ctx.translate(9, 25.5); 836 | ctx.rotate(0); 837 | ctx.arc(0, 0, 0.5, 0, 3.141592653589793, 0); 838 | ctx.rotate(0); 839 | ctx.translate(-9, -25.5); 840 | ctx.translate(9, 25.5); 841 | ctx.rotate(0); 842 | ctx.arc(0, 0, 0.5, 3.141592653589793, 6.283185307179586, 0); 843 | ctx.rotate(0); 844 | ctx.translate(-9, -25.5); 845 | ctx.moveTo(14.933, 15.75); 846 | ctx.translate(14.495025042545517, 15.508616850991054); 847 | ctx.rotate(0.5235987755982988); 848 | ctx.scale(0.3333333333333333, 1); 849 | ctx.arc(0, 0, 1.5, -0.006629074978609724, -3.1349551104967315, 0); 850 | ctx.scale(3, 1); 851 | ctx.rotate(-0.5235987755982988); 852 | ctx.translate(-14.495025042545517, -15.508616850991054); 853 | ctx.translate(14.504974957454483, 15.491383149008946); 854 | ctx.rotate(0.5235987755982988); 855 | ctx.scale(0.3333333333333333, 1); 856 | ctx.arc(0, 0, 1.5, 3.1349635786111834, 0.0066375430930616375, 0); 857 | ctx.scale(3, 1); 858 | ctx.rotate(-0.5235987755982988); 859 | ctx.translate(-14.504974957454483, -15.491383149008946); 860 | ctx.closePath(); 861 | ctx.fill("evenodd"); 862 | ctx.stroke(); 863 | ctx.restore(); 864 | ctx.restore(); 865 | ctx.restore(); 866 | }, 867 | }, 868 | "wP.svg": { 869 | draw: function (ctx) { 870 | ctx.save(); 871 | ctx.strokeStyle = "rgba(0,0,0,0)"; 872 | ctx.miterLimit = 4; 873 | ctx.font = ""; 874 | ctx.font = " 10px sans-serif"; 875 | ctx.save(); 876 | ctx.fillStyle = "#fff"; 877 | ctx.strokeStyle = "#000"; 878 | ctx.lineWidth = 1.5; 879 | ctx.lineCap = "round"; 880 | ctx.font = " 10px sans-serif"; 881 | ctx.beginPath(); 882 | ctx.moveTo(22.5, 9); 883 | ctx.bezierCurveTo(20.29, 9, 18.5, 10.79, 18.5, 13); 884 | ctx.bezierCurveTo(18.5, 13.89, 18.79, 14.71, 19.28, 15.379999999999999); 885 | ctx.bezierCurveTo(17.33, 16.5, 16, 18.59, 16, 21); 886 | ctx.bezierCurveTo(16, 23.03, 16.94, 24.84, 18.41, 26.03); 887 | ctx.bezierCurveTo(15.41, 27.09, 11, 31.580000000000002, 11, 39.5); 888 | ctx.lineTo(34, 39.5); 889 | ctx.bezierCurveTo(34, 31.58, 29.59, 27.09, 26.59, 26.03); 890 | ctx.bezierCurveTo(28.06, 24.84, 29, 23.03, 29, 21); 891 | ctx.bezierCurveTo(29, 18.59, 27.67, 16.5, 25.72, 15.379999999999999); 892 | ctx.bezierCurveTo( 893 | 26.209999999999997, 894 | 14.709999999999999, 895 | 26.5, 896 | 13.889999999999999, 897 | 26.5, 898 | 13 899 | ); 900 | ctx.bezierCurveTo(26.5, 10.79, 24.71, 9, 22.5, 9); 901 | ctx.closePath(); 902 | ctx.fill(); 903 | ctx.stroke(); 904 | ctx.restore(); 905 | ctx.restore(); 906 | }, 907 | }, 908 | "wQ.svg": { 909 | draw: function (ctx) { 910 | ctx.save(); 911 | ctx.strokeStyle = "rgba(0,0,0,0)"; 912 | ctx.miterLimit = 4; 913 | ctx.font = ""; 914 | ctx.font = " 10px sans-serif"; 915 | ctx.save(); 916 | ctx.fillStyle = "#fff"; 917 | ctx.strokeStyle = "#000"; 918 | ctx.lineWidth = 1.5; 919 | ctx.lineCap = "round"; 920 | ctx.lineJoin = "round"; 921 | ctx.font = " 10px sans-serif"; 922 | ctx.save(); 923 | ctx.fillStyle = "#fff"; 924 | ctx.strokeStyle = "#000"; 925 | ctx.font = " 10px sans-serif"; 926 | ctx.beginPath(); 927 | ctx.moveTo(8, 12); 928 | ctx.translate(6, 12); 929 | ctx.rotate(0); 930 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0); 931 | ctx.rotate(0); 932 | ctx.translate(-6, -12); 933 | ctx.translate(6, 12); 934 | ctx.rotate(0); 935 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0); 936 | ctx.rotate(0); 937 | ctx.translate(-6, -12); 938 | ctx.moveTo(24.5, 7.5); 939 | ctx.translate(22.5, 7.5); 940 | ctx.rotate(0); 941 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0); 942 | ctx.rotate(0); 943 | ctx.translate(-22.5, -7.5); 944 | ctx.translate(22.5, 7.5); 945 | ctx.rotate(0); 946 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0); 947 | ctx.rotate(0); 948 | ctx.translate(-22.5, -7.5); 949 | ctx.closePath(); 950 | ctx.moveTo(41, 12); 951 | ctx.translate(39, 12); 952 | ctx.rotate(0); 953 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0); 954 | ctx.rotate(0); 955 | ctx.translate(-39, -12); 956 | ctx.translate(39, 12); 957 | ctx.rotate(0); 958 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0); 959 | ctx.rotate(0); 960 | ctx.translate(-39, -12); 961 | ctx.closePath(); 962 | ctx.moveTo(16, 8.5); 963 | ctx.translate(14, 8.5); 964 | ctx.rotate(0); 965 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0); 966 | ctx.rotate(0); 967 | ctx.translate(-14, -8.5); 968 | ctx.translate(14, 8.5); 969 | ctx.rotate(0); 970 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0); 971 | ctx.rotate(0); 972 | ctx.translate(-14, -8.5); 973 | ctx.closePath(); 974 | ctx.moveTo(33, 9); 975 | ctx.translate(31, 9); 976 | ctx.rotate(0); 977 | ctx.arc(0, 0, 2, 0, 3.141592653589793, 0); 978 | ctx.rotate(0); 979 | ctx.translate(-31, -9); 980 | ctx.translate(31, 9); 981 | ctx.rotate(0); 982 | ctx.arc(0, 0, 2, 3.141592653589793, 6.283185307179586, 0); 983 | ctx.rotate(0); 984 | ctx.translate(-31, -9); 985 | ctx.closePath(); 986 | ctx.fill("evenodd"); 987 | ctx.stroke(); 988 | ctx.restore(); 989 | ctx.save(); 990 | ctx.fillStyle = "#fff"; 991 | ctx.strokeStyle = "#000"; 992 | ctx.lineCap = "butt"; 993 | ctx.font = " 10px sans-serif"; 994 | ctx.beginPath(); 995 | ctx.moveTo(9, 26); 996 | ctx.bezierCurveTo(17.5, 24.5, 30, 24.5, 36, 26); 997 | ctx.lineTo(38, 14); 998 | ctx.lineTo(31, 25); 999 | ctx.lineTo(31, 11); 1000 | ctx.lineTo(25.5, 24.5); 1001 | ctx.lineTo(22.5, 9.5); 1002 | ctx.lineTo(19.5, 24.5); 1003 | ctx.lineTo(14, 10.5); 1004 | ctx.lineTo(14, 25); 1005 | ctx.lineTo(7, 14); 1006 | ctx.lineTo(9, 26); 1007 | ctx.closePath(); 1008 | ctx.fill("evenodd"); 1009 | ctx.stroke(); 1010 | ctx.restore(); 1011 | ctx.save(); 1012 | ctx.fillStyle = "#fff"; 1013 | ctx.strokeStyle = "#000"; 1014 | ctx.lineCap = "butt"; 1015 | ctx.font = " 10px sans-serif"; 1016 | ctx.beginPath(); 1017 | ctx.moveTo(9, 26); 1018 | ctx.bezierCurveTo(9, 28, 10.5, 28, 11.5, 30); 1019 | ctx.bezierCurveTo(12.5, 31.5, 12.5, 31, 12, 33.5); 1020 | ctx.bezierCurveTo(10.5, 34.5, 10.5, 36, 10.5, 36); 1021 | ctx.bezierCurveTo(9, 37.5, 11, 38.5, 11, 38.5); 1022 | ctx.bezierCurveTo(17.5, 39.5, 27.5, 39.5, 34, 38.5); 1023 | ctx.bezierCurveTo(34, 38.5, 35.5, 37.5, 34, 36); 1024 | ctx.bezierCurveTo(34, 36, 34.5, 34.5, 33, 33.5); 1025 | ctx.bezierCurveTo(32.5, 31, 32.5, 31.5, 33.5, 30); 1026 | ctx.bezierCurveTo(34.5, 28, 36, 28, 36, 26); 1027 | ctx.bezierCurveTo(27.5, 24.5, 17.5, 24.5, 9, 26); 1028 | ctx.closePath(); 1029 | ctx.fill("evenodd"); 1030 | ctx.stroke(); 1031 | ctx.restore(); 1032 | ctx.save(); 1033 | ctx.fillStyle = "rgba(0,0,0,0)"; 1034 | ctx.strokeStyle = "#000"; 1035 | ctx.font = " 10px sans-serif"; 1036 | ctx.beginPath(); 1037 | ctx.moveTo(11.5, 30); 1038 | ctx.bezierCurveTo(15, 29, 30, 29, 33.5, 30); 1039 | ctx.moveTo(12, 33.5); 1040 | ctx.bezierCurveTo(18, 32.5, 27, 32.5, 33, 33.5); 1041 | ctx.fill("evenodd"); 1042 | ctx.stroke(); 1043 | ctx.restore(); 1044 | ctx.restore(); 1045 | ctx.restore(); 1046 | }, 1047 | }, 1048 | "wR.svg": { 1049 | draw: function (ctx) { 1050 | ctx.save(); 1051 | ctx.strokeStyle = "rgba(0,0,0,0)"; 1052 | ctx.miterLimit = 4; 1053 | ctx.font = ""; 1054 | ctx.font = " 10px sans-serif"; 1055 | ctx.save(); 1056 | ctx.fillStyle = "#fff"; 1057 | ctx.strokeStyle = "#000"; 1058 | ctx.lineWidth = 1.5; 1059 | ctx.lineCap = "round"; 1060 | ctx.lineJoin = "round"; 1061 | ctx.font = " 10px sans-serif"; 1062 | ctx.save(); 1063 | ctx.fillStyle = "#fff"; 1064 | ctx.strokeStyle = "#000"; 1065 | ctx.lineCap = "butt"; 1066 | ctx.font = " 10px sans-serif"; 1067 | ctx.beginPath(); 1068 | ctx.moveTo(9, 39); 1069 | ctx.lineTo(36, 39); 1070 | ctx.lineTo(36, 36); 1071 | ctx.lineTo(9, 36); 1072 | ctx.lineTo(9, 39); 1073 | ctx.closePath(); 1074 | ctx.moveTo(12, 36); 1075 | ctx.lineTo(12, 32); 1076 | ctx.lineTo(33, 32); 1077 | ctx.lineTo(33, 36); 1078 | ctx.lineTo(12, 36); 1079 | ctx.closePath(); 1080 | ctx.moveTo(11, 14); 1081 | ctx.lineTo(11, 9); 1082 | ctx.lineTo(15, 9); 1083 | ctx.lineTo(15, 11); 1084 | ctx.lineTo(20, 11); 1085 | ctx.lineTo(20, 9); 1086 | ctx.lineTo(25, 9); 1087 | ctx.lineTo(25, 11); 1088 | ctx.lineTo(30, 11); 1089 | ctx.lineTo(30, 9); 1090 | ctx.lineTo(34, 9); 1091 | ctx.lineTo(34, 14); 1092 | ctx.fill("evenodd"); 1093 | ctx.stroke(); 1094 | ctx.restore(); 1095 | ctx.save(); 1096 | ctx.fillStyle = "#fff"; 1097 | ctx.strokeStyle = "#000"; 1098 | ctx.font = " 10px sans-serif"; 1099 | ctx.beginPath(); 1100 | ctx.moveTo(34, 14); 1101 | ctx.lineTo(31, 17); 1102 | ctx.lineTo(14, 17); 1103 | ctx.lineTo(11, 14); 1104 | ctx.fill("evenodd"); 1105 | ctx.stroke(); 1106 | ctx.restore(); 1107 | ctx.save(); 1108 | ctx.fillStyle = "#fff"; 1109 | ctx.strokeStyle = "#000"; 1110 | ctx.lineCap = "butt"; 1111 | ctx.lineJoin = "miter"; 1112 | ctx.font = " 10px sans-serif"; 1113 | ctx.beginPath(); 1114 | ctx.moveTo(31, 17); 1115 | ctx.lineTo(31, 29.5); 1116 | ctx.lineTo(14, 29.5); 1117 | ctx.lineTo(14, 17); 1118 | ctx.fill("evenodd"); 1119 | ctx.stroke(); 1120 | ctx.restore(); 1121 | ctx.save(); 1122 | ctx.fillStyle = "#fff"; 1123 | ctx.strokeStyle = "#000"; 1124 | ctx.font = " 10px sans-serif"; 1125 | ctx.beginPath(); 1126 | ctx.moveTo(31, 29.5); 1127 | ctx.lineTo(32.5, 32); 1128 | ctx.lineTo(12.5, 32); 1129 | ctx.lineTo(14, 29.5); 1130 | ctx.fill("evenodd"); 1131 | ctx.stroke(); 1132 | ctx.restore(); 1133 | ctx.save(); 1134 | ctx.fillStyle = "rgba(0,0,0,0)"; 1135 | ctx.strokeStyle = "#000"; 1136 | ctx.lineJoin = "miter"; 1137 | ctx.font = " 10px sans-serif"; 1138 | ctx.beginPath(); 1139 | ctx.moveTo(11, 14); 1140 | ctx.lineTo(34, 14); 1141 | ctx.fill("evenodd"); 1142 | ctx.stroke(); 1143 | ctx.restore(); 1144 | ctx.restore(); 1145 | ctx.restore(); 1146 | }, 1147 | }, 1148 | }; 1149 | -------------------------------------------------------------------------------- /art/chess.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | const { chessPieces } = require("./chess-pieces"); 4 | 5 | let ChessGame = require("chess.js"); 6 | if (ChessGame.Chess) { 7 | ChessGame = ChessGame.Chess; 8 | } 9 | 10 | function getGame(movesBuffer) { 11 | const game = new ChessGame(); 12 | 13 | for (let i = 0; i < movesBuffer.byteLength; i++) { 14 | let moves = game.moves(); 15 | game.move(moves[Math.floor((movesBuffer[i] / 256) * moves.length)]); 16 | } 17 | 18 | return game; 19 | } 20 | 21 | function getPgn(movesBuffer) { 22 | return getGame(movesBuffer).pgn({ newline_char: "
" }); 23 | } 24 | 25 | class Chess extends Art { 26 | constructor() { 27 | super({ 28 | moves: 16, 29 | }); 30 | this.filename = "chess.js"; 31 | this.created = "24 Apr 2021"; 32 | } 33 | 34 | getDescription({ movesBuffer }) { 35 | return ` 36 | We use the excellent chess.js library 37 | to simulate a chess game where each move is generated from the moves buffer. That is, 38 | for each byte in moves, we cycle through the list of all possible moves 39 | and choose the one at the index specified by the byte (modulo the length of the list of moves). 40 | 41 | Even though each player only plays 8 moves, they're probably not very good moves so 42 | you're likely to generate a never-seen-before game. 43 | 44 | If you'd like to analyze or continue the game above, you can do so by heading to 45 | lichess.org/paste and entering the following PGN: 46 | 47 | ${getPgn(movesBuffer)} 48 | `; 49 | } 50 | 51 | squareToDrawFn(square) { 52 | if (!square) { 53 | return () => {}; 54 | } 55 | 56 | const filename = `${square.color}${square.type.toUpperCase()}.svg`; 57 | return chessPieces[filename].draw; 58 | } 59 | 60 | // Unused 61 | squareToAscii(square) { 62 | return square 63 | ? { 64 | k: "♔♚", 65 | q: "♕♛", 66 | r: "♖♜", 67 | b: "♗♝", 68 | n: "♘♞", 69 | p: "♙♟", 70 | }[square.type][square.color === "w" ? 0 : 1] 71 | : ""; 72 | } 73 | 74 | draw(ctx, { movesBuffer }) { 75 | const w = ctx.canvas.width; 76 | const h = ctx.canvas.height; 77 | 78 | const game = getGame(movesBuffer); 79 | 80 | const squareSize = _(140, Math.min(w, h)); 81 | ctx.font = `${squareSize}px monospace`; 82 | ctx.fillStyle = "rgb(0, 0, 0)"; 83 | 84 | const leftPadding = w / 2 - (8 * squareSize) / 2; 85 | const topPadding = h / 2 - (8 * squareSize) / 2; 86 | 87 | const board = game.board(); 88 | for (let r = 0; r < 8; r++) { 89 | for (let c = 0; c < 8; c++) { 90 | const x = leftPadding + c * squareSize; 91 | const y = topPadding + r * squareSize; 92 | 93 | if (r % 2 !== c % 2) { 94 | ctx.save(); 95 | ctx.fillStyle = "rgb(180, 180, 180)"; 96 | ctx.fillRect(x, y, squareSize, squareSize); 97 | ctx.restore(); 98 | } 99 | 100 | const SVG_PIECE_SIZE = 45; 101 | const drawPiece = this.squareToDrawFn(board[r][c]); 102 | ctx.save(); 103 | ctx.transform( 104 | squareSize / SVG_PIECE_SIZE, 105 | 0, 106 | 0, 107 | squareSize / SVG_PIECE_SIZE, 108 | x, 109 | y 110 | ); 111 | drawPiece(ctx); 112 | ctx.restore(); 113 | } 114 | } 115 | } 116 | } 117 | 118 | exports.Chess = Chess; 119 | exports.getPgn = getPgn; 120 | -------------------------------------------------------------------------------- /art/circle.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | 3 | class Circle extends Art { 4 | constructor() { 5 | super({ 6 | x1: 2, 7 | r1: 2, 8 | x2: 2, 9 | r2: 2, 10 | x3: 2, 11 | r3: 2, 12 | x4: 2, 13 | r4: 2, 14 | }); 15 | this.filename = "circle.js"; 16 | this.created = "28 Mar 2021"; 17 | } 18 | 19 | getDescription() { 20 | return ` 21 | From the template we gather ${ 22 | Object.keys(this.template).length / 2 23 | } circles and their radii and place 24 | them on the horizon. Intersections of these circles (if any) are labeled by drawing a chord between the two 25 | intersection points. 26 | `; 27 | } 28 | 29 | drawCircle(ctx, x, r) { 30 | const h = ctx.canvas.height; 31 | ctx.beginPath(); 32 | ctx.arc(x, h / 2, r, 0, 2 * Math.PI); 33 | ctx.stroke(); 34 | } 35 | 36 | drawIntersection(ctx, x1, r1, x2, r2) { 37 | const h = ctx.canvas.height; 38 | const d = x2 - x1; 39 | 40 | // https://mathworld.wolfram.com/Circle-CircleIntersection.html 41 | const intersectionOffset = (d * d - r2 * r2 + r1 * r1) / (2 * d); 42 | const y = Math.sqrt( 43 | (4 * d * d * r1 * r1 - Math.pow(d * d - r2 * r2 + r1 * r1, 2)) / 44 | (4 * d * d) 45 | ); 46 | 47 | ctx.beginPath(); 48 | ctx.moveTo(x1 + intersectionOffset, h / 2 - y); 49 | ctx.lineTo(x1 + intersectionOffset, h / 2 + y); 50 | ctx.stroke(); 51 | } 52 | 53 | draw(ctx, { x1, r1, x2, r2, x3, r3, x4, r4 }) { 54 | const w = ctx.canvas.width; 55 | const h = ctx.canvas.height; 56 | ctx.lineWidth = 3; 57 | 58 | const xs = [x1, x2, x3, x4].map((x) => Math.round(x * w)); 59 | const rs = [r1, r2, r3, r4].map((r) => r * 0.6 * w); 60 | 61 | xs.forEach((x, i) => { 62 | this.drawCircle(ctx, x, rs[i]); 63 | }); 64 | 65 | // Pair up each circle to draw an intersection chord (if any) 66 | [ 67 | [0, 1], 68 | [0, 2], 69 | [0, 3], 70 | [1, 2], 71 | [1, 3], 72 | [2, 3], 73 | ].forEach(([a, b]) => { 74 | this.drawIntersection(ctx, xs[a], rs[a], xs[b], rs[b]); 75 | }); 76 | } 77 | } 78 | 79 | exports.Circle = Circle; 80 | -------------------------------------------------------------------------------- /art/collatz.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Collatz extends Art { 5 | constructor() { 6 | super({ 7 | input: 8, 8 | }); 9 | this.filename = "collatz.js"; 10 | this.created = "14 Apr 2021"; 11 | } 12 | 13 | getDescription() { 14 | return ` 15 | We convert input to a number, and use it as the first number in a 16 | Collatz sequence 17 | (i.e. even n => n / 2, odd n => 3n+1) until the number reaches 1. 18 | 19 | Each iteration is drawn as a bit string where 1s are filled and 0s are empty, continuing to the next line and 20 | wrapping back to the top when necessary. 21 | 22 | It is unknown if the Collatz sequence reaches 1 for every input, but we know that 23 | generalized Collatz sequences 24 | can be used to perform arbitrary computations and their behavior is therefore undecidable. 25 | `; 26 | } 27 | 28 | bitSize(ctx) { 29 | return _(8, ctx.canvas.height); 30 | } 31 | 32 | bufferToBitString(buff) { 33 | let arr = []; 34 | for (let i = 0; i < buff.byteLength; i++) { 35 | arr = arr.concat( 36 | buff[i] 37 | .toString(2) 38 | .padStart(8, "0") 39 | .split("") 40 | .map((s) => parseInt(s)) 41 | ); 42 | } 43 | return arr; 44 | } 45 | 46 | halfBitString(bitString) { 47 | return bitString.slice(1); 48 | } 49 | 50 | addBitString(a, b) { 51 | const result = []; 52 | const maxLength = Math.max(a.length, b.length); 53 | let carry = 0; 54 | for (let i = 0; i < maxLength; i++) { 55 | const a_ = a[i] || 0; 56 | const b_ = b[i] || 0; 57 | const sum = a_ + b_ + carry; 58 | result.push(sum % 2); 59 | carry = sum > 1 ? 1 : 0; 60 | } 61 | if (carry) { 62 | result.push(carry); 63 | } 64 | return result; 65 | } 66 | 67 | triplePlusOneBitString(bitString) { 68 | const doubleBitString = [0].concat(bitString); 69 | return this.addBitString( 70 | // 3n 71 | this.addBitString(bitString, doubleBitString), 72 | // +1 73 | [1] 74 | ); 75 | } 76 | 77 | drawBitString(ctx, x, y, bitString) { 78 | const bs = this.bitSize(ctx); 79 | for (let i = 0; i < bitString.length; i++) { 80 | if (bitString[i]) { 81 | ctx.beginPath(); 82 | ctx.rect(x + i * bs, y, bs, bs); 83 | ctx.fill(); 84 | } 85 | } 86 | } 87 | 88 | draw(ctx, { inputBuffer }) { 89 | const bs = this.bitSize(ctx); 90 | let x = 0; 91 | let y = 0; 92 | let current = this.bufferToBitString(inputBuffer); 93 | 94 | let maxWidthOfColumn = 0; 95 | 96 | ctx.fillStyle = "rgb(0, 0, 0)"; 97 | 98 | while (current.length > 0 && x * bs <= ctx.canvas.width) { 99 | maxWidthOfColumn = Math.max(maxWidthOfColumn, current.length); 100 | this.drawBitString(ctx, x * bs, y * bs, current); 101 | 102 | // Make sure we draw the `1` bit :) 103 | if (current.length == 1) { 104 | break; 105 | } 106 | 107 | current = current[0] 108 | ? this.triplePlusOneBitString(current) 109 | : this.halfBitString(current); 110 | y++; 111 | 112 | const GAP = 2; 113 | if (y * bs >= ctx.canvas.height) { 114 | y = 0; 115 | x += maxWidthOfColumn + GAP; 116 | maxWidthOfColumn = 0; 117 | } 118 | } 119 | } 120 | } 121 | 122 | exports.Collatz = Collatz; 123 | -------------------------------------------------------------------------------- /art/combinators.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Combinators extends Art { 5 | constructor() { 6 | super({ 7 | branches: 32, 8 | }); 9 | this.created = "06 Jun 2021"; 10 | this.filename = "combinators.js"; 11 | this.hidden = true; 12 | } 13 | 14 | nthDepthFirst(tree, n) { 15 | function leaves(tree) { 16 | if (tree.value) { 17 | return [tree]; 18 | } else { 19 | return leaves(tree.left).concat(leaves(tree.right)); 20 | } 21 | } 22 | 23 | const ls = leaves(tree); 24 | return ls[n % ls.length]; 25 | } 26 | 27 | getValues(byte) { 28 | const choices = "SKI"; 29 | return [ 30 | choices[(17 * byte) % choices.length], 31 | choices[(23 * byte) % choices.length], 32 | ]; 33 | } 34 | 35 | fork(leaf, byte) { 36 | delete leaf.value; 37 | let [left, right] = this.getValues(byte); 38 | leaf.left = { 39 | value: left, 40 | }; 41 | leaf.right = { 42 | value: right, 43 | }; 44 | } 45 | 46 | treeToString(tree) { 47 | if (tree.value) { 48 | return tree.value; 49 | } else { 50 | return ( 51 | "(" + this.treeToString(tree.left) + this.treeToString(tree.right) + ")" 52 | ); 53 | } 54 | } 55 | 56 | draw(ctx, { branchesBuffer }) { 57 | let tree = { 58 | value: "I", 59 | }; 60 | branchesBuffer.forEach((byte) => { 61 | let node = this.nthDepthFirst(tree, byte); 62 | this.fork(node, byte); 63 | }); 64 | console.log(this.treeToString(tree)); 65 | } 66 | } 67 | 68 | exports.Combinators = Combinators; 69 | -------------------------------------------------------------------------------- /art/divisions.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Divisions extends Art { 5 | constructor() { 6 | super({ 7 | divisions: 32, 8 | }); 9 | this.created = "05 Jun 2021"; 10 | this.filename = "divisions.js"; 11 | } 12 | 13 | getDescription({ divisionsBuffer }) { 14 | return ` 15 | Begin with a single rectangle covering the entire canvas. 16 | 17 | For each of the ${divisionsBuffer.length} bytes in divisions, 18 | determine which rectangle to split (byte % rectangles.length) and 19 | divide that rectangle in half either horizontally or vertically (byte % 2). 20 | `; 21 | } 22 | 23 | draw(ctx, { divisionsBuffer }) { 24 | const w = ctx.canvas.width; 25 | const h = ctx.canvas.height; 26 | 27 | ctx.lineWidth = _(5, w); 28 | 29 | let regions = [{ x: 0, y: 0, w, h }]; 30 | 31 | for (let i = 0; i < divisionsBuffer.length; i += 1) { 32 | const idx = divisionsBuffer[i] % regions.length; 33 | const direction = divisionsBuffer[i] % 2; 34 | const region = regions[idx]; 35 | 36 | if (direction === 0) { 37 | regions.splice( 38 | idx, 39 | 1, 40 | { x: region.x, y: region.y, w: region.w, h: region.h / 2 }, 41 | { 42 | x: region.x, 43 | y: region.y + region.h / 2, 44 | w: region.w, 45 | h: region.h / 2, 46 | } 47 | ); 48 | } else { 49 | regions.splice( 50 | idx, 51 | 1, 52 | { x: region.x, y: region.y, w: region.w / 2, h: region.h }, 53 | { 54 | x: region.x + region.w / 2, 55 | y: region.y, 56 | w: region.w / 2, 57 | h: region.h, 58 | } 59 | ); 60 | } 61 | } 62 | 63 | regions.forEach(({ x, y, w, h }) => { 64 | ctx.beginPath(); 65 | ctx.rect(x, y, w, h); 66 | ctx.stroke(); 67 | }); 68 | } 69 | } 70 | 71 | exports.Divisions = Divisions; 72 | -------------------------------------------------------------------------------- /art/element-markov.js: -------------------------------------------------------------------------------- 1 | // Generated with the help of https://github.com/jdan/markov.rb 2 | // 3 | // m = Markov.new(options[:n]) 4 | // names = STDIN.read.split.map &:chomp 5 | // names.each do |name| 6 | // m.digest(name) 7 | // end 8 | // 9 | // + require 'json' 10 | // + puts m.chain.to_json 11 | 12 | exports.chain = { 13 | start: [ 14 | "hy", 15 | "he", 16 | "li", 17 | "be", 18 | "bo", 19 | "ca", 20 | "ni", 21 | "ox", 22 | "fl", 23 | "ne", 24 | "so", 25 | "ma", 26 | "al", 27 | "si", 28 | "ph", 29 | "su", 30 | "ch", 31 | "ar", 32 | "po", 33 | "ca", 34 | "sc", 35 | "ti", 36 | "va", 37 | "ch", 38 | "ma", 39 | "ir", 40 | "co", 41 | "ni", 42 | "co", 43 | "zi", 44 | "ga", 45 | "ge", 46 | "ar", 47 | "se", 48 | "br", 49 | "kr", 50 | "ru", 51 | "st", 52 | "yt", 53 | "zi", 54 | "ni", 55 | "mo", 56 | "te", 57 | "ru", 58 | "rh", 59 | "pa", 60 | "si", 61 | "ca", 62 | "in", 63 | "ti", 64 | "an", 65 | "te", 66 | "io", 67 | "xe", 68 | "ca", 69 | "ba", 70 | "la", 71 | "ce", 72 | "pr", 73 | "ne", 74 | "pr", 75 | "sa", 76 | "eu", 77 | "ga", 78 | "te", 79 | "dy", 80 | "ho", 81 | "er", 82 | "th", 83 | "yt", 84 | "lu", 85 | "ha", 86 | "ta", 87 | "tu", 88 | "rh", 89 | "os", 90 | "ir", 91 | "pl", 92 | "go", 93 | "me", 94 | "th", 95 | "le", 96 | "bi", 97 | "po", 98 | "as", 99 | "ra", 100 | "fr", 101 | "ra", 102 | "ac", 103 | "th", 104 | "pr", 105 | "ur", 106 | "ne", 107 | "pl", 108 | "am", 109 | "cu", 110 | "be", 111 | "ca", 112 | "ei", 113 | "fe", 114 | "me", 115 | "no", 116 | "la", 117 | "ru", 118 | "du", 119 | "se", 120 | "bo", 121 | "ha", 122 | "me", 123 | "da", 124 | "ro", 125 | "co", 126 | "ni", 127 | "fl", 128 | "mo", 129 | "li", 130 | "te", 131 | "og", 132 | ], 133 | hy: ["yd"], 134 | yd: ["dr"], 135 | dr: ["ro"], 136 | ro: [ 137 | "og", 138 | "on", 139 | "og", 140 | "om", 141 | "on", 142 | "om", 143 | "on", 144 | "om", 145 | "op", 146 | "os", 147 | "ot", 148 | "oe", 149 | "ov", 150 | ], 151 | og: ["ge", "ge", "ga"], 152 | ge: ["en", "en", "en", "er", "en"], 153 | en: [ 154 | "end", 155 | "end", 156 | "end", 157 | "ni", 158 | "ni", 159 | "nu", 160 | "ni", 161 | "no", 162 | "end", 163 | "ni", 164 | "nd", 165 | "nc", 166 | "nt", 167 | "ni", 168 | "nn", 169 | ], 170 | he: ["el", "en", "en", "er"], 171 | el: ["li", "end", "le", "ll", "li", "le", "li"], 172 | li: ["iu", "it", "iu", "ic", "iu", "in", "iu", "iu", "iu", "if", "iu", "iv"], 173 | iu: [ 174 | "um", 175 | "um", 176 | "um", 177 | "um", 178 | "um", 179 | "um", 180 | "um", 181 | "um", 182 | "um", 183 | "um", 184 | "um", 185 | "um", 186 | "um", 187 | "um", 188 | "um", 189 | "um", 190 | "um", 191 | "um", 192 | "um", 193 | "um", 194 | "um", 195 | "um", 196 | "um", 197 | "um", 198 | "um", 199 | "um", 200 | "um", 201 | "um", 202 | "um", 203 | "um", 204 | "um", 205 | "um", 206 | "um", 207 | "um", 208 | "um", 209 | "um", 210 | "um", 211 | "um", 212 | "um", 213 | "um", 214 | "um", 215 | "um", 216 | "um", 217 | "um", 218 | "um", 219 | "um", 220 | "um", 221 | "um", 222 | "um", 223 | "um", 224 | "um", 225 | "um", 226 | "um", 227 | "um", 228 | "um", 229 | "um", 230 | "um", 231 | "um", 232 | "um", 233 | "um", 234 | "um", 235 | "um", 236 | "um", 237 | "um", 238 | "um", 239 | "um", 240 | "um", 241 | "um", 242 | "um", 243 | "um", 244 | "um", 245 | "um", 246 | "um", 247 | "um", 248 | "um", 249 | "um", 250 | "um", 251 | "um", 252 | "um", 253 | ], 254 | um: [ 255 | "end", 256 | "end", 257 | "end", 258 | "end", 259 | "end", 260 | "mi", 261 | "end", 262 | "end", 263 | "end", 264 | "end", 265 | "end", 266 | "end", 267 | "end", 268 | "end", 269 | "end", 270 | "end", 271 | "end", 272 | "end", 273 | "end", 274 | "end", 275 | "end", 276 | "end", 277 | "end", 278 | "end", 279 | "end", 280 | "end", 281 | "end", 282 | "end", 283 | "end", 284 | "end", 285 | "end", 286 | "end", 287 | "end", 288 | "end", 289 | "end", 290 | "end", 291 | "end", 292 | "end", 293 | "end", 294 | "end", 295 | "end", 296 | "end", 297 | "end", 298 | "end", 299 | "end", 300 | "end", 301 | "end", 302 | "end", 303 | "end", 304 | "end", 305 | "end", 306 | "end", 307 | "end", 308 | "end", 309 | "end", 310 | "end", 311 | "end", 312 | "end", 313 | "end", 314 | "end", 315 | "end", 316 | "end", 317 | "end", 318 | "end", 319 | "end", 320 | "end", 321 | "end", 322 | "end", 323 | "end", 324 | "end", 325 | "end", 326 | "end", 327 | "end", 328 | "end", 329 | "end", 330 | "end", 331 | "end", 332 | "end", 333 | "end", 334 | "end", 335 | "end", 336 | "end", 337 | "end", 338 | "end", 339 | ], 340 | it: ["th", "tr", "ta", "tn"], 341 | th: ["hi", "he", "ha", "hi", "hu", "ha", "end", "ho", "he"], 342 | hi: ["iu", "iu"], 343 | be: ["er", "er", "el"], 344 | er: [ 345 | "ry", 346 | "end", 347 | "rm", 348 | "end", 349 | "ri", 350 | "rb", 351 | "rb", 352 | "rb", 353 | "rc", 354 | "ri", 355 | "rk", 356 | "rm", 357 | "rf", 358 | "ri", 359 | "rn", 360 | "ro", 361 | "rm", 362 | ], 363 | ry: ["yl", "yp", "end"], 364 | yl: ["ll"], 365 | ll: ["li", "li", "la", "lu", "li"], 366 | bo: ["or", "on", "or", "oh"], 367 | or: ["ro", "ri", "ru", "ri", "ri", "rn", "rd", "rg", "ri"], 368 | on: [ 369 | "end", 370 | "end", 371 | "end", 372 | "end", 373 | "end", 374 | "end", 375 | "end", 376 | "nt", 377 | "ni", 378 | "ny", 379 | "end", 380 | "ni", 381 | "end", 382 | "ni", 383 | "ni", 384 | "end", 385 | ], 386 | ca: ["ar", "al", "an", "ad", "ae", "al"], 387 | ar: ["rb", "rg", "rs", "ri", "ri", "rm"], 388 | rb: ["bo", "bi", "bi", "bi"], 389 | ni: [ 390 | "it", 391 | "iu", 392 | "iu", 393 | "ic", 394 | "iu", 395 | "ic", 396 | "iu", 397 | "iu", 398 | "io", 399 | "iu", 400 | "iu", 401 | "iu", 402 | "iu", 403 | "iu", 404 | "iu", 405 | "iu", 406 | "iu", 407 | "iu", 408 | "iu", 409 | "iu", 410 | "iu", 411 | "iu", 412 | "iu", 413 | "ic", 414 | "ih", 415 | "iu", 416 | ], 417 | tr: ["ro", "ro", "ri"], 418 | ox: ["xy"], 419 | xy: ["yg"], 420 | yg: ["ge"], 421 | fl: ["lu", "le"], 422 | lu: ["uo", "um", "ur", "ut", "um", "ut"], 423 | uo: ["or"], 424 | ri: [ 425 | "in", 426 | "in", 427 | "iu", 428 | "iu", 429 | "iu", 430 | "iu", 431 | "iu", 432 | "id", 433 | "iu", 434 | "ic", 435 | "iu", 436 | "iu", 437 | "iu", 438 | "iu", 439 | ], 440 | in: [ 441 | "ne", 442 | "ni", 443 | "ne", 444 | "nc", 445 | "ne", 446 | "nd", 447 | "end", 448 | "ne", 449 | "ni", 450 | "nu", 451 | "ne", 452 | "ni", 453 | "ni", 454 | "ns", 455 | "ni", 456 | "ne", 457 | ], 458 | ne: [ 459 | "end", 460 | "eo", 461 | "es", 462 | "end", 463 | "es", 464 | "end", 465 | "et", 466 | "end", 467 | "eo", 468 | "end", 469 | "ep", 470 | "er", 471 | "es", 472 | "end", 473 | "es", 474 | ], 475 | eo: ["on", "od", "od"], 476 | so: ["od", "on"], 477 | od: ["di", "di", "di", "dy", "dy"], 478 | di: ["iu", "iu", "iu", "iu", "iu", "iu", "iu", "in", "iu", "iu", "iu"], 479 | ma: ["ag", "an", "an", "ar"], 480 | ag: ["gn"], 481 | gn: ["ne"], 482 | es: ["si", "se", "si", "ss", "ss"], 483 | si: ["iu", "il", "iu", "il", "iu", "iu", "iu", "in"], 484 | al: ["lu", "lc", "lt", "ll", "ll", "lu", "ll", "li"], 485 | mi: ["in", "iu", "in", "iu", "iu", "iu", "iu", "iu", "iu"], 486 | il: ["li", "lv"], 487 | ic: ["co", "ck", "end", "ci", "ci"], 488 | co: ["on", "ob", "op", "on", "op", "ov"], 489 | ph: ["ho", "ho"], 490 | ho: ["os", "or", "od", "ol", "or", "on"], 491 | os: ["sp", "si", "sm", "sc"], 492 | sp: ["ph", "pr"], 493 | ru: ["us", "ub", "ut", "ut"], 494 | us: ["end"], 495 | su: ["ul"], 496 | ul: ["lf", "li"], 497 | lf: ["fu"], 498 | fu: ["ur"], 499 | ur: ["end", "ri", "ro", "ry", "ra", "ri"], 500 | ch: ["hl", "hr", "hn"], 501 | hl: ["lo"], 502 | lo: ["or", "on"], 503 | rg: ["go", "gi"], 504 | go: ["on", "ol"], 505 | po: ["ot", "ol"], 506 | ot: ["ta", "ta"], 507 | ta: ["as", "an", "an", "al", "at", "ac", "ad"], 508 | as: ["ss", "se", "st", "ss"], 509 | ss: ["si", "si", "si", "so"], 510 | lc: ["ci"], 511 | ci: ["iu", "iu", "iu", "iu", "iu"], 512 | sc: ["ca", "co"], 513 | an: [ 514 | "nd", 515 | "ni", 516 | "na", 517 | "ng", 518 | "ne", 519 | "ni", 520 | "nt", 521 | "nt", 522 | "nu", 523 | "nt", 524 | "nc", 525 | "ni", 526 | "ne", 527 | ], 528 | nd: ["di", "di", "de"], 529 | ti: ["it", "iu", "iu", "in", "im", "iu", "in", "in", "in", "in", "iu"], 530 | va: ["an"], 531 | na: ["ad"], 532 | ad: ["di", "di", "dm", "do", "end", "do", "di", "dt"], 533 | hr: ["ro", "ri"], 534 | om: ["mi", "mi", "me"], 535 | ng: ["ga", "gs"], 536 | ga: ["an", "al", "ad", "an"], 537 | se: ["end", "en", "el", "eo", "ea"], 538 | ir: ["ro", "rc", "ri"], 539 | ob: ["ba", "bi", "be"], 540 | ba: ["al", "ar"], 541 | lt: ["end"], 542 | ck: ["ke"], 543 | ke: ["el", "el"], 544 | op: ["pp", "pi", "pe"], 545 | pp: ["pe"], 546 | pe: ["er", "er"], 547 | zi: ["in", "ir"], 548 | nc: ["end", "ci", "ci"], 549 | rm: ["ma", "mi", "ms", "mo"], 550 | rs: ["se"], 551 | le: ["en", "ea", "ev", "er"], 552 | br: ["ro"], 553 | kr: ["ry"], 554 | yp: ["pt"], 555 | pt: ["to", "tu"], 556 | to: ["on", "on"], 557 | ub: ["bi", "bn"], 558 | bi: ["id", "iu", "iu", "iu", "iu", "is"], 559 | id: ["di", "di"], 560 | st: ["tr", "te", "ta", "te", "ta"], 561 | nt: ["ti", "ti", "th", "ta", "tg"], 562 | yt: ["tt", "tt"], 563 | tt: ["tr", "te"], 564 | rc: ["co", "cu"], 565 | io: ["ob", "od"], 566 | mo: ["ol", "on", "os", "or"], 567 | ol: ["ly", "li", "lm", "ld", "lo"], 568 | ly: ["yb"], 569 | yb: ["bd"], 570 | bd: ["de"], 571 | de: ["en", "el"], 572 | nu: ["um", "um", "um"], 573 | te: ["ec", "el", "er", "er", "et", "en", "ei", "en"], 574 | ec: ["ch"], 575 | hn: ["ne"], 576 | et: ["ti", "th", "ti"], 577 | ut: ["th", "te", "th", "to", "th"], 578 | rh: ["ho", "he"], 579 | pa: ["al"], 580 | la: ["ad", "an", "at", "aw"], 581 | lv: ["ve"], 582 | ve: ["er", "er"], 583 | dm: ["mi"], 584 | im: ["mo"], 585 | ny: ["end"], 586 | xe: ["en"], 587 | no: ["on", "ob"], 588 | ae: ["es"], 589 | ha: ["an", "af", "al", "as"], 590 | ce: ["er"], 591 | pr: ["ra", "ro", "ro", "ro"], 592 | ra: ["as", "ad", "an", "ad", "an"], 593 | dy: ["ym", "ym", "ys"], 594 | ym: ["mi", "mi"], 595 | me: ["et", "er", "er", "en", "ei"], 596 | sa: ["am"], 597 | am: ["ma", "me"], 598 | eu: ["ur"], 599 | pi: ["iu"], 600 | do: ["ol", "on"], 601 | ys: ["sp"], 602 | lm: ["mi"], 603 | hu: ["ul"], 604 | af: ["fn"], 605 | fn: ["ni"], 606 | tu: ["un", "un"], 607 | un: ["ng", "ni"], 608 | gs: ["st"], 609 | sm: ["mi", "mu"], 610 | pl: ["la", "lu"], 611 | at: ["ti", "ti"], 612 | ld: ["end"], 613 | cu: ["ur", "ur"], 614 | ea: ["ad", "ab"], 615 | is: ["sm"], 616 | mu: ["ut"], 617 | fr: ["ra"], 618 | ac: ["ct", "ct"], 619 | ct: ["ti", "ti"], 620 | ep: ["pt"], 621 | rk: ["ke"], 622 | if: ["fo"], 623 | fo: ["or", "or"], 624 | rn: ["ni", "ni"], 625 | ei: ["in", "in", "it"], 626 | ns: ["st"], 627 | fe: ["er"], 628 | ev: ["vi"], 629 | vi: ["iu", "iu", "iu"], 630 | aw: ["wr"], 631 | wr: ["re"], 632 | re: ["en"], 633 | rf: ["fo"], 634 | rd: ["di"], 635 | du: ["ub"], 636 | bn: ["ni"], 637 | ab: ["bo"], 638 | gi: ["iu"], 639 | oh: ["hr"], 640 | tn: ["ne"], 641 | da: ["ar"], 642 | ms: ["st"], 643 | dt: ["ti"], 644 | oe: ["en"], 645 | tg: ["ge"], 646 | ih: ["ho"], 647 | ov: ["vi", "vi"], 648 | iv: ["ve"], 649 | nn: ["ne"], 650 | }; 651 | 652 | exports.elements = new Set([ 653 | "hydrogen", 654 | "helium", 655 | "lithium", 656 | "beryllium", 657 | "boron", 658 | "carbon", 659 | "nitrogen", 660 | "oxygen", 661 | "fluorine", 662 | "neon", 663 | "sodium", 664 | "magnesium", 665 | "aluminium", 666 | "silicon", 667 | "phosphorus", 668 | "sulfur", 669 | "chlorine", 670 | "argon", 671 | "potassium", 672 | "calcium", 673 | "scandium", 674 | "titanium", 675 | "vanadium", 676 | "chromium", 677 | "manganese", 678 | "iron", 679 | "cobalt", 680 | "nickel", 681 | "copper", 682 | "zinc", 683 | "gallium", 684 | "germanium", 685 | "arsenic", 686 | "selenium", 687 | "bromine", 688 | "krypton", 689 | "rubidium", 690 | "strontium", 691 | "yttrium", 692 | "zirconium", 693 | "niobium", 694 | "molybdenum", 695 | "technetium", 696 | "ruthenium", 697 | "rhodium", 698 | "palladium", 699 | "silver", 700 | "cadmium", 701 | "indium", 702 | "tin", 703 | "antimony", 704 | "tellurium", 705 | "iodine", 706 | "xenon", 707 | "caesium", 708 | "barium", 709 | "lanthanum", 710 | "cerium", 711 | "praseodymium", 712 | "neodymium", 713 | "promethium", 714 | "samarium", 715 | "europium", 716 | "gadolinium", 717 | "terbium", 718 | "dysprosium", 719 | "holmium", 720 | "erbium", 721 | "thulium", 722 | "ytterbium", 723 | "lutetium", 724 | "hafnium", 725 | "tantalum", 726 | "tungsten", 727 | "rhenium", 728 | "osmium", 729 | "iridium", 730 | "platinum", 731 | "gold", 732 | "mercury", 733 | "thallium", 734 | "lead", 735 | "bismuth", 736 | "polonium", 737 | "astatine", 738 | "radon", 739 | "francium", 740 | "radium", 741 | "actinium", 742 | "thorium", 743 | "protactinium", 744 | "uranium", 745 | "neptunium", 746 | "plutonium", 747 | "americium", 748 | "curium", 749 | "berkelium", 750 | "californium", 751 | "einsteinium", 752 | "fermium", 753 | "mendelevium", 754 | "nobelium", 755 | "lawrencium", 756 | "rutherfordium", 757 | "dubnium", 758 | "seaborgium", 759 | "bohrium", 760 | "hassium", 761 | "meitnerium", 762 | "darmstadtium", 763 | "roentgenium", 764 | "copernicium", 765 | "nihonium", 766 | "flerovium", 767 | "moscovium", 768 | "livermorium", 769 | "tennessine", 770 | "oganesson", 771 | ]); 772 | -------------------------------------------------------------------------------- /art/element.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | const { chain, elements } = require("./element-markov.js"); 4 | 5 | function getName(buffer) { 6 | let currentNode = "start"; 7 | let result = ""; 8 | 9 | for (let i = 0; i < buffer.length; i++) { 10 | let options = chain[currentNode]; 11 | let nonterminatingOptions = options.filter((o) => o !== "end"); 12 | 13 | let next = options[Math.floor((buffer[i] / 256) * options.length)]; 14 | 15 | if (next === "end") { 16 | break; 17 | } 18 | 19 | currentNode = next; 20 | if (result === "") { 21 | result = currentNode; 22 | } else { 23 | result += currentNode[currentNode.length - 1]; 24 | } 25 | } 26 | 27 | return result[0].toUpperCase() + result.slice(1); 28 | } 29 | 30 | class Element extends Art { 31 | constructor() { 32 | super({ 33 | traverse: 16, 34 | protons: 2, 35 | weight: 2, 36 | rotations: 8, 37 | }); 38 | this.filename = "element.js"; 39 | this.created = "01 Aug 2021"; 40 | } 41 | 42 | getDescription() { 43 | return ` 44 | Begin with a Markov chain built using the 45 | bigrams of the chemical elements (up to Oganesson (Og, 118) as of 1 Aug 2021). For example, 46 | the element oxygen forms the chain START -> ox -> xy -> yg -> ge -> en -> END. 47 | 48 | Combine these chains 49 | (things get interesting when one bigram can go in many directions such as 50 | "li") 51 | and traverse the graph by picking the next node in the graph using the traverse buffer. 52 | Continue until END to get the name of your element, which may be the name of an existing element. 53 | 54 | This markov chain was generated with markov.rb 55 | and converted to JSON. 56 | 57 | The atomic number of our new element is computed by multiplying the protons vector by 300. 58 | The atomic weight is computed using atomicNumber * (1.5 + weight). 59 | 60 | From the atomic number, draw electrons according to the 61 | Bohr model, where each ring can hold 2n² 62 | electrons. Rotate each ring according to the nth byte in the rotations buffer. 63 | `; 64 | } 65 | 66 | getAtomicNumber(protons) { 67 | // return Math.floor(119 + protons * 100); 68 | return Math.floor(protons * 300); 69 | } 70 | 71 | draw(ctx, { traverseBuffer, protons, weight, rotationsBuffer }) { 72 | const w = ctx.canvas.width; 73 | const h = ctx.canvas.height; 74 | 75 | const name = getName(traverseBuffer); 76 | const left = 100; 77 | 78 | let atomicNumber = this.getAtomicNumber(protons); 79 | ctx.font = `bold ${_(130, h)}px Arial`; 80 | ctx.fillStyle = "rgb(0, 0, 0)"; 81 | ctx.fillText(atomicNumber, _(left, w), _(180, h)); 82 | 83 | ctx.font = `bold ${_(240, h)}px Arial`; 84 | ctx.fillStyle = "rgb(0, 0, 0)"; 85 | ctx.fillText(name.slice(0, 2), _(left - 8, w), h - _(280, h)); 86 | 87 | ctx.font = `bold ${_(80, h)}px Arial`; 88 | ctx.fillStyle = "rgb(0, 0, 0)"; 89 | ctx.fillText(name, _(left, w), h - _(190, h)); 90 | 91 | let atomicWeight = atomicNumber * (1.5 + weight); 92 | ctx.font = `bold ${_(80, h)}px Arial`; 93 | ctx.fillStyle = "rgb(0, 0, 0)"; 94 | ctx.fillText(atomicWeight.toFixed(2), _(left, w), h - _(105, h)); 95 | 96 | const bohrCenterX = _(830, w); 97 | const bohrCenterY = h / 2; 98 | // (w * 7) / 24 - nice outer width 99 | const innerRadius = _(45, w); 100 | 101 | ctx.lineWidth = _(2, w); 102 | ctx.beginPath(); 103 | ctx.arc(bohrCenterX, bohrCenterY, innerRadius, 0, 2 * Math.PI); 104 | ctx.stroke(); 105 | 106 | let shells = []; 107 | for (let i = 1; atomicNumber > 0; i++) { 108 | let electrons = Math.min(2 * i * i, atomicNumber); 109 | atomicNumber -= electrons; 110 | shells.push(electrons); 111 | } 112 | 113 | const electronRadius = 6; 114 | ctx.fillStyle = "rgb(255, 255, 255)"; 115 | shells.forEach((electrons, i) => { 116 | const radius = innerRadius * (i + 2); 117 | ctx.beginPath(); 118 | ctx.arc(bohrCenterX, bohrCenterY, radius, 0, 2 * Math.PI); 119 | ctx.stroke(); 120 | 121 | for (let n = 0; n < electrons; n++) { 122 | const theta = 123 | (2 * Math.PI * n) / electrons + 124 | (rotationsBuffer[i] / 256) * 2 * Math.PI; 125 | 126 | ctx.save(); 127 | ctx.beginPath(); 128 | ctx.lineWidth = _(5, w); 129 | ctx.arc( 130 | bohrCenterX + radius * Math.cos(theta), 131 | bohrCenterY + radius * Math.sin(theta), 132 | _(electronRadius, w), 133 | 0, 134 | 2 * Math.PI 135 | ); 136 | ctx.stroke(); 137 | ctx.fill(); 138 | ctx.restore(); 139 | } 140 | }); 141 | } 142 | } 143 | 144 | exports.Element = Element; 145 | exports.getName = getName; 146 | -------------------------------------------------------------------------------- /art/epicycles.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | class Epicycles extends Art { 5 | constructor() { 6 | super({ 7 | l1: 4, 8 | v1: 4, 9 | l2: 4, 10 | v2: 4, 11 | }); 12 | this.filename = "epicycles.js"; 13 | this.created = "12 Mar 2022"; 14 | } 15 | 16 | getDescription() { 17 | return ` 18 | Generate four numbers: Two representing the lengths of two arms, and two 19 | representing the speed at which they rotate. The two arms can range from 20 | 0 to half the width of the canvas. The two speeds can range from -1/10 to 21 | 1/10 (radians per unit of time). 22 | 23 | The first arm is affixed to the center of the canvas, and the second arm 24 | is affixed to the end of the first arm. Both arms begin pointing east. 25 | 26 | At each unit of time, increase the angle of the first arm according to 27 | v1 without rotating the second arm. Then, increase the angle 28 | of the second arm according to v2. Draw a dot at the end of 29 | the second arm. Repeat this process 2000 times. 30 | `; 31 | } 32 | 33 | draw(ctx, { l1, l2, v1, v2 }) { 34 | const w = ctx.canvas.width; 35 | const h = ctx.canvas.height; 36 | const s = Math.min(w, h); 37 | 38 | ctx.fillStyle = "rgb(0, 0, 0)"; 39 | 40 | const FRAMES = 2000; 41 | for (let i = 0; i < FRAMES; i++) { 42 | const radialUnit = 1 / 5; 43 | 44 | const x = 45 | (l1 / 2) * Math.cos((v1 - 0.5) * i * radialUnit) + 46 | (l2 / 2) * Math.cos((v2 - 0.5) * i * radialUnit); 47 | const y = 48 | (l1 / 2) * Math.sin((v1 - 0.5) * i * radialUnit) + 49 | (l2 / 2) * Math.sin((v2 - 0.5) * i * radialUnit); 50 | 51 | ctx.beginPath(); 52 | ctx.arc( 53 | project(x, -1, 1, w / 2 - s / 2, w / 2 + s / 2), 54 | project(y, -1, 1, h / 2 - s / 2, h / 2 + s / 2), 55 | _(4, s), 56 | 0, 57 | 2 * Math.PI 58 | ); 59 | ctx.fill(); 60 | } 61 | } 62 | } 63 | 64 | exports.Epicycles = Epicycles; 65 | -------------------------------------------------------------------------------- /art/fifteen.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | 3 | class Fifteen extends Art { 4 | constructor() { 5 | super({ 6 | moves: 32, 7 | }); 8 | this.filename = "fifteen.js"; 9 | this.created = "21 Feb 2022"; 10 | } 11 | 12 | getDescription() { 13 | return ` 14 | Begin with the initial 15 puzzle, 15 | where numbers are arranged in a 4x4 grid starting at 1 and increasing to 15. 16 | The last square is left empty. 17 | 18 | For each of the bytes in moves, split into four 2-bit pairs 19 | and use each of them to encode a single "move" N/E/S/W. 20 | 21 | For each individual "move," go to the empty space and attempt to grab the 22 | tile in the N/E/S/W direction. If there is no tile there (such as, if we are 23 | along an edge), skip the move. 24 | 25 | After each valid move, draw the current grid on the canvas. Use "black" for 26 | the empty square, and increase the lightness for each tile (tile 15 being white). 27 | `; 28 | } 29 | 30 | getEmptyPosition(grid) { 31 | for (let x = 0; x < 4; x++) { 32 | for (let y = 0; y < 4; y++) { 33 | if (grid[y][x] === 0) { 34 | return { x, y }; 35 | } 36 | } 37 | } 38 | } 39 | 40 | computeIdealLayout(w, h, grids) { 41 | const idealNegativeSpace = (w * h) / 2; 42 | let bestLayout = { negativeSpace: Infinity }; 43 | 44 | for (let columns = 1; columns < grids.length; columns++) { 45 | for (let puzzleSize = 8; puzzleSize < 200; puzzleSize += 4) { 46 | for (let padding = puzzleSize / 4; padding < puzzleSize; padding += 1) { 47 | let rows = Math.ceil(grids.length / columns); 48 | let layoutWidth = columns * puzzleSize + (columns + 1) * padding; 49 | let layoutHeight = rows * puzzleSize + (rows + 1) * padding; 50 | 51 | // Doesn't fit, continue 52 | if (layoutWidth > w || layoutHeight > h) { 53 | continue; 54 | } 55 | 56 | const negativeSpace = 57 | // Size of the canvas 58 | w * h - 59 | // Subtract the area of the puzzles 60 | puzzleSize * puzzleSize * columns * rows; 61 | 62 | if ( 63 | Math.abs(idealNegativeSpace - negativeSpace) < 64 | Math.abs(idealNegativeSpace - bestLayout.negativeSpace) 65 | ) { 66 | bestLayout = { 67 | negativeSpace, 68 | columns, 69 | rows, 70 | layoutWidth, 71 | layoutHeight, 72 | padding, 73 | puzzleSize, 74 | }; 75 | } 76 | } 77 | } 78 | } 79 | 80 | return bestLayout; 81 | } 82 | 83 | drawPuzzle(ctx, x, y, puzzleSize, grid) { 84 | const pixelSize = puzzleSize / 4; 85 | 86 | grid.forEach((n, idx) => { 87 | // prob scale 88 | const gray = (n / 15) * 255; 89 | ctx.fillStyle = `rgb(${gray}, ${gray}, ${gray})`; 90 | 91 | ctx.beginPath(); 92 | ctx.rect( 93 | x + pixelSize * (idx % 4), 94 | y + pixelSize * Math.floor(idx / 4), 95 | pixelSize, 96 | pixelSize 97 | ); 98 | ctx.fill(); 99 | }); 100 | } 101 | 102 | draw(ctx, { movesBuffer }) { 103 | const w = ctx.canvas.width; 104 | const h = ctx.canvas.height; 105 | 106 | const currentGrid = [ 107 | [1, 2, 3, 4], 108 | [5, 6, 7, 8], 109 | [9, 10, 11, 12], 110 | [13, 14, 15, 0], 111 | ]; 112 | 113 | const grids = [[].concat.apply([], currentGrid)]; 114 | 115 | // From each byte we get one of N/E/S/W, representing 116 | // the direction of the block we'll be moving 117 | const moves = [].concat( 118 | ...Array.from(movesBuffer).map((byte) => [ 119 | byte >> 6, 120 | (byte >> 4) % 4, 121 | (byte >> 2) % 4, 122 | byte % 4, 123 | ]) 124 | ); 125 | 126 | moves.forEach((move) => { 127 | const { x, y } = this.getEmptyPosition(currentGrid); 128 | const tmp = currentGrid[y][x]; 129 | let changed = false; 130 | if (move === 0 && y > 0) { 131 | currentGrid[y][x] = currentGrid[y - 1][x]; 132 | currentGrid[y - 1][x] = tmp; 133 | changed = true; 134 | } else if (move === 1 && x < 3) { 135 | currentGrid[y][x] = currentGrid[y][x + 1]; 136 | currentGrid[y][x + 1] = tmp; 137 | changed = true; 138 | } else if (move === 2 && y < 3) { 139 | currentGrid[y][x] = currentGrid[y + 1][x]; 140 | currentGrid[y + 1][x] = tmp; 141 | changed = true; 142 | } else if (move === 3 && x > 0) { 143 | currentGrid[y][x] = currentGrid[y][x - 1]; 144 | currentGrid[y][x - 1] = tmp; 145 | changed = true; 146 | } 147 | 148 | if (changed) { 149 | grids.push([].concat.apply([], currentGrid)); 150 | } 151 | }); 152 | 153 | const { rows, columns, layoutWidth, layoutHeight, padding, puzzleSize } = 154 | this.computeIdealLayout(w * 0.9, h * 0.9, grids); 155 | 156 | grids.forEach((grid, idx) => { 157 | const row = Math.floor(idx / columns); 158 | const column = idx % columns; 159 | 160 | const x = 161 | Math.floor(w / 2 - layoutWidth / 2) + 162 | column * puzzleSize + 163 | (column + 1) * padding; 164 | const y = 165 | Math.floor(h / 2 - layoutHeight / 2) + 166 | row * puzzleSize + 167 | (row + 1) * padding; 168 | 169 | this.drawPuzzle(ctx, x, y, puzzleSize, grid); 170 | }); 171 | } 172 | } 173 | 174 | exports.Fifteen = Fifteen; 175 | -------------------------------------------------------------------------------- /art/fraction.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, bigIntOfBuffer } = require("./util.js"); 3 | 4 | class Fraction extends Art { 5 | constructor() { 6 | super({ 7 | a: 4, 8 | b: 4, 9 | }); 10 | this.filename = "fraction.js"; 11 | this.created = "29 Apr 2021"; 12 | } 13 | 14 | getDescription() { 15 | return ` 16 | Convert a and b into integers and form them into a proper fraction 17 | (a fraction whose numerator is less than the denominator). 18 | 19 | We then show the process of turning this fraction into a sum of unit fractions. This is commonly 20 | referred to as an Egyptian fraction. 21 | 22 | In particular, our algorithm for doing so is a 23 | greedy one. 24 | The numbers grow fast very quickly. 25 | `; 26 | } 27 | 28 | drawFraction(ctx, fontSize, lineHeight, numer, denom, x, y, maxWidth) { 29 | // If we've halted iteration or run into an error 30 | if (denom === "...") { 31 | ctx.fillText("...", x, y + lineHeight); 32 | return ctx.measureText("..."); 33 | } 34 | 35 | const oneCharWidth = ctx.measureText("0").width; 36 | const numerWidth = ctx.measureText(numer.toString()).width; 37 | const denomWidth = Math.min( 38 | Math.floor(maxWidth / oneCharWidth) * oneCharWidth, 39 | ctx.measureText(denom.toString()).width 40 | ); 41 | 42 | ctx.fillText( 43 | numer.toString(), 44 | x + (denomWidth - numerWidth) / 2, 45 | y + fontSize 46 | ); 47 | ctx.beginPath(); 48 | ctx.moveTo(x, y + lineHeight); 49 | ctx.lineTo(x + denomWidth, y + lineHeight); 50 | ctx.stroke(); 51 | 52 | const charsPerLine = Math.floor(maxWidth / oneCharWidth); 53 | const denomString = denom.toString(); 54 | let line = 0; 55 | 56 | for (let idx = 0; idx < denomString.length; idx += charsPerLine, line++) { 57 | ctx.fillText( 58 | denomString.slice(idx, idx + charsPerLine), 59 | x, 60 | y + fontSize * (line + 2) 61 | ); 62 | } 63 | 64 | return { 65 | width: denomWidth, 66 | height: lineHeight + line * fontSize, 67 | }; 68 | } 69 | 70 | draw(ctx, { aBuffer, bBuffer }) { 71 | const a = bigIntOfBuffer(aBuffer); 72 | const b = bigIntOfBuffer(bBuffer); 73 | 74 | let numer = a < b ? a : b; 75 | let denom = a < b ? b : a; 76 | const fractions = []; 77 | 78 | const w = ctx.canvas.width; 79 | const h = ctx.canvas.height; 80 | const leftPadding = _(30, w); 81 | const topPadding = _(30, h); 82 | 83 | const f = 36; 84 | const fontSize = _(f, w); 85 | const lineHeight = _(f * 1.2, w); 86 | 87 | const equationPadding = _(30, w); 88 | 89 | ctx.font = `${fontSize}px monospace`; 90 | ctx.fillStyle = "rgb(0, 0, 0)"; 91 | const equalsSignWidth = ctx.measureText("=").width; 92 | 93 | const fractionWidth = this.drawFraction( 94 | ctx, 95 | fontSize, 96 | lineHeight, 97 | numer, 98 | denom, 99 | leftPadding, 100 | topPadding, 101 | Infinity // lol 102 | ).width; 103 | 104 | const room = 105 | w - 106 | 2 * leftPadding - 107 | fractionWidth - 108 | equalsSignWidth - 109 | 2 * equationPadding; 110 | 111 | let idx = 0; 112 | const MAX_ITERATION = 20; 113 | for (; numer > 0 && idx < MAX_ITERATION; idx++) { 114 | if (denom % numer === 0n) { 115 | fractions.push(denom / numer); 116 | break; 117 | } 118 | 119 | let greedy = denom / numer + 1n; 120 | fractions.push(greedy); 121 | 122 | // numer/denom - 1/greedy 123 | // (numer*greedy)/(denom*greedy) - denom/(denom*greedy) 124 | try { 125 | numer = numer * greedy - denom; 126 | denom = denom * greedy; 127 | } catch (e) { 128 | break; 129 | } 130 | } 131 | 132 | // If we've run into an error or halted iteration for time 133 | if (denom % numer !== 0n) { 134 | fractions.push("..."); 135 | } 136 | 137 | let y = topPadding; 138 | fractions.forEach((denom, idx) => { 139 | // draw = or + 140 | ctx.fillText( 141 | idx === 0 ? "=" : "+", 142 | leftPadding + fractionWidth + equationPadding, 143 | y + 1.25 * lineHeight // 1.25 is arbitrary but the equals sign is not very tall 144 | ); 145 | 146 | // draw fraction 147 | const { height } = this.drawFraction( 148 | ctx, 149 | fontSize, 150 | lineHeight, 151 | 1, 152 | denom, 153 | leftPadding + fractionWidth + equalsSignWidth + 2 * equationPadding, 154 | y, 155 | room 156 | ); 157 | 158 | y += height; 159 | }); 160 | } 161 | } 162 | 163 | exports.Fraction = Fraction; 164 | -------------------------------------------------------------------------------- /art/honeycomb.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class HoneyComb extends Art { 5 | constructor() { 6 | super({ 7 | paths: 19, 8 | }); 9 | 10 | this.filename = "honeycomb.js"; 11 | this.created = "13 Feb 2022"; 12 | } 13 | 14 | getDescription({ pathsBuffer }) { 15 | return ` 16 | Arrange ${pathsBuffer.length} hexagons into one larger hexagon. For 17 | each of the ${pathsBuffer.length} bytes in the buffer: If the byte is 18 | greater than 216, do nothing. Otherwise, use the byte value 19 | to generate two numbers a, b between 0 and 5. 20 | 21 | Draw a path connecting side a to side b, or a 22 | dead end if they are equal. 23 | `; 24 | } 25 | 26 | drawHexagon(ctx, pattern, x, y, sideLength) { 27 | // Edge 28 | ctx.beginPath(); 29 | for (let i = 0; i <= 6; i++) { 30 | const theta = Math.PI / 6 + (i * 2 * Math.PI) / 6; 31 | const [px, py] = [ 32 | x + sideLength * Math.cos(theta), 33 | y + sideLength * Math.sin(theta), 34 | ]; 35 | 36 | if (i === 0) { 37 | ctx.moveTo(px, py); 38 | } else { 39 | ctx.lineTo(px, py); 40 | } 41 | } 42 | ctx.stroke(); 43 | 44 | const [a, b] = [Math.floor(pattern / 36), pattern % 6]; 45 | 46 | const gap = sideLength / 4; 47 | const l = (sideLength * Math.sqrt(3)) / 2; 48 | 49 | const aTheta = (a * 2 * Math.PI) / 6; 50 | const bTheta = (b * 2 * Math.PI) / 6; 51 | 52 | if (a === b) { 53 | ctx.beginPath(); 54 | ctx.moveTo( 55 | x + l * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2), 56 | y + l * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2) 57 | ); 58 | ctx.lineTo( 59 | x + gap * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2), 60 | y + gap * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2) 61 | ); 62 | 63 | ctx.lineTo( 64 | x + gap * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2), 65 | y + gap * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2) 66 | ); 67 | ctx.lineTo( 68 | x + l * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2), 69 | y + l * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2) 70 | ); 71 | 72 | ctx.stroke(); 73 | } else { 74 | // A 75 | ctx.beginPath(); 76 | ctx.moveTo( 77 | x + l * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2), 78 | y + l * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2) 79 | ); 80 | ctx.lineTo( 81 | x + gap * Math.cos(aTheta) - (gap / 2) * Math.cos(aTheta - Math.PI / 2), 82 | y + gap * Math.sin(aTheta) - (gap / 2) * Math.sin(aTheta - Math.PI / 2) 83 | ); 84 | 85 | // todo - arc? 86 | 87 | ctx.lineTo( 88 | x + gap * Math.cos(bTheta) + (gap / 2) * Math.cos(bTheta - Math.PI / 2), 89 | y + gap * Math.sin(bTheta) + (gap / 2) * Math.sin(bTheta - Math.PI / 2) 90 | ); 91 | ctx.lineTo( 92 | x + l * Math.cos(bTheta) + (gap / 2) * Math.cos(bTheta - Math.PI / 2), 93 | y + l * Math.sin(bTheta) + (gap / 2) * Math.sin(bTheta - Math.PI / 2) 94 | ); 95 | ctx.stroke(); 96 | 97 | // B 98 | 99 | ctx.beginPath(); 100 | ctx.moveTo( 101 | x + l * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2), 102 | y + l * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2) 103 | ); 104 | ctx.lineTo( 105 | x + gap * Math.cos(aTheta) + (gap / 2) * Math.cos(aTheta - Math.PI / 2), 106 | y + gap * Math.sin(aTheta) + (gap / 2) * Math.sin(aTheta - Math.PI / 2) 107 | ); 108 | 109 | // todo - arc? 110 | 111 | ctx.lineTo( 112 | x + gap * Math.cos(bTheta) - (gap / 2) * Math.cos(bTheta - Math.PI / 2), 113 | y + gap * Math.sin(bTheta) - (gap / 2) * Math.sin(bTheta - Math.PI / 2) 114 | ); 115 | ctx.lineTo( 116 | x + l * Math.cos(bTheta) - (gap / 2) * Math.cos(bTheta - Math.PI / 2), 117 | y + l * Math.sin(bTheta) - (gap / 2) * Math.sin(bTheta - Math.PI / 2) 118 | ); 119 | ctx.stroke(); 120 | } 121 | } 122 | 123 | draw(ctx, { pathsBuffer }) { 124 | const w = ctx.canvas.width; 125 | const h = ctx.canvas.height; 126 | const s = Math.max(w, h); 127 | ctx.lineWidth = _(3, s); 128 | 129 | const sideLength = _(80, s); 130 | 131 | // https://stackoverflow.com/questions/2459402/hexagonal-grid-coordinates-to-pixel-coordinates 132 | [ 133 | [-2, 2], 134 | [-1, 2], 135 | [0, 2], 136 | [-2, 1], 137 | [-1, 1], 138 | [0, 1], 139 | [1, 1], 140 | [-2, 0], 141 | [-1, 0], 142 | [0, 0], 143 | [1, 0], 144 | [2, 0], 145 | [-1, -1], 146 | [0, -1], 147 | [1, -1], 148 | [2, -1], 149 | [0, -2], 150 | [1, -2], 151 | [2, -2], 152 | ].forEach(([a, b], idx) => { 153 | const x = w / 2 - Math.sqrt(3) * sideLength * (b / 2 + a); 154 | const y = h / 2 - (3 / 2) * sideLength * b; 155 | if (pathsBuffer[idx] < 216) { 156 | this.drawHexagon(ctx, pathsBuffer[idx], x, y, sideLength); 157 | } 158 | }); 159 | } 160 | } 161 | 162 | exports.HoneyComb = HoneyComb; 163 | -------------------------------------------------------------------------------- /art/julia.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | class Julia extends Art { 5 | constructor() { 6 | super({ 7 | real: 16, 8 | imaginary: 16, 9 | }); 10 | // not very interesting 11 | this.hidden = true; 12 | this.filename = "julia.js"; 13 | this.created = "10 Oct 2023"; 14 | } 15 | 16 | getDescription({ real, imaginary }) { 17 | return ` 18 | z = z^2 + ${this.getReal(real)} + ${this.getImaginary(imaginary)}i 19 | `; 20 | } 21 | 22 | getReal(real) { 23 | return project(real, 0, 1, -1, 1); 24 | } 25 | 26 | getImaginary(imaginary) { 27 | return project(imaginary, 0, 1, -1, 1); 28 | } 29 | 30 | draw(ctx, { real, imaginary }) { 31 | const cr = this.getReal(real); 32 | const ci = this.getImaginary(imaginary); 33 | 34 | const w = ctx.canvas.width; 35 | const h = ctx.canvas.height; 36 | 37 | const s = _(8, w); 38 | const ITERATIONS = 20; 39 | 40 | const aspectRatio = w / h; 41 | 42 | for (let x = 0; x < w; x += s) { 43 | for (let y = 0; y < h; y += s) { 44 | const im = project(y, 0, h, -1, 1); 45 | const re = project(x, 0, w, -1 * aspectRatio, 1 * aspectRatio); 46 | 47 | // TODO: after this works, a "flow" would be more interesting 48 | let zr = re; 49 | let zi = im; 50 | for (let i = 0; i < ITERATIONS; i++) { 51 | // z = z^2 + c 52 | const zr2 = zr * zr - zi * zi; 53 | const zi2 = 2 * zr * zi; 54 | 55 | zr = zr2 + cr; 56 | zi = zi2 + ci; 57 | } 58 | 59 | let shade = project(zr * zr + zi * zi, 0, 4, 0, 255); 60 | if (Number.isNaN(shade)) { 61 | shade = 255; 62 | } 63 | 64 | if (shade < 0) { 65 | shade = 0; 66 | } 67 | if (shade > 255) { 68 | shade = 255; 69 | } 70 | 71 | ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`; 72 | ctx.fillRect(x, y, s, s); 73 | } 74 | } 75 | 76 | const s2 = _(32, w); 77 | for (let x = 0; x < w; x += s2) { 78 | for (let y = 0; y < h; y += s2) { 79 | const re = project(x, 0, w, -1 * aspectRatio, 1 * aspectRatio); 80 | const im = project(y, 0, h, -1, 1); 81 | 82 | // Get the next coordinate 83 | const zr2 = re * re - im * im; 84 | const zi2 = 2 * re * im; 85 | const re2 = zr2 + cr; 86 | const im2 = zi2 + ci; 87 | 88 | const dy = im2 - im; 89 | const dx = re2 - re; 90 | const r = project(Math.sqrt(dx * dx + dy * dy), 0, 2, 0, s2 / 2); 91 | const theta = Math.atan2(dy, dx); 92 | 93 | // draw a line of length s, centered as (x + s/2, y + s/2), with angle theta 94 | ctx.strokeStyle = "rgb(0, 0, 0)"; 95 | ctx.beginPath(); 96 | ctx.moveTo(x + s2 / 2, y + s2 / 2); 97 | ctx.lineTo( 98 | x + s2 / 2 + r * Math.cos(theta), 99 | y + s2 / 2 + r * Math.sin(theta) 100 | ); 101 | ctx.stroke(); 102 | 103 | // let shade = project(zr * zr + zi * zi, 0, 4, 0, 255); 104 | // if (Number.isNaN(shade)) { 105 | // shade = 255; 106 | // } 107 | 108 | // if (shade < 0) { 109 | // shade = 0; 110 | // } 111 | // if (shade > 255) { 112 | // shade = 255; 113 | // } 114 | 115 | // ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`; 116 | // ctx.fillRect(x, y, s, s); 117 | } 118 | } 119 | } 120 | } 121 | 122 | exports.Julia = Julia; 123 | -------------------------------------------------------------------------------- /art/knots.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | class Knots extends Art { 5 | constructor() { 6 | super({ 7 | values: 32, 8 | }); 9 | 10 | this.filename = "knots.js"; 11 | this.created = "20 Feb 2022"; 12 | } 13 | 14 | getDescription() { 15 | return ` 16 | For each byte in in the buffer, place a point on a line. Extend an arc 17 | from the previous point to the current point - alternating between above 18 | and below the horizon. The radius of the arc should be halfway the distance 19 | between the two points. 20 | 21 | This technique comes from an excellent Numberphile video on the 22 | Recamán sequence. 23 | 24 | I previously used this technique to draw knots using the values of the 25 | Collatz sequence, instead 26 | of random numbers from a hash as in this piece. 27 | `; 28 | } 29 | 30 | draw(ctx, { valuesBuffer }) { 31 | // Side length of the bounding box 32 | const s = Math.min(ctx.canvas.width, ctx.canvas.height); 33 | 34 | // Width of 3 based on `s` 35 | ctx.lineWidth = _(3, s); 36 | 37 | // Padding on either side 38 | const PADDING = 0.1 * s; 39 | 40 | for (let i = 1; i < valuesBuffer.length; i++) { 41 | const a = project(valuesBuffer[i - 1], 0, 255, PADDING, s - PADDING); 42 | const b = project(valuesBuffer[i], 0, 255, PADDING, s - PADDING); 43 | const midpoint = (a + b) / 2; 44 | 45 | ctx.beginPath(); 46 | 47 | ctx.arc( 48 | // Compute the x-coordinate of the center 49 | ctx.canvas.width / 2 - s / 2 + midpoint, 50 | 51 | // y-coordinate is always halfway up 52 | ctx.canvas.height / 2, 53 | 54 | // radius is half the distance between `a` and `b` 55 | Math.abs(a - b) / 2, 56 | 57 | // We want the arc to be above the line, then below the line 58 | i % 2 ? 0 : Math.PI, 59 | i % 2 ? Math.PI : 2 * Math.PI 60 | ); 61 | 62 | ctx.stroke(); 63 | } 64 | } 65 | } 66 | 67 | exports.Knots = Knots; 68 | -------------------------------------------------------------------------------- /art/mario.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Mario extends Art { 5 | // requires SRC_ROOT/vendor/roms/mariobros.nes 6 | constructor() { 7 | super({ 8 | inputs: 32, 9 | }); 10 | this.hidden = true; 11 | this.filename = "mario.js"; 12 | this.created = "18 Apr 2021"; 13 | } 14 | 15 | drawBuffer(ctx, frameBuffer) { 16 | const WIDTH = 256; 17 | const HEIGHT = 240; 18 | const verticalPadding = 8; 19 | const horizontalPadding = 8; 20 | 21 | // https://github.com/bfirsh/jsnes-web/blob/master/src/Screen.js 22 | const imageData = ctx.getImageData( 23 | 0, 24 | 0, 25 | WIDTH - 2 * horizontalPadding, 26 | HEIGHT - 2 * verticalPadding 27 | ); 28 | const buf = new ArrayBuffer(imageData.data.length); 29 | const buf32 = new Uint32Array(buf); 30 | 31 | // TODO: 10px border on either side 32 | for (let y = verticalPadding; y < HEIGHT - verticalPadding; ++y) { 33 | for (let x = horizontalPadding; x < WIDTH - horizontalPadding; ++x) { 34 | const nesPx = y * WIDTH + x; 35 | const bufPx = 36 | (y - verticalPadding) * (WIDTH - 2 * horizontalPadding) + 37 | (x - horizontalPadding); 38 | // Convert pixel from NES BGR to canvas ABGR 39 | buf32[bufPx] = 0xff000000 | frameBuffer[nesPx]; // Full alpha 40 | } 41 | } 42 | 43 | imageData.data.set(new Uint8ClampedArray(buf)); 44 | ctx.putImageData(imageData, 0, 0); 45 | 46 | // turn off antialiasing 47 | ctx.imageSmoothingEnabled = false; 48 | const scale = Math.min( 49 | ctx.canvas.width / (WIDTH - 2 * horizontalPadding), 50 | ctx.canvas.height / (HEIGHT - 2 * verticalPadding) 51 | ); 52 | 53 | ctx.scale(scale, scale); 54 | // TODO - center and draw some black/white bars 55 | ctx.drawImage(ctx.canvas, 0, 0); 56 | } 57 | 58 | buttonPress(nes, button, holdFrames = 1) { 59 | nes.buttonDown(1, button); 60 | for (let i = 0; i < holdFrames; i++) { 61 | nes.frame(); 62 | } 63 | nes.buttonUp(1, button); 64 | nes.frame(); 65 | } 66 | 67 | draw(ctx, { inputsBuffer }, { nes, getFrameBuffer }) { 68 | const BUTTON_A = 0; 69 | const BUTTON_B = 1; 70 | const BUTTON_UP = 4; 71 | const BUTTON_DOWN = 5; 72 | const BUTTON_LEFT = 6; 73 | const BUTTON_RIGHT = 7; 74 | 75 | inputsBuffer.forEach((v) => { 76 | const options = [ 77 | //BUTTON_LEFT, 78 | BUTTON_UP, 79 | BUTTON_RIGHT, 80 | BUTTON_RIGHT, 81 | BUTTON_RIGHT, 82 | BUTTON_RIGHT, 83 | BUTTON_LEFT, 84 | BUTTON_A, 85 | BUTTON_A, 86 | BUTTON_B, 87 | ]; 88 | 89 | const button = options[Math.floor((v / 256) * options.length)]; 90 | // hold for 10 frames 91 | this.buttonPress(nes, button, 10); 92 | }); 93 | 94 | this.drawBuffer(ctx, getFrameBuffer()); 95 | } 96 | } 97 | 98 | exports.Mario = Mario; 99 | -------------------------------------------------------------------------------- /art/nes.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Nes extends Art { 5 | // requires SRC_ROOT/vendor/roms/**/*.nes 6 | constructor() { 7 | super({ 8 | rom: 12, 9 | }); 10 | this.hidden = true; 11 | this.filename = "nes.js"; 12 | this.created = "1 Mar 2022"; 13 | } 14 | 15 | drawBuffer(ctx, secondCtx, frameBuffer) { 16 | const WIDTH = 256; 17 | const HEIGHT = 240; 18 | const verticalPadding = 8; 19 | const horizontalPadding = 8; 20 | 21 | const w = WIDTH - 2 * horizontalPadding; 22 | const h = HEIGHT - 2 * verticalPadding; 23 | 24 | // https://github.com/bfirsh/jsnes-web/blob/master/src/Screen.js 25 | const imageData = secondCtx.getImageData(0, 0, w, h); 26 | 27 | const buf = new ArrayBuffer(imageData.data.length); 28 | const buf32 = new Uint32Array(buf); 29 | 30 | // TODO: 10px border on either side 31 | for (let y = verticalPadding; y < HEIGHT - verticalPadding; ++y) { 32 | for (let x = horizontalPadding; x < WIDTH - horizontalPadding; ++x) { 33 | const nesPx = y * WIDTH + x; 34 | const bufPx = (y - verticalPadding) * w + (x - horizontalPadding); 35 | // Convert pixel from NES BGR to canvas ABGR 36 | buf32[bufPx] = 0xff000000 | frameBuffer[nesPx]; // Full alpha 37 | } 38 | } 39 | 40 | imageData.data.set(new Uint8ClampedArray(buf)); 41 | secondCtx.putImageData(imageData, 0, 0); 42 | 43 | const scale = Math.min( 44 | Math.floor(ctx.canvas.width / w), 45 | Math.floor(ctx.canvas.height / h) 46 | ); 47 | 48 | ctx.scale(scale); 49 | 50 | ctx.drawImage( 51 | secondCtx.canvas, 52 | 0, 53 | 0, 54 | w, 55 | h, 56 | Math.floor(ctx.canvas.width / 2 - (w * scale) / 2), 57 | Math.floor(ctx.canvas.height / 2 - (h * scale) / 2), 58 | w * scale, 59 | h * scale 60 | ); 61 | } 62 | 63 | draw(ctx, { rom }, { nes, getFrameBuffer, roms, fs, path, secondCtx }) { 64 | let romPath = roms[Math.floor(rom * roms.length)]; 65 | let romData = fs.readFileSync(romPath, { 66 | encoding: "binary", 67 | }); 68 | 69 | let loaded = false; 70 | 71 | while (!loaded) { 72 | try { 73 | nes.loadROM(romData); 74 | loaded = true; 75 | } catch (e) { 76 | // random :( 77 | romPath = roms[Math.floor(Math.random() * roms.length)]; 78 | romData = fs.readFileSync(romPath, { 79 | encoding: "binary", 80 | }); 81 | } 82 | } 83 | 84 | // Wait 200 frames 85 | for (let i = 0; i < 500; i++) { 86 | try { 87 | nes.frame(); 88 | } catch (e) { 89 | continue; 90 | } 91 | } 92 | 93 | this.drawBuffer(ctx, secondCtx, getFrameBuffer()); 94 | ctx.font = `bold ${_(30, ctx.canvas.width)}px monospace`; 95 | ctx.textAlign = "center"; 96 | ctx.fillStyle = "rgb(0, 0, 0)"; 97 | ctx.fillText( 98 | path.parse(romPath).name, 99 | ctx.canvas.width / 2, 100 | ctx.canvas.height - 40 101 | ); 102 | } 103 | } 104 | 105 | exports.Nes = Nes; 106 | -------------------------------------------------------------------------------- /art/network.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | const NUM_POINTS = 12; 5 | 6 | class Network extends Art { 7 | constructor() { 8 | super({ 9 | x: 1, 10 | y: 1, 11 | others: (NUM_POINTS - 1) * 2, 12 | }); 13 | this.filename = "network.js"; 14 | this.created = "22 May 2022"; 15 | } 16 | 17 | getDescription() { 18 | return ` 19 | Read ${NUM_POINTS} byte pairs from the array, representing 20 | a series of (x, y) points. Draw each point on the canvas, 21 | connecting it to every other point. 22 | 23 | Then, determine the 24 | Minimum spanning tree 25 | of the points: the continguous series of lines which connect to every point 26 | while minimizing the total length of the lines. 27 | 28 | You can use 29 | Kruskal's algorithm 30 | to determine this. 31 | 32 |
 33 |   algorithm Kruskal(G) is
 34 |     F:= ∅
 35 |     for each v ∈ G.V do
 36 |         MAKE-SET(v)
 37 |     for each (u, v) in G.E ordered by weight(u, v), increasing do
 38 |         if FIND-SET(u) ≠ FIND-SET(v) then
 39 |             F:= F ∪ {(u, v)} ∪ {(v, u)}
 40 |             UNION(FIND-SET(u), FIND-SET(v))
 41 |     return F
42 | 43 | Draw the lines in the Minimum spanning tree in bold. 44 | `; 45 | } 46 | 47 | kruskal({ edges, vertices }) { 48 | // https://en.wikipedia.org/wiki/Kruskal%27s_algorithm 49 | // 50 | // F:= ∅ 51 | // for each v ∈ G.V do 52 | // MAKE-SET(v) 53 | // for each (u, v) in G.E ordered by weight(u, v), increasing do 54 | // if FIND-SET(u) ≠ FIND-SET(v) then 55 | // F:= F ∪ {(u, v)} ∪ {(v, u)} 56 | // UNION(FIND-SET(u), FIND-SET(v)) 57 | // return F 58 | 59 | const F = new Set([]); 60 | const disjointSets = new Set( 61 | vertices.map((v) => new Set([`${v[0]},${v[1]}`])) 62 | ); 63 | 64 | const sortedEdges = edges.slice(); 65 | sortedEdges.sort((e1, e2) => { 66 | // Oh dear 67 | const d1 = 68 | (e1[0][0] - e1[1][0]) * (e1[0][0] - e1[1][0]) + 69 | (e1[0][1] - e1[1][1]) * (e1[0][1] - e1[1][1]); 70 | const d2 = 71 | (e2[0][0] - e2[1][0]) * (e2[0][0] - e2[1][0]) + 72 | (e2[0][1] - e2[1][1]) * (e2[0][1] - e2[1][1]); 73 | return d1 - d2; 74 | }); 75 | 76 | console.log( 77 | sortedEdges.map( 78 | ([u, v]) => 79 | (u[1] - u[0]) * (u[1] - u[0]) + (v[1] - v[0]) * (v[1] - v[0]) 80 | ) 81 | ); 82 | 83 | sortedEdges.forEach(([u, v]) => { 84 | const uSet = [...disjointSets].find((set) => set.has(`${u[0]},${u[1]}`)); 85 | const vSet = [...disjointSets].find((set) => set.has(`${v[0]},${v[1]}`)); 86 | 87 | if (uSet !== vSet) { 88 | F.add([u, v]); 89 | disjointSets.delete(uSet); 90 | disjointSets.delete(vSet); 91 | disjointSets.add(new Set([...uSet, ...vSet])); 92 | } 93 | }); 94 | 95 | return [...F]; 96 | } 97 | 98 | draw(ctx, { xBuffer, yBuffer, othersBuffer }) { 99 | const w = ctx.canvas.width; 100 | const h = ctx.canvas.height; 101 | 102 | const coords = [...xBuffer, ...yBuffer, ...othersBuffer]; 103 | 104 | const vertices = []; 105 | for (let i = 0; i < NUM_POINTS; i++) { 106 | vertices.push([coords[2 * i], coords[2 * i + 1]]); 107 | } 108 | 109 | const edges = []; 110 | for (let i = 0; i < NUM_POINTS - 1; i++) { 111 | for (let j = i + 1; j < NUM_POINTS; j++) { 112 | edges.push([ 113 | [coords[2 * i], coords[2 * i + 1]], 114 | [coords[2 * j], coords[2 * j + 1]], 115 | ]); 116 | } 117 | } 118 | 119 | // Padding 120 | const p = _(120, w); 121 | 122 | // Draw the network of lines 123 | ctx.lineWidth = _(1, w); 124 | edges.forEach((edge) => { 125 | ctx.beginPath(); 126 | ctx.moveTo( 127 | project(edge[0][0], 0, 256, p, w - p), 128 | project(edge[0][1], 0, 256, p, h - p) 129 | ); 130 | ctx.lineTo( 131 | project(edge[1][0], 0, 256, p, w - p), 132 | project(edge[1][1], 0, 256, p, h - p) 133 | ); 134 | ctx.stroke(); 135 | }); 136 | 137 | // Draw the minimum spanning tree 138 | const mst = this.kruskal({ edges, vertices }); 139 | ctx.lineWidth = _(10, w); 140 | mst.forEach((edge) => { 141 | ctx.beginPath(); 142 | ctx.moveTo( 143 | project(edge[0][0], 0, 256, p, w - p), 144 | project(edge[0][1], 0, 256, p, h - p) 145 | ); 146 | ctx.lineTo( 147 | project(edge[1][0], 0, 256, p, w - p), 148 | project(edge[1][1], 0, 256, p, h - p) 149 | ); 150 | ctx.stroke(); 151 | }); 152 | 153 | // Draw circles for every node 154 | ctx.fillStyle = `rgb(255, 255, 255)`; 155 | vertices.forEach(([x, y]) => { 156 | ctx.beginPath(); 157 | ctx.arc( 158 | project(x, 0, 256, p, w - p), 159 | project(y, 0, 256, p, h - p), 160 | _(15, w), 161 | 0, 162 | 2 * Math.PI 163 | ); 164 | ctx.fill(); 165 | ctx.stroke(); 166 | }); 167 | } 168 | } 169 | 170 | exports.Network = Network; 171 | -------------------------------------------------------------------------------- /art/noise.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Noise extends Art { 5 | constructor() { 6 | super({ 7 | test: 32, 8 | }); 9 | this.filename = "noise.js"; 10 | this.created = "09 Jun 2021"; 11 | } 12 | 13 | getDescription() { 14 | return ` 15 | Divide the canvas into squares and color square (x, y) 16 | a shade of gray (0-255) using the formula: 17 | 18 | Math.abs(Math.sin(test * x * y)) * 256 19 | `; 20 | } 21 | 22 | getSize(ctx) { 23 | return _(12, ctx.canvas.width); 24 | } 25 | 26 | draw(ctx, { test }) { 27 | const s = this.getSize(ctx); 28 | 29 | for (let x = 0; x < ctx.canvas.width; x += s) { 30 | for (let y = 0; y < ctx.canvas.height; y += s) { 31 | const g = Math.floor( 32 | Math.abs(Math.sin(test * (x / s + 1) * (y / s + 1))) * 256 33 | ); 34 | ctx.fillStyle = `rgb(${g}, ${g}, ${g})`; 35 | ctx.fillRect(x, y, s, s); 36 | } 37 | } 38 | } 39 | } 40 | 41 | exports.Noise = Noise; 42 | -------------------------------------------------------------------------------- /art/pieces.js: -------------------------------------------------------------------------------- 1 | const { Circle } = require("./circle.js"); 2 | const { Boxes } = require("./boxes.js"); 3 | const { Stocks } = require("./stocks.js"); 4 | const { Collatz } = require("./collatz.js"); 5 | const { Mario } = require("./mario.js"); 6 | const { Chess } = require("./chess.js"); 7 | const { Walk } = require("./walk.js"); 8 | const { Fraction } = require("./fraction.js"); 9 | const { Semicircle } = require("./semicircle.js"); 10 | const { Turing } = require("./turing.js"); 11 | const { Divisions } = require("./divisions.js"); 12 | const { QuasiFlake } = require("./quasiflake.js"); 13 | const { Combinators } = require("./combinators.js"); 14 | const { Noise } = require("./noise.js"); 15 | const { Sandpiles } = require("./sandpiles.js"); 16 | const { Element } = require("./element.js"); 17 | const { Rings } = require("./rings.js"); 18 | const { HoneyComb } = require("./honeycomb.js"); 19 | const { Knots } = require("./knots.js"); 20 | const { Fifteen } = require("./fifteen.js"); 21 | const { Automata } = require("./automata.js"); 22 | const { Nes } = require("./nes.js"); 23 | const { Epicycles } = require("./epicycles.js"); 24 | const { Network } = require("./network.js"); 25 | const { Voronoi } = require("./voronoi.js"); 26 | const { Julia } = require("./julia.js"); 27 | const { ThreeBody } = require("./three-body.js"); 28 | 29 | module.exports = { 30 | circles: Circle, 31 | boxes: Boxes, 32 | stocks: Stocks, 33 | collatz: Collatz, 34 | mario: Mario, 35 | chess: Chess, 36 | walk: Walk, 37 | fraction: Fraction, 38 | semicircle: Semicircle, 39 | turing: Turing, 40 | divisions: Divisions, 41 | quasiflake: QuasiFlake, 42 | combinators: Combinators, 43 | noise: Noise, 44 | sandpiles: Sandpiles, 45 | element: Element, 46 | rings: Rings, 47 | honeycomb: HoneyComb, 48 | knots: Knots, 49 | fifteen: Fifteen, 50 | automata: Automata, 51 | nes: Nes, 52 | epicycles: Epicycles, 53 | network: Network, 54 | voronoi: Voronoi, 55 | julia: Julia, 56 | threebody: ThreeBody, 57 | }; 58 | -------------------------------------------------------------------------------- /art/quasiflake.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class QuasiFlake extends Art { 5 | constructor() { 6 | super({ 7 | fractures: 32, 8 | }); 9 | this.hidden = true; 10 | this.filename = "quasiflake.js"; 11 | this.created = "05 Jun 2021"; 12 | } 13 | 14 | getDescription({ fracturesBuffer }) { 15 | return ` 16 | Begin with three line segments forming an equilateral triangle. For each 17 | of the ${fracturesBuffer.length} bytes in fractures, grab 18 | the (byte % segments.length)th segment and split it into three sections 19 | of equal length. Remove the middle segment, replacing it with two segments of 20 | the same length arranged to form a mountain. 21 | 22 | If we repeated this process indefinitely on every segment (and not just those 23 | selected with fractures), we would form a 24 | Koch snowflake. 25 | `; 26 | } 27 | 28 | draw(ctx, { fracturesBuffer }) { 29 | let lines = [ 30 | { theta: -Math.PI / 3, r: 1 }, 31 | { theta: Math.PI, r: 1 }, 32 | { theta: Math.PI / 3, r: 1 }, 33 | ]; 34 | fracturesBuffer.forEach((byte) => { 35 | const idx = byte % lines.length; 36 | const line = lines[idx]; 37 | 38 | // ___ -> _/\_ 39 | lines.splice( 40 | idx, 41 | 1, 42 | { 43 | theta: line.theta, 44 | r: line.r / 3, 45 | }, 46 | { 47 | theta: line.theta + Math.PI / 3, 48 | r: line.r / 3, 49 | }, 50 | { 51 | theta: line.theta - Math.PI / 3, 52 | r: line.r / 3, 53 | }, 54 | { 55 | theta: line.theta, 56 | r: line.r / 3, 57 | } 58 | ); 59 | }); 60 | 61 | let w = ctx.canvas.width; 62 | let h = ctx.canvas.height; 63 | 64 | // Center as large a square as possible on the canvas 65 | ctx.lineWidth = _(5, w); 66 | let scale = 0.75 * Math.min(w, h); 67 | 68 | let x = w / 2; 69 | let y = h / 2 - (scale * Math.sqrt(3)) / 3; 70 | 71 | ctx.beginPath(); 72 | ctx.moveTo(x, y); 73 | 74 | lines.forEach(({ theta, r }) => { 75 | x += r * scale * Math.cos(theta); 76 | y -= r * scale * Math.sin(theta); 77 | ctx.lineTo(x, y); 78 | }); 79 | 80 | ctx.stroke(); 81 | } 82 | } 83 | 84 | exports.QuasiFlake = QuasiFlake; 85 | -------------------------------------------------------------------------------- /art/rings.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | class Rings extends Art { 5 | constructor() { 6 | super({ 7 | init: 2, 8 | thickness: 5, 9 | changes: 20, 10 | }); 11 | 12 | this.filename = "rings.js"; 13 | this.created = "30 Jan 2022"; 14 | } 15 | 16 | getDescription() { 17 | return ` 18 | Start with a ring of radius init and width of 19 | thickness. For each subsequent byte in changes, 20 | adjust the radius where a byte less than 128 decreases it, and a byte 21 | above 128 increases it. 22 | `; 23 | } 24 | 25 | draw(ctx, { init, thickness, changesBuffer }) { 26 | ctx.lineWidth = 3; 27 | 28 | const w = ctx.canvas.width; 29 | const h = ctx.canvas.height; 30 | const e = _(thickness * 40 + 10, w); 31 | 32 | const scale = _(30, w); 33 | let currentRadius = _(init * 40, w); 34 | changesBuffer.forEach((byte, idx) => { 35 | const x = project(idx, changesBuffer.length - 1, 0, 0.2 * w, 0.8 * w); 36 | const y = project(idx, changesBuffer.length - 1, 0, 0.6 * h, 0.4 * h); 37 | 38 | const delta = (scale * (byte - 128)) / 128; 39 | 40 | const r = Math.abs(currentRadius); 41 | 42 | ctx.beginPath(); 43 | ctx.arc(x, y, r, 0, 2 * Math.PI); 44 | ctx.arc(x, y, r + e, 0, 2 * Math.PI, true); 45 | ctx.stroke(); 46 | ctx.fill(); 47 | 48 | currentRadius += delta; 49 | }); 50 | } 51 | } 52 | 53 | exports.Rings = Rings; 54 | -------------------------------------------------------------------------------- /art/sandpiles.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, bigIntOfBuffer } = require("./util.js"); 3 | 4 | class Sandpiles extends Art { 5 | constructor() { 6 | super({ 7 | pile1: 3, 8 | pile2: 3, 9 | pile3: 3, 10 | pile4: 3, 11 | pile5: 3, 12 | pile6: 3, 13 | pile7: 3, 14 | pile8: 3, 15 | }); 16 | this.filename = "sandpiles.js"; 17 | this.created = "17 Jun 2021"; 18 | } 19 | 20 | getDescription() { 21 | return ` 22 | For each of the ${Object.keys(this.template).length} piles, read ${ 23 | this.template.pile1 24 | } bytes as a triple (x, y, amount). At position (x, y), place 25 | amount grains. Then, for all cells in the grid, if there are 26 | 4 or more grains, spill one grain in each of the four cardinal directions. Repeat 27 | this process until no grains fall. 28 | 29 | This is a cellular automaton known as the Abelian 30 | sanpile model and it produces surprisingly interesting and complex patterns. When two piles interact, the result is 31 | mostly a jumbled mess. 32 | `; 33 | } 34 | 35 | getSize(ctx) { 36 | return _(30, ctx.canvas.width); 37 | } 38 | 39 | draw(ctx, props) { 40 | const s = this.getSize(ctx); 41 | const w = Math.ceil(ctx.canvas.width / s); 42 | const h = Math.ceil(ctx.canvas.height / s); 43 | 44 | let grid = []; 45 | 46 | for (let i = 0; i < Object.keys(this.template).length; i++) { 47 | const buf = props[`pile${i + 1}Buffer`]; 48 | 49 | let x = Math.floor((buf[0] / 256) * w); 50 | let y = Math.floor((buf[1] / 256) * h); 51 | 52 | grid[[x, y]] = buf[2]; 53 | } 54 | 55 | // avalanche 56 | let shouldPropagate = true; 57 | while (shouldPropagate) { 58 | shouldPropagate = false; 59 | 60 | Object.keys(grid).forEach((pos) => { 61 | const [x, y] = pos.split(",").map((i) => parseInt(i)); 62 | if (x < 0 || y < 0 || x > w || y > h) { 63 | return; 64 | } 65 | 66 | if (grid[[x, y]] >= 4) { 67 | let spillover = Math.floor(grid[[x, y]] / 4); 68 | grid[[x + 1, y]] = (grid[[x + 1, y]] || 0) + spillover; 69 | grid[[x, y + 1]] = (grid[[x, y + 1]] || 0) + spillover; 70 | grid[[x - 1, y]] = (grid[[x - 1, y]] || 0) + spillover; 71 | grid[[x, y - 1]] = (grid[[x, y - 1]] || 0) + spillover; 72 | grid[[x, y]] = grid[[x, y]] % 4; 73 | shouldPropagate = true; 74 | } 75 | }); 76 | } 77 | 78 | for (let x = 0; x < w; x++) { 79 | for (let y = 0; y < h; y++) { 80 | let value = +grid[[x, y]]; 81 | if (!value) continue; 82 | 83 | if (value > 3) { 84 | value = 3; 85 | } 86 | let g = [255, 160, 90, 40][value]; 87 | ctx.fillStyle = `rgb(${g}, ${g}, ${g})`; 88 | ctx.fillRect(x * s, y * s, s, s); 89 | } 90 | } 91 | } 92 | } 93 | 94 | exports.Sandpiles = Sandpiles; 95 | -------------------------------------------------------------------------------- /art/semicircle.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Semicircle extends Art { 5 | constructor() { 6 | super({ 7 | first: 5, 8 | second: 5, 9 | third: 5, 10 | fourth: 5, 11 | }); 12 | this.filename = "semicircle.js"; 13 | this.created = "25 May 2021"; 14 | } 15 | 16 | getDescription({ firstBuffer }) { 17 | return ` 18 | From the template we form ${ 19 | Object.keys(this.template).length 20 | } semicircles, 21 | each built by extracting an x, y, count, distance, direction tuple from 22 | the buffer. 23 | 24 | This piece was inspired by the amazing semicircles in Saskia Freeke's "Geometric Shapes" 25 | [link]. 26 | `; 27 | } 28 | 29 | drawSemicircle(ctx, buffer) { 30 | const w = ctx.canvas.width; 31 | const h = ctx.canvas.height; 32 | 33 | // x and y from [0.25*w, 0.75*w] 34 | const x = (buffer[0] / 256) * (w / 2) + w / 4; 35 | const y = (buffer[0] / 256) * (h / 2) + h / 4; 36 | 37 | // fixed radius of 200 38 | const radius = _(200, Math.min(w, h)); 39 | 40 | // count from 2 to 21 41 | const count = Math.floor((20 * buffer[2]) / 256) + 2; 42 | 43 | const distance = Math.floor((40 * buffer[3]) / 256) + 4; 44 | 45 | // theta is 0, pi/2, pi, or 3pi/2 46 | const theta = ((buffer[4] % 4) * Math.PI) / 2; 47 | 48 | for (let i = 0; i < count; i++) { 49 | // Compute the center of the flat edge 50 | const x_ = x + i * distance * Math.cos(theta + Math.PI / 2); 51 | const y_ = y + i * distance * Math.sin(theta + Math.PI / 2); 52 | 53 | ctx.lineWidth = 3; 54 | 55 | ctx.beginPath(); 56 | 57 | // flat side 58 | ctx.moveTo(x_ - radius * Math.cos(theta), y_ - radius * Math.sin(theta)); 59 | ctx.lineTo(x_ + radius * Math.cos(theta), y_ + radius * Math.sin(theta)); 60 | 61 | // curve 62 | ctx.arc(x_, y_, radius, theta, theta + Math.PI); 63 | 64 | ctx.stroke(); 65 | } 66 | } 67 | 68 | draw(ctx, { firstBuffer, secondBuffer, thirdBuffer, fourthBuffer }) { 69 | [firstBuffer, secondBuffer, thirdBuffer, fourthBuffer].forEach((buff) => { 70 | this.drawSemicircle(ctx, buff); 71 | }); 72 | } 73 | } 74 | 75 | exports.Semicircle = Semicircle; 76 | -------------------------------------------------------------------------------- /art/stocks.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Stocks extends Art { 5 | constructor() { 6 | super({ 7 | name: 4, 8 | date: 2, 9 | open: 1, 10 | moves: 25, 11 | }); 12 | this.filename = "stocks.js"; 13 | this.created = "18 Apr 2021"; 14 | } 15 | 16 | getDescription() { 17 | return ` 18 | We generate a ${this.template.moves}-day Candlestick chart 19 | using the name buffer to compute a random 4-digit stock symbol. 20 | The stock opens at a random value specified by open on a random day generated 21 | from date. For each byte in the moves buffer we generate a 22 | close, high, and low using the following equations. 23 | 24 | const close = Math.sin(0.1337 * movesBuffer[i]) * closeVariance + open; 25 | 26 | const low = 27 | Math.min(open, close) - 28 | Math.abs(Math.sin(0.4242 * movesBuffer[i]) * lowHighVariance); 29 | 30 | const high = 31 | Math.max(open, close) + 32 | Math.abs(Math.sin(0.1729 * movesBuffer[i]) * lowHighVariance); 33 | 34 | These four numbers are used to draw each candlestick, and the global high and low are 35 | rendered in the bottom right. 36 | `; 37 | } 38 | 39 | draw(ctx, { nameBuffer, date, open, movesBuffer }) { 40 | const w = ctx.canvas.width; 41 | const h = ctx.canvas.height; 42 | const s = Math.min(w, h); 43 | 44 | // Name 45 | const name = Array.from(nameBuffer) 46 | .map((b) => String.fromCharCode((b % 26) + 65)) 47 | .join(""); 48 | ctx.font = `bold ${_(80, s)}px monospace`; 49 | ctx.fillStyle = "rgb(0, 0, 0)"; 50 | ctx.fillText("$" + name, _(60, w), h - _(80, h)); 51 | 52 | // Graph 53 | ctx.lineWidth = _(3, w); 54 | const leftPadding = _(236, w); 55 | const topPadding = _(140, h); 56 | const bottomPadding = _(400, h); 57 | let barWidth = _(20, w); 58 | const halfBarWidth = Math.floor(barWidth / 2) + 1; 59 | 60 | const barDistance = _(40, w); 61 | 62 | const sticks = []; 63 | let lastClose = open * 128 + 50; 64 | for (let i = 0; i < movesBuffer.byteLength; i++) { 65 | const closeVariance = 10; 66 | const lowHighVariance = 5; 67 | 68 | const open = lastClose; 69 | const close = Math.sin(0.1337 * movesBuffer[i]) * closeVariance + open; 70 | const low = 71 | Math.min(open, close) - 72 | Math.abs(Math.sin(0.4242 * movesBuffer[i]) * lowHighVariance); 73 | const high = 74 | Math.max(open, close) + 75 | Math.abs(Math.sin(0.1729 * movesBuffer[i]) * lowHighVariance); 76 | 77 | lastClose = close; 78 | 79 | sticks.push([low, open, close, high]); 80 | } 81 | 82 | const min = Math.min(...sticks.map((v) => v[0])); 83 | const max = Math.max(...sticks.map((v) => v[3])); 84 | 85 | sticks 86 | // map [low, open, close, high] to canvas y-coordinates 87 | .map((stick) => 88 | stick.map( 89 | (el) => 90 | ((el - min) / (max - min)) * (h - (topPadding + bottomPadding)) + 91 | topPadding 92 | ) 93 | ) 94 | .forEach(([low, open, close, high], i) => { 95 | const x = leftPadding + i * barDistance; 96 | 97 | ctx.beginPath(); 98 | 99 | ctx.moveTo(x + halfBarWidth, low); 100 | ctx.lineTo(x + halfBarWidth, Math.min(open, close)); 101 | 102 | ctx.moveTo(x + halfBarWidth, high); 103 | ctx.lineTo(x + halfBarWidth, Math.max(open, close)); 104 | 105 | ctx.rect(x, open, barWidth, close - open); 106 | ctx.stroke(); 107 | }); 108 | 109 | // Labels 110 | const labelPadding = _(64, w); 111 | ctx.font = `bold ${_(36, s)}px monospace`; 112 | ctx.textAlign = "right"; 113 | ctx.fillText( 114 | `${movesBuffer.byteLength}d high: ${max.toFixed(2)}`, 115 | w - _(80, w), 116 | h - _(120, h) 117 | ); 118 | ctx.fillText( 119 | `${movesBuffer.byteLength}d low: ${min.toFixed(2)}`, 120 | w - _(80, w), 121 | h - _(80, h) 122 | ); 123 | 124 | // Dates 125 | ctx.font = `bold ${_(30, s)}px monospace`; 126 | ctx.textAlign = "left"; 127 | 128 | function dateFromDay(year, day) { 129 | var date = new Date(year, 0); // initialize a date in `year-01-01` 130 | return new Date(date.setDate(day)); // add the number of days 131 | } 132 | 133 | const firstDay = Math.floor(date * 365); 134 | 135 | const markers = 4; 136 | 137 | for (let i = 0; i < markers + 1; i++) { 138 | const date = dateFromDay( 139 | 2021, 140 | firstDay + i * ((movesBuffer.byteLength - 1) / markers) 141 | ); 142 | 143 | ctx.textAlign = "center"; 144 | ctx.fillText( 145 | `${date.getMonth() + 1}/${date.getDate()}`, 146 | leftPadding + 147 | barDistance * i * ((movesBuffer.byteLength - 1) / markers) + 148 | halfBarWidth, 149 | h - bottomPadding + labelPadding 150 | ); 151 | } 152 | 153 | // Prices 154 | ctx.textAlign = "right"; 155 | ctx.fillText(max.toFixed(2), leftPadding - labelPadding, topPadding); 156 | ctx.fillText(min.toFixed(2), leftPadding - labelPadding, h - bottomPadding); 157 | ctx.fillText( 158 | ((max + min) / 2).toFixed(2), 159 | leftPadding - labelPadding, 160 | (topPadding + h - bottomPadding) / 2 161 | ); 162 | } 163 | } 164 | 165 | exports.Stocks = Stocks; 166 | -------------------------------------------------------------------------------- /art/three-body.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | 4 | const G = 0.0000001; 5 | 6 | class ThreeBody extends Art { 7 | constructor() { 8 | super({ 9 | m1: 4, 10 | x1: 3, 11 | y1: 3, 12 | m2: 4, 13 | x2: 3, 14 | y2: 3, 15 | m3: 4, 16 | x3: 3, 17 | y3: 3, 18 | }); 19 | this.filename = "three-body.js"; 20 | this.created = "03 Feb 2024"; 21 | } 22 | 23 | getDescription() { 24 | return ` 25 | Place three bodies in space. Each body is defined by 10 bytes. The first 4 control 26 | its mass. The next 3 control its starting x position (0, 1). The next 3 control its 27 | starting y position (0, 1). All bodies start at rest. 28 | 29 | Then, simulate the gravitational forces between the bodies. We run the simulating 1000 30 | and use a value of ${G} for the gravitational constant. 31 | `; 32 | } 33 | 34 | arc(ctx, x, y, r) { 35 | const w = ctx.canvas.width; 36 | const h = ctx.canvas.height; 37 | const s = Math.min(w, h); 38 | 39 | const px = project(x, 0, 1, w / 2 - s / 2, w / 2 + s / 2); 40 | const py = project(y, 0, 1, h / 2 - s / 2, h / 2 + s / 2); 41 | 42 | ctx.arc(px, py, r, 0, 2 * Math.PI); 43 | } 44 | 45 | draw(ctx, { m1, x1, y1, m2, x2, y2, m3, x3, y3 }) { 46 | let body1 = { x: x1, y: y1, vx: 0, vy: 0 }; 47 | let body2 = { x: x2, y: y2, vx: 0, vy: 0 }; 48 | let body3 = { x: x3, y: y3, vx: 0, vy: 0 }; 49 | 50 | ctx.fillStyle = "rgb(0, 0, 0)"; 51 | ctx.strokeStyle = "rgb(0, 0, 0)"; 52 | 53 | const FRAMES = 1000; 54 | for (let i = 0; i < FRAMES; i++) { 55 | // graviational force between m1 and m2 56 | let d12 = Math.sqrt((body1.x - body2.x) ** 2 + (body1.y - body2.y) ** 2); 57 | let f12 = (G * m1 * m2) / d12 ** 2; 58 | let theta12 = Math.atan2(body2.y - body1.y, body2.x - body1.x); 59 | 60 | // graviational force between m1 and m3 61 | let d13 = Math.sqrt((body1.x - body3.x) ** 2 + (body1.y - body3.y) ** 2); 62 | let f13 = (G * m1 * m3) / d13 ** 2; 63 | let theta13 = Math.atan2(body3.y - body1.y, body3.x - body1.x); 64 | 65 | // graviational force between m2 and m3 66 | let d23 = Math.sqrt((body2.x - body3.x) ** 2 + (body2.y - body3.y) ** 2); 67 | let f23 = (G * m2 * m3) / d23 ** 2; 68 | let theta23 = Math.atan2(body3.y - body2.y, body3.x - body2.x); 69 | 70 | // Update position of m1 71 | let ax1 = (f12 * Math.cos(theta12) + f13 * Math.cos(theta13)) / m1; 72 | let ay1 = (f12 * Math.sin(theta12) + f13 * Math.sin(theta13)) / m1; 73 | body1.vx += ax1; 74 | body1.vy += ay1; 75 | body1.x += body1.vx; 76 | body1.y += body1.vy; 77 | 78 | // Update position of m2 79 | let ax2 = (-f12 * Math.cos(theta12) + f23 * Math.cos(theta23)) / m2; 80 | let ay2 = (-f12 * Math.sin(theta12) + f23 * Math.sin(theta23)) / m2; 81 | body2.vx += ax2; 82 | body2.vy += ay2; 83 | body2.x += body2.vx; 84 | body2.y += body2.vy; 85 | 86 | // Update position of m3 87 | let ax3 = (-f13 * Math.cos(theta13) - f23 * Math.cos(theta23)) / m3; 88 | let ay3 = (-f13 * Math.sin(theta13) - f23 * Math.sin(theta23)) / m3; 89 | body3.vx += ax3; 90 | body3.vy += ay3; 91 | body3.x += body3.vx; 92 | body3.y += body3.vy; 93 | 94 | ctx.beginPath(); 95 | this.arc(ctx, body1.x, body1.y, 1); 96 | ctx.fill(); 97 | 98 | ctx.beginPath(); 99 | this.arc(ctx, body2.x, body2.y, 1); 100 | ctx.fill(); 101 | 102 | ctx.beginPath(); 103 | this.arc(ctx, body3.x, body3.y, 1); 104 | ctx.fill(); 105 | } 106 | 107 | ctx.beginPath(); 108 | this.arc(ctx, body1.x, body1.y, 10); 109 | ctx.stroke(); 110 | 111 | ctx.beginPath(); 112 | this.arc(ctx, body2.x, body2.y, 10); 113 | ctx.stroke(); 114 | 115 | ctx.beginPath(); 116 | this.arc(ctx, body3.x, body3.y, 10); 117 | ctx.stroke(); 118 | } 119 | } 120 | 121 | exports.ThreeBody = ThreeBody; 122 | -------------------------------------------------------------------------------- /art/turing.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Turing extends Art { 5 | constructor() { 6 | super({ 7 | α0: 3, 8 | β0: 3, 9 | γ0: 3, 10 | α1: 3, 11 | β1: 3, 12 | γ1: 3, 13 | α2: 3, 14 | β2: 3, 15 | γ2: 3, 16 | input: 5, 17 | }); 18 | this.filename = "turing.js"; 19 | this.created = "28 May 2021"; 20 | } 21 | 22 | transitionTable(params) { 23 | function triplet(key) { 24 | return { 25 | write: Math.floor((params[key + "Buffer"][0] / 256) * 3), 26 | move: Math.floor((params[key + "Buffer"][1] / 256) * 2), 27 | nextState: "αβγ"[Math.floor((params[key + "Buffer"][0] / 256) * 3)], 28 | }; 29 | } 30 | 31 | return { 32 | 0: { 33 | α: triplet("α0"), 34 | β: triplet("β0"), 35 | γ: triplet("γ0"), 36 | }, 37 | 1: { 38 | α: triplet("α1"), 39 | β: triplet("β1"), 40 | γ: triplet("γ1"), 41 | }, 42 | 2: { 43 | α: triplet("α2"), 44 | β: triplet("β2"), 45 | γ: triplet("γ2"), 46 | }, 47 | }; 48 | } 49 | 50 | transitionTableHtml(params) { 51 | const table = this.transitionTable(params); 52 | const body = [0, 1, 2] 53 | .map((i) => { 54 | const row = "αβγ" 55 | .split("") 56 | .map((state) => { 57 | const entry = table[i][state]; 58 | return ` 59 | ${entry.write} 60 | ${entry.move ? "R" : "L"} 61 | ${entry.nextState} 62 | `; 63 | }) 64 | .join(""); 65 | 66 | return ` 67 | 68 | ${i} 69 | ${row} 70 | 71 | `; 72 | }) 73 | .join(""); 74 | 75 | return ` 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ${body} 97 |
SymbolState αState βState γ
WriteMoveStateWriteMoveStateWriteMoveState
98 | `; 99 | } 100 | 101 | getDescription(params) { 102 | return ` 103 | From the hash we construct a 3-symbol, 3-state 104 | Turing machine 105 | with the following transition table: 106 | 107 |
108 |
${this.transitionTableHtml(params)}
109 |
110 | 111 | We begin at state α on a tape seeded with input, 112 | and use the lookup table to determine which symbol to write (a white square, 113 | gray square, or black square), 114 | which direction to move the cursor (left or right), and which state to become. 115 | (For artistic purposes, the "halt" state H never appears). 116 | 117 | With each transition, we draw the one-dimensional tape on the canvas, and move down 118 | a line. The result is a two-dimensional drawing that grows off the sides of the screen, empties 119 | out completely, or draws something interesting. 120 | 121 | Turing machines are named after Alan Turing, 122 | who developed them while researching the 123 | Entscheidungsproblem. These machines 124 | form the basis of "computation" - following a series of steps on some input to produce some output. 125 | In order to construct a universal Turing Machine (one which can compute anything), we 126 | need more states and symbols*. 127 | 128 | [*] 129 | Universal machines with the following amounts of (state, symbol) have been found - (15, 2), 130 | (9, 3), (6, 4), (5, 5), (4, 6), (3, 9), and (2, 18) 131 | `; 132 | } 133 | 134 | transition(table, tape, cursorPosition, state) { 135 | let value = tape[cursorPosition]; 136 | let { write, move, nextState } = table[value][state]; 137 | 138 | tape[cursorPosition] = write; 139 | return [move ? cursorPosition + 1 : cursorPosition - 1, nextState]; 140 | } 141 | 142 | drawTape(ctx, tape, cursorPosition, bitSize, y) { 143 | const widthInBits = Math.floor(ctx.canvas.width / bitSize); 144 | const start = Math.floor(tape.length / 2 - widthInBits / 2); 145 | const end = Math.floor(tape.length / 2 + widthInBits / 2); 146 | 147 | for (let i = start; i < end; i++) { 148 | const x = (i - start) * bitSize; 149 | if (i === cursorPosition) { 150 | ctx.lineWidth = _(3, ctx.canvas.width); 151 | ctx.strokeStyle = "rgb(255, 255, 255)"; 152 | ctx.fillStyle = "rgb(0, 0, 0)"; 153 | ctx.beginPath(); 154 | ctx.moveTo(x + bitSize / 2, y); 155 | ctx.lineTo(x, y - bitSize / 2); 156 | ctx.lineTo(x + bitSize, y - bitSize / 2); 157 | ctx.lineTo(x + bitSize / 2, y); 158 | ctx.stroke(); 159 | ctx.fill(); 160 | } 161 | 162 | if (tape[i]) { 163 | const shade = (tape[i] - 1) * 128; 164 | ctx.fillStyle = `rgb(${shade}, ${shade}, ${shade})`; 165 | 166 | ctx.beginPath(); 167 | ctx.rect(x, y, bitSize, bitSize); 168 | ctx.fill(); 169 | } 170 | } 171 | } 172 | 173 | bufferToTernary(buffer) { 174 | let base10 = buffer.reduce((acc, d) => acc * 256 + d, 1); 175 | return base10 176 | .toString(3) 177 | .split("") 178 | .map((d) => parseInt(d)); 179 | } 180 | 181 | draw(ctx, params) { 182 | const w = ctx.canvas.width; 183 | const h = ctx.canvas.height; 184 | 185 | // 12px bits at 1200px looks decent 186 | const bitSize = _(12, w); 187 | 188 | // Initialize a tape large enough to handle all possible movements 189 | let tape = Array.from({ 190 | length: 2 * Math.floor(Math.max(w, h) / bitSize) + 2, 191 | }).map((_) => 0); 192 | 193 | let cursorPosition = Math.floor(tape.length / 2); 194 | let state = "α"; 195 | 196 | // Place `input` on the tape 197 | const input = this.bufferToTernary(params.inputBuffer); 198 | for (let i = 0; i < input.length; i++) { 199 | tape[cursorPosition - Math.floor(input.length / 2) + i] = input[i]; 200 | } 201 | 202 | // Generate the transition table 203 | const table = this.transitionTable(params); 204 | 205 | for (let i = 0; i < Math.floor(h / bitSize) + 1; i++) { 206 | [cursorPosition, state] = this.transition( 207 | table, 208 | tape, 209 | cursorPosition, 210 | state 211 | ); 212 | 213 | this.drawTape(ctx, tape, cursorPosition, bitSize, i * bitSize); 214 | } 215 | } 216 | } 217 | 218 | exports.Turing = Turing; 219 | -------------------------------------------------------------------------------- /art/util.js: -------------------------------------------------------------------------------- 1 | exports._ = (measurement, dimension) => 2 | // Map from [0, 1320] to [0, dimension] 3 | // 4 | // Useful when we know a width of "200" looks good at 1320px and 5 | // want to scale it. 6 | Math.round((measurement * dimension) / 1320); 7 | 8 | exports.project = (val, valMin, valMax, desiredMin, desiredMax) => 9 | ((val - valMin) / (valMax - valMin)) * (desiredMax - desiredMin) + desiredMin; 10 | 11 | exports.bigIntOfBuffer = (buffer) => { 12 | let res = 0n; 13 | buffer.forEach((item, idx) => { 14 | res *= 256n; 15 | res += BigInt(item); 16 | }); 17 | return res; 18 | }; 19 | -------------------------------------------------------------------------------- /art/voronoi.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _, project } = require("./util.js"); 3 | const V = require("voronoi"); 4 | 5 | const NUM_POINTS = 16; 6 | 7 | class Voronoi extends Art { 8 | constructor() { 9 | super({ 10 | x: 1, 11 | y: 1, 12 | others: (NUM_POINTS - 1) * 2, 13 | }); 14 | 15 | this.filename = "voronoi.js"; 16 | this.created = "30 Dec 2022"; 17 | } 18 | 19 | getDescription() { 20 | return ` 21 | Read ${NUM_POINTS} byte pairs from the array, representing 22 | a series of (x, y) points. Do not draw the points on the canvas. Partition 23 | the plane into a Voronoi diagram, 24 | identifying regions of the graph which are closest to each point. 25 | 26 | Under the hood we use @gorhill's excellent 27 | Voronoi library, 28 | which took no time at all to plug in. 29 | `; 30 | } 31 | 32 | draw(ctx, { xBuffer, yBuffer, othersBuffer }) { 33 | const w = ctx.canvas.width; 34 | const h = ctx.canvas.height; 35 | ctx.lineWidth = _(5, w); 36 | 37 | const voronoiGenerator = new V(); 38 | 39 | const coords = [...xBuffer, ...yBuffer, ...othersBuffer]; 40 | const vertices = []; 41 | for (let i = 0; i < NUM_POINTS; i++) { 42 | vertices.push({ 43 | x: project(coords[2 * i], 0, 256, 0, w), 44 | y: project(coords[2 * i + 1], 0, 256, 0, h), 45 | }); 46 | } 47 | 48 | const { edges } = voronoiGenerator.compute(vertices, { 49 | xl: 0, 50 | xr: w, 51 | yt: 0, 52 | yb: h, 53 | }); 54 | 55 | for (let i = 0; i < edges.length; i++) { 56 | ctx.beginPath(); 57 | ctx.moveTo(edges[i].va.x, edges[i].va.y); 58 | ctx.lineTo(edges[i].vb.x, edges[i].vb.y); 59 | ctx.stroke(); 60 | } 61 | } 62 | } 63 | 64 | exports.Voronoi = Voronoi; 65 | -------------------------------------------------------------------------------- /art/walk.js: -------------------------------------------------------------------------------- 1 | const { Art } = require("./_base.js"); 2 | const { _ } = require("./util.js"); 3 | 4 | class Walk extends Art { 5 | constructor() { 6 | super({ 7 | turns: 32, 8 | }); 9 | this.filename = "walk.js"; 10 | this.created = "26 Apr 2021"; 11 | } 12 | 13 | getDescription() { 14 | return ` 15 | We illustrate a random walk, starting at 16 | (0, 0) with a direction of "east." For each bit in the moves buffer (256 in total): 17 | we walk forward, then turn "left" if the bit is 1 and "right" if it is 0. 18 | 19 | There is no underlying pattern to the illustration since it is a random walk. This is in contrast 20 | to the elegance of one of my favorite patterns, the dragon curve. 21 | `; 22 | } 23 | 24 | draw(ctx, { turnsBuffer }) { 25 | const w = ctx.canvas.width; 26 | const h = ctx.canvas.height; 27 | const s = Math.min(w, h); 28 | const WALK_LENGTH = _(10, s); 29 | let dir = { dx: 1, dy: 0 }; 30 | let pos = { x: 0, y: 0 }; 31 | const path = [pos]; 32 | 33 | const bits = Array.from(turnsBuffer) 34 | .map((byte) => { 35 | return byte 36 | .toString(2) 37 | .padStart(8, "0") 38 | .split("") 39 | .map((i) => parseInt(i)); 40 | }) 41 | .reduce((acc, bits) => acc.concat(bits)); 42 | 43 | bits.forEach((bit) => { 44 | // TODO: get 8 turns from each one? 45 | pos = { 46 | x: pos.x + WALK_LENGTH * dir.dx, 47 | y: pos.y + WALK_LENGTH * dir.dy, 48 | }; 49 | path.push(pos); 50 | 51 | if (dir.dx === 1) { 52 | dir = { 53 | dx: 0, 54 | dy: bit ? -1 : 1, 55 | }; 56 | } else if (dir.dy === 1) { 57 | dir = { 58 | dx: bit ? 1 : -1, 59 | dy: 0, 60 | }; 61 | } else if (dir.dx === -1) { 62 | dir = { 63 | dx: 0, 64 | dy: bit ? 1 : -1, 65 | }; 66 | } else { 67 | dir = { 68 | dx: bit ? -1 : 1, 69 | dy: 0, 70 | }; 71 | } 72 | }); 73 | 74 | const minX = Math.min(...path.map(({ x }) => x)); 75 | const maxX = Math.max(...path.map(({ x }) => x)); 76 | const minY = Math.min(...path.map(({ y }) => y)); 77 | const maxY = Math.max(...path.map(({ y }) => y)); 78 | const maxSideLength = Math.max(maxX - minX, maxY - minY); 79 | 80 | const PADDING = _(120, s); 81 | 82 | ctx.lineWidth = _(6, s); 83 | ctx.beginPath(); 84 | path.forEach(({ x, y }, idx) => { 85 | const x_ = 86 | ((x - minX) / maxSideLength) * (s - PADDING - PADDING) + PADDING; 87 | const y_ = 88 | ((y - minY) / maxSideLength) * (s - PADDING - PADDING) + PADDING; 89 | 90 | if (idx === 0) { 91 | ctx.moveTo(x_, y_); 92 | } else { 93 | ctx.lineTo(x_, y_); 94 | } 95 | }); 96 | ctx.stroke(); 97 | } 98 | } 99 | 100 | exports.Walk = Walk; 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hashart", 3 | "version": "0.0.9", 4 | "scripts": { 5 | "dev": "next dev", 6 | "dev:server": "nodemon server.js", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "chess.js": "^0.11.0", 12 | "voronoi": "^1.0.0" 13 | }, 14 | "devDependencies": { 15 | "canvas": "^2.7.0", 16 | "classnames": "^2.2.6", 17 | "ejs": "^3.1.6", 18 | "express": "^4.17.1", 19 | "glob": "^7.2.0", 20 | "jsnes": "^1.1.0", 21 | "lodash.debounce": "^4.0.8", 22 | "next": "^11.1.2", 23 | "nodemon": "^2.0.7", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/[piece]/[seed].js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | import { useEffect, useRef } from "react"; 6 | import classnames from "classnames"; 7 | import debounce from "lodash.debounce"; 8 | 9 | import pieces from "../../art/pieces.js"; 10 | import styles from "./art.module.css"; 11 | 12 | function Hash({ parts }) { 13 | return ( 14 |
15 | {parts.map(({ name, bytes, normalized }) => ( 16 |
23 |
{name}
24 |
{bytes}
25 |
26 | ))} 27 |
28 | ); 29 | } 30 | 31 | function Art({ piece, seed, hashString }) { 32 | const router = useRouter(); 33 | const art = new pieces[piece](); 34 | const hash = new Uint8Array(Buffer.from(hashString, "hex")); 35 | 36 | const canvasEl = useRef(null); 37 | useEffect(() => { 38 | if (!canvasEl || !hash) return; 39 | let ctx = canvasEl.current.getContext("2d"); 40 | art.render(ctx, hash); 41 | }, [canvasEl, art, hashString]); 42 | 43 | function handleChange(e) { 44 | if (/[^\.\s]/.test(e.target.value)) { 45 | router.replace(`/${piece}/${encodeURIComponent(e.target.value)}`); 46 | } 47 | } 48 | 49 | return ( 50 |
51 | 52 | {piece} | hash.jordanscales.com 53 | 54 | 58 | 59 | 65 | 66 | 67 | 68 | 69 |
70 |

{piece}

71 |
72 | 73 | home 74 | 75 | {art.filename ? ( 76 | <> 77 | {" . "} 78 | 81 | github 82 | 83 | 84 | ) : null} 85 |
86 |
87 | 88 |
89 |
90 |
91 | 92 |
93 | 99 | {art.debounce ? " (debounced for performance)" : null} 100 |
101 |
102 | 103 |
104 | {hash ? : null} 105 |
106 | 107 | 113 | 114 | 138 |
139 | ); 140 | } 141 | 142 | export default Art; 143 | 144 | export async function getServerSideProps(context) { 145 | const { piece, seed } = context.params; 146 | const shaSum = crypto.createHash("sha256"); 147 | shaSum.update(seed); 148 | const buffer = shaSum.digest(); 149 | 150 | return { 151 | props: { 152 | piece, 153 | seed, 154 | hashString: buffer.toString("hex"), 155 | }, 156 | }; 157 | } 158 | -------------------------------------------------------------------------------- /pages/[piece]/art.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: baseline; 5 | } 6 | 7 | .canvas { 8 | width: 660px; 9 | height: 495px; 10 | margin: 0 auto; 11 | border: 2px solid black; 12 | } 13 | 14 | .explanation { 15 | display: flex; 16 | margin-bottom: 12px; 17 | } 18 | 19 | .bytes { 20 | font-weight: bold; 21 | border-top: 1px solid #666; 22 | } 23 | 24 | .unused { 25 | color: #aaa; 26 | } 27 | 28 | .segment { 29 | margin-right: 8px; 30 | } 31 | 32 | .paragraph { 33 | margin: 1em 0; 34 | } 35 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import "./style.css"; 3 | 4 | export default function App({ Component, pageProps }) { 5 | return ( 6 |
7 | 8 | 9 | hash 10 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import pieces from "../art/pieces.js"; 5 | 6 | export default function Index() { 7 | return ( 8 |
9 | 10 | index | hash.jordanscales.com 11 | 12 | 13 | 17 | 21 | 22 | 23 | 24 | 25 |
26 |

hash

27 |
28 | 29 |

30 | Art generated from{" "} 31 | 32 | SHA-256 33 | {" "} 34 | hashes. by{" "} 35 | 36 | jdan 37 | 38 | . 39 |

40 | 41 |

42 | Source available{" "} 43 | 44 | on GitHub 45 | 46 | . 47 |

48 |

Browse the collection:

49 |
    50 | {Object.keys(pieces) 51 | .filter((name) => !new pieces[name]().hidden) 52 | .reverse() 53 | .map((name) => ( 54 |
  • 55 | {new pieces[name]().created ? ( 56 | <> 57 | {new pieces[name]().created} 58 | {" - "} 59 | 60 | ) : null} 61 | 62 | {name} 63 | 64 |
  • 65 | ))} 66 |
67 | 68 |

69 | This art was created for{" "} 70 | 71 | small e-ink displays 72 | 73 | , such as my setup below. 74 |

75 | 76 | A photo of two stuffed animals next to a wooden frame with a digital screen in the middle of it. The screen contains a piece of art consisting of semicircles stacked on top of each other tightly, almost resembling a Slinky, scattered around the canvas 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /pages/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 40px auto 80px; 3 | width: 660px; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | body, 11 | input { 12 | font-family: monospace; 13 | font-size: 15px; 14 | } 15 | 16 | code { 17 | background-color: #eee; 18 | } 19 | 20 | ul { 21 | padding-left: 2ch; 22 | } 23 | 24 | li { 25 | padding: 2px 0; 26 | } 27 | 28 | td { 29 | text-align: center; 30 | } 31 | -------------------------------------------------------------------------------- /public/flat-eric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdan/hashart/62b739a3d5aafe4bef3fd4a24ba3b786b0958ce2/public/flat-eric.png -------------------------------------------------------------------------------- /scripts/chess.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Chess game finder 3 | * 4 | * Usage: 5 | * $ node scripts/chess.js "1. d4 d5 2. c4" jdan 6 | * Using 3 bytes to find 1. d4 d5 2. c4 7 | * Found! jdan1574 8 | * 9 | * https://hash.jordanscales.com/chess/jdan1574 will have the opening "1. d4 d5 2. c4" 10 | */ 11 | const crypto = require("crypto"); 12 | const { getPgn } = require("../art/chess.js"); 13 | 14 | function pgnOfSeed(str, bytes) { 15 | const shaSum = crypto.createHash("sha256"); 16 | shaSum.update(str); 17 | const buffer = shaSum.digest(); 18 | return getPgn(buffer.slice(0, bytes)); 19 | } 20 | 21 | let prefix = process.argv[3] || ""; 22 | let i = 0; 23 | 24 | // The art takes 16 bytes from the buffer, but let's only 25 | // take the number of bytes we need to speed up the pgn calcuation 26 | let requiredBytes = process.argv[2].replace(/\d+\.\s/g, "").split(" ").length; 27 | console.log(`Using ${requiredBytes} bytes to find ${process.argv[2]}`); 28 | 29 | while (!pgnOfSeed(prefix + i, requiredBytes).startsWith(process.argv[2])) { 30 | i++; 31 | } 32 | 33 | console.log("Found!", prefix + i); 34 | -------------------------------------------------------------------------------- /scripts/find-element.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Element name finder 3 | * 4 | * Usage: 5 | * $ node scripts/find-element.js Ph jdan 6 | * seed: jdan52 = Phodinium 7 | * 8 | * https://hash.jordanscales.com/element/jdan52 will have the name "Phondinium" 9 | */ 10 | const crypto = require("crypto"); 11 | const { getName } = require("../art/element.js"); 12 | 13 | function nameOfSeed(str) { 14 | const shaSum = crypto.createHash("sha256"); 15 | shaSum.update(str); 16 | const buffer = shaSum.digest(); 17 | return getName(buffer.slice(0, 12)); 18 | } 19 | 20 | let prefix = process.argv[3] || ""; 21 | let i = 0; 22 | 23 | // TODO: Test if the markov chain even contains the target string 24 | while (!nameOfSeed(prefix + i).startsWith(process.argv[2])) { 25 | i++; 26 | } 27 | 28 | console.log(`seed: ${prefix + i} = ${nameOfSeed(prefix + i)}`); 29 | -------------------------------------------------------------------------------- /scripts/generate-state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Using vendor/rom.nes, generates a snapshot of the game after a series 3 | * of preliminary loading screens and button presses 4 | */ 5 | 6 | const fs = require("fs"); 7 | const path = require("path"); 8 | const jsnes = require("jsnes"); 9 | 10 | const VENDOR_PATH = path.join(__dirname, "../vendor"); 11 | const ROM_PATH = path.join(VENDOR_PATH, "roms/mariobros.nes"); 12 | const STATE_PATH = path.join(VENDOR_PATH, "state.json"); 13 | 14 | // Copied from art.js 15 | function buttonPress(nes, button, holdFrames = 1) { 16 | nes.buttonDown(1, button); 17 | for (let i = 0; i < holdFrames; i++) { 18 | nes.frame(); 19 | } 20 | nes.buttonUp(1, button); 21 | nes.frame(); 22 | } 23 | 24 | module.exports = () => { 25 | if (!fs.existsSync(ROM_PATH)) { 26 | console.log( 27 | "vendor/roms/mariobros.nes not found, skipping state generation..." 28 | ); 29 | return; 30 | } else if (fs.existsSync(STATE_PATH)) { 31 | return fs.readFileSync(STATE_PATH, "utf-8"); 32 | } 33 | 34 | const nes = new jsnes.NES(); 35 | const romData = fs.readFileSync(ROM_PATH, { encoding: "binary" }); 36 | 37 | nes.loadROM(romData); 38 | 39 | const BUTTON_START = 3; 40 | const BUTTON_RIGHT = 7; 41 | 42 | // Wait 137 frames for the menu to load 43 | for (let i = 0; i < 137; i++) { 44 | nes.frame(); 45 | } 46 | buttonPress(nes, BUTTON_START); 47 | 48 | // Wait 31 games for the first game to load 49 | for (let i = 0; i < 31; i++) { 50 | nes.frame(); 51 | } 52 | 53 | buttonPress(nes, BUTTON_START); 54 | 55 | // Wait 162 frames for level 1-1 to start 56 | for (let i = 0; i < 162; i++) { 57 | nes.frame(); 58 | } 59 | 60 | // Walk right for 30 frames 61 | buttonPress(nes, BUTTON_RIGHT, 30); 62 | 63 | // Wait 30 frames for the walking animation to stop 64 | for (let i = 0; i < 30; i++) { 65 | nes.frame(); 66 | } 67 | 68 | const state = JSON.stringify(nes.toJSON()); 69 | fs.writeFileSync(STATE_PATH, state); 70 | return state; 71 | }; 72 | -------------------------------------------------------------------------------- /scripts/numerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Numerator minimizer 3 | * 4 | * Usage: 5 | * $ node scripts/numerator.js jdan 6 | * ... 7 | * jdan136845 12092n 8 | * ... 9 | * 10 | * https://hash.jordanscales.com/fraction/jdan136845 will have a numerator of "12092" 11 | */ 12 | const crypto = require("crypto"); 13 | const { bigIntOfBuffer } = require("../art/util.js"); 14 | 15 | function numeratorOfSeed(str) { 16 | const shaSum = crypto.createHash("sha256"); 17 | shaSum.update(str); 18 | const buffer = shaSum.digest(); 19 | const a = bigIntOfBuffer(Array.from(buffer.slice(0, 4))); 20 | const b = bigIntOfBuffer(Array.from(buffer.slice(4, 8))); 21 | 22 | return a < b ? a : b; 23 | } 24 | 25 | let prefix = process.argv[2] || ""; 26 | let i = 0; 27 | let minNumerator = Infinity; 28 | while (minNumerator) { 29 | const numer = numeratorOfSeed(prefix + i); 30 | if (numer < minNumerator) { 31 | console.log(prefix + i, numer); 32 | minNumerator = numer; 33 | } 34 | 35 | i++; 36 | } 37 | -------------------------------------------------------------------------------- /scripts/ticker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ticker finder 3 | * 4 | * Usage: 5 | * $ node scripts/ticker.js JDAN cool 6 | * Found! cool728113 7 | * 8 | * https://hash.jordanscales.com/stocks/cool728113 will have the ticket "JDAN" 9 | */ 10 | const crypto = require("crypto"); 11 | 12 | function tickerOfSeed(str) { 13 | const shaSum = crypto.createHash("sha256"); 14 | shaSum.update(str); 15 | const buffer = shaSum.digest(); 16 | const name = Array.from(buffer.slice(0, 4)); 17 | return name.map((v) => String.fromCharCode((v % 26) + 65)).join(""); 18 | } 19 | 20 | let prefix = process.argv[3] || ""; 21 | let i = 0; 22 | while (tickerOfSeed(prefix + i) !== process.argv[2]) { 23 | i++; 24 | } 25 | 26 | console.log("Found!", prefix + i); 27 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const crypto = require("crypto"); 3 | const { createCanvas } = require("canvas"); 4 | const ejs = require("ejs"); 5 | const fs = require("fs"); 6 | const glob = require("glob"); 7 | const jsnes = require("jsnes"); 8 | const path = require("path"); 9 | const pieces = require("./art/pieces.js"); 10 | const generateState = require("./scripts/generate-state.js"); 11 | 12 | const state = generateState(); 13 | const app = express(); 14 | 15 | app.use(express.json()); 16 | 17 | app.use((req, res, next) => { 18 | console.log(`${new Date()} ${req.ip} ${req.path}`); 19 | next(); 20 | }); 21 | 22 | app.get("/", (req, res) => { 23 | res.send(` 24 | Provide a piece, width, height, and seed 25 | 36 | 37 | Provide a piece and have the server pick a random seed 38 | 41 | 42 | Have the server pick both the piece and seed 43 | 46 | 47 | Provide a piece and seed with a default resolution of 1320x1320 48 | 51 | 52 | Display a 5x5 grid of randomly generated pieces 53 | 56 | 57 | Adjust the pieces that appear in the random rotation 58 |
    59 |
  • /admin (requires ADMIN_PASSWORD environment variable)
  • 60 |
61 | `); 62 | }); 63 | 64 | function sendArt(res, { piece, width, height, seed }) { 65 | const art = new pieces[piece](); 66 | const canvas = createCanvas(parseInt(width), parseInt(height)); 67 | const ctx = canvas.getContext("2d"); 68 | 69 | let props = {}; 70 | if (piece === "mario") { 71 | if (!state) { 72 | res.send( 73 | "State snapshot not found (does $ROOT/vendor/roms/mariobros.nes exist?)" 74 | ); 75 | return; 76 | } 77 | 78 | let latestFrameBuffer = null; 79 | const nes = new jsnes.NES({ 80 | onFrame: function (frameBuffer) { 81 | latestFrameBuffer = frameBuffer; 82 | }, 83 | }); 84 | 85 | nes.fromJSON(JSON.parse(state)); 86 | nes.frame(); 87 | 88 | props = { 89 | nes, 90 | getFrameBuffer() { 91 | return latestFrameBuffer; 92 | }, 93 | }; 94 | } else if (piece === "nes") { 95 | let latestFrameBuffer = null; 96 | const nes = new jsnes.NES({ 97 | onFrame: function (frameBuffer) { 98 | latestFrameBuffer = frameBuffer; 99 | }, 100 | }); 101 | 102 | const romsGlob = path.join(__dirname, "vendor/roms/**/*.nes"); 103 | const roms = glob.sync(romsGlob); 104 | if (roms.length === 0) { 105 | res.send("No roms found (place them in $ROOT/vendor/roms)"); 106 | return; 107 | } 108 | 109 | const secondCanvas = createCanvas(parseInt(width), parseInt(height)); 110 | 111 | props = { 112 | nes, 113 | roms, 114 | fs, 115 | path, 116 | getFrameBuffer() { 117 | return latestFrameBuffer; 118 | }, 119 | secondCtx: secondCanvas.getContext("2d"), 120 | }; 121 | } 122 | 123 | const shaSum = crypto.createHash("sha256"); 124 | shaSum.update(seed); 125 | const buffer = shaSum.digest(); 126 | const hash = new Uint8Array(buffer); 127 | 128 | art.render(ctx, hash, props); 129 | res.set("Content-Type", "image/png"); 130 | 131 | /** 132 | * `createPNGStream` is handy but unfortunately for my e-ink display 133 | * I need a proper Content-Length set. So we'll dump the stream into 134 | * a buffer and send it as a whole. 135 | */ 136 | var buffs = []; 137 | const pngStream = canvas.createPNGStream(); 138 | pngStream.on("data", function (d) { 139 | buffs.push(d); 140 | }); 141 | pngStream.on("end", function () { 142 | const buff = Buffer.concat(buffs); 143 | res.set("Content-Length", buff.byteLength); 144 | res.send(buff); 145 | }); 146 | } 147 | 148 | function defaultPieces() { 149 | return Object.keys(pieces).reduce( 150 | (acc, key) => ({ ...acc, [key]: true }), 151 | {} 152 | ); 153 | } 154 | 155 | function getEnabledPieces() { 156 | if (!fs.existsSync("db.json")) { 157 | return defaultPieces(); 158 | } else { 159 | return JSON.parse(fs.readFileSync("db.json")); 160 | } 161 | } 162 | 163 | function setEnabledPieces(pieces) { 164 | fs.writeFileSync("db.json", JSON.stringify(pieces, null, 2)); 165 | } 166 | 167 | app.get("/admin", (req, res) => { 168 | if (!process.env.ADMIN_PASSWORD) { 169 | res.statusCode = 404; 170 | res.send("Not found"); 171 | } else { 172 | ejs.renderFile( 173 | "admin.ejs", 174 | { 175 | pieces: Object.keys(pieces), 176 | enabledPieces: getEnabledPieces(), 177 | }, 178 | (err, str) => { 179 | if (err) { 180 | res.statusCode = 500; 181 | res.send("Error"); 182 | } else { 183 | res.send(str); 184 | } 185 | } 186 | ); 187 | } 188 | }); 189 | 190 | app.post("/admin", (req, res) => { 191 | if ( 192 | !process.env.ADMIN_PASSWORD || 193 | req.header("Authorization") !== `Bearer ${process.env.ADMIN_PASSWORD}` 194 | ) { 195 | res.statusCode = 404; 196 | res.send("Not found"); 197 | } else { 198 | let enabledPieces = getEnabledPieces(); 199 | 200 | Object.keys(req.body).forEach((key) => { 201 | if (req.body[key]) { 202 | enabledPieces[key] = true; 203 | } else { 204 | enabledPieces[key] = false; 205 | } 206 | }); 207 | 208 | const allFalse = !Object.keys(enabledPieces).some( 209 | (key) => enabledPieces[key] 210 | ); 211 | 212 | if (allFalse) { 213 | res.statusCode = 404; 214 | res.send("Not found"); 215 | } else { 216 | setEnabledPieces(enabledPieces); 217 | res.send("Ok"); 218 | } 219 | } 220 | }); 221 | 222 | app.get("/random/:width/:height/random.png", (req, res) => { 223 | const { width, height } = req.params; 224 | 225 | const enabledPieces = getEnabledPieces(); 226 | const pieces = Object.keys(enabledPieces).filter((key) => enabledPieces[key]); 227 | 228 | const pieceKeys = 229 | state == null ? pieces.filter((name) => name !== "mario") : pieces; 230 | const piece = pieceKeys[Math.floor(Math.random() * pieceKeys.length)]; 231 | const seed = Math.random() + ""; 232 | 233 | sendArt(res, { piece, seed, width, height }); 234 | }); 235 | 236 | app.get("/:piece/grid", (req, res) => { 237 | const { piece } = req.params; 238 | 239 | let images = ""; 240 | for (let i = 0; i < 25; i++) { 241 | const seed = Math.random() + ""; 242 | images += ``; 243 | } 244 | 245 | res.send(` 246 | 247 | 248 | 249 | ${piece} grid 250 | 258 | 259 | 260 |
261 | ${images} 262 |
263 | 264 | 265 | `); 266 | }); 267 | 268 | app.get("/:piece/:width/:height/random.png", (req, res) => { 269 | const { piece, width, height } = req.params; 270 | const seed = Math.random() + ""; 271 | 272 | sendArt(res, { piece, seed, width, height }); 273 | }); 274 | 275 | app.get("/:piece/:width/:height/:seed.png", (req, res) => { 276 | const { piece, seed, width, height } = req.params; 277 | sendArt(res, { piece, seed, width, height }); 278 | }); 279 | 280 | app.get("/:piece/:seed.png", (req, res) => { 281 | const { piece, seed } = req.params; 282 | sendArt(res, { piece, seed, width: 1320, height: 1320 }); 283 | }); 284 | 285 | const port = process.env.VIRTUAL_PORT || 3000; 286 | app.listen(port, "0.0.0.0", () => { 287 | console.log(`Listening on http://localhost:${port}`); 288 | }); 289 | --------------------------------------------------------------------------------