├── LICENSE ├── README.md ├── StippleGen ├── README.txt ├── StippleGen.pde └── data │ ├── grace.jpg │ └── header.txt └── examples ├── inputs ├── circle2.png ├── sphere2.png └── zener.jpg └── screenshots └── 7368233514_59f7d25603.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | (This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.) 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | {description} 474 | Copyright (C) {year} {fullname} 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | {signature of Ty Coon}, 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | 506 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StippleGen 2 | Software that can create stipple drawings and “TSP art,” from image files 3 | 4 | Copyright (C) 2016 by Windell H. Oskay, www.evilmadscientist.com 5 | 6 | ![Snap! Screenshot](https://raw.github.com/evil-mad/stipplegen/master/examples/screenshots/7368233514_59f7d25603.jpg) 7 | 8 | This is a program, written in [Processing](https://www.processing.org), that can take an image file in .jpg, .png, or .gif format, and create from it either a vector stipple drawing or a [TSP](https://en.wikipedia.org/wiki/Travelling_salesman_problem) art vector drawing. 9 | 10 | An implementation of Weighted Voronoi Stippling: 11 | https://web.archive.org/web/20210106025705/https://mrl.cs.nyu.edu/~ajsecord/stipples.html 12 | 13 | Full Documentation: http://wiki.evilmadscience.com/StippleGen 14 | 15 | Blog post about the release: http://www.evilmadscientist.com/go/stipple2 16 | 17 | -------------------------------------------------------------------------------- /StippleGen/README.txt: -------------------------------------------------------------------------------- 1 | 2 | StippleGen_2_40 3 | 4 | SVG Stipple Generator, v. 2.4.0 5 | Copyright (C) 2016 by Windell H. Oskay, www.evilmadscientist.com 6 | 7 | Full Documentation: http://wiki.evilmadscientist.com/StippleGen 8 | Blog post about the release: http://www.evilmadscientist.com/go/stipple2 9 | 10 | 11 | An implementation of Weighted Voronoi Stippling: 12 | http://mrl.nyu.edu/~ajsecord/stipples.html 13 | 14 | ******************************************************************************* 15 | 16 | Change Log: 17 | 18 | v 2.4 19 | * Processing 3.0.1 20 | 21 | v 2.3 22 | * Forked from 2.1.1 23 | * Fixed saving bug 24 | 25 | v 2.20 26 | * [Cancelled development branch.] 27 | 28 | v 2.1.1 29 | * Faster now, with number of stipples calculated at a time. 30 | 31 | v 2.1.0 32 | * Now compiling in Processing 2.0b6 33 | * selectInput() and selectOutput() calls modified for Processing 2. 34 | 35 | v 2.02 36 | * Force files to end in .svg 37 | * Fix bug that gave wrong size to stipple files saved white stipples on black background 38 | 39 | v 2.01: 40 | * Improved handling of Save process, to prevent accidental "not saving" by users. 41 | 42 | v 2.0: 43 | * Add tone reversal option (white on black / black on white) 44 | * Reduce vertical extent of GUI, to reduce likelihood of cropping on small screens 45 | * Speling corections 46 | * Fixed a bug that caused unintended cropping of long, wide images 47 | * Reorganized GUI controls 48 | * Fail less disgracefully when a bad image type is selected. 49 | 50 | ******************************************************************************* 51 | 52 | 53 | 54 | Program is based on the Toxic Libs Library ( http://toxiclibs.org/ ) 55 | & example code: 56 | http://forum.processing.org/topic/toxiclib-voronoi-example-sketch 57 | 58 | 59 | Additional inspiration: 60 | Stipple Cam from Jim Bumgardner 61 | http://joyofprocessing.com/blog/2011/11/stipple-cam/ 62 | 63 | and 64 | 65 | MeshLibDemo.pde - Demo of Lee Byron's Mesh library, by 66 | Marius Watz - http://workshop.evolutionzone.com/ 67 | 68 | 69 | Requires ControlP5 library and Toxic Libs library: 70 | http://www.sojamo.de/libraries/controlP5/ 71 | http://bitbucket.org/postspectacular/toxiclibs/downloads/ 72 | -------------------------------------------------------------------------------- /StippleGen/StippleGen.pde: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | StippleGen_2_40 4 | 5 | SVG Stipple Generator, v. 2.40 6 | Copyright (C) 2016 by Windell H. Oskay, www.evilmadscientist.com 7 | 8 | Full Documentation: http://wiki.evilmadscientist.com/StippleGen 9 | Blog post about the release: http://www.evilmadscientist.com/go/stipple2 10 | 11 | An implementation of Weighted Voronoi Stippling: 12 | http://mrl.nyu.edu/~ajsecord/stipples.html 13 | 14 | ******************************************************************************* 15 | 16 | Change Log: 17 | 18 | v 2.4 19 | * Compiling in Processing 3.0.1 20 | * Add GUI option to fill circles with a spiral 21 | 22 | v 2.3 23 | * Forked from 2.1.1 24 | * Fixed saving bug 25 | 26 | v 2.20 27 | * [Cancelled development branch.] 28 | 29 | v 2.1.1 30 | * Faster now, with number of stipples calculated at a time. 31 | 32 | v 2.1.0 33 | * Now compiling in Processing 2.0b6 34 | * selectInput() and selectOutput() calls modified for Processing 2. 35 | 36 | v 2.02 37 | * Force files to end in .svg 38 | * Fix bug that gave wrong size to stipple files saved white stipples on black background 39 | 40 | v 2.01: 41 | * Improved handling of Save process, to prevent accidental "not saving" by users. 42 | 43 | v 2.0: 44 | * Add tone reversal option (white on black / black on white) 45 | * Reduce vertical extent of GUI, to reduce likelihood of cropping on small screens 46 | * Speling corections 47 | * Fixed a bug that caused unintended cropping of long, wide images 48 | * Reorganized GUI controls 49 | * Fail less disgracefully when a bad image type is selected. 50 | 51 | ******************************************************************************* 52 | 53 | Program is based on the Toxic Libs Library ( http://toxiclibs.org/ ) 54 | & example code: 55 | http://forum.processing.org/topic/toxiclib-voronoi-example-sketch 56 | 57 | Additional inspiration: 58 | Stipple Cam from Jim Bumgardner 59 | http://joyofprocessing.com/blog/2011/11/stipple-cam/ 60 | 61 | and 62 | 63 | MeshLibDemo.pde - Demo of Lee Byron's Mesh library, by 64 | Marius Watz - http://workshop.evolutionzone.com/ 65 | 66 | Requires ControlP5 library and Toxic Libs library: 67 | http://www.sojamo.de/libraries/controlP5/ 68 | http://bitbucket.org/postspectacular/toxiclibs/downloads/ 69 | 70 | */ 71 | 72 | /* 73 | * This is free software; you can redistribute it and/or 74 | * modify it under the terms of the GNU Lesser General Public 75 | * License as published by the Free Software Foundation; either 76 | * version 2.1 of the License, or (at your option) any later version. 77 | * 78 | * http://creativecommons.org/licenses/LGPL/2.1/ 79 | * 80 | * This library is distributed in the hope that it will be useful, 81 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 82 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 83 | * Lesser General Public License for more details. 84 | * 85 | * You should have received a copy of the GNU Lesser General Public 86 | * License along with this library; if not, write to the Free Software 87 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 88 | */ 89 | 90 | // You need the controlP5 library from http://www.sojamo.de/libraries/controlP5/ 91 | import controlP5.*; 92 | 93 | //You need the Toxic Libs library: http://hg.postspectacular.com/toxiclibs/downloads 94 | import toxi.geom.*; 95 | import toxi.geom.mesh2d.*; 96 | import toxi.util.datatypes.*; 97 | import toxi.processing.*; 98 | 99 | import javax.swing.UIManager; 100 | import javax.swing.JFileChooser; 101 | 102 | // helper class for rendering 103 | ToxiclibsSupport gfx; 104 | 105 | // Feel free to play with these three default settings 106 | float cutoff = 0; 107 | float minDotSize = 1.75; 108 | float dotSizeFactor = 4; 109 | // Max value is normally 10000. Press 'x' key to allow 50000 stipples. (SLOW) 110 | int maxParticles = 2000; 111 | 112 | //Scale each cell to fit in a cellBuffer-sized square window for computing the centroid. 113 | int cellBuffer = 100; 114 | 115 | // Display window and GUI area sizes: 116 | int mainwidth; 117 | int mainheight; 118 | int borderWidth; 119 | int ctrlheight; 120 | int textColumnStart; 121 | 122 | float lowBorderX; 123 | float hiBorderX; 124 | float lowBorderY; 125 | float hiBorderY; 126 | 127 | float maxDotSize; 128 | boolean reInitiallizeArray; 129 | boolean pausemode; 130 | boolean fileLoaded; 131 | boolean saveNow; 132 | String savePath; 133 | String[] fileOutput; 134 | 135 | boolean fillingCircles; 136 | 137 | String statusDisplay = "Initializing, please wait. :)"; 138 | float millisLastFrame = 0; 139 | float frameTime = 0; 140 | 141 | float errorTime; 142 | String errorDisplay = ""; 143 | boolean errorDisp = false; 144 | 145 | int generation; 146 | int particleRouteLength; 147 | int routeStep; 148 | 149 | boolean invertImg; 150 | boolean fileModeTSP; 151 | boolean tempShowCells; 152 | boolean showBG, showPath, showCells; 153 | 154 | int vorPointsAdded; 155 | boolean voronoiCalculated; 156 | 157 | int cellsTotal, cellsCalculated, cellsCalculatedLast; 158 | 159 | int[] particleRoute; 160 | Vec2D[] particles; 161 | 162 | ControlP5 cp5; 163 | Voronoi voronoi; 164 | Polygon2D regionList[]; 165 | PolygonClipper2D clip; 166 | PImage img, imgload, imgblur; 167 | 168 | void LoadImageAndScale() { 169 | int tempx = 0; 170 | int tempy = 0; 171 | 172 | img = createImage(mainwidth, mainheight, RGB); 173 | imgblur = createImage(mainwidth, mainheight, RGB); 174 | 175 | img.loadPixels(); 176 | 177 | for (int i = 0; i < img.pixels.length; i++) { 178 | img.pixels[i] = color(invertImg ? 0 : 255); 179 | } 180 | 181 | img.updatePixels(); 182 | 183 | if (!fileLoaded) { 184 | // Load a demo image, at least until we have a "real" image to work with. 185 | // Image from: http://commons.wikimedia.org/wiki/File:Kelly,_Grace_(Rear_Window).jpg 186 | imgload = loadImage("grace.jpg"); // Load demo image 187 | } 188 | 189 | if ((imgload.width > mainwidth) || (imgload.height > mainheight)) { 190 | if (((float)imgload.width / (float)imgload.height) > ((float)mainwidth / (float)mainheight)) 191 | { 192 | imgload.resize(mainwidth, 0); 193 | } else { 194 | imgload.resize(0, mainheight); 195 | } 196 | } 197 | 198 | if (imgload.height < (mainheight - 2)) { 199 | tempy = (int)((mainheight - imgload.height) / 2) ; 200 | } 201 | if (imgload.width < (mainwidth - 2)) { 202 | tempx = (int)((mainwidth - imgload.width) / 2) ; 203 | } 204 | 205 | img.copy(imgload, 0, 0, imgload.width, imgload.height, tempx, tempy, imgload.width, imgload.height); 206 | // For background image! 207 | 208 | /* 209 | // Optional gamma correction for background image. 210 | img.loadPixels(); 211 | 212 | float tempFloat; 213 | float GammaValue = 1.0; // Normally in the range 0.25 - 4.0 214 | 215 | for (int i = 0; i < img.pixels.length; i++) { 216 | tempFloat = brightness(img.pixels[i])/255; 217 | img.pixels[i] = color(floor(255 * pow(tempFloat,GammaValue))); 218 | } 219 | img.updatePixels(); 220 | */ 221 | 222 | imgblur.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height); 223 | // This is a duplicate of the background image, that we will apply a blur to, 224 | // to reduce "high frequency" noise artifacts. 225 | 226 | // Low-level blur filter to elminate pixel-to-pixel noise artifacts. 227 | imgblur.filter(BLUR, 1); 228 | imgblur.loadPixels(); 229 | } 230 | 231 | void MainArraySetup() { 232 | // Main particle array initialization (to be called whenever necessary): 233 | LoadImageAndScale(); 234 | 235 | // image(img, 0, 0); // SHOW BG IMG 236 | 237 | particles = new Vec2D[maxParticles]; 238 | 239 | // Fill array by "rejection sampling" 240 | int i = 0; 241 | while (i < maxParticles) { 242 | float fx = lowBorderX + random(hiBorderX - lowBorderX); 243 | float fy = lowBorderY + random(hiBorderY - lowBorderY); 244 | 245 | float p = brightness(imgblur.pixels[ floor(fy)*imgblur.width + floor(fx) ])/255; 246 | // OK to use simple floor_ rounding here, because this is a one-time operation, 247 | // creating the initial distribution that will be iterated. 248 | 249 | if (invertImg) { 250 | p = 1 - p; 251 | } 252 | 253 | if (random(1) >= p ) { 254 | Vec2D p1 = new Vec2D(fx, fy); 255 | particles[i] = p1; 256 | i++; 257 | } 258 | } 259 | 260 | particleRouteLength = 0; 261 | generation = 0; 262 | millisLastFrame = millis(); 263 | routeStep = 0; 264 | voronoiCalculated = false; 265 | cellsCalculated = 0; 266 | vorPointsAdded = 0; 267 | voronoi = new Voronoi(); // Erase mesh 268 | tempShowCells = true; 269 | fileModeTSP = false; 270 | } 271 | 272 | void setup() { 273 | borderWidth = 6; 274 | mainwidth = 800; 275 | mainheight = 600; 276 | ctrlheight = 110; 277 | fillingCircles = true; 278 | 279 | size(800, 710); 280 | 281 | gfx = new ToxiclibsSupport(this); 282 | 283 | lowBorderX = borderWidth; //mainwidth*0.01; 284 | hiBorderX = mainwidth - borderWidth; //mainwidth*0.98; 285 | lowBorderY = borderWidth; // mainheight*0.01; 286 | hiBorderY = mainheight - borderWidth; //mainheight*0.98; 287 | 288 | int innerWidth = mainwidth - 2 * borderWidth; 289 | int innerHeight = mainheight - 2 * borderWidth; 290 | 291 | Rect rect = new Rect(lowBorderX, lowBorderY, innerWidth, innerHeight); 292 | clip = new SutherlandHodgemanClipper(rect); 293 | 294 | MainArraySetup(); // Main particle array setup 295 | 296 | frameRate(24); 297 | smooth(); 298 | noStroke(); 299 | fill(153); // Background fill color, for control section 300 | 301 | textFont(createFont("SansSerif", 10)); 302 | 303 | cp5 = new ControlP5(this); 304 | 305 | int leftcolumwidth = 225; 306 | int guiTop = mainheight + 15; 307 | int gui2ndRow = 4; // Spacing for firt row after group heading 308 | int guiRowSpacing = 14; // Spacing for subsequent rows 309 | int buttonHeight = mainheight + 19 + int(round(2.25 * guiRowSpacing)); 310 | 311 | ControlGroup l3 = cp5.addGroup("Primary controls (Changing will restart)", 10, guiTop, 225); 312 | 313 | cp5.addSlider("sliderStipples", 10, 10000, maxParticles, 10, gui2ndRow, 150, 10) 314 | .setGroup(l3); 315 | 316 | cp5.addButton("buttonInvertImg", 10, 10, gui2ndRow + guiRowSpacing, 190, 10) 317 | .setCaptionLabel("Black stipples, White Background") 318 | .setGroup(l3); 319 | 320 | cp5.addButton("buttonLoadFile", 10, 10, buttonHeight, 175, 10) 321 | .setCaptionLabel("LOAD IMAGE FILE (.PNG, .JPG, or .GIF)"); 322 | 323 | cp5.addButton("buttonQuit", 10, 205, buttonHeight, 30, 10) 324 | .setCaptionLabel("Quit"); 325 | 326 | cp5.addButton("buttonSaveStipples", 10, 25, buttonHeight + guiRowSpacing, 160, 10) 327 | .setCaptionLabel("Save Stipple File (.SVG format)"); 328 | 329 | cp5.addButton("buttonSavePath", 10, 25, buttonHeight + 2 * guiRowSpacing, 160, 10) 330 | .setCaptionLabel("Save \"TSP\" Path (.SVG format)"); 331 | 332 | cp5.addButton("buttonFillCircles", 10, 10, buttonHeight + 3 * guiRowSpacing, 190, 10) 333 | .setCaptionLabel("Generate Filled circles in output"); 334 | 335 | ControlGroup l5 = cp5.addGroup("Display Options - Updated on next generation", leftcolumwidth+50, guiTop, 225); 336 | 337 | cp5.addSlider("sliderMinDotSize", .5, 8, 2, 10, 4, 140, 10) 338 | .setCaptionLabel("Min. Dot Size") 339 | .setValue(minDotSize) 340 | .setGroup(l5); 341 | 342 | cp5.addSlider("sliderDotSizeRange", 0, 20, 5, 10, 18, 140, 10) 343 | .setCaptionLabel("Dot Size Range") 344 | .setValue(dotSizeFactor) 345 | .setGroup(l5); 346 | 347 | cp5.addSlider("sliderWhiteCutoff", 0, 1, 0, 10, 32, 140, 10) 348 | .setCaptionLabel("White Cutoff") 349 | .setValue(cutoff) 350 | .setGroup(l5); 351 | 352 | cp5.addButton("buttonImgOnOff", 10, 10, 46, 90, 10) 353 | .setCaptionLabel("Image BG >> Hide") 354 | .setGroup(l5); 355 | 356 | cp5.addButton("buttonCellsOnOff", 10, 110, 46, 90, 10) 357 | .setCaptionLabel("Cells >> Hide") 358 | .setGroup(l5); 359 | 360 | cp5.addButton("buttonPause", 10, 10, 60, 190, 10) 361 | .setCaptionLabel("Pause (to calculate TSP path)") 362 | .setGroup(l5); 363 | 364 | cp5.addButton("buttonOrderOnOff", 10, 10, 74, 190, 10) 365 | .setCaptionLabel("Plotting path >> shown while paused") 366 | .setGroup(l5); 367 | 368 | textColumnStart = 2 * leftcolumwidth + 100; 369 | maxDotSize = getMaxDotSize(minDotSize); 370 | 371 | saveNow = false; 372 | showBG = false; 373 | showPath = true; 374 | showCells = false; 375 | pausemode = false; 376 | invertImg = false; 377 | fileLoaded = false; 378 | reInitiallizeArray = false; 379 | } 380 | 381 | void fileSelected(File selection) { 382 | if (selection == null) { 383 | println("Window was closed or the user hit cancel."); 384 | } else { 385 | //println("User selected " + selection.getAbsolutePath()); 386 | 387 | String loadPath = selection.getAbsolutePath(); 388 | 389 | // If a file was selected, print path to file 390 | println("Loaded file: " + loadPath); 391 | 392 | String[] p = splitTokens(loadPath, "."); 393 | String ext = p[p.length - 1].toLowerCase(); 394 | 395 | boolean fileOK = false; 396 | fileOK = fileOK || ext.equals("gif"); 397 | fileOK = fileOK || ext.equals("jpg"); 398 | fileOK = fileOK || ext.equals("tga"); 399 | fileOK = fileOK || ext.equals("png"); 400 | 401 | println("File OK: " + fileOK); 402 | 403 | if (fileOK) { 404 | imgload = loadImage(loadPath); 405 | fileLoaded = true; 406 | reInitiallizeArray = true; 407 | } else { 408 | // Can't load file 409 | errorDisplay = "ERROR: BAD FILE TYPE"; 410 | errorTime = millis(); 411 | errorDisp = true; 412 | } 413 | } 414 | } 415 | 416 | void buttonLoadFile(float theValue) { 417 | println(":::LOAD JPG, GIF or PNG FILE:::"); 418 | selectInput("Select a file to process:", "fileSelected"); // Opens file chooser 419 | } 420 | 421 | void buttonSavePath(float theValue) { 422 | fileModeTSP = true; 423 | saveSvg(0); 424 | } 425 | 426 | void buttonSaveStipples(float theValue) { 427 | fileModeTSP = false; 428 | saveSvg(0); 429 | } 430 | 431 | void SavefileSelected(File selection) { 432 | if (selection == null) { 433 | // If a file was not selected 434 | println("No output file was selected..."); 435 | errorDisplay = "ERROR: NO FILE NAME CHOSEN."; 436 | errorTime = millis(); 437 | errorDisp = true; 438 | } else { 439 | savePath = selection.getAbsolutePath(); 440 | String[] p = splitTokens(savePath, "."); 441 | boolean fileOK = p[p.length - 1].toLowerCase().equals("svg"); 442 | if (!fileOK) savePath = savePath + ".svg"; 443 | 444 | // If a file was selected, print path to folder 445 | println("Save file: " + savePath); 446 | saveNow = true; 447 | showPath = true; 448 | 449 | errorDisplay = "SAVING FILE..."; 450 | errorTime = millis(); 451 | errorDisp = true; 452 | } 453 | } 454 | 455 | void saveSvg(float theValue) { 456 | if (!pausemode) { 457 | buttonPause(0.0); 458 | errorDisplay = "Error: PAUSE before saving."; 459 | errorTime = millis(); 460 | errorDisp = true; 461 | } else { 462 | selectOutput("Output .svg file name:", "SavefileSelected"); 463 | } 464 | } 465 | 466 | void buttonQuit(float theValue) { 467 | exit(); 468 | } 469 | 470 | void buttonOrderOnOff(float theValue) { 471 | Button orderOnOff = (Button)cp5.getController("buttonOrderOnOff"); 472 | if (showPath) { 473 | showPath = false; 474 | orderOnOff.setCaptionLabel("Plotting path >> Hide"); 475 | } else { 476 | showPath = true; 477 | orderOnOff.setCaptionLabel("Plotting path >> Shown while paused"); 478 | } 479 | } 480 | 481 | void buttonCellsOnOff(float theValue) { 482 | Button cellsOnOff = (Button)cp5.getController("buttonCellsOnOff"); 483 | if (showCells) { 484 | showCells = false; 485 | cellsOnOff.setCaptionLabel("Cells >> Hide"); 486 | } else { 487 | showCells = true; 488 | cellsOnOff.setCaptionLabel("Cells >> Show"); 489 | } 490 | } 491 | 492 | void buttonImgOnOff(float theValue) { 493 | Button imgOnOffButton = (Button)cp5.getController("buttonImgOnOff"); 494 | if (showBG) { 495 | showBG = false; 496 | imgOnOffButton.setCaptionLabel("Image BG >> Hide"); 497 | } else { 498 | showBG = true; 499 | imgOnOffButton.setCaptionLabel("Image BG >> Show"); 500 | } 501 | } 502 | 503 | void buttonInvertImg(float theValue) { 504 | Slider cutoffSlider = (Slider)cp5.getController("sliderWhiteCutoff"); 505 | Button invertImgButton = (Button)cp5.getController("buttonInvertImg"); 506 | if (invertImg) { 507 | invertImg = false; 508 | invertImgButton.setCaptionLabel("Black stipples, White background"); 509 | cutoffSlider.setCaptionLabel("White Cutoff"); 510 | } else { 511 | invertImg = true; 512 | invertImgButton.setCaptionLabel("White stipples, Black background"); 513 | cutoffSlider.setCaptionLabel("Black Cutoff"); 514 | } 515 | 516 | reInitiallizeArray = true; 517 | pausemode = false; 518 | } 519 | 520 | void buttonFillCircles(float theValue) { 521 | Button fillCircleButton = (Button)cp5.getController("buttonFillCircles"); 522 | if (fillingCircles) { 523 | fillingCircles = false; 524 | fillCircleButton.setCaptionLabel("Generate Open circles in output"); 525 | } else { 526 | fillingCircles = true; 527 | fillCircleButton.setCaptionLabel("Generate Filled circles in output"); 528 | } 529 | } 530 | 531 | void buttonPause(float theValue) { 532 | // Main particle array setup (to be repeated if necessary): 533 | Button pauseButton = (Button)cp5.getController("buttonPause"); 534 | if (pausemode) { 535 | pausemode = false; 536 | println("Resuming."); 537 | pauseButton.setCaptionLabel("Pause (to calculate TSP path)"); 538 | } else { 539 | pausemode = true; 540 | println("Paused. Press PAUSE again to resume."); 541 | pauseButton.setCaptionLabel("Paused (calculating TSP path)"); 542 | } 543 | routeStep = 0; 544 | } 545 | 546 | boolean overRect(int x, int y, int width, int height) { 547 | return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; 548 | } 549 | 550 | void sliderStipples(int inValue) { 551 | if (maxParticles != inValue) { 552 | println("Update: Stipple Count -> " + inValue); 553 | reInitiallizeArray = true; 554 | pausemode = false; 555 | } 556 | } 557 | 558 | void sliderMinDotSize(float inValue) { 559 | if (minDotSize != inValue) { 560 | println("Update: sliderMinDotSize -> " + inValue); 561 | minDotSize = inValue; 562 | maxDotSize = getMaxDotSize(minDotSize); 563 | } 564 | } 565 | 566 | void sliderDotSizeRange(float inValue) { 567 | if (dotSizeFactor != inValue) { 568 | println("Update: Dot Size Range -> " + inValue); 569 | dotSizeFactor = inValue; 570 | maxDotSize = getMaxDotSize(minDotSize); 571 | } 572 | } 573 | 574 | void sliderWhiteCutoff(float inValue) { 575 | if (cutoff != inValue) { 576 | println("Update: White_Cutoff -> " + inValue); 577 | cutoff = inValue; 578 | routeStep = 0; // Reset TSP path 579 | } 580 | } 581 | 582 | float getMaxDotSize(float minDotSize) { 583 | return minDotSize * (1 + dotSizeFactor); 584 | } 585 | 586 | void doBackgrounds() { 587 | if (showBG) { 588 | image(img, 0, 0); // Show original (cropped and scaled, but not blurred!) image in background 589 | } else { 590 | fill(invertImg ? 0 : 255); 591 | rect(0, 0, mainwidth, mainheight); 592 | } 593 | } 594 | 595 | void optimizePlotPath() { 596 | int temp; 597 | // Calculate and show "optimized" plotting path, beneath points. 598 | 599 | statusDisplay = "Optimizing plotting path"; 600 | /* 601 | if (routeStep % 100 == 0) { 602 | println("RouteStep:" + routeStep); 603 | println("fps = " + frameRate ); 604 | } 605 | */ 606 | 607 | Vec2D p1; 608 | 609 | if (routeStep == 0) { 610 | float cutoffScaled = 1 - cutoff; 611 | // Begin process of optimizing plotting route, by flagging particles that will be shown. 612 | 613 | particleRouteLength = 0; 614 | 615 | boolean particleRouteTemp[] = new boolean[maxParticles]; 616 | 617 | for (int i = 0; i < maxParticles; ++i) { 618 | particleRouteTemp[i] = false; 619 | 620 | int px = (int) particles[i].x; 621 | int py = (int) particles[i].y; 622 | 623 | if ((px >= imgblur.width) || (py >= imgblur.height) || (px < 0) || (py < 0)) { 624 | continue; 625 | } 626 | 627 | float v = (brightness(imgblur.pixels[py * imgblur.width + px])) / 255; 628 | 629 | if (invertImg) { 630 | v = 1 - v; 631 | } 632 | 633 | if (v < cutoffScaled) { 634 | particleRouteTemp[i] = true; 635 | particleRouteLength++; 636 | } 637 | } 638 | 639 | particleRoute = new int[particleRouteLength]; 640 | int tempCounter = 0; 641 | for (int i = 0; i < maxParticles; ++i) { 642 | if (particleRouteTemp[i]) { 643 | particleRoute[tempCounter] = i; 644 | tempCounter++; 645 | } 646 | } 647 | // These are the ONLY points to be drawn in the tour. 648 | } 649 | 650 | if (routeStep < (particleRouteLength - 2)) { 651 | // Nearest neighbor ("Simple, Greedy") algorithm path optimization: 652 | 653 | int StopPoint = routeStep + 1000; // 1000 steps per frame displayed; you can edit this number! 654 | 655 | if (StopPoint > (particleRouteLength - 1)) { 656 | StopPoint = particleRouteLength - 1; 657 | } 658 | 659 | for (int i = routeStep; i < StopPoint; ++i) { 660 | p1 = particles[particleRoute[routeStep]]; 661 | int ClosestParticle = 0; 662 | float distMin = Float.MAX_VALUE; 663 | 664 | for (int j = routeStep + 1; j < (particleRouteLength - 1); ++j) { 665 | Vec2D p2 = particles[particleRoute[j]]; 666 | 667 | float dx = p1.x - p2.x; 668 | float dy = p1.y - p2.y; 669 | float distance = (float) (dx*dx+dy*dy); // Only looking for closest; do not need sqrt factor! 670 | 671 | if (distance < distMin) { 672 | ClosestParticle = j; 673 | distMin = distance; 674 | } 675 | } 676 | 677 | temp = particleRoute[routeStep + 1]; 678 | // p1 = particles[particleRoute[routeStep + 1]]; 679 | particleRoute[routeStep + 1] = particleRoute[ClosestParticle]; 680 | particleRoute[ClosestParticle] = temp; 681 | 682 | if (routeStep < (particleRouteLength - 1)) { 683 | routeStep++; 684 | } else { 685 | println("Now optimizing plot path" ); 686 | } 687 | } 688 | } else { // Initial routing is complete 689 | // 2-opt heuristic optimization: 690 | // Identify a pair of edges that would become shorter by reversing part of the tour. 691 | 692 | for (int i = 0; i < 90000; ++i) { // 1000 tests per frame; you can edit this number. 693 | int indexA = floor(random(particleRouteLength - 1)); 694 | int indexB = floor(random(particleRouteLength - 1)); 695 | 696 | if (Math.abs(indexA - indexB) < 2) { 697 | continue; 698 | } 699 | 700 | if (indexB < indexA) { // swap A, B. 701 | temp = indexB; 702 | indexB = indexA; 703 | indexA = temp; 704 | } 705 | 706 | Vec2D a0 = particles[particleRoute[indexA]]; 707 | Vec2D a1 = particles[particleRoute[indexA + 1]]; 708 | Vec2D b0 = particles[particleRoute[indexB]]; 709 | Vec2D b1 = particles[particleRoute[indexB + 1]]; 710 | 711 | // Original distance: 712 | float dx = a0.x - a1.x; 713 | float dy = a0.y - a1.y; 714 | float distance = (float)(dx*dx+dy*dy); // Only a comparison; do not need sqrt factor! 715 | dx = b0.x - b1.x; 716 | dy = b0.y - b1.y; 717 | distance += (float)(dx*dx+dy*dy); // Only a comparison; do not need sqrt factor! 718 | 719 | // Possible shorter distance? 720 | dx = a0.x - b0.x; 721 | dy = a0.y - b0.y; 722 | float distance2 = (float)(dx*dx+dy*dy); // Only a comparison; do not need sqrt factor! 723 | dx = a1.x - b1.x; 724 | dy = a1.y - b1.y; 725 | distance2 += (float)(dx*dx+dy*dy); // Only a comparison; do not need sqrt factor! 726 | 727 | if (distance2 < distance) { 728 | // Reverse tour between a1 and b0. 729 | 730 | int indexhigh = indexB; 731 | int indexlow = indexA + 1; 732 | 733 | // println("Shorten!" + frameRate ); 734 | 735 | while (indexhigh > indexlow) { 736 | temp = particleRoute[indexlow]; 737 | particleRoute[indexlow] = particleRoute[indexhigh]; 738 | particleRoute[indexhigh] = temp; 739 | 740 | indexhigh--; 741 | indexlow++; 742 | } 743 | } 744 | } 745 | } 746 | 747 | frameTime = (millis() - millisLastFrame) / 1000; 748 | millisLastFrame = millis(); 749 | } 750 | 751 | void doPhysics() { // Iterative relaxation via weighted Lloyd's algorithm. 752 | int temp; 753 | int CountTemp; 754 | 755 | if (!voronoiCalculated) { 756 | // Part I: Calculate voronoi cell diagram of the points. 757 | 758 | statusDisplay = "Calculating Voronoi diagram "; 759 | 760 | // float millisBaseline = millis(); // Baseline for timing studies 761 | // println("Baseline. Time = " + (millis() - millisBaseline) ); 762 | 763 | if (vorPointsAdded == 0) { 764 | voronoi = new Voronoi(); // Erase mesh 765 | } 766 | 767 | temp = vorPointsAdded + 500; // This line: VoronoiPointsPerPass (Feel free to edit this number.) 768 | if (temp > maxParticles) { 769 | temp = maxParticles; 770 | } 771 | 772 | for (int i = vorPointsAdded; i < temp; i++) { 773 | // Optional, for diagnostics::: 774 | // println("particles[i].x, particles[i].y " + particles[i].x + ", " + particles[i].y ); 775 | 776 | voronoi.addPoint(new Vec2D(particles[i].x, particles[i].y )); 777 | vorPointsAdded++; 778 | } 779 | 780 | if (vorPointsAdded >= maxParticles) { 781 | // println("Points added. Time = " + (millis() - millisBaseline) ); 782 | 783 | cellsTotal = voronoi.getRegions().size(); 784 | vorPointsAdded = 0; 785 | cellsCalculated = 0; 786 | cellsCalculatedLast = 0; 787 | 788 | regionList = new Polygon2D[cellsTotal]; 789 | 790 | int i = 0; 791 | for (Polygon2D poly : voronoi.getRegions()) { 792 | regionList[i++] = poly; // Build array of polygons 793 | } 794 | voronoiCalculated = true; 795 | } 796 | } else { // Part II: Calculate weighted centroids of cells. 797 | // float millisBaseline = millis(); 798 | // println("fps = " + frameRate ); 799 | 800 | statusDisplay = "Calculating weighted centroids"; 801 | 802 | temp = cellsCalculated + 500; // This line: CentroidsPerPass (Feel free to edit this number.) 803 | // Higher values give slightly faster computation, but a less responsive GUI. 804 | // Default value: 500 805 | 806 | // Time/frame @ 100: 2.07 @ 50 frames in 807 | // Time/frame @ 200: 1.575 @ 50 808 | // Time/frame @ 500: 1.44 @ 50 809 | 810 | if (temp > cellsTotal) { 811 | temp = cellsTotal; 812 | } 813 | 814 | for (int i=cellsCalculated; i< temp; i++) { 815 | float xMax = 0; 816 | float xMin = mainwidth; 817 | float yMax = 0; 818 | float yMin = mainheight; 819 | float xt, yt; 820 | 821 | Polygon2D region = clip.clipPolygon(regionList[i]); 822 | 823 | for (Vec2D v : region.vertices) { 824 | xt = v.x; 825 | yt = v.y; 826 | 827 | if (xt < xMin) xMin = xt; 828 | if (xt > xMax) xMax = xt; 829 | if (yt < yMin) yMin = yt; 830 | if (yt > yMax) yMax = yt; 831 | } 832 | 833 | float xDiff = xMax - xMin; 834 | float yDiff = yMax - yMin; 835 | float maxSize = max(xDiff, yDiff); 836 | float minSize = min(xDiff, yDiff); 837 | 838 | float scaleFactor = 1.0; 839 | 840 | // Maximum voronoi cell extent should be between 841 | // cellBuffer/2 and cellBuffer in size. 842 | 843 | while (maxSize > cellBuffer) { 844 | scaleFactor *= 0.5; 845 | maxSize *= 0.5; 846 | } 847 | 848 | while (maxSize < (cellBuffer / 2)) { 849 | scaleFactor *= 2; 850 | maxSize *= 2; 851 | } 852 | 853 | if ((minSize * scaleFactor) > (cellBuffer/2)) { 854 | // Special correction for objects of near-unity (square-like) aspect ratio, 855 | // which have larger area *and* where it is less essential to find the exact centroid: 856 | scaleFactor *= 0.5; 857 | } 858 | 859 | float StepSize = (1/scaleFactor); 860 | 861 | float xSum = 0; 862 | float ySum = 0; 863 | float dSum = 0; 864 | float PicDensity = 1.0; 865 | 866 | if (invertImg) { 867 | for (float x=xMin; x<=xMax; x += StepSize) { 868 | for (float y=yMin; y<=yMax; y += StepSize) { 869 | Vec2D p0 = new Vec2D(x, y); 870 | if (region.containsPoint(p0)) { 871 | // Thanks to polygon clipping, NO vertices will be beyond the sides of imgblur. 872 | PicDensity = 0.001 + (brightness(imgblur.pixels[ round(y)*imgblur.width + round(x) ])); 873 | 874 | xSum += PicDensity * x; 875 | ySum += PicDensity * y; 876 | dSum += PicDensity; 877 | } 878 | } 879 | } 880 | } else { 881 | for (float x=xMin; x<=xMax; x += StepSize) { 882 | for (float y=yMin; y<=yMax; y += StepSize) { 883 | Vec2D p0 = new Vec2D(x, y); 884 | if (region.containsPoint(p0)) { 885 | // Thanks to polygon clipping, NO vertices will be beyond the sides of imgblur. 886 | PicDensity = 255.001 - (brightness(imgblur.pixels[ round(y)*imgblur.width + round(x) ])); 887 | 888 | xSum += PicDensity * x; 889 | ySum += PicDensity * y; 890 | dSum += PicDensity; 891 | } 892 | } 893 | } 894 | } 895 | 896 | if (dSum > 0) { 897 | xSum /= dSum; 898 | ySum /= dSum; 899 | } 900 | 901 | Vec2D centr; 902 | 903 | float xTemp = xSum; 904 | float yTemp = ySum; 905 | 906 | if ((xTemp <= lowBorderX) || (xTemp >= hiBorderX) || (yTemp <= lowBorderY) || (yTemp >= hiBorderY)) { 907 | // If new centroid is computed to be outside the visible region, use the geometric centroid instead. 908 | // This will help to prevent runaway points due to numerical artifacts. 909 | centr = region.getCentroid(); 910 | xTemp = centr.x; 911 | yTemp = centr.y; 912 | 913 | // Enforce sides, if absolutely necessary: (Failure to do so *will* cause a crash, eventually.) 914 | 915 | if (xTemp <= lowBorderX) xTemp = lowBorderX + 1; 916 | if (xTemp >= hiBorderX) xTemp = hiBorderX - 1; 917 | if (yTemp <= lowBorderY) yTemp = lowBorderY + 1; 918 | if (yTemp >= hiBorderY) yTemp = hiBorderY - 1; 919 | } 920 | 921 | particles[i].x = xTemp; 922 | particles[i].y = yTemp; 923 | 924 | cellsCalculated++; 925 | } 926 | 927 | // println("cellsCalculated = " + cellsCalculated ); 928 | // println("cellsTotal = " + cellsTotal ); 929 | 930 | if (cellsCalculated >= cellsTotal) { 931 | voronoiCalculated = false; 932 | generation++; 933 | println("Generation = " + generation ); 934 | 935 | frameTime = (millis() - millisLastFrame)/1000; 936 | millisLastFrame = millis(); 937 | } 938 | } 939 | } 940 | 941 | String makeSpiral ( float xOrigin, float yOrigin, float turns, float radius) 942 | { 943 | float resolution = 20.0; 944 | 945 | float AngleStep = TAU / resolution; 946 | float ScaledRadiusPerTurn = radius / (TAU * turns); 947 | 948 | String spiralSVG = " 1.0) { // For small enough circles, skip the fill, and just draw the circle. 957 | for (int i = startPoint; i <= stopPoint; i = i+1) { 958 | angle = i * AngleStep; 959 | x = xOrigin + ScaledRadiusPerTurn * angle * cos(angle); 960 | y = yOrigin + ScaledRadiusPerTurn * angle * sin(angle); 961 | spiralSVG += x + "," + y + " "; 962 | } 963 | } 964 | 965 | // Last turn is a circle: 966 | float CircleRad = ScaledRadiusPerTurn * angle; 967 | 968 | for (int i = 0; i <= resolution; i = i+1) { 969 | angle += AngleStep; 970 | x = xOrigin + radius * cos(angle); 971 | y = yOrigin + radius * sin(angle); 972 | 973 | spiralSVG += x + "," + y + " "; 974 | } 975 | 976 | spiralSVG += "\" />" ; 977 | return spiralSVG; 978 | } 979 | 980 | void draw() { 981 | int i = 0; 982 | int temp; 983 | float dotScale = (maxDotSize - minDotSize); 984 | float cutoffScaled = 1 - cutoff; 985 | 986 | if (reInitiallizeArray) { 987 | // Only change maxParticles here! 988 | maxParticles = (int)cp5.getController("sliderStipples").getValue(); 989 | MainArraySetup(); 990 | reInitiallizeArray = false; 991 | } 992 | 993 | if (pausemode && !voronoiCalculated) { 994 | optimizePlotPath(); 995 | } else { 996 | doPhysics(); 997 | } 998 | 999 | if (pausemode) { 1000 | doBackgrounds(); 1001 | 1002 | // Draw paths: 1003 | 1004 | if (showPath) { 1005 | stroke(128, 128, 255); // Stroke color (blue) 1006 | strokeWeight (1); 1007 | 1008 | for (i = 0; i < particleRouteLength - 1; ++i) { 1009 | Vec2D p1 = particles[particleRoute[i]]; 1010 | Vec2D p2 = particles[particleRoute[i + 1]]; 1011 | line(p1.x, p1.y, p2.x, p2.y); 1012 | } 1013 | } 1014 | 1015 | stroke(invertImg ? 255 : 0); 1016 | fill (invertImg ? 0 : 255); 1017 | strokeWeight(1); 1018 | 1019 | for ( i = 0; i < particleRouteLength; ++i) { 1020 | // Only show "routed" particles-- those above the white cutoff. 1021 | 1022 | Vec2D p1 = particles[particleRoute[i]]; 1023 | int px = (int)p1.x; 1024 | int py = (int)p1.y; 1025 | 1026 | float v = (brightness(imgblur.pixels[py * imgblur.width + px])) / 255; 1027 | 1028 | if (invertImg) v = 1 - v; 1029 | 1030 | if (fillingCircles) { 1031 | strokeWeight(maxDotSize - v * dotScale); 1032 | point(px, py); 1033 | } else { 1034 | float dotSize = maxDotSize - v * dotScale; 1035 | ellipse(px, py, dotSize, dotSize); 1036 | } 1037 | } 1038 | } else { // NOT in pause mode. i.e., just displaying stipples. 1039 | if (cellsCalculated == 0) { 1040 | doBackgrounds(); 1041 | 1042 | tempShowCells = generation == 0; 1043 | 1044 | if (showCells || tempShowCells) { // Draw voronoi cells, over background. 1045 | strokeWeight(1); 1046 | noFill(); 1047 | 1048 | stroke(invertImg && !showBG ? 100 : 200); 1049 | 1050 | i = 0; 1051 | for (Polygon2D poly : voronoi.getRegions()) { 1052 | //regionList[i++] = poly; 1053 | gfx.polygon2D(clip.clipPolygon(poly)); 1054 | } 1055 | } 1056 | 1057 | if (showCells) { 1058 | // Show "before and after" centroids, when polygons are shown. 1059 | 1060 | strokeWeight(minDotSize); // Normal w/ Min & Max dot size 1061 | for ( i = 0; i < maxParticles; ++i) { 1062 | 1063 | int px = (int)particles[i].x; 1064 | int py = (int)particles[i].y; 1065 | 1066 | if ((px >= imgblur.width) || (py >= imgblur.height) || (px < 0) || (py < 0)) 1067 | continue; 1068 | { 1069 | //Uncomment the following four lines, if you wish to display the "before" dots at weighted sizes. 1070 | //float v = (brightness(imgblur.pixels[ py*imgblur.width + px ]))/255; 1071 | //if (invertImg) 1072 | //v = 1 - v; 1073 | //strokeWeight (maxDotSize - v * dotScale); 1074 | point(px, py); 1075 | } 1076 | } 1077 | } 1078 | } else { 1079 | // Stipple calculation is still underway 1080 | 1081 | if (tempShowCells) { 1082 | doBackgrounds(); 1083 | tempShowCells = false; 1084 | } 1085 | 1086 | stroke(invertImg ? 255 : 0); 1087 | fill(invertImg ? 0 : 255); 1088 | strokeWeight(1); 1089 | 1090 | for (i = cellsCalculatedLast; i < cellsCalculated; ++i) { 1091 | int px = (int)particles[i].x; 1092 | int py = (int)particles[i].y; 1093 | 1094 | if ((px >= imgblur.width) || (py >= imgblur.height) || (px < 0) || (py < 0)) 1095 | continue; 1096 | { 1097 | float v = (brightness(imgblur.pixels[py * imgblur.width + px])) / 255; 1098 | 1099 | if (invertImg) v = 1 - v; 1100 | 1101 | if (v < cutoffScaled) { 1102 | if (fillingCircles) { 1103 | strokeWeight(maxDotSize - v * dotScale); 1104 | point(px, py); 1105 | } else { 1106 | float dotSize = maxDotSize - v * dotScale; 1107 | ellipse(px, py, dotSize, dotSize); 1108 | } 1109 | } 1110 | } 1111 | } 1112 | 1113 | cellsCalculatedLast = cellsCalculated; 1114 | } 1115 | } 1116 | 1117 | noStroke(); 1118 | fill(100); // Background fill color 1119 | rect(0, mainheight, mainwidth, height); // Control area fill 1120 | 1121 | // Underlay for hyperlink: 1122 | if (overRect(textColumnStart - 10, mainheight + 35, 205, 20) ) { 1123 | fill(150); 1124 | rect(textColumnStart - 10, mainheight + 35, 205, 20); 1125 | } 1126 | 1127 | fill(255); // Text color 1128 | 1129 | text("StippleGen 2 (v. 2.4.0)", textColumnStart, mainheight + 15); 1130 | text("by Evil Mad Scientist Laboratories", textColumnStart, mainheight + 30); 1131 | text("www.evilmadscientist.com/go/stipple2", textColumnStart, mainheight + 50); 1132 | 1133 | text("Generations completed: " + generation, textColumnStart, mainheight + 85); 1134 | text("Time/Frame: " + frameTime + " s", textColumnStart, mainheight + 100); 1135 | 1136 | if (errorDisp) { 1137 | fill(255, 0, 0); // Text color 1138 | text(errorDisplay, textColumnStart, mainheight + 70); 1139 | errorDisp = !(millis() - errorTime > 8000); 1140 | } else { 1141 | text("Status: " + statusDisplay, textColumnStart, mainheight + 70); 1142 | } 1143 | 1144 | if (saveNow) { 1145 | statusDisplay = "Saving SVG File"; 1146 | saveNow = false; 1147 | 1148 | fileOutput = loadStrings("header.txt"); 1149 | 1150 | String rowTemp; 1151 | 1152 | float SVGscale = (800.0 / (float) mainheight); 1153 | int xOffset = (int)(1600 - (SVGscale * mainwidth / 2)); 1154 | int yOffset = (int)(400 - (SVGscale * mainheight / 2)); 1155 | 1156 | if (fileModeTSP) { // Plot the PATH between the points only. 1157 | println("Save TSP File (SVG)"); 1158 | 1159 | // Path header:: 1160 | rowTemp = ""); // End path description 1173 | } else { 1174 | println("Save Stipple File (SVG)"); 1175 | 1176 | for (i = 0; i < particleRouteLength; ++i) { 1177 | Vec2D p1 = particles[particleRoute[i]]; 1178 | 1179 | int px = floor(p1.x); 1180 | int py = floor(p1.y); 1181 | 1182 | float v = (brightness(imgblur.pixels[py * imgblur.width + px])) / 255; 1183 | 1184 | if (invertImg) v = 1 - v; 1185 | 1186 | float dotrad = (maxDotSize - v * dotScale) / 2; 1187 | 1188 | float xTemp = SVGscale * p1.x + xOffset; 1189 | float yTemp = SVGscale * p1.y + yOffset; 1190 | 1191 | if (fillingCircles) { 1192 | rowTemp = makeSpiral(xTemp, yTemp, dotrad / 2.0, dotrad); 1193 | } else { 1194 | rowTemp = " "; 1195 | } 1196 | //Typ: 1197 | 1198 | fileOutput = append(fileOutput, rowTemp); 1199 | } 1200 | } 1201 | 1202 | // SVG footer: 1203 | fileOutput = append(fileOutput, ""); 1204 | saveStrings(savePath, fileOutput); 1205 | fileModeTSP = false; // reset for next time 1206 | 1207 | if (fileModeTSP) { 1208 | errorDisplay = "TSP Path .SVG file Saved"; 1209 | } else { 1210 | errorDisplay = "Stipple .SVG file saved "; 1211 | } 1212 | 1213 | errorTime = millis(); 1214 | errorDisp = true; 1215 | } 1216 | } 1217 | 1218 | void mousePressed() { 1219 | // rect(textColumnStart, mainheight, 200, 75); 1220 | if (overRect(textColumnStart - 15, mainheight + 35, 205, 20) ) { 1221 | link("http://www.evilmadscientist.com/go/stipple2"); 1222 | } 1223 | } 1224 | 1225 | void keyPressed() { 1226 | if (key == 'x') { // If this program doesn't run slowly enough for you, 1227 | // simply press the 'x' key on your keyboard. :) 1228 | cp5.getController("sliderStipples").setMax(50000.0); 1229 | } 1230 | } 1231 | -------------------------------------------------------------------------------- /StippleGen/data/grace.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evil-mad/stipplegen/ef2e3ca3c52af298e9e63b3c3d8f9a3414d18656/StippleGen/data/grace.jpg -------------------------------------------------------------------------------- /StippleGen/data/header.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 32 | 34 | 35 | 37 | image/svg+xml 38 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | -------------------------------------------------------------------------------- /examples/inputs/circle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evil-mad/stipplegen/ef2e3ca3c52af298e9e63b3c3d8f9a3414d18656/examples/inputs/circle2.png -------------------------------------------------------------------------------- /examples/inputs/sphere2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evil-mad/stipplegen/ef2e3ca3c52af298e9e63b3c3d8f9a3414d18656/examples/inputs/sphere2.png -------------------------------------------------------------------------------- /examples/inputs/zener.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evil-mad/stipplegen/ef2e3ca3c52af298e9e63b3c3d8f9a3414d18656/examples/inputs/zener.jpg -------------------------------------------------------------------------------- /examples/screenshots/7368233514_59f7d25603.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evil-mad/stipplegen/ef2e3ca3c52af298e9e63b3c3d8f9a3414d18656/examples/screenshots/7368233514_59f7d25603.jpg --------------------------------------------------------------------------------