├── .gitignore ├── LICENSE ├── README.md ├── basic ├── index.html └── js │ └── entry.js ├── dist ├── animatedRings.bundle.js ├── basic.bundle.js ├── orbits.bundle.js ├── phyllotaxis.bundle.js ├── playground.bundle.js ├── reducingRadii.bundle.js ├── rings.bundle.js └── ringsInMotion.bundle.js ├── index.html ├── media ├── basic.png ├── orbits-1.webm ├── orbits-2.webm ├── orbits-3.webm ├── orbits.gif ├── orbits.png ├── phyllotaxis.png ├── playground.png ├── reducing-radii.png ├── rings-in-motion.gif ├── rings-in-motion.png ├── rings-in-motion.webm └── rings.png ├── orbits ├── index.html └── js │ ├── Ring.js │ └── entry.js ├── package-lock.json ├── package.json ├── phyllotaxis ├── index.html └── js │ └── entry.js ├── playground ├── index.html └── js │ ├── Ring.js │ ├── entry.js │ ├── export-functions.js │ ├── gui-functions.js │ ├── math-functions.js │ └── voronoi-functions.js ├── reducing-radii ├── index.html └── js │ └── entry.js ├── rings-in-motion ├── index.html └── js │ ├── Ring.js │ └── entry.js ├── rings ├── index.html └── js │ └── entry.js ├── style.css └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Series of visual experiments exploring Voronoi diagrams using JavaScript 2 | 3 | ### Inspiration 4 | * [Ornaments in a Box](https://www.flickr.com/photos/quasimondo/albums/72157632200834828) series by Mario Klingemann 5 | 6 | ## Packages used 7 | 8 | * [d3-delaunay](https://github.com/d3/d3-delaunay) for fast, robust Voronoi diagram generation. 9 | * [p5.js](https://www.npmjs.com/package/p5) for canvas drawing and miscellaneous helper functions (like `lerp` and `map`). 10 | * [Webpack](https://webpack.js.org/) for modern JS (ES6) syntax, code modularization, bundling, and serving locally. 11 | * [dat.GUI](https://github.com/dataarts/dat.gui) for a parametric UI 12 | 13 | ## Key commands 14 | 15 | | Key | Action | 16 | |--- |--- | 17 | | `r` | Reset sketch | 18 | | `i` | Invert colors | 19 | | `p` | Toggle visibility of points | 20 | | `b` | Toggle blur effect | 21 | 22 | ## Install notes 23 | 24 | 1. Run `npm install` 25 | 2. Run `npm run serve` 26 | 27 | ## Screenshots 28 | 29 | ![Basic Voronoi](media/basic.png) 30 | 31 | ![Rings](media/rings.png) 32 | 33 | ![Rings in motion](media/rings-in-motion.gif) 34 | 35 | ![Orbits](media/orbits.gif) 36 | 37 | ![Playground](media/playground.png) 38 | 39 | ![Reducing radii](media/reducing-radii.png) 40 | 41 | ![Phyllotaxis](media/phyllotaxis.png) -------------------------------------------------------------------------------- /basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Basic Voronoi 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /basic/js/entry.js: -------------------------------------------------------------------------------- 1 | import {Delaunay} from "d3-delaunay"; 2 | import {toPath} from 'svg-points'; 3 | import {saveAs} from 'file-saver'; 4 | 5 | let showPoints = false, 6 | invertColors = false, 7 | points; 8 | 9 | const sketch = function (p5) { 10 | /* 11 | Setup 12 | ===== 13 | */ 14 | p5.setup = function () { 15 | p5.createCanvas(window.innerWidth, window.innerHeight); 16 | generatePoints(); 17 | } 18 | 19 | /* 20 | Draw 21 | ==== 22 | */ 23 | p5.draw = function () { 24 | drawVoronoi(); 25 | drawPoints(); 26 | } 27 | 28 | 29 | /* 30 | Key released handler 31 | ==================== 32 | */ 33 | p5.keyReleased = function () { 34 | switch (p5.key) { 35 | case 'r': 36 | generatePoints(); 37 | break; 38 | 39 | case 'p': 40 | showPoints = !showPoints; 41 | break; 42 | 43 | case 'i': 44 | invertColors = !invertColors; 45 | break; 46 | 47 | case 'e': 48 | exportSVG(); 49 | break; 50 | } 51 | } 52 | 53 | 54 | /* 55 | Custom functions 56 | ================ 57 | */ 58 | 59 | function generatePoints() { 60 | points = []; 61 | for(let i=0; i<100; i++) { 62 | points.push([ 63 | p5.random(window.innerWidth), 64 | p5.random(window.innerHeight) 65 | ]) 66 | } 67 | } 68 | 69 | // Draw the Voronoi diagram for a set of points 70 | function drawVoronoi() { 71 | if (invertColors) { 72 | p5.background(0); 73 | p5.stroke(255); 74 | } else { 75 | p5.background(255); 76 | p5.stroke(0); 77 | } 78 | 79 | p5.noFill(); 80 | 81 | const polygons = getVoronoiPolygons(); 82 | 83 | // Draw raw polygons 84 | for (const polygon of polygons) { 85 | p5.beginShape(); 86 | 87 | for (const vertex of polygon) { 88 | p5.vertex(vertex[0], vertex[1]); 89 | } 90 | 91 | p5.endShape(); 92 | } 93 | } 94 | 95 | // Get an array of polygons (arrays of [x,y] pairs) using Voronoi 96 | function getVoronoiPolygons() { 97 | const delaunay = Delaunay.from(points); 98 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 99 | const simplifiedPolygons = []; 100 | 101 | for(let cell of voronoi.cellPolygons()) { 102 | let polygon = []; 103 | 104 | for(let vertex of cell) { 105 | polygon.push([vertex[0], vertex[1]]); 106 | } 107 | 108 | simplifiedPolygons.push(polygon); 109 | } 110 | 111 | return simplifiedPolygons; 112 | } 113 | 114 | // Draw the points 115 | function drawPoints() { 116 | if (showPoints) { 117 | p5.noStroke(); 118 | 119 | if (invertColors) { 120 | p5.fill(50); 121 | } else { 122 | p5.fill(200); 123 | } 124 | 125 | for (let point of points) { 126 | p5.ellipse(point[0], point[1], 5); 127 | } 128 | } 129 | } 130 | 131 | function exportSVG() { 132 | // Set up element 133 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 134 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 135 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 136 | svg.setAttribute('width', window.innerWidth); 137 | svg.setAttribute('height', window.innerHeight); 138 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 139 | 140 | let polygons = getVoronoiPolygons(); 141 | 142 | for(let polygon of polygons) { 143 | svg.appendChild( createPathElFromPoints(polygon) ); 144 | } 145 | 146 | // Generate the document as a Blob and force a download as a timestamped .svg file 147 | const svgDoctype = ''; 148 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 149 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 150 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 151 | } 152 | 153 | function createPathElFromPoints(points) { 154 | let pointsString = ''; 155 | 156 | for(let [index, point] of points.entries()) { 157 | pointsString += point[0] + ',' + point[1]; 158 | 159 | if(index < points.length - 1) { 160 | pointsString += ' '; 161 | } 162 | } 163 | 164 | let d = toPath({ 165 | type: 'polyline', 166 | points: pointsString 167 | }); 168 | 169 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 170 | pathEl.setAttribute('d', d); 171 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 172 | 173 | return pathEl; 174 | } 175 | } 176 | 177 | // Launch the sketch using p5js in instantiated mode 178 | new p5(sketch); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 2D Voronoi experiments in JavaScript 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 |

2D Voronoi experiments in JavaScript

38 | 39 |

Voronoi diagrams are a way of partitioning space into cells based on a set of points such that every location within each cell is closer to it's generating point than to any other point. They have been studied for a very long time and found to be useful in many practical and artistic applications.

40 | 41 |

Of particular interest to me is how Voronoi diagrams can be used to closely model natural space partitioning behaviors seen biological and physical systems like diatoms, radiolaria, bubbles / foams, cracked mud, tree canopies, leaf venation, and more.

42 | 43 |

This page collects a few visual experiments I built in modern JavaScript using p5.js and d3-delaunay.

44 | 45 | 46 | 47 | View the source code on Github 48 | 49 |
50 | 51 |
52 | 106 |
107 | 108 | -------------------------------------------------------------------------------- /media/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/basic.png -------------------------------------------------------------------------------- /media/orbits-1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/orbits-1.webm -------------------------------------------------------------------------------- /media/orbits-2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/orbits-2.webm -------------------------------------------------------------------------------- /media/orbits-3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/orbits-3.webm -------------------------------------------------------------------------------- /media/orbits.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/orbits.gif -------------------------------------------------------------------------------- /media/orbits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/orbits.png -------------------------------------------------------------------------------- /media/phyllotaxis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/phyllotaxis.png -------------------------------------------------------------------------------- /media/playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/playground.png -------------------------------------------------------------------------------- /media/reducing-radii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/reducing-radii.png -------------------------------------------------------------------------------- /media/rings-in-motion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/rings-in-motion.gif -------------------------------------------------------------------------------- /media/rings-in-motion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/rings-in-motion.png -------------------------------------------------------------------------------- /media/rings-in-motion.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/rings-in-motion.webm -------------------------------------------------------------------------------- /media/rings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwebb/2d-voronoi-experiments/0d295899ac38c1566b25d78426f38ff8b7589776/media/rings.png -------------------------------------------------------------------------------- /orbits/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Orbits 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /orbits/js/Ring.js: -------------------------------------------------------------------------------- 1 | export default class Ring { 2 | constructor(numPoints, radius, centerX = undefined, centerY = undefined) { 3 | this.numPoints = numPoints; 4 | this.radius = radius; 5 | this.radiusOffset = 0; 6 | this.radiusOffsetScaler = Math.random() * (10 - -10) + -10; 7 | 8 | this.points = []; 9 | 10 | this.angle = 0; 11 | this.velocity = (Math.random() * (10 - 7) + 7) / 10000; 12 | this.velocity = ((Math.random() * (1 - -1) + -1) < 0) ? this.velocity *= -1 : this.velocity; 13 | this.targetAngle = 0; 14 | 15 | this.center = {}; 16 | this.center.x = (centerX != undefined) ? centerX : window.innerWidth / 2; 17 | this.center.y = (centerY != undefined) ? centerY : window.innerHeight / 2; 18 | 19 | this.animationMode = 'rotation'; 20 | 21 | this.subrings = []; 22 | 23 | this.generate(); 24 | } 25 | 26 | // Stuff this.points with real point coordinates using this.numPoints and this.radius 27 | generate() { 28 | this.points = []; 29 | for (let i = 0; i < this.numPoints; i++) { 30 | this.points.push([ 31 | this.center.x + this.radius * Math.cos(((360 / this.numPoints) * (Math.PI/180) * i) + this.angle), 32 | this.center.y + this.radius * Math.sin(((360 / this.numPoints) * (Math.PI/180) * i) + this.angle) 33 | ]); 34 | } 35 | 36 | for(let ring of this.subrings) { 37 | ring.generate(); 38 | } 39 | } 40 | 41 | // Add this.velocity to this.angle until it reaches this.targetAngle (with easing) 42 | iterate() { 43 | switch(this.animationMode) { 44 | case 'rotation': 45 | this.angle += this.velocity; 46 | break; 47 | 48 | case 'radius': 49 | this.radius += (Math.sin(this.radiusOffset * (Math.PI/180)) * Math.cos(this.radiusOffset * (Math.PI/180))) * this.radiusOffsetScaler; 50 | 51 | if(this.radiusOffset + 1 >= 360) { 52 | this.radiusOffset = 0; 53 | } else { 54 | this.radiusOffset++; 55 | } 56 | 57 | break; 58 | } 59 | 60 | for(let [index,ring] of this.subrings.entries()) { 61 | ring.center.x = this.points[index][0]; 62 | ring.center.y = this.points[index][1]; 63 | ring.iterate(); 64 | } 65 | 66 | this.generate(); 67 | } 68 | } -------------------------------------------------------------------------------- /orbits/js/entry.js: -------------------------------------------------------------------------------- 1 | import {Delaunay} from "d3-delaunay"; 2 | import {toPath} from 'svg-points'; 3 | import {saveAs} from 'file-saver'; 4 | import Ring from "./Ring"; 5 | 6 | let showPoints = false, 7 | invertColors = false, 8 | useBlurEffect = false, 9 | isPaused = false, 10 | points, 11 | rings; 12 | 13 | const EVEN = 0, 14 | ODD = 1, 15 | ALTERNATING = 2, 16 | ANY = 3; 17 | let ROW_TYPE = EVEN; 18 | 19 | let currentRowType = EVEN; 20 | 21 | const sketch = function (p5) { 22 | /* 23 | Setup 24 | ===== 25 | */ 26 | p5.setup = function () { 27 | p5.createCanvas(window.innerWidth, window.innerHeight); 28 | generatePoints(); 29 | } 30 | 31 | /* 32 | Draw 33 | ==== 34 | */ 35 | p5.draw = function () { 36 | // Move all the rings 37 | if(!isPaused) { 38 | for(let ring of rings) { 39 | ring.iterate(); 40 | } 41 | 42 | // Get the latest points 43 | points = getPoints(); 44 | 45 | drawVoronoi(); 46 | drawPoints(); 47 | } 48 | } 49 | 50 | 51 | /* 52 | Custom functions 53 | ================ 54 | */ 55 | 56 | // Build an array of polygons (arrays of [x,y] pairs) extracted from Voronoi package 57 | function getVoronoiPolygons() { 58 | const delaunay = Delaunay.from(points); 59 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 60 | const simplifiedPolygons = []; 61 | 62 | for(let cell of voronoi.cellPolygons()) { 63 | let polygon = []; 64 | 65 | for(let vertex of cell) { 66 | polygon.push([vertex[0], vertex[1]]); 67 | } 68 | 69 | simplifiedPolygons.push(polygon); 70 | } 71 | 72 | return simplifiedPolygons; 73 | } 74 | 75 | 76 | // Draw the Voronoi diagram for a set of points 77 | function drawVoronoi() { 78 | // Set colors 79 | if (invertColors) { 80 | if(useBlurEffect) { 81 | p5.background(0, 20); 82 | } else { 83 | p5.background(0); 84 | } 85 | 86 | p5.stroke(255); 87 | } else { 88 | if(useBlurEffect) { 89 | p5.background(255, 25); 90 | } else { 91 | p5.background(255); 92 | } 93 | 94 | p5.stroke(0); 95 | } 96 | 97 | p5.noFill(); 98 | 99 | // Extract polygons from Voronoi diagram 100 | const polygons = getVoronoiPolygons(); 101 | 102 | // Draw raw polygons 103 | for (const polygon of polygons) { 104 | p5.beginShape(); 105 | 106 | for (const vertex of polygon) { 107 | p5.vertex(vertex[0], vertex[1]); 108 | } 109 | 110 | p5.endShape(); 111 | } 112 | } 113 | 114 | // Draw dots for each of the points 115 | function drawPoints() { 116 | if (showPoints) { 117 | // Set colors 118 | p5.noStroke(); 119 | 120 | if (invertColors) { 121 | p5.fill(100); 122 | } else { 123 | p5.fill(200); 124 | } 125 | 126 | // Draw the points 127 | for (let point of points) { 128 | p5.ellipse(point[0], point[1], 5); 129 | } 130 | } 131 | } 132 | 133 | function generatePoints() { 134 | points = [], rings = []; 135 | let numRings = parseInt(p5.random(3,10)); 136 | const maxRadius = (window.innerWidth > window.innerHeight) ? window.innerHeight/2 - 10 : window.innerWidth/2 - 10; 137 | const minRadius = p5.random(10, 30); 138 | let currentRadius = maxRadius; 139 | const radiusStep = (maxRadius - minRadius) / numRings; 140 | 141 | // Generate set of points for Voronoi diagram 142 | for (let i = 0; i < numRings; i++) { 143 | let numPoints, range = [], ring; 144 | 145 | if(i != 1) { 146 | // Rings near the center look better with fewer points 147 | if (i > 3) { 148 | range[0] = 5; 149 | range[1] = 10; 150 | } else { 151 | range[0] = 20; 152 | range[1] = 100; 153 | } 154 | 155 | // TODO: make range proportional to i. 156 | 157 | // Generate a random number of points based on selected "row type" 158 | switch (ROW_TYPE) { 159 | case EVEN: 160 | numPoints = getRandomEvenNumber(range[0], range[1]); 161 | break; 162 | 163 | case ODD: 164 | numPoints = getRandomOddNumber(range[0], range[1]); 165 | break; 166 | 167 | case ALTERNATING: 168 | switch (currentRowType) { 169 | case EVEN: 170 | numPoints = getRandomEvenNumber(range[0], range[1]); 171 | currentRowType = ODD; 172 | break; 173 | 174 | case ODD: 175 | numPoints = getRandomOddNumber(range[0], range[1]); 176 | currentRowType = EVEN; 177 | break; 178 | } 179 | 180 | break; 181 | 182 | case ANY: 183 | numPoints = parseInt(p5.random(range[0], range[1])); 184 | break; 185 | } 186 | 187 | ring = new Ring(numPoints, currentRadius); 188 | 189 | 190 | // Add sub-rings to the outermost main ring 191 | } else { 192 | numPoints = parseInt(p5.random(5,12)); 193 | 194 | ring = new Ring(numPoints, currentRadius); 195 | 196 | let subringPoints = parseInt(p5.random(9,50)); 197 | let subringRadius = parseInt(p5.random(20,150)); 198 | let subringRadiusOffsetScaler = Math.random() * (10 - -10) + -10; 199 | 200 | for(let point of ring.points) { 201 | let subring = new Ring(subringPoints, subringRadius, point[0], point[1]); 202 | subring.velocity = .01; 203 | subring.radiusOffsetScaler = subringRadiusOffsetScaler / 5; 204 | subring.animationMode = 'rotation'; 205 | 206 | ring.subrings.push(subring); 207 | } 208 | } 209 | 210 | rings.push(ring); 211 | currentRadius -= radiusStep + p5.random(-radiusStep/2, radiusStep); 212 | } 213 | 214 | points = getPoints(); 215 | } 216 | 217 | function getPoints() { 218 | let pts = []; 219 | 220 | // Extract all points from all rings and sub-rings 221 | for(let ring of rings) { 222 | for(let point of ring.points) { 223 | pts.push(point); 224 | } 225 | 226 | for(let subring of ring.subrings) { 227 | for(let point of subring.points) { 228 | pts.push(point); 229 | } 230 | } 231 | } 232 | 233 | return pts; 234 | } 235 | 236 | function getRandomEvenNumber(min, max) { 237 | let num = parseInt(p5.random(min, max)); 238 | 239 | if (num % 2 > 0) { 240 | if (num - 1 < min) { 241 | num++; 242 | } else { 243 | num--; 244 | } 245 | } 246 | 247 | return num; 248 | } 249 | 250 | function getRandomOddNumber(min, max) { 251 | let num = parseInt(p5.random(min, max)); 252 | 253 | if (num % 2 == 0) { 254 | if (num - 1 < min) { 255 | num++; 256 | } else { 257 | num--; 258 | } 259 | } 260 | 261 | return num; 262 | } 263 | 264 | function exportSVG() { 265 | // Set up element 266 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 267 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 268 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 269 | svg.setAttribute('width', window.innerWidth); 270 | svg.setAttribute('height', window.innerHeight); 271 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 272 | 273 | let polygons = getVoronoiPolygons(); 274 | 275 | for(let polygon of polygons) { 276 | svg.appendChild( createPathElFromPoints(polygon) ); 277 | } 278 | 279 | // Generate the document as a Blob and force a download as a timestamped .svg file 280 | const svgDoctype = ''; 281 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 282 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 283 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 284 | } 285 | 286 | function createPathElFromPoints(points) { 287 | let pointsString = ''; 288 | 289 | for(let [index, point] of points.entries()) { 290 | pointsString += point[0] + ',' + point[1]; 291 | 292 | if(index < points.length - 1) { 293 | pointsString += ' '; 294 | } 295 | } 296 | 297 | let d = toPath({ 298 | type: 'polyline', 299 | points: pointsString 300 | }); 301 | 302 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 303 | pathEl.setAttribute('d', d); 304 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 305 | 306 | return pathEl; 307 | } 308 | 309 | 310 | /* 311 | Key released handler 312 | ==================== 313 | */ 314 | p5.keyReleased = function () { 315 | switch (p5.key) { 316 | case 'r': 317 | generatePoints(); 318 | break; 319 | 320 | case 'p': 321 | showPoints = !showPoints; 322 | break; 323 | 324 | case 'i': 325 | invertColors = !invertColors; 326 | break; 327 | 328 | case 'b': 329 | useBlurEffect = !useBlurEffect; 330 | break; 331 | 332 | case ' ': 333 | isPaused = !isPaused; 334 | break; 335 | 336 | case 'e': 337 | exportSVG(); 338 | break; 339 | 340 | case '1': 341 | ROW_TYPE = EVEN; 342 | generatePoints(); 343 | break; 344 | 345 | case '2': 346 | ROW_TYPE = ODD; 347 | generatePoints(); 348 | break; 349 | 350 | case '3': 351 | ROW_TYPE = ALTERNATING; 352 | generatePoints(); 353 | break; 354 | 355 | case '4': 356 | ROW_TYPE = ANY; 357 | generatePoints(); 358 | break; 359 | } 360 | } 361 | } 362 | 363 | // Launch the sketch using p5js in instantiated mode 364 | new p5(sketch); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2d-voronoi-experiments", 3 | "version": "1.0.0", 4 | "description": "Series of visual experiments exploring Voronoi diagrams using JavaScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "serve": "webpack-dev-server" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/jasonwebb/2d-voronoi-experiments.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/jasonwebb/2d-voronoi-experiments/issues" 18 | }, 19 | "homepage": "https://github.com/jasonwebb/2d-voronoi-experiments#readme", 20 | "devDependencies": { 21 | "d3-delaunay": "^4.1.5", 22 | "dat.gui": "^0.7.6", 23 | "file-saver": "^2.0.2", 24 | "path": "^0.12.7", 25 | "svg-points": "^6.0.1", 26 | "webpack": "^4.41.2", 27 | "webpack-cli": "^3.3.10", 28 | "webpack-dev-server": "^3.9.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phyllotaxis/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Phyllotaxis 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /phyllotaxis/js/entry.js: -------------------------------------------------------------------------------- 1 | import {Delaunay} from "d3-delaunay"; 2 | import {toPath} from 'svg-points'; 3 | import {saveAs} from 'file-saver'; 4 | 5 | let showPoints = false, 6 | invertColors = false, 7 | points; 8 | 9 | const sketch = function (p5) { 10 | /* 11 | Setup 12 | ===== 13 | */ 14 | p5.setup = function () { 15 | p5.createCanvas(window.innerWidth, window.innerHeight); 16 | p5.background(0); 17 | generatePoints(); 18 | } 19 | 20 | 21 | /* 22 | Draw 23 | ==== 24 | */ 25 | p5.draw = function () { 26 | drawVoronoi(); 27 | drawPoints(); 28 | } 29 | 30 | 31 | /* 32 | Key released handler 33 | ==================== 34 | */ 35 | p5.keyReleased = function () { 36 | switch (p5.key) { 37 | case 'r': 38 | generatePoints(); 39 | break; 40 | 41 | case 'p': 42 | showPoints = !showPoints; 43 | break; 44 | 45 | case 'i': 46 | invertColors = !invertColors; 47 | break; 48 | 49 | case 'e': 50 | exportSVG(); 51 | break; 52 | } 53 | } 54 | 55 | 56 | /* 57 | Custom functions 58 | ================ 59 | */ 60 | 61 | // Draw the Voronoi diagram for a set of points 62 | function drawVoronoi() { 63 | if (invertColors) { 64 | p5.background(0); 65 | p5.stroke(255); 66 | } else { 67 | p5.background(255); 68 | p5.stroke(0); 69 | } 70 | 71 | p5.noFill(); 72 | 73 | const polygons = getVoronoiPolygons(); 74 | 75 | // Draw raw polygons 76 | for (const polygon of polygons) { 77 | p5.beginShape(); 78 | 79 | for (const vertex of polygon) { 80 | p5.vertex(vertex[0], vertex[1]); 81 | } 82 | 83 | p5.endShape(); 84 | } 85 | } 86 | 87 | // Draw dots for each of the points 88 | function drawPoints() { 89 | if (showPoints) { 90 | p5.noStroke(); 91 | 92 | if (invertColors) { 93 | p5.fill(100); 94 | } else { 95 | p5.fill(200); 96 | } 97 | 98 | for (let point of points) { 99 | p5.ellipse(point[0], point[1], 5); 100 | } 101 | } 102 | } 103 | 104 | // Get an array of polygons (arrays of [x,y] pairs) using Voronoi 105 | function getVoronoiPolygons() { 106 | const delaunay = Delaunay.from(points); 107 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 108 | const simplifiedPolygons = []; 109 | 110 | for(let cell of voronoi.cellPolygons()) { 111 | let polygon = []; 112 | 113 | for(let vertex of cell) { 114 | polygon.push([vertex[0], vertex[1]]); 115 | } 116 | 117 | simplifiedPolygons.push(polygon); 118 | } 119 | 120 | return simplifiedPolygons; 121 | } 122 | 123 | function generatePoints() { 124 | points = []; 125 | let numCircles = 1500, 126 | golden_ratio = (Math.sqrt(5)+1)/2 - 1, 127 | golden_angle = golden_ratio * (2*Math.PI), 128 | circle_rad = window.innerWidth/2; 129 | 130 | 131 | for(let i=0; i element 146 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 147 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 148 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 149 | svg.setAttribute('width', window.innerWidth); 150 | svg.setAttribute('height', window.innerHeight); 151 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 152 | 153 | let polygons = getVoronoiPolygons(); 154 | 155 | for(let polygon of polygons) { 156 | svg.appendChild( createPathElFromPoints(polygon) ); 157 | } 158 | 159 | // Generate the document as a Blob and force a download as a timestamped .svg file 160 | const svgDoctype = ''; 161 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 162 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 163 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 164 | } 165 | 166 | function createPathElFromPoints(points) { 167 | let pointsString = ''; 168 | 169 | for(let [index, point] of points.entries()) { 170 | pointsString += point[0] + ',' + point[1]; 171 | 172 | if(index < points.length - 1) { 173 | pointsString += ' '; 174 | } 175 | } 176 | 177 | let d = toPath({ 178 | type: 'polyline', 179 | points: pointsString 180 | }); 181 | 182 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 183 | pathEl.setAttribute('d', d); 184 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 185 | 186 | return pathEl; 187 | } 188 | } 189 | 190 | // Launch the sketch using p5js in instantiated mode 191 | new p5(sketch); -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Radial Voronoi playground 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /playground/js/Ring.js: -------------------------------------------------------------------------------- 1 | export default class Ring { 2 | constructor(numPoints, radius, centerX = undefined, centerY = undefined) { 3 | this.numPoints = numPoints; 4 | this.radius = radius; 5 | this.radiusOffset = 0; 6 | 7 | this.points = []; 8 | 9 | this.angle = 0; 10 | this.velocity = (Math.random() * (10 - 7) + 7) / 10000; 11 | this.velocity = ((Math.random() * (1 - -1) + -1) < 0) ? this.velocity *= -1 : this.velocity; 12 | this.animationMode = 'rotation'; 13 | this._animationCounter = 0; 14 | 15 | this.center = {}; 16 | this.center.x = (centerX != undefined) ? centerX : window.innerWidth / 2; 17 | this.center.y = (centerY != undefined) ? centerY : window.innerHeight / 2; 18 | 19 | this.subrings = []; 20 | 21 | this.generate(); 22 | } 23 | 24 | // Stuff this.points with real point coordinates using this.numPoints and this.radius 25 | generate() { 26 | this.points = []; 27 | for (let i = 0; i < this.numPoints; i++) { 28 | this.points.push([ 29 | this.center.x + (this.radius + this.radiusOffset) * Math.cos(((360 / this.numPoints) * (Math.PI/180) * i) + this.angle), 30 | this.center.y + (this.radius + this.radiusOffset) * Math.sin(((360 / this.numPoints) * (Math.PI/180) * i) + this.angle) 31 | ]); 32 | } 33 | 34 | for(let ring of this.subrings) { 35 | ring.generate(); 36 | } 37 | } 38 | 39 | iterate() { 40 | switch(this.animationMode) { 41 | case 'rotation': 42 | this.angle += this.velocity; 43 | break; 44 | 45 | case 'radius': 46 | this.radius += (Math.sin(this._animationCounter * (Math.PI/180)) * Math.cos(this._animationCounter * (Math.PI/180))) * 10; 47 | 48 | if(this._animationCounter + 1 >= 360) { 49 | this._animationCounter = 0; 50 | } else { 51 | this._animationCounter++; 52 | } 53 | 54 | break; 55 | } 56 | 57 | for(let [index,subring] of this.subrings.entries()) { 58 | subring.center.x = this.points[index][0]; 59 | subring.center.y = this.points[index][1]; 60 | subring.iterate(); 61 | } 62 | 63 | this.generate(); 64 | } 65 | } -------------------------------------------------------------------------------- /playground/js/entry.js: -------------------------------------------------------------------------------- 1 | import * as dat from 'dat.gui'; 2 | import Ring from "./Ring"; 3 | import { exportSVG } from './export-functions'; 4 | import { getVoronoiPolygons } from './voronoi-functions'; 5 | 6 | let isPaused = false, 7 | points = [], 8 | cells = [], 9 | rings = [], 10 | gui, 11 | guiElements = {}, 12 | guiVariables = { 13 | rings: [], 14 | numRings: 3, 15 | invertColors: false, 16 | blurEffect: false, 17 | showPoints: false 18 | }; 19 | 20 | const maxPossibleRadius = (window.innerWidth > window.innerHeight) ? window.innerHeight/2 - 10 : window.innerWidth/2 - 10; 21 | 22 | const sketch = function (p5) { 23 | /* 24 | Setup 25 | ===== 26 | */ 27 | p5.setup = function () { 28 | p5.createCanvas(window.innerWidth, window.innerHeight); 29 | initializeGUI(); 30 | rebuildGUI(); 31 | generateRings(); 32 | } 33 | 34 | 35 | /* 36 | Draw 37 | ==== 38 | */ 39 | p5.draw = function () { 40 | // Move all the rings 41 | if(!isPaused) { 42 | for(let ring of rings) { 43 | ring.iterate(); 44 | } 45 | 46 | // Get the latest points 47 | points = getPoints(); 48 | } 49 | 50 | drawVoronoi(); 51 | drawPoints(); 52 | } 53 | 54 | 55 | /* 56 | Drawing functions 57 | ================= 58 | */ 59 | 60 | // Draw the Voronoi diagram for a set of points 61 | function drawVoronoi() { 62 | // Set colors 63 | if (guiVariables.invertColors) { 64 | if(guiVariables.blurEffect) { 65 | p5.background(0, 20); 66 | } else { 67 | p5.background(0); 68 | } 69 | 70 | p5.stroke(255); 71 | } else { 72 | if(guiVariables.blurEffect) { 73 | p5.background(255, 25); 74 | } else { 75 | p5.background(255); 76 | } 77 | 78 | p5.stroke(0); 79 | } 80 | 81 | p5.noFill(); 82 | 83 | // Extract polygons from Voronoi diagram 84 | cells = getVoronoiPolygons(points); 85 | 86 | // Draw raw polygons 87 | for (const cell of cells) { 88 | p5.beginShape(); 89 | 90 | for (const vertex of cell) { 91 | p5.vertex(vertex[0], vertex[1]); 92 | } 93 | 94 | p5.endShape(); 95 | } 96 | } 97 | 98 | // Draw dots for each of the points 99 | function drawPoints() { 100 | if (guiVariables.showPoints) { 101 | // Set colors 102 | p5.noStroke(); 103 | 104 | if (guiVariables.invertColors) { 105 | p5.fill(100); 106 | } else { 107 | p5.fill(200); 108 | } 109 | 110 | // Draw the points 111 | for (let point of points) { 112 | p5.ellipse(point[0], point[1], 5); 113 | } 114 | } 115 | } 116 | 117 | 118 | /* 119 | Create Ring objects 120 | =================== 121 | */ 122 | 123 | // Create Ring objects based on current values from GUI 124 | function generateRings() { 125 | rings = []; 126 | 127 | // Create Ring objects using params stored in guiVariables 128 | for(let ringParams of guiVariables.rings) { 129 | let ring = new Ring(ringParams.numPoints, ringParams.radius); 130 | ring.radiusOffset = ringParams.radiusOffset; 131 | ring.animationMode = ringParams.hasOwnProperty('animationMode') ? ringParams.animationMode : ring.animationMode; 132 | ring.velocity = ringParams.velocity / 10000; 133 | 134 | // Generate subrings for each point of this ring, if specified 135 | if(ringParams.hasSubrings) { 136 | for(let point of ring.points) { 137 | let subring = new Ring(ringParams.subringNumPoints, ringParams.subringRadius, point[0], point[1]); 138 | subring.animationMode = ringParams.subringAnimationMode; 139 | subring.velocity = ringParams.subringVelocity / 10000; 140 | ring.subrings.push(subring); 141 | } 142 | } 143 | 144 | rings.push(ring); 145 | } 146 | 147 | // Run a single iteration when paused to force a new diagram to be drawn 148 | if(isPaused) { 149 | for(let ring of rings) { 150 | ring.iterate(); 151 | } 152 | 153 | // Get the latest points 154 | points = getPoints(); 155 | } 156 | } 157 | 158 | function getPoints() { 159 | let pts = []; 160 | 161 | // Extract all points from all rings and sub-rings 162 | for(let ring of rings) { 163 | for(let point of ring.points) { 164 | pts.push(point); 165 | } 166 | 167 | for(let subring of ring.subrings) { 168 | for(let point of subring.points) { 169 | pts.push(point); 170 | } 171 | } 172 | } 173 | 174 | return pts; 175 | } 176 | 177 | // Generate evenly-space radius values for each ring - called when numRings changes 178 | function calculateRingRadii() { 179 | for(let [index, ringParams] of guiVariables.rings.entries()) { 180 | ringParams.radius = guiVariables.maxRadius - ((guiVariables.maxRadius - guiVariables.minRadius) / guiVariables.numRings) * (index); 181 | } 182 | } 183 | 184 | // Adjust number of rings in params array based on changes to numRings slider 185 | function adjustRingArray() { 186 | if(guiVariables.numRings < guiVariables.rings.length) { // numRings decreased - reduce the array size 187 | guiVariables.rings = guiVariables.rings.slice(0, guiVariables.numRings); 188 | 189 | } else if(guiVariables.numRings > guiVariables.rings.length) { // numRings increased - generate new ring params 190 | for(let i=0; i < Math.abs(guiVariables.numRings - guiVariables.rings.length); i++) { 191 | guiVariables.rings.push(generateRandomRingParams()); 192 | } 193 | } 194 | } 195 | 196 | 197 | /* 198 | UI functions 199 | ============ 200 | */ 201 | 202 | // Create the UI panel with random initial values 203 | function initializeGUI() { 204 | gui = new dat.GUI(); 205 | gui.width = 300; 206 | randomize(); 207 | } 208 | 209 | // Build the dat.gui interface 210 | function rebuildGUI(openedFolders = []) { 211 | // Remove all elements so that ring folders can be added/removed 212 | for(let element of Object.keys(guiElements)) { 213 | if(element != 'folders') { 214 | gui.remove(guiElements[element]); 215 | } else { 216 | for(let folder of guiElements[element]) { 217 | gui.removeFolder(folder); 218 | } 219 | } 220 | } 221 | 222 | guiElements = {}; 223 | 224 | // Button functions 225 | let guiFunctions = { 226 | randomize: function() { randomize(); rebuildGUI(); generateRings(); }, 227 | export: function() { exportSVG(cells) }, 228 | // delete: function() {} 229 | } 230 | 231 | // Number of rings slider 232 | guiElements.numRingsSlider = gui.add(guiVariables, 'numRings', 1, 10, 1).name('Number of rings').onChange(function() { 233 | adjustRingArray(); 234 | calculateRingRadii(); 235 | generateRings(); 236 | }); 237 | 238 | // Max/min radii 239 | guiElements.maxRadiusSlider = gui.add(guiVariables, 'maxRadius', 100, maxPossibleRadius, 1).name('Maximum radius').onChange(function() { 240 | calculateRingRadii(); 241 | generateRings(); 242 | }); 243 | 244 | guiElements.minRadiusSlider = gui.add(guiVariables, 'minRadius', 1, 100, 1).name('Minimum radius').onChange(function() { 245 | calculateRingRadii(); 246 | generateRings(); 247 | }); 248 | 249 | // Folders for each ring 250 | guiElements.folders = []; 251 | for(let [index, ring] of guiVariables.rings.entries()) { 252 | let folder = gui.addFolder('Ring ' + (index + 1)); 253 | 254 | // Ring options 255 | folder.add(ring, 'numPoints', 1, 100, 1).name('Number of points').onChange(generateRings); 256 | folder.add(ring, 'radiusOffset', -((guiVariables.maxRadius - guiVariables.minRadius) / guiVariables.numRings), (guiVariables.maxRadius - guiVariables.minRadius) / guiVariables.numRings).name('Radius offset').onChange(generateRings); 257 | folder.add(ring, 'animationMode', ['rotation', 'radius']).name('Animation mode').onChange(generateRings); 258 | folder.add(ring, 'velocity', -100, 100, 1).name('Velocity').onChange(generateRings); 259 | 260 | folder.add(ring, 'hasSubrings').name('Has subrings').onChange(function(checked) { 261 | if(checked) { 262 | ring.subringNumPoints = parseInt(p5.random(1, 100)); 263 | ring.subringRadius = p5.random(10, 100); 264 | ring.subringAnimationMode = 'rotation'; 265 | ring.subringVelocity = p5.random(-.01, .01); 266 | } else { 267 | delete ring.subringNumPoints; 268 | delete ring.subringRadius; 269 | delete ring.subringAnimationMode; 270 | delete ring.subringVelocity; 271 | } 272 | 273 | let openedFolders = getOpenedFolderIndices(); 274 | 275 | generateRings(); 276 | rebuildGUI(openedFolders); 277 | }); 278 | 279 | // Subring options 280 | if(ring.hasSubrings) { 281 | folder.add(ring, 'subringNumPoints', 1, 100, 1).name('Subring point count').onChange(generateRings); 282 | folder.add(ring, 'subringRadius', 10, 100).name('Subring radius').onChange(generateRings); 283 | folder.add(ring, 'subringAnimationMode', ['rotation', 'radius']).name('Subring animation').onChange(generateRings); 284 | folder.add(ring, 'subringVelocity', -100, 100, 1).name('Subring velocity').onChange(generateRings); 285 | } 286 | 287 | // Delete button 288 | // folder.add(guiFunctions, 'delete').name('Delete this ring').onChange(function() { 289 | // if(Object.keys(gui.__folders).length > 1) { 290 | // // Reduce and update the numRings slider value 291 | // guiVariables.numRings--; 292 | // guiElements.numRingsSlider.setValue(guiVariables.numRings); 293 | 294 | // // Remove this folder from the GUI 295 | // guiElements.folders = guiElements.folders.slice(index, index+1); 296 | // gui.removeFolder(folder); 297 | 298 | // generateRings(); 299 | // } else { 300 | // alert('nice try'); 301 | // } 302 | // }); 303 | 304 | // Open this folder if requested 305 | if(openedFolders.includes(index)) { 306 | folder.open(); 307 | } 308 | 309 | guiElements.folders.push(folder); 310 | } 311 | 312 | // Feature toggles 313 | guiElements.invertColorsCheckbox = gui.add(guiVariables, 'invertColors').name('Invert colors'); 314 | guiElements.blurEffectCheckbox = gui.add(guiVariables, 'blurEffect').name('Blur effect'); 315 | guiElements.showPointsCheckbox = gui.add(guiVariables, 'showPoints').name('Show points'); 316 | 317 | // Buttons 318 | guiElements.randomizeButton = gui.add(guiFunctions, 'randomize').name('Randomize'); 319 | guiElements.exportButton = gui.add(guiFunctions, 'export').name('Export as SVG'); 320 | } 321 | 322 | 323 | // Randomize the parameters stored in guiVariables 324 | function randomize() { 325 | guiVariables.numRings = parseInt(p5.random(3,10)); 326 | guiVariables.maxRadius = p5.random(maxPossibleRadius - 200, maxPossibleRadius); 327 | guiVariables.minRadius = 10; 328 | 329 | guiVariables.rings = []; 330 | 331 | for(let i = 0; i < guiVariables.numRings; i++) { 332 | let ringParams, pointDensity; 333 | 334 | // Rings near the center look better with fewer points 335 | if (i > 3) { 336 | pointDensity = 'LOW'; 337 | } else { 338 | pointDensity = 'HIGH'; 339 | } 340 | 341 | ringParams = generateRandomRingParams(pointDensity); 342 | guiVariables.rings.push(ringParams); 343 | } 344 | 345 | calculateRingRadii(); 346 | } 347 | 348 | // Generate random parameters for a single ring 349 | function generateRandomRingParams(pointDensity = 'HIGH') { 350 | let ringParams = {}, range = []; 351 | 352 | switch(pointDensity) { 353 | case 'LOW': 354 | range[0] = 5; 355 | range[1] = 10; 356 | break; 357 | case 'HIGH': 358 | range[0] = 20; 359 | range[1] = 70; 360 | break; 361 | } 362 | 363 | ringParams.numPoints = parseInt(p5.random(range[0], range[1])); 364 | ringParams.radiusOffset = p5.random( -((guiVariables.maxRadius - guiVariables.minRadius) / guiVariables.numRings) / 2, (guiVariables.maxRadius - guiVariables.minRadius) / guiVariables.numRings / 2); 365 | ringParams.animationMode = 'rotation'; 366 | ringParams.velocity = p5.random(-10, 10); 367 | ringParams.hasSubrings = false; 368 | 369 | return ringParams; 370 | } 371 | 372 | // Get a list of all the folders that are currently opened 373 | function getOpenedFolderIndices() { 374 | let openedFolders = [], 375 | folderElements = Object.values(gui.__folders); 376 | 377 | for(let i=0; i element 6 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 7 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 8 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 9 | svg.setAttribute('width', window.innerWidth); 10 | svg.setAttribute('height', window.innerHeight); 11 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 12 | 13 | for(let polygon of polygons) { 14 | svg.appendChild( createPathElFromPoints(polygon) ); 15 | } 16 | 17 | // Generate the document as a Blob and force a download as a timestamped .svg file 18 | const svgDoctype = ''; 19 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 20 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 21 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 22 | } 23 | 24 | export function createPathElFromPoints(points) { 25 | let pointsString = ''; 26 | 27 | for(let [index, point] of points.entries()) { 28 | pointsString += point[0] + ',' + point[1]; 29 | 30 | if(index < points.length - 1) { 31 | pointsString += ' '; 32 | } 33 | } 34 | 35 | let d = toPath({ 36 | type: 'polyline', 37 | points: pointsString 38 | }); 39 | 40 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 41 | pathEl.setAttribute('d', d); 42 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 43 | 44 | return pathEl; 45 | } 46 | -------------------------------------------------------------------------------- /playground/js/gui-functions.js: -------------------------------------------------------------------------------- 1 | 2 | // Get a list of all the folders that are currently opened 3 | export function getOpenedFolderIndices() { 4 | let openedFolders = [], 5 | folderElements = Object.values(gui.__folders); 6 | 7 | for(let i=0; i 0) { 6 | if (num - 1 < min) { 7 | num++; 8 | } else { 9 | num--; 10 | } 11 | } 12 | 13 | return num; 14 | } 15 | 16 | export function getRandomOddNumber(min, max) { 17 | let num = parseInt(Math.random() * (max - min) + min); 18 | 19 | if (num % 2 == 0) { 20 | if (num - 1 < min) { 21 | num++; 22 | } else { 23 | num--; 24 | } 25 | } 26 | 27 | return num; 28 | } -------------------------------------------------------------------------------- /playground/js/voronoi-functions.js: -------------------------------------------------------------------------------- 1 | import { Delaunay } from "d3-delaunay"; 2 | 3 | // Build an array of polygons (arrays of [x,y] pairs) extracted from Voronoi package 4 | export function getVoronoiPolygons(points) { 5 | const delaunay = Delaunay.from(points); 6 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 7 | const simplifiedPolygons = []; 8 | 9 | for(let cell of voronoi.cellPolygons()) { 10 | let polygon = []; 11 | 12 | for(let vertex of cell) { 13 | polygon.push([vertex[0], vertex[1]]); 14 | } 15 | 16 | simplifiedPolygons.push(polygon); 17 | } 18 | 19 | return simplifiedPolygons; 20 | } -------------------------------------------------------------------------------- /reducing-radii/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Reducing radii 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /reducing-radii/js/entry.js: -------------------------------------------------------------------------------- 1 | import {Delaunay} from "d3-delaunay"; 2 | import {toPath} from 'svg-points'; 3 | import {saveAs} from 'file-saver'; 4 | 5 | let showPoints = false, 6 | invertColors = false, 7 | points; 8 | 9 | const sketch = function (p5) { 10 | /* 11 | Setup 12 | ===== 13 | */ 14 | p5.setup = function () { 15 | p5.createCanvas(window.innerWidth, window.innerHeight); 16 | p5.background(0); 17 | generatePoints(); 18 | } 19 | 20 | 21 | /* 22 | Draw 23 | ==== 24 | */ 25 | p5.draw = function () { 26 | drawVoronoi(); 27 | drawPoints(); 28 | } 29 | 30 | 31 | /* 32 | Key released handler 33 | ==================== 34 | */ 35 | p5.keyReleased = function () { 36 | switch (p5.key) { 37 | case 'r': 38 | generatePoints(); 39 | break; 40 | 41 | case 'p': 42 | showPoints = !showPoints; 43 | break; 44 | 45 | case 'i': 46 | invertColors = !invertColors; 47 | break; 48 | 49 | case 'e': 50 | exportSVG(); 51 | break; 52 | } 53 | } 54 | 55 | 56 | /* 57 | Custom functions 58 | ================ 59 | */ 60 | 61 | // Draw the Voronoi diagram for a set of points 62 | function drawVoronoi() { 63 | if (invertColors) { 64 | p5.background(0); 65 | p5.stroke(255); 66 | } else { 67 | p5.background(255); 68 | p5.stroke(0); 69 | } 70 | 71 | p5.noFill(); 72 | 73 | const polygons = getVoronoiPolygons(); 74 | 75 | // Draw raw polygons 76 | for (const polygon of polygons) { 77 | p5.beginShape(); 78 | 79 | for (const vertex of polygon) { 80 | p5.vertex(vertex[0], vertex[1]); 81 | } 82 | 83 | p5.endShape(); 84 | } 85 | } 86 | 87 | // Draw dots for each of the points 88 | function drawPoints() { 89 | if (showPoints) { 90 | p5.noStroke(); 91 | 92 | if (invertColors) { 93 | p5.fill(100); 94 | } else { 95 | p5.fill(200); 96 | } 97 | 98 | for (let point of points) { 99 | p5.ellipse(point[0], point[1], 5); 100 | } 101 | } 102 | } 103 | 104 | // Get an array of polygons (arrays of [x,y] pairs) using Voronoi 105 | function getVoronoiPolygons() { 106 | const delaunay = Delaunay.from(points); 107 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 108 | const simplifiedPolygons = []; 109 | 110 | for(let cell of voronoi.cellPolygons()) { 111 | let polygon = []; 112 | 113 | for(let vertex of cell) { 114 | polygon.push([vertex[0], vertex[1]]); 115 | } 116 | 117 | simplifiedPolygons.push(polygon); 118 | } 119 | 120 | return simplifiedPolygons; 121 | } 122 | 123 | function generatePoints() { 124 | points = []; 125 | const maxRadius = (window.innerWidth > window.innerHeight) ? window.innerHeight/2 - 10 : window.innerWidth/2 - 10; 126 | let currentRadius = 100, 127 | radiusStep = 75, 128 | numRings = 17; 129 | 130 | // Create one point in the middle 131 | points.push([window.innerWidth / 2, window.innerHeight /2]); 132 | 133 | // Generate set of points for Voronoi diagram 134 | for (let i = 0; i < numRings; i++) { 135 | let numPoints = i*i*3; 136 | 137 | for(let j = 0; j < numPoints; j++) { 138 | points.push([ 139 | window.innerWidth/2 + currentRadius * Math.cos(p5.radians((360 / numPoints) * j)), 140 | window.innerHeight/2 + currentRadius * Math.sin(p5.radians((360 / numPoints) * j)) 141 | ]); 142 | } 143 | 144 | // Decrease radius for next ring 145 | currentRadius += radiusStep; 146 | radiusStep *= .77; 147 | } 148 | } 149 | 150 | // Export an SVG file of the current geometry 151 | function exportSVG() { 152 | // Set up element 153 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 154 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 155 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 156 | svg.setAttribute('width', window.innerWidth); 157 | svg.setAttribute('height', window.innerHeight); 158 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 159 | 160 | let polygons = getVoronoiPolygons(); 161 | 162 | for(let polygon of polygons) { 163 | svg.appendChild( createPathElFromPoints(polygon) ); 164 | } 165 | 166 | // Generate the document as a Blob and force a download as a timestamped .svg file 167 | const svgDoctype = ''; 168 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 169 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 170 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 171 | } 172 | 173 | function createPathElFromPoints(points) { 174 | let pointsString = ''; 175 | 176 | for(let [index, point] of points.entries()) { 177 | pointsString += point[0] + ',' + point[1]; 178 | 179 | if(index < points.length - 1) { 180 | pointsString += ' '; 181 | } 182 | } 183 | 184 | let d = toPath({ 185 | type: 'polyline', 186 | points: pointsString 187 | }); 188 | 189 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 190 | pathEl.setAttribute('d', d); 191 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 192 | 193 | return pathEl; 194 | } 195 | } 196 | 197 | // Launch the sketch using p5js in instantiated mode 198 | new p5(sketch); -------------------------------------------------------------------------------- /rings-in-motion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Rings in motion 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /rings-in-motion/js/Ring.js: -------------------------------------------------------------------------------- 1 | export default class Ring { 2 | constructor(numPoints, radius, centerX = undefined, centerY = undefined) { 3 | this.numPoints = numPoints; 4 | this.radius = radius; 5 | this.radiusOffset = 0; 6 | this.radiusOffsetScaler = Math.random() * (10 - -10) + -10; 7 | 8 | this.points = []; 9 | 10 | this.angle = 0; 11 | this.velocity = (Math.random() * (10 - 7) + 7) / 10000; 12 | this.velocity = ((Math.random() * (1 - -1) + -1) < 0) ? this.velocity *= -1 : this.velocity; 13 | this.targetAngle = 0; 14 | 15 | this.center = {}; 16 | this.center.x = (centerX != undefined) ? centerX : window.innerWidth / 2; 17 | this.center.y = (centerY != undefined) ? centerY : window.innerHeight / 2; 18 | 19 | this.generate(); 20 | } 21 | 22 | // Stuff this.points with real point objects using this.numPoints and this.radius 23 | generate() { 24 | this.points = []; 25 | for (let i = 0; i < this.numPoints; i++) { 26 | this.points.push([ 27 | this.center.x + this.radius * Math.cos(((360 / this.numPoints) * (Math.PI/180) * i) + this.angle), 28 | this.center.y + this.radius * Math.sin(((360 / this.numPoints) * (Math.PI/180) * i) + this.angle) 29 | ]); 30 | } 31 | } 32 | 33 | // Add this.velocity to this.angle until it reaches this.targetAngle (with easing) 34 | iterate() { 35 | // this.angle += this.velocity; 36 | 37 | this.radius += (Math.sin(this.radiusOffset * (Math.PI/180)) * Math.cos(this.radiusOffset * (Math.PI/180))) * this.radiusOffsetScaler; 38 | 39 | if(this.radiusOffset + 1 >= 360) { 40 | this.radiusOffset = 0; 41 | } else { 42 | this.radiusOffset++; 43 | } 44 | 45 | this.generate(); 46 | } 47 | } -------------------------------------------------------------------------------- /rings-in-motion/js/entry.js: -------------------------------------------------------------------------------- 1 | import {Delaunay} from "d3-delaunay"; 2 | import {toPath} from 'svg-points'; 3 | import {saveAs} from 'file-saver'; 4 | import Ring from "./Ring"; 5 | 6 | let showPoints = false, 7 | invertColors = false, 8 | useBlurEffect = false, 9 | isPaused = false, 10 | points, 11 | rings; 12 | 13 | const EVEN = 0, 14 | ODD = 1, 15 | ALTERNATING = 2, 16 | ANY = 3; 17 | let ROW_TYPE = EVEN; 18 | 19 | let currentRowType = EVEN; 20 | 21 | const sketch = function (p5) { 22 | /* 23 | Setup 24 | ===== 25 | */ 26 | p5.setup = function () { 27 | p5.createCanvas(window.innerWidth, window.innerHeight); 28 | generatePoints(); 29 | } 30 | 31 | /* 32 | Draw 33 | ==== 34 | */ 35 | p5.draw = function () { 36 | // Move all the rings 37 | if(!isPaused) { 38 | for(let ring of rings) { 39 | ring.iterate(); 40 | } 41 | 42 | // Get the latest points 43 | points = getPoints(); 44 | 45 | drawVoronoi(); 46 | drawPoints(); 47 | } 48 | } 49 | 50 | 51 | /* 52 | Custom functions 53 | ================ 54 | */ 55 | 56 | // Build an array of polygons (arrays of [x,y] pairs) extracted from Voronoi package 57 | function getVoronoiPolygons() { 58 | const delaunay = Delaunay.from(points); 59 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 60 | const simplifiedPolygons = []; 61 | 62 | for(let cell of voronoi.cellPolygons()) { 63 | let polygon = []; 64 | 65 | for(let vertex of cell) { 66 | polygon.push([vertex[0], vertex[1]]); 67 | } 68 | 69 | simplifiedPolygons.push(polygon); 70 | } 71 | 72 | return simplifiedPolygons; 73 | } 74 | 75 | 76 | // Draw the Voronoi diagram for a set of points 77 | function drawVoronoi() { 78 | // Set colors 79 | if (invertColors) { 80 | if(useBlurEffect) { 81 | p5.background(0, 20); 82 | } else { 83 | p5.background(0); 84 | } 85 | 86 | p5.stroke(255); 87 | } else { 88 | if(useBlurEffect) { 89 | p5.background(255, 25); 90 | } else { 91 | p5.background(255); 92 | } 93 | 94 | p5.stroke(0); 95 | } 96 | 97 | p5.noFill(); 98 | 99 | // Extract polygons from Voronoi diagram 100 | const polygons = getVoronoiPolygons(); 101 | 102 | // Draw raw polygons 103 | for (const polygon of polygons) { 104 | p5.beginShape(); 105 | 106 | for (const vertex of polygon) { 107 | p5.vertex(vertex[0], vertex[1]); 108 | } 109 | 110 | p5.endShape(); 111 | } 112 | } 113 | 114 | // Draw dots for each of the points 115 | function drawPoints() { 116 | if (showPoints) { 117 | // Set colors 118 | p5.noStroke(); 119 | 120 | if (invertColors) { 121 | p5.fill(100); 122 | } else { 123 | p5.fill(200); 124 | } 125 | 126 | // Draw the points 127 | for (let point of points) { 128 | p5.ellipse(point[0], point[1], 5); 129 | } 130 | } 131 | } 132 | 133 | function generatePoints() { 134 | points = [], rings = []; 135 | let numRings = parseInt(p5.random(5, 20)); 136 | const maxRadius = (window.innerWidth > window.innerHeight) ? window.innerHeight/2 - 10 : window.innerWidth/2 - 10; 137 | const minRadius = p5.random(10, 30); 138 | let currentRadius = maxRadius; 139 | const radiusStep = (maxRadius - minRadius) / numRings; 140 | 141 | // Generate set of points for Voronoi diagram 142 | for (let i = 0; i < numRings; i++) { 143 | let numPoints, range = []; 144 | 145 | // Rings near the center look better with fewer points 146 | if (i > 3) { 147 | range[0] = 5; 148 | range[1] = 10; 149 | } else { 150 | range[0] = 20; 151 | range[1] = 200; 152 | } 153 | 154 | // TODO: make range proportional to i. 155 | 156 | // Generate a random number of points based on selected "row type" 157 | switch (ROW_TYPE) { 158 | case EVEN: 159 | numPoints = getRandomEvenNumber(range[0], range[1]); 160 | break; 161 | 162 | case ODD: 163 | numPoints = getRandomOddNumber(range[0], range[1]); 164 | break; 165 | 166 | case ALTERNATING: 167 | switch (currentRowType) { 168 | case EVEN: 169 | numPoints = getRandomEvenNumber(range[0], range[1]); 170 | currentRowType = ODD; 171 | break; 172 | 173 | case ODD: 174 | numPoints = getRandomOddNumber(range[0], range[1]); 175 | currentRowType = EVEN; 176 | break; 177 | } 178 | 179 | break; 180 | 181 | case ANY: 182 | numPoints = parseInt(p5.random(range[0], range[1])); 183 | break; 184 | } 185 | 186 | // Generate points arranged in a ring 187 | rings.push(new Ring(numPoints, currentRadius)); 188 | 189 | currentRadius -= radiusStep + p5.random(-radiusStep/2, radiusStep); 190 | } 191 | 192 | points = getPoints(); 193 | } 194 | 195 | function getPoints() { 196 | let pts = []; 197 | 198 | // Extract all points from all rings for Voronoi 199 | for(let ring of rings) { 200 | for(let point of ring.points) { 201 | pts.push(point); 202 | } 203 | } 204 | 205 | return pts; 206 | } 207 | 208 | function getRandomEvenNumber(min, max) { 209 | let num = parseInt(p5.random(min, max)); 210 | 211 | if (num % 2 > 0) { 212 | if (num - 1 < min) { 213 | num++; 214 | } else { 215 | num--; 216 | } 217 | } 218 | 219 | return num; 220 | } 221 | 222 | function getRandomOddNumber(min, max) { 223 | let num = parseInt(p5.random(min, max)); 224 | 225 | if (num % 2 == 0) { 226 | if (num - 1 < min) { 227 | num++; 228 | } else { 229 | num--; 230 | } 231 | } 232 | 233 | return num; 234 | } 235 | 236 | function exportSVG() { 237 | // Set up element 238 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 239 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 240 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 241 | svg.setAttribute('width', window.innerWidth); 242 | svg.setAttribute('height', window.innerHeight); 243 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 244 | 245 | let polygons = getVoronoiPolygons(); 246 | 247 | for(let polygon of polygons) { 248 | svg.appendChild( createPathElFromPoints(polygon) ); 249 | } 250 | 251 | // Generate the document as a Blob and force a download as a timestamped .svg file 252 | const svgDoctype = ''; 253 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 254 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 255 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 256 | } 257 | 258 | function createPathElFromPoints(points) { 259 | let pointsString = ''; 260 | 261 | for(let [index, point] of points.entries()) { 262 | pointsString += point[0] + ',' + point[1]; 263 | 264 | if(index < points.length - 1) { 265 | pointsString += ' '; 266 | } 267 | } 268 | 269 | let d = toPath({ 270 | type: 'polyline', 271 | points: pointsString 272 | }); 273 | 274 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 275 | pathEl.setAttribute('d', d); 276 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 277 | 278 | return pathEl; 279 | } 280 | 281 | 282 | 283 | /* 284 | Key released handler 285 | ==================== 286 | */ 287 | p5.keyReleased = function () { 288 | switch (p5.key) { 289 | case 'r': 290 | generatePoints(); 291 | break; 292 | 293 | case 'p': 294 | showPoints = !showPoints; 295 | break; 296 | 297 | case 'i': 298 | invertColors = !invertColors; 299 | break; 300 | 301 | case 'b': 302 | useBlurEffect = !useBlurEffect; 303 | break; 304 | 305 | case 'e': 306 | exportSVG(); 307 | break; 308 | 309 | case ' ': 310 | isPaused = !isPaused; 311 | break; 312 | 313 | case '1': 314 | ROW_TYPE = EVEN; 315 | generatePoints(); 316 | break; 317 | 318 | case '2': 319 | ROW_TYPE = ODD; 320 | generatePoints(); 321 | break; 322 | 323 | case '3': 324 | ROW_TYPE = ALTERNATING; 325 | generatePoints(); 326 | break; 327 | 328 | case '4': 329 | ROW_TYPE = ANY; 330 | generatePoints(); 331 | break; 332 | } 333 | } 334 | } 335 | 336 | // Launch the sketch using p5js in instantiated mode 337 | new p5(sketch); -------------------------------------------------------------------------------- /rings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Rings 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /rings/js/entry.js: -------------------------------------------------------------------------------- 1 | import {Delaunay} from "d3-delaunay"; 2 | import {toPath} from 'svg-points'; 3 | import {saveAs} from 'file-saver'; 4 | 5 | let showPoints = false, 6 | invertColors = false, 7 | points; 8 | 9 | // Number of points per ring/row 10 | const EVEN = 0, 11 | ODD = 1, 12 | ALTERNATING = 2, 13 | ANY = 3; 14 | let ROW_TYPE = EVEN; 15 | 16 | let currentRowType = EVEN; 17 | 18 | const sketch = function (p5) { 19 | /* 20 | Setup 21 | ===== 22 | */ 23 | p5.setup = function () { 24 | p5.createCanvas(window.innerWidth, window.innerHeight); 25 | p5.background(0); 26 | generatePoints(); 27 | } 28 | 29 | 30 | /* 31 | Draw 32 | ==== 33 | */ 34 | p5.draw = function () { 35 | drawVoronoi(); 36 | drawPoints(); 37 | } 38 | 39 | 40 | /* 41 | Key released handler 42 | ==================== 43 | */ 44 | p5.keyReleased = function () { 45 | switch (p5.key) { 46 | case 'r': 47 | generatePoints(); 48 | break; 49 | 50 | case 'p': 51 | showPoints = !showPoints; 52 | break; 53 | 54 | case 'i': 55 | invertColors = !invertColors; 56 | break; 57 | 58 | case 'e': 59 | exportSVG(); 60 | break; 61 | 62 | case '1': 63 | ROW_TYPE = EVEN; 64 | generatePoints(); 65 | break; 66 | 67 | case '2': 68 | ROW_TYPE = ODD; 69 | generatePoints(); 70 | break; 71 | 72 | case '3': 73 | ROW_TYPE = ALTERNATING; 74 | generatePoints(); 75 | break; 76 | 77 | case '4': 78 | ROW_TYPE = ANY; 79 | generatePoints(); 80 | break; 81 | } 82 | } 83 | 84 | 85 | /* 86 | Custom functions 87 | ================ 88 | */ 89 | 90 | // Draw the Voronoi diagram for a set of points 91 | function drawVoronoi() { 92 | if (invertColors) { 93 | p5.background(0); 94 | p5.stroke(255); 95 | } else { 96 | p5.background(255); 97 | p5.stroke(0); 98 | } 99 | 100 | p5.noFill(); 101 | 102 | const polygons = getVoronoiPolygons(); 103 | 104 | // Draw raw polygons 105 | for (const polygon of polygons) { 106 | p5.beginShape(); 107 | 108 | for (const vertex of polygon) { 109 | p5.vertex(vertex[0], vertex[1]); 110 | } 111 | 112 | p5.endShape(); 113 | } 114 | } 115 | 116 | // Draw dots for each of the points 117 | function drawPoints() { 118 | if (showPoints) { 119 | p5.noStroke(); 120 | 121 | if (invertColors) { 122 | p5.fill(100); 123 | } else { 124 | p5.fill(200); 125 | } 126 | 127 | for (let point of points) { 128 | p5.ellipse(point[0], point[1], 5); 129 | } 130 | } 131 | } 132 | 133 | // Get an array of polygons (arrays of [x,y] pairs) using Voronoi 134 | function getVoronoiPolygons() { 135 | const delaunay = Delaunay.from(points); 136 | const voronoi = delaunay.voronoi([0, 0, window.innerWidth, window.innerHeight]); 137 | const simplifiedPolygons = []; 138 | 139 | for(let cell of voronoi.cellPolygons()) { 140 | let polygon = []; 141 | 142 | for(let vertex of cell) { 143 | polygon.push([vertex[0], vertex[1]]); 144 | } 145 | 146 | simplifiedPolygons.push(polygon); 147 | } 148 | 149 | return simplifiedPolygons; 150 | } 151 | 152 | function generatePoints() { 153 | points = []; 154 | let numRings = parseInt(p5.random(5, 20)); 155 | const maxRadius = (window.innerWidth > window.innerHeight) ? window.innerHeight/2 - 10 : window.innerWidth/2 - 10; 156 | const minRadius = p5.random(10, 30); 157 | let currentRadius = maxRadius; 158 | const radiusStep = (maxRadius - minRadius) / numRings; 159 | 160 | // Generate set of points for Voronoi diagram 161 | for (let i = 0; i < numRings; i++) { 162 | let numPoints, range = []; 163 | 164 | // Rings near the center look better with fewer points 165 | if (i > 3) { 166 | range[0] = 5; 167 | range[1] = 10; 168 | } else { 169 | range[0] = 20; 170 | range[1] = 100; 171 | } 172 | 173 | // TODO: make range proportional to i. 174 | 175 | // Generate a random number of points based on selected "row type" 176 | switch (ROW_TYPE) { 177 | case EVEN: 178 | numPoints = getRandomEvenNumber(range[0], range[1]); 179 | break; 180 | 181 | case ODD: 182 | numPoints = getRandomOddNumber(range[0], range[1]); 183 | break; 184 | 185 | case ALTERNATING: 186 | switch (currentRowType) { 187 | case EVEN: 188 | numPoints = getRandomEvenNumber(range[0], range[1]); 189 | currentRowType = ODD; 190 | break; 191 | 192 | case ODD: 193 | numPoints = getRandomOddNumber(range[0], range[1]); 194 | currentRowType = EVEN; 195 | break; 196 | } 197 | 198 | break; 199 | 200 | case ANY: 201 | numPoints = parseInt(p5.random(range[0], range[1])); 202 | break; 203 | } 204 | 205 | // Generate 2D array of evenly-spaced points ([x,y]) in a circle 206 | for(let j = 0; j < numPoints; j++) { 207 | points.push([ 208 | window.innerWidth/2 + currentRadius * Math.cos(p5.radians((360 / numPoints) * j)), 209 | window.innerHeight/2 + currentRadius * Math.sin(p5.radians((360 / numPoints) * j)) 210 | ]); 211 | } 212 | 213 | // Decrease radius for next ring 214 | currentRadius -= radiusStep + p5.random(-radiusStep/2, radiusStep); 215 | } 216 | } 217 | 218 | function getRandomEvenNumber(min, max) { 219 | let num = parseInt(p5.random(min, max)); 220 | 221 | if (num % 2 > 0) { 222 | if (num - 1 < min) { 223 | num++; 224 | } else { 225 | num--; 226 | } 227 | } 228 | 229 | return num; 230 | } 231 | 232 | function getRandomOddNumber(min, max) { 233 | let num = parseInt(p5.random(min, max)); 234 | 235 | if (num % 2 == 0) { 236 | if (num - 1 < min) { 237 | num++; 238 | } else { 239 | num--; 240 | } 241 | } 242 | 243 | return num; 244 | } 245 | 246 | // Export an SVG file of the current geometry 247 | function exportSVG() { 248 | // Set up element 249 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 250 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 251 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 252 | svg.setAttribute('width', window.innerWidth); 253 | svg.setAttribute('height', window.innerHeight); 254 | svg.setAttribute('viewBox', '0 0 ' + window.innerWidth + ' ' + window.innerHeight); 255 | 256 | let polygons = getVoronoiPolygons(); 257 | 258 | for(let polygon of polygons) { 259 | svg.appendChild( createPathElFromPoints(polygon) ); 260 | } 261 | 262 | // Generate the document as a Blob and force a download as a timestamped .svg file 263 | const svgDoctype = ''; 264 | const serializedSvg = (new XMLSerializer()).serializeToString(svg); 265 | const blob = new Blob([svgDoctype, serializedSvg], { type: 'image/svg+xml;' }) 266 | saveAs(blob, 'voronoi-' + Date.now() + '.svg'); 267 | } 268 | 269 | function createPathElFromPoints(points) { 270 | let pointsString = ''; 271 | 272 | for(let [index, point] of points.entries()) { 273 | pointsString += point[0] + ',' + point[1]; 274 | 275 | if(index < points.length - 1) { 276 | pointsString += ' '; 277 | } 278 | } 279 | 280 | let d = toPath({ 281 | type: 'polyline', 282 | points: pointsString 283 | }); 284 | 285 | let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 286 | pathEl.setAttribute('d', d); 287 | pathEl.setAttribute('style', 'fill: none; stroke: black; stroke-width: 1'); 288 | 289 | return pathEl; 290 | } 291 | } 292 | 293 | // Launch the sketch using p5js in instantiated mode 294 | new p5(sketch); -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Didact+Gothic'); 2 | 3 | body { 4 | padding: 0 15%; 5 | font-family: Verdana, Geneva, Tahoma, sans-serif; 6 | } 7 | 8 | @media screen and (max-width: 1024px) { 9 | body { 10 | padding: 0 4%; 11 | } 12 | } 13 | 14 | 15 | /* 16 | Header 17 | ====== 18 | Introduction and source link on home page 19 | */ 20 | header { 21 | margin-top: 40px; 22 | margin-bottom: 50px !important; 23 | } 24 | 25 | @media screen and (max-width: 768px) { 26 | header { 27 | margin-top: 20px; 28 | } 29 | } 30 | 31 | header .button { 32 | margin-top: 20px; 33 | } 34 | 35 | header .button .fab { 36 | padding-right: 10px; 37 | } 38 | 39 | 40 | /* 41 | Main section 42 | ============ 43 | Contains list of linked images/videos on home page 44 | */ 45 | main {} 46 | 47 | main ul { 48 | list-style: none; 49 | margin: 0 auto; 50 | padding: 20px 0 100px 0; 51 | /* width: 70%; */ 52 | } 53 | 54 | @media screen and (max-width: 1024px) { 55 | main ul { 56 | width: 100%; 57 | padding: 0; 58 | } 59 | } 60 | 61 | main ul li { 62 | margin: 0; 63 | } 64 | 65 | main ul li a { 66 | display: block; 67 | position: relative; 68 | } 69 | 70 | main ul li a .overlay { 71 | position: absolute; 72 | top: 0; 73 | left: 0; 74 | width: 100%; 75 | height: 100%; 76 | background-color: rgba(0,0,0,.9); 77 | opacity: 0; 78 | color: white; 79 | display: flex; 80 | justify-content: center; 81 | align-items: center; 82 | transition: opacity .5s ease-in-out; 83 | font-family: 'Didact Gothic', sans-serif; 84 | font-size: 60px; 85 | } 86 | 87 | main ul li a:hover .overlay { 88 | opacity: 1; 89 | } 90 | 91 | main ul li a img, 92 | main ul li a video { 93 | width: 100%; 94 | height: auto; 95 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { 6 | basic: path.resolve('basic/js/entry.js'), 7 | rings: path.resolve('rings/js/entry.js'), 8 | ringsInMotion: path.resolve('rings-in-motion/js/entry.js'), 9 | orbits: path.resolve('orbits/js/entry.js'), 10 | playground: path.resolve('playground/js/entry.js'), 11 | reducingRadii: path.resolve('reducing-radii/js/entry.js'), 12 | phyllotaxis: path.resolve('phyllotaxis/js/entry.js') 13 | }, 14 | devtool: 'inline-source-map', 15 | devServer: { 16 | host: '127.0.0.1', 17 | port: 9001, 18 | publicPath: '/dist/', 19 | contentBase: path.resolve('./'), 20 | compress: true, 21 | open: true, 22 | watchContentBase: true 23 | }, 24 | output: { 25 | filename: '[name].bundle.js', 26 | path: path.resolve('dist') 27 | } 28 | } --------------------------------------------------------------------------------