├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .pylintrc ├── Documentation ├── Altepetl │ └── Examples │ │ ├── basic_01.svg │ │ ├── basic_02.svg │ │ ├── basic_03.svg │ │ └── basic_04.svg ├── Comitl │ └── Examples │ │ ├── basic_01.svg │ │ ├── basic_02.svg │ │ ├── basic_03.svg │ │ ├── basic_04.svg │ │ ├── basic_05.svg │ │ ├── basic_06.svg │ │ ├── basic_07.svg │ │ ├── basic_08.svg │ │ ├── basic_09.svg │ │ ├── basic_10.svg │ │ ├── basic_11.svg │ │ └── basic_12.svg ├── Temo │ └── Examples │ │ ├── basic_01.svg │ │ ├── basic_02.svg │ │ ├── basic_03.svg │ │ ├── basic_04.svg │ │ ├── basic_05.svg │ │ ├── basic_06.svg │ │ └── basic_07.svg └── Teocuitlatl │ └── Examples │ ├── basic_01.svg │ ├── basic_02.svg │ ├── basic_03.svg │ ├── basic_04.svg │ └── basic_05.svg ├── LICENSE ├── README.md ├── altepetl.md ├── altepetl.py ├── comitl.md ├── comitl.py ├── requirements.txt ├── temo.md ├── temo.py ├── teocuitlatl.md └── teocuitlatl.py /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the Bug** 11 | A clear and concise description of what the issue is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected Behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots, or generated example files, to help explain the problem. 21 | 22 | **System Information (please complete the following information):** 23 | - OS: [e.g. macOS 10.10, Raspian] 24 | - Python Version [e.g. 3.7.6] 25 | 26 | **Additional Context** 27 | Add any other context about the problem here, if applicable. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an Idea for this Project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 20 * * 4' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['python'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | expected-line-ending-format=LF 3 | indent-string=\t 4 | 5 | [MESSAGES CONTROL] 6 | # Sanity. 7 | disable=invalid-name, 8 | too-many-arguments, 9 | too-many-nested-blocks, 10 | too-many-branches, 11 | too-many-statements, 12 | too-few-public-methods, 13 | too-many-instance-attributes, 14 | too-many-locals, 15 | line-too-long, 16 | import-outside-toplevel, 17 | consider-using-f-string 18 | 19 | 20 | [MISCELLANEOUS] 21 | notes=FIXME,XXX, TODO -------------------------------------------------------------------------------- /Documentation/Altepetl/Examples/basic_01.svg: -------------------------------------------------------------------------------- 1 | An Altepetl Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Altepetl/Examples/basic_02.svg: -------------------------------------------------------------------------------- 1 | An Altepetl Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Altepetl/Examples/basic_03.svg: -------------------------------------------------------------------------------- 1 | An Altepetl Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Altepetl/Examples/basic_04.svg: -------------------------------------------------------------------------------- 1 | An Altepetl Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_01.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_02.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_03.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_04.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_05.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_06.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_07.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_08.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_09.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_10.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_11.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Comitl/Examples/basic_12.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Documentation/Temo/Examples/basic_01.svg: -------------------------------------------------------------------------------- 1 | A Temo Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Temo/Examples/basic_02.svg: -------------------------------------------------------------------------------- 1 | A Temo Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Temo/Examples/basic_03.svg: -------------------------------------------------------------------------------- 1 | A Temo Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Temo/Examples/basic_04.svg: -------------------------------------------------------------------------------- 1 | A Temo Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Temo/Examples/basic_05.svg: -------------------------------------------------------------------------------- 1 | A Temo Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Temo/Examples/basic_07.svg: -------------------------------------------------------------------------------- 1 | A Temo Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Teocuitlatl/Examples/basic_01.svg: -------------------------------------------------------------------------------- 1 | A Teocuitlatl Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Teocuitlatl/Examples/basic_04.svg: -------------------------------------------------------------------------------- 1 | A Teocuitlatl Artwork 2 | -------------------------------------------------------------------------------- /Documentation/Teocuitlatl/Examples/basic_05.svg: -------------------------------------------------------------------------------- 1 | A Teocuitlatl Artwork 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The “Macuahuitl” Generative Art Toolbox 2 | 3 | [![GitHub](https://img.shields.io/github/license/the-real-tokai/macuahuitl?color=green&label=License&style=flat)](https://github.com/the-real-tokai/macuahuitl/blob/master/LICENSE) 4 | [![GitHub Code Size in Bytes](https://img.shields.io/github/languages/code-size/the-real-tokai/macuahuitl?label=Code%20Size&style=flat)](https://github.com/the-real-tokai/macuahuitl/) 5 | [![IRC](https://img.shields.io/badge/IRC-irc.libera.chat:6697%20%23macuahuitl-orange&style=flat)](https://web.libera.chat/?channel=#macuahuitl) 6 | [![Twitter Follow](https://img.shields.io/twitter/follow/binaryriot?color=blue&label=Follow%20%40binaryriot&style=flat)](https://twitter.com/binaryriot) 7 | 8 | ## Synopsis 9 | 10 | *The “Macuahuiltl” Generative Art Toolbox* is a collection of personal `Python 3` scripts to 11 | generate artworks or elements for artworks that can either be used directly or processed further 12 | in common vector design software packages. 13 | 14 | All scripts output `Scalable Vector Graphics` (SVG) image files directly which easily can be 15 | loaded in common vector graphics application like `InkScape`, `Affinity Designer`, `Adobe 16 | Illustrator`, and so on. A few of the scripts also can output a custom `JSON`-based format of 17 | particle or shape descriptions which then can be further processed by related scripts or 18 | imported into 3D graphic software packages like `Blender` or `CINEMA 4D` for rendering. 19 | 20 | ## The “How?” and The “Why?” 21 | 22 | Usually to automatize generation of shapes or pattern for vector illustrations —which would 23 | be *way too boring* to create manually in Affinity Designer— the scripts start out with a quick 24 | visual idea and just a handful lines of quick code as proof of concept. 25 | 26 | Once that's done crazy silliness starts by making every variable of the algorithm user-configurable 27 | via a CLI interface. The result: the scripts explode in code size exponentially. Not that this 28 | really makes a lot of sense or would revolutionize the world… it's simply **fun stuff** done for 29 | recreation. So here we are! 😅 30 | 31 | ## Script Overview 32 | 33 | ### Comitl 34 | 35 | Figure 1 - Comitl Example Figure 2 - Comitl Example Figure 3 - Comitl Example Figure 4 - Comitl Example Figure 5 - Comitl Example 36 | 37 | Comitl concentrically arranges randomly sized arcs into a pretty disc shape. Output is generated as a set of vector shapes in Scalable 38 | Vector Graphics (SVG) format and printed on the standard output stream. Comitl also supports SVG animations, optional PNG output 39 | (with `cairosvg`), and has countless of parameters for configuration of the generated shape. 40 | 41 | [Direct Download](https://raw.githubusercontent.com/the-real-tokai/macuahuitl/master/comitl.py) | [Documentation](comitl.md) 42 | 43 | ### Altepetl 44 | 45 | Figure 1 - Altepetl Example Figure 2 - Altepetl Example Figure 3 - Altepetl Example Figure 4 - Altepetl Example 46 | 47 | Altepetl implements an artful grid-based layout of "U"-shapes; inspired by some of generative art pioneer [Véra Molnar](http://www.veramolnar.com)'s 48 | artworks. Output is generated as a set of vector shapes in Scalable Vector Graphics (SVG) format and printed on the standard output stream. Altepetl 49 | also supports optional PNG output (with `cairosvg`), and has a handful of parameters for configuration of the generated artwork. 50 | 51 | [Direct Download](https://raw.githubusercontent.com/the-real-tokai/macuahuitl/master/altepetl.py) | [Documentation](altepetl.md) 52 | 53 | ### Atlatl 54 | 55 | coming soon. 56 | 57 | ### Xonacatl 58 | 59 | coming soon. 60 | 61 | ### Acahualli 62 | 63 | coming soon. 64 | 65 | ### Ohtli 66 | 67 | coming soon. 68 | 69 | ### Temo 70 | 71 | Figure 1 - Temo Example Figure 2 - Temo Example Figure 3 - Temo Example Figure 4 - Temo Example 72 | 73 | Temo creates a colorful maze inspired by a famous one line C64 BASIC program (`10 PRINT CHR$(205.5+RND(1)); : GOTO 10`). Output is generated as a set 74 | of vector shapes in Scalable Vector Graphics (SVG) format and printed on the standard output stream. Temo also supports optional PNG output (with `cairosvg`), 75 | and has a handful of parameters for configuration of the generated artwork. 76 | 77 | [Direct Download](https://raw.githubusercontent.com/the-real-tokai/macuahuitl/master/temo.py) | [Documentation](temo.md) 78 | 79 | ### Teocuitlatl 80 | 81 | Figure 1 Figure 2 Figure 3 Figure 4 Figure 5 82 | 83 | Creates a grid of colored squares that are accentuated with smaller squares or discs inspired by works of *Victor Vasarely*. Additional 84 | color palettes of works of other op-art artists including *Bridget Riley* and *Ellsworth Kelly* are included for further experimentation 85 | with the theme. Output is generated as a set of vector shapes in Scalable Vector Graphics (SVG) format and printed on the standard 86 | output stream. Teocuitlatl also supports optional PNG output (with `cairosvg`), and has a handful of parameters for configuration of the 87 | generated artwork. 88 | 89 | [Direct Download](https://raw.githubusercontent.com/the-real-tokai/macuahuitl/master/teocuitlatl.py) | [Documentation](teocuitlatl.md) 90 | 91 | ## Copyright and License 92 | 93 | Copyright © 2019-2021 Christian Rosentreter 94 | (https://www.binaryriot.org/) 95 | 96 | All scripts are free software: you can redistribute them and/or modify them under the terms of the [GNU Affero General Public License](LICENSE) as 97 | published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 98 | -------------------------------------------------------------------------------- /altepetl.md: -------------------------------------------------------------------------------- 1 | 2 | # Altepetl 3 | 4 | `altepetl` *noun* — Classical Nahuatl: [1] city-state — local, ethnically-based political entity constituted of smaller units known as calpolli. 5 | 6 | ## Synopsis 7 | 8 | Implements an artful grid-based layout of "U"-shapes; inspired by some of generative art pioneer [Véra Molnar](http://www.veramolnar.com)'s artworks. 9 | 10 | ## Requirements 11 | 12 | An installation of `Python 3` (any version above v3.5 will do fine). For the optional `PNG` output support an installation of 13 | the `cairosvg` 3rd-party Python module is recommended. The module can be installed with Python's package manager: 14 | 15 | ``` shell 16 | pip --install cairosvg --user 17 | ``` 18 | 19 | ## Output Examples 20 | 21 | Figure 1 Figure 2 22 | Figure 3 Figure 4 23 | 24 | ## Usage 25 | 26 | ``` 27 | usage: altepetl.py [-V] [-h] [--columns INT] [--rows INT] [--scale FLOAT] 28 | [--gap FLOAT] [--shape-variation FLOAT] 29 | [--offset-jiggle FLOAT] [--random-seed INT] 30 | [--separate-paths] [--negative] [--frame FLOAT] 31 | [-o FILENAME] [--output-size INT] 32 | 33 | Startup: 34 | -V, --version show version number and exit 35 | -h, --help show this help message and exit 36 | 37 | Algorithm: 38 | --columns INT number of grid columns  [:11] 39 | --rows INT number of grid rows  [:11] 40 | --scale FLOAT base scale factor of the grid elements [:10.0] 41 | --gap FLOAT non-random base gap between grid elements [:5.0] 42 | --shape-variation FLOAT 43 | variation factor for the shape's inner "cut out" area 44 |  [:1.0] 45 | --offset-jiggle FLOAT 46 | randomizing factor for horizontal and vertical shifts 47 | of the element's coordinates  [:2.0] 48 | --random-seed INT fixed initialization of the random number generator 49 | for predictable results 50 | 51 | Miscellaneous: 52 | --separate-paths generate separate elements for each element 53 | --negative inverse the output colors 54 | --frame FLOAT extra spacing around the grid (additionally to 55 | potential gap spacing on the outside)  [:20.0] 56 | 57 | Output: 58 | -o FILENAME, --output FILENAME 59 | optionally rasterize the generated vector paths and 60 | write the result into a PNG file (requires the 61 | `svgcairo' Python module) 62 | --output-size INT force pixel width of the raster image, height is 63 | automatically calculated; if omitted the generated SVG 64 | viewbox dimensions are used 65 | ``` 66 | 67 | ### Usage Examples 68 | ``` shell 69 | # Generate a SVG file 70 | ./altepetl.py --columns=8 -rows=5 > output.svg 71 | 72 | # Rasterize directly into a PNG file (requires "cairosvg") 73 | ./altepetl.py --negative -o output.png 74 | ``` 75 | 76 | ``` shell 77 | # Preview output with ImageMagick's "convert" and Preview.app (Mac OS X) 78 | ./altepetl.py --columns=4 --rows=4 --random-seed=12345 | convert svg:- png:- | open -f -a Preview.app 79 | 80 | # Preview output with ImageMagick's "convert" and "display" (Linux/BSD/etc.) 81 | ./altepetl.py --columns=4 --rows=4 --random-seed=12345 | convert svg:- png:- | display 82 | ``` 83 | 84 | ## History 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
1.218-Jun-2020Fixed PNG saving when `--output-size` was not passed.
1.109-Jun-2020Initial public source code release
-------------------------------------------------------------------------------- /altepetl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Altepetl 4 | Implements an artful grid-based layout of "U"-shapes; inspired by some of 5 | generative art pioneer Véra Molnar's artworks. 6 | 7 | Copyright © 2020 Christian Rosentreter 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU Affero General Public License as published 11 | by the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU Affero General Public License for more details. 18 | 19 | You should have received a copy of the GNU Affero General Public License 20 | along with this program. If not, see . 21 | 22 | $Id: altepetl.py 144 2020-06-18 17:04:01Z tokai $ 23 | """ 24 | 25 | import random 26 | import argparse 27 | import sys 28 | import xml.etree.ElementTree as xtree 29 | 30 | __author__ = 'Christian Rosentreter' 31 | __version__ = '1.2' 32 | __all__ = ['USquare'] 33 | 34 | 35 | 36 | class USquare(): 37 | """SVG description for a square 'U' shape, optionally rotated by 90° and/ or flipped.""" 38 | 39 | dmod = {'n':['h', 'v', 1], 'e':['v', 'h', 1], 'w':['v', 'h', -1], 's':['h', 'v', -1]} 40 | 41 | def __init__(self, x, y, scale=1.0, direction='n', variation=0.0): 42 | self.x = x 43 | self.y = y 44 | self.scale = scale 45 | self.direction = direction 46 | self.variation = variation 47 | 48 | def __str__(self): 49 | mh, mv, m2 = self.dmod[self.direction] 50 | m2 *= self.scale 51 | v = 0.18 * min(self.variation, 1.0) 52 | 53 | return ''.join(str(s) for s in [ 54 | 'M', -0.5 * m2 + self.x, 55 | ' ', -0.5 * m2 + self.y, 56 | mh, (0.2 + v) * m2, 57 | mv, 0.8 * m2, 58 | mh, (0.6 - v) * m2, 59 | mv, -0.8 * m2, 60 | mh, 0.2 * m2, 61 | mv, 1.0 * m2, 62 | mh, -1.0 * m2, 63 | 'Z', '' 64 | ]) 65 | 66 | 67 | def main(): 68 | """Let's make a work of art.""" 69 | 70 | ap = argparse.ArgumentParser( 71 | description=('Implements an artful grid-based layout of "U"-shapes; inspired ' 72 | 'by some of generative art pioneer Véra Molnar\'s artworks.'), 73 | epilog='Report bugs, request features, or provide suggestions via https://github.com/the-real-tokai/macuahuitl/issues', 74 | add_help=False, 75 | ) 76 | 77 | g = ap.add_argument_group('Startup') 78 | g.add_argument('-V', '--version', action='version', help="show version number and exit", version='%(prog)s {}'.format(__version__), ) 79 | g.add_argument('-h', '--help', action='help', help='show this help message and exit') 80 | 81 | g = ap.add_argument_group('Algorithm') 82 | g.add_argument('--columns', metavar='INT', type=int, help='number of grid columns  [:11]', default=11) 83 | g.add_argument('--rows', metavar='INT', type=int, help='number of grid rows  [:11]', default=11) 84 | g.add_argument('--scale', metavar='FLOAT', type=float, help='base scale factor of the grid elements [:10.0]', default=10.0) 85 | g.add_argument('--gap', metavar='FLOAT', type=float, help='non-random base gap between grid elements [:5.0]', default=5.0) 86 | g.add_argument('--shape-variation', metavar='FLOAT', type=float, help='variation factor for the shape\'s inner "cut out" area  [:1.0]', default=1.0) 87 | g.add_argument('--offset-jiggle', metavar='FLOAT', type=float, help='randomizing factor for horizontal and vertical shifts of the element\'s coordinates  [:2.0]', default=2.0) 88 | g.add_argument('--random-seed', metavar='INT', type=int, help='fixed initialization of the random number generator for predictable results') 89 | 90 | g = ap.add_argument_group('Miscellaneous') 91 | g.add_argument('--separate-paths', action='store_true', help='generate separate elements for each element') 92 | g.add_argument('--negative', action='store_true', help='inverse the output colors') 93 | g.add_argument('--frame', metavar='FLOAT', type=float, help='extra spacing around the grid (additionally to potential gap spacing on the outside)  [:20.0]', default=20.0) 94 | 95 | g = ap.add_argument_group('Output') 96 | g.add_argument('-o', '--output', metavar='FILENAME', type=str, help='optionally rasterize the generated vector paths and write the result into a PNG file (requires the `svgcairo\' Python module)') 97 | g.add_argument('--output-size', metavar='INT', type=int, help='force pixel width of the raster image, height is automatically calculated; if omitted the generated SVG viewbox dimensions are used') 98 | 99 | user_input = ap.parse_args() 100 | 101 | 102 | grid_x = user_input.columns 103 | grid_y = user_input.rows 104 | grid_size = user_input.scale 105 | grid_gap = user_input.gap 106 | variation = user_input.shape_variation 107 | jiggle = user_input.offset_jiggle 108 | frame = user_input.frame 109 | 110 | chaos = random.Random(user_input.random_seed) 111 | grid_offset = grid_size + grid_gap 112 | col1, col2 = 'white', 'black' 113 | if user_input.negative: 114 | col1, col2 = col2, col1 115 | 116 | squares = [] 117 | for x in range(0, grid_x): 118 | for y in range(0, grid_y): 119 | dx = (x * grid_offset) + (grid_offset / 2.0) + frame + chaos.uniform(-jiggle, jiggle) 120 | dy = (y * grid_offset) + (grid_offset / 2.0) + frame + chaos.uniform(-jiggle, jiggle) 121 | squares.append(USquare(dx, dy, grid_size, chaos.choice('news'), chaos.uniform(0.0, variation))) 122 | 123 | vbw = int((grid_offset * grid_x) + (frame * 2.0)) 124 | vbh = int((grid_offset * grid_y) + (frame * 2.0)) 125 | 126 | svg = xtree.Element('svg', {'width':'100%', 'height':'100%', 'xmlns':'http://www.w3.org/2000/svg', 'viewBox':'0 0 {} {}'.format(vbw, vbh)}) 127 | title = xtree.SubElement(svg, 'title') 128 | title.text = 'An Altepetl Artwork' 129 | 130 | xtree.SubElement(svg, 'rect', {'id':'background', 'x':'0', 'y':'0', 'width':str(vbw), 'height':str(vbh), 'fill':col1}) 131 | if user_input.separate_paths: 132 | svg_g = xtree.SubElement(svg, 'g', {'id':'grid-of-us', 'stroke-width':'0', 'fill':col2}) 133 | for si, s in enumerate(squares): 134 | xtree.SubElement(svg_g, 'path', {'id':'element-{}'.format(si), 'd':str(s)}) 135 | else: 136 | xtree.SubElement(svg, 'path', {'id':'grid-of-us', 'stroke-width':'0', 'fill':col2, 'd':''.join(str(s) for s in squares)}) 137 | 138 | rawxml = xtree.tostring(svg, encoding='unicode') 139 | 140 | if not user_input.output: 141 | print(rawxml) 142 | else: 143 | try: 144 | import os 145 | from cairosvg import svg2png 146 | 147 | w = vbw if user_input.output_size is None else user_input.output_size 148 | 149 | svg2png(bytestring=rawxml, 150 | write_to=os.path.realpath(os.path.expanduser(user_input.output)), 151 | output_width=int(w), 152 | output_height=int(w * vbh / vbw) 153 | ) 154 | except ImportError as e: 155 | print('Couldn\'t rasterize nor write a PNG file. Required Python module \'cairosvg\' is not available: {}'.format(str(e)), file=sys.stderr) 156 | 157 | 158 | if __name__ == '__main__': 159 | main() 160 | -------------------------------------------------------------------------------- /comitl.md: -------------------------------------------------------------------------------- 1 | 2 | # Comitl 3 | 4 | `comitl` *noun* — Classical Nahuatl: [1] cooking pot, pan — a rounded or cylindrical container used to heat up meals over a fire. 5 | 6 | ## Synopsis 7 | 8 | Concentrically arranges randomly sized arcs into a pretty disc shape. Output is generated as a set of vector shapes in Scalable 9 | Vector Graphics (SVG) format and printed on the standard output stream. 10 | 11 | ## Requirements 12 | 13 | An installation of `Python 3` (any version above v3.5 will do fine). For the optional `PNG` output support an installation of 14 | the `cairosvg` 3rd-party Python module is recommended. The module can be installed with Python's package manager: 15 | 16 | ``` shell 17 | pip --install cairosvg --user 18 | ``` 19 | 20 | ## Output Examples 21 | 22 | Figure 1 Figure 2 Figure 3 Figure 4 Figure 5 Figure 6 Figure 7 Figure 8 Figure 9 Figure 10 Figure 11 Figure 12 23 | 24 | ## Usage 25 | 26 | ``` 27 | usage: comitl.py [-V] [-h] [--circles INT] [--stroke-width FLOAT] 28 | [--gap FLOAT] [--inner-radius FLOAT] [--hoffset FLOAT] 29 | [--voffset FLOAT] [--color COLOR] [--random-seed INT] 30 | [--randomize] [--separate-paths] 31 | [--outline-mode {both,outside,inside,none}] 32 | [--background-color COLOR] [--disc-color COLOR] 33 | [--animation-mode {random,bidirectional,cascade-in,cascade-out}] 34 | [--animation-duration FLOAT] [--animation-offset FLOAT] 35 | [-o FILENAME] [--output-size INT] 36 | 37 | Startup: 38 | -V, --version show version number and exit 39 | -h, --help show this help message and exit 40 | 41 | Algorithm: 42 | --circles INT number of concentric arc elements to generate inside 43 | the disc  [:21] 44 | --stroke-width FLOAT width of the generated strokes  [:6] 45 | --gap FLOAT distance between the generated strokes 46 | --inner-radius FLOAT setup inner disc radius to create an annular shape 47 | --hoffset FLOAT shift the whole disc horizontally  [:0.0] 48 | --voffset FLOAT shift the whole disc vertically  [:0.0] 49 | --color COLOR SVG compliant color specification or identifier 50 |  [:black] 51 | --random-seed INT fixed initialization of the random number generator 52 | for predictable results 53 | --randomize generate truly random disc layouts; other algorithm 54 | values provided via command line parameters are 55 | utilized as limits 56 | 57 | Miscellaneous: 58 | --separate-paths generate separate elements for each arc; 59 | automatically implied when animation support is 60 | enabled 61 | --outline-mode {both,outside,inside,none} 62 | generate bounding outline circles  [:both] 63 | --background-color COLOR 64 | SVG compliant color specification or identifier; adds 65 | a background to the SVG output 66 | --disc-color COLOR SVG compliant color specification or identifier; fills 67 | the background of the generated disc by adding an 68 | extra element 69 | --animation-mode {random,bidirectional,cascade-in,cascade-out} 70 | enables SVG support 71 | --animation-duration FLOAT 72 | defines base duration of one full 360° arc rotation 73 | (in seconds); negative inputs switch to counter- 74 | clockwise base direction  [:6.0] 75 | --animation-offset FLOAT 76 | offset the animation (in seconds) to support rendering 77 | to frame sequences for frame based animation formats. 78 |  [:0] 79 | 80 | Output: 81 | -o FILENAME, --output FILENAME 82 | optionally rasterize the generated vector paths and 83 | write the result into a PNG file (requires the 84 | `svgcairo' Python module) 85 | --output-size INT force pixel width and height of the raster image; if 86 | omitted the generated SVG viewbox dimensions are used 87 | ``` 88 | 89 | ### Usage Examples 90 | ``` shell 91 | # Generate a SVG file 92 | ./comitl.py --circles=10 --color=green > output.svg 93 | 94 | # Rasterize directly into a PNG file (requires "cairosvg") 95 | ./comitl.py --circles=10 --disc-color=black --color="#fff" -o output.png 96 | ``` 97 | 98 | ``` shell 99 | # Preview output with ImageMagick's "convert" and Preview.app (Mac OS X) 100 | ./comitl.py --randomise | convert svg:- png:- | open -f -a Preview.app 101 | 102 | # Preview output with ImageMagick's "convert" and "display" (Linux/BSD/etc.) 103 | ./comitl.py --randomise | convert svg:- png:- | display 104 | ``` 105 | 106 | Creating frame-based animations (for use in VFX applications like BlackMagic Fusion, After Effects, etc.) is 107 | possible by utilizing the `--animation-offset` parameter to manually advance the SVG animation, f.ex. a quick bash script 108 | like this would create a 10s clip in `Apple ProRes 4444` format with the help of `ffmpeg`: 109 | 110 | ~~~ shell 111 | #!/bin/bash 112 | 113 | if mkdir -p './output_frames'; then 114 | framerate=`bc -l <<< "30000/1001"` # NTSC 115 | 116 | for frame in {0..299}; do 117 | offset=`bc -l <<< "${frame}/${framerate}"` 118 | filename=`printf 'frame%04d.png' ${frame}` 119 | 120 | ./comitl.py \ 121 | --random-seed=12345 \ 122 | --animation-mode=random \ 123 | --animation-offset="${offset}" \ 124 | --color=#334455 \ 125 | --background-color=#ddeeff \ 126 | --output-size=480 \ 127 | -o "./output_frames/${filename}" 128 | done 129 | 130 | ffmpeg \ 131 | -f image2 \ 132 | -framerate "${framerate}" \ 133 | -i "./output_frames/frame%04d.png" \ 134 | -c:v prores_ks -profile:v 4 \ 135 | "./output_frames/output.mov" 136 | fi 137 | ~~~ 138 | 139 | ## History 140 | 141 | 142 | 143 | 144 | 145 | 151 | 152 | 153 | 154 | 155 | 160 | 161 | 162 | 163 | 164 | 175 | 176 | 177 | 178 | 179 | 186 | 187 | 188 | 189 | 190 | 196 | 197 | 198 | 199 | 200 | 207 | 208 | 209 | 210 | 211 | 212 | 213 |
1.730-May-2020 146 |
    147 |
  • Fixed: random seeds passed via `--random-seed` parameter were ignored for the two random animation modes 148 |
  • The `SVGArcPathSegment` class now can handle arcs larger than 360° (not used by Comitl though) 149 |
150 |
1.622-May-2020 156 |
    157 |
  • Added support for SVG animations with 4 different modes (`--animation-mode`, `--animation-duration`, and `--animation-offset` options) 158 |
159 |
1.522-May-2020 165 |
    166 |
  • Added support for rasterized PNG output (using `cairosvg`) 167 |
  • Added `--separate-paths` option 168 |
  • Utilize Python's own xml module to generate the SVG data (not that this really improves the code in any way) 169 |
  • Improved render aspects to avoid various stroke overlapping issues 170 |
  • Added `--background-color` and `--disc-color` options 171 |
  • Added `--outline-mode` option 172 |
  • Constant ratio for the image border area 173 |
174 |
1.420-May-2020 180 |
    181 |
  • Allow arc specification in degree 182 |
  • Arcs are now initialized with angular offsets and ranges 183 |
  • Improved code reusability and scope separation 184 |
185 |
1.319-May-2020 191 |
    192 |
  • Refactored in preparation for new features 193 |
  • Optimizations 194 |
195 |
1.218-May-2020 201 |
    202 |
  • Utilize SVG groups in the generated output 203 |
  • Added automatic identifiers to the SVG elements 204 |
  • Updated help text 205 |
206 |
1.118-May-2020Initial public source code release
-------------------------------------------------------------------------------- /comitl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Comitl 4 | Concentrically arranges randomly sized arcs into a pretty disc shape. 5 | 6 | Copyright © 2020 Christian Rosentreter 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU Affero General Public License as published 10 | by the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU Affero General Public License for more details. 17 | 18 | You should have received a copy of the GNU Affero General Public License 19 | along with this program. If not, see . 20 | 21 | $Id: comitl.py 122 2020-05-30 03:40:01Z tokai $ 22 | """ 23 | 24 | import math 25 | import random 26 | import argparse 27 | import sys 28 | import os 29 | import xml.etree.ElementTree as xtree 30 | 31 | 32 | __author__ = 'Christian Rosentreter' 33 | __version__ = '1.7' 34 | __all__ = ['SVGArcPathSegment'] 35 | 36 | 37 | 38 | class SVGArcPathSegment(): 39 | """An 'arc' SVG path segment.""" 40 | 41 | def __init__(self, offset=0.0, angle=90.0, radius=1.0, x=0.0, y=0.0): 42 | self.offset = offset 43 | self.angle = angle 44 | self.radius = radius 45 | self.x = x 46 | self.y = y 47 | 48 | def __str__(self): 49 | if self.angle == 0: 50 | return '' 51 | 52 | if abs(self.angle) < 360: 53 | path_format = ( 54 | 'M {sx} {sy} ' 55 | 'A {rd} {rd} 0 {fl} 1 {dx} {dy}' 56 | ) 57 | ts = (self.offset - 180.0) * math.pi / -180.0 58 | td = (self.offset + self.angle - 180.0) * math.pi / -180.0 59 | else: 60 | path_format = ( 61 | 'M {sx} {sy} ' 62 | 'A {rd} {rd} 0 0 1 {dx} {dy} ' # essentially a circle formed by… 63 | 'A {rd} {rd} 0 1 1 {sx} {sy} ' # … two 180° arcs 64 | 'Z' 65 | ) 66 | ts = 0 67 | td = math.pi 68 | 69 | return path_format.format( 70 | sx=round(self.x + self.radius * math.sin(ts), 9), 71 | sy=round(self.y + self.radius * math.cos(ts), 9), 72 | rd=round(self.radius, 9), 73 | fl=int(abs(ts - td) > math.pi), 74 | dx=round(self.x + self.radius * math.sin(td), 9), 75 | dy=round(self.y + self.radius * math.cos(td), 9) 76 | ) 77 | 78 | 79 | def main(): 80 | """First, build fire. Second, start coffee.""" 81 | 82 | ap = argparse.ArgumentParser( 83 | description=('Concentrically arranges randomly sized arcs into a pretty disc shape. Output is ' 84 | 'generated as a set of vector shapes in Scalable Vector Graphics (SVG) format and printed ' 85 | 'on the standard output stream.'), 86 | epilog='Report bugs, request features, or provide suggestions via https://github.com/the-real-tokai/macuahuitl/issues', 87 | add_help=False, 88 | ) 89 | 90 | g = ap.add_argument_group('Startup') 91 | g.add_argument('-V', '--version', action='version', help="show version number and exit", version='%(prog)s {}'.format(__version__), ) 92 | g.add_argument('-h', '--help', action='help', help='show this help message and exit') 93 | 94 | g = ap.add_argument_group('Algorithm') 95 | g.add_argument('--circles', metavar='INT', type=int, help='number of concentric arc elements to generate inside the disc  [:21]', default=21) 96 | g.add_argument('--stroke-width', metavar='FLOAT', type=float, help='width of the generated strokes  [:6]', default=6.0) 97 | g.add_argument('--gap', metavar='FLOAT', type=float, help='distance between the generated strokes') 98 | g.add_argument('--inner-radius', metavar='FLOAT', type=float, help='setup inner disc radius to create an annular shape') 99 | g.add_argument('--hoffset', metavar='FLOAT', type=float, help='shift the whole disc horizontally  [:0.0]', default=0.0) 100 | g.add_argument('--voffset', metavar='FLOAT', type=float, help='shift the whole disc vertically  [:0.0]', default=0.0) 101 | g.add_argument('--color', metavar='COLOR', type=str, help='SVG compliant color specification or identifier  [:black]', default='black') 102 | g.add_argument('--random-seed', metavar='INT', type=int, help='fixed initialization of the random number generator for predictable results') 103 | g.add_argument('--randomize', action='store_true', help='generate truly random disc layouts; other algorithm values provided via command line parameters are utilized as limits') 104 | 105 | g = ap.add_argument_group('Miscellaneous') 106 | g.add_argument('--separate-paths', action='store_true', help='generate separate elements for each arc; automatically implied when animation support is enabled') 107 | g.add_argument('--outline-mode', help='generate bounding outline circles  [:both]', choices=['both', 'outside', 'inside', 'none'], default='both') 108 | g.add_argument('--background-color', metavar='COLOR', type=str, help='SVG compliant color specification or identifier; adds a background to the SVG output') 109 | g.add_argument('--disc-color', metavar='COLOR', type=str, help='SVG compliant color specification or identifier; fills the background of the generated disc by adding an extra element') 110 | g.add_argument('--animation-mode', help='enables SVG support', choices=['random', 'bidirectional', 'cascade-in', 'cascade-out']) 111 | g.add_argument('--animation-duration', metavar='FLOAT', type=float, help='defines base duration of one full 360° arc rotation (in seconds); negative inputs switch to counter-clockwise base direction  [:6.0]', default=6.0) 112 | g.add_argument('--animation-offset', metavar='FLOAT', type=float, help='offset the animation (in seconds) to support rendering to frame sequences for frame based animation formats.  [:0]', default=0.0) 113 | 114 | g = ap.add_argument_group('Output') 115 | g.add_argument('-o', '--output', metavar='FILENAME', type=str, help='optionally rasterize the generated vector paths and write the result into a PNG file (requires the `svgcairo\' Python module)') 116 | g.add_argument('--output-size', metavar='INT', type=int, help='force pixel width and height of the raster image; if omitted the generated SVG viewbox dimensions are used') 117 | 118 | user_input = ap.parse_args() 119 | 120 | 121 | # Initialize… 122 | # 123 | chaos = random.Random(user_input.random_seed) 124 | circles = user_input.circles 125 | stroke = abs(user_input.stroke_width) if user_input.stroke_width else 1.0 126 | gap = user_input.gap if (user_input.gap is not None) else stroke 127 | radius = abs(user_input.inner_radius) if (user_input.inner_radius is not None) else stroke 128 | x = user_input.hoffset 129 | y = user_input.voffset 130 | color = user_input.color 131 | 132 | if user_input.randomize: 133 | circles = chaos.randrange(0, circles) if circles else 0 134 | stroke = chaos.uniform(0, stroke) 135 | stroke = 1.0 if stroke == 0 else stroke 136 | gap = chaos.uniform(0, gap) 137 | radius = chaos.uniform(0, radius) 138 | x = chaos.uniform(-x, x) if x else 0.0 139 | y = chaos.uniform(-y, y) if y else 0.0 140 | color = '#{:02x}{:02x}{:02x}'.format(chaos.randrange(0, 255), chaos.randrange(0, 255), chaos.randrange(0, 255)) 141 | # TODO: randomize background and disc color too when the respective parameters are used 142 | # (needs to respect color harmonies) 143 | 144 | if radius < stroke: 145 | radius = stroke 146 | 147 | 148 | # Generate data… 149 | # 150 | outlines = [] 151 | arcs = [] 152 | 153 | if user_input.outline_mode in ('both', 'inside'): 154 | outlines.append({'x':x, 'y':y, 'r':radius}) 155 | radius += (gap + stroke) 156 | 157 | for _ in range(circles): 158 | # Calculate angular space requirement for the "round" stroke caps to avoid some overlapping 159 | sqrd2 = 2.0 * math.pow(radius, 2.0) 160 | theta = ((2.0 * math.acos((sqrd2 - math.pow((stroke / 2.0), 2.0)) / sqrd2)) * (180.0 / math.pi)) 161 | 162 | arcs.append(SVGArcPathSegment(offset=chaos.uniform(0, 359.0), angle=chaos.uniform(0, 359.0 - theta), radius=radius, x=x, y=y)) 163 | radius += (gap + stroke) 164 | 165 | if user_input.outline_mode in ('both', 'outside'): 166 | outlines.append({'x':x, 'y':y, 'r':radius}) 167 | else: 168 | radius -= (gap + stroke) 169 | 170 | 171 | # Generate SVG/XML… 172 | # 173 | def _f(v, max_digits=9): 174 | if isinstance(v, float): 175 | v = round(v, max_digits) 176 | return v if isinstance(v, str) else str(v) 177 | 178 | vb_dim = (radius + (stroke * 0.5)) * (256.0 / (256.0 - 37.35)) # 37px border for 256x256; a golden ratio in there… somewhere… 179 | vb_off = _f(vb_dim * -1.0, 2) 180 | vb_dim = _f(vb_dim * 2.0, 2) 181 | config = {'stroke':color, 'stroke-width':_f(stroke), 'fill':'none'} 182 | 183 | svg = xtree.Element('svg', {'width':'100%', 'height':'100%', 'xmlns':'http://www.w3.org/2000/svg', 'viewBox':'{o} {o} {s} {s}'.format(o=vb_off, s=vb_dim)}) 184 | 185 | title = xtree.SubElement(svg, 'title') 186 | title.text = 'A Comitl Artwork' 187 | 188 | if user_input.background_color: 189 | xtree.SubElement(svg, 'rect', {'id':'background', 'x':vb_off, 'y':vb_off, 'width':vb_dim, 'height':vb_dim, 'fill':user_input.background_color}) 190 | 191 | svg_m = xtree.SubElement(svg, 'g', {'id':'comitl-disc'}) 192 | 193 | if user_input.disc_color: 194 | xtree.SubElement(svg_m, 'circle', {'id':'disc-background', 'cx':_f(x), 'cy':_f(y), 'r':_f(radius), 'fill':user_input.disc_color}) 195 | 196 | if arcs: 197 | if user_input.separate_paths or user_input.animation_mode: 198 | svg_ga = xtree.SubElement(svg_m, 'g', {'id':'arcs'}) 199 | for aid, a in enumerate(arcs): 200 | 201 | svg_arc = xtree.SubElement(svg_ga, 'path', {'id':'arc-{}'.format(aid+1), 'stroke-linecap':'round', **config}) 202 | shift = 0.0 203 | 204 | if user_input.animation_mode: 205 | if user_input.animation_mode == 'cascade-out': 206 | d = user_input.animation_duration * ((aid+1) * 0.25) # TODO: 1/4 decay value could be configurable 207 | elif user_input.animation_mode == 'cascade-in': 208 | d = user_input.animation_duration * ((len(arcs)-aid+1) * 0.25) 209 | else: 210 | # limits duration range into a 50% variation window to avoid super fast arcs with values closer to 0 211 | d = chaos.uniform(abs(user_input.animation_duration) * 0.5, abs(user_input.animation_duration)) # TODO: variation could be configurable 212 | if user_input.animation_duration < 0: 213 | d *= -1 # restore user direction 214 | if (user_input.animation_mode == 'bidirectional') and (chaos.random() < 0.5): 215 | d *= -1 # switch direction randomly 216 | 217 | shift = (360.0 / d) * user_input.animation_offset 218 | 219 | xtree.SubElement(svg_arc, 'animateTransform', { 220 | 'attributeName': 'transform', 221 | 'type': 'rotate', 222 | 'from': '{} {} {}'.format(360 if d < 0 else 0, x, y), 223 | 'to': '{} {} {}'.format( 0 if d < 0 else 360, x, y), 224 | 'dur': '{}s'.format(abs(d)), 225 | 'repeatCount': 'indefinite' 226 | }) 227 | 228 | a.offset += shift 229 | svg_arc.set('d', str(a)) 230 | else: 231 | xtree.SubElement(svg_m, 'path', {'id':'arcs', 'd':''.join(map(str, arcs)), 'stroke-linecap':'round', **config}) 232 | 233 | if outlines: 234 | svg_go = xtree.SubElement(svg_m, 'g', {'id':'outlines'}) 235 | for oid, o in enumerate(outlines): 236 | xtree.SubElement(svg_go, 'circle', {'id':'outline-{}'.format(oid+1), 'cx':_f(o['x']), 'cy':_f(o['y']), 'r':_f(o['r']), **config}) 237 | 238 | svg.append(xtree.Comment(' Generator: comitl.py {} (https://github.com/the-real-tokai/macuahuitl) '.format(__version__))) 239 | 240 | rawxml = xtree.tostring(svg, encoding='unicode') 241 | 242 | 243 | # Send happy little arcs out into the world… 244 | # 245 | if not user_input.output: 246 | print(rawxml) 247 | else: 248 | try: 249 | from cairosvg import svg2png 250 | svg2png(bytestring=rawxml, 251 | write_to=os.path.realpath(os.path.expanduser(user_input.output)), 252 | output_width=user_input.output_size, 253 | output_height=user_input.output_size 254 | ) 255 | except ImportError as e: 256 | print('Couldn\'t rasterize nor write a PNG file. Required Python module \'cairosvg\' is not available: {}'.format(str(e)), file=sys.stderr) 257 | 258 | 259 | if __name__ == "__main__": 260 | main() 261 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cairosvg>=2.4 -------------------------------------------------------------------------------- /temo.md: -------------------------------------------------------------------------------- 1 | 2 | # Temo 3 | 4 | `temo` *verb* — Classical Nahuatl: [1] to descend, to go down. 5 | 6 | ## Synopsis 7 | 8 | Creates a colorful maze inspired by a famous one line C64 BASIC program (`10 PRINT CHR$(205.5+RND(1)); : GOTO 10`). Output is generated as a set 9 | of vector shapes in Scalable Vector Graphics (SVG) format and printed on the standard output stream. 10 | 11 | ## Requirements 12 | 13 | An installation of `Python 3` (any version above v3.5 will do fine). For the optional `PNG` output support an installation of 14 | the `cairosvg` 3rd-party Python module is recommended. The module can be installed with Python's package manager: 15 | 16 | ``` shell 17 | pip --install cairosvg --user 18 | ``` 19 | 20 | ## Output Examples 21 | 22 | Figure 1 Figure 2 Figure 3 Figure 4 23 | Figure 5 Figure 6 Figure 7 24 | 25 | ## Usage 26 | 27 | ``` 28 | usage: temo.py [-V] [-h] [--columns INT] [--rows INT] [--scale FLOAT] 29 | [--random-seed INT] [--frame FLOAT] [--stroke-width FLOAT] 30 | [--background-color COLOR] [--hue-shift FLOAT] 31 | [--hue-shift-line FLOAT] [--best-path-width FLOAT] 32 | [--schotter-falloff {infinite,horizontal,vertical,radial,box,random}] 33 | [--schotter-inverse] [--schotter-rotation FLOAT] 34 | [--schotter-offset FLOAT] [-o FILENAME] [--output-size INT] 35 | 36 | Startup: 37 | -V, --version show version number and exit 38 | -h, --help show this help message and exit 39 | 40 | Algorithm: 41 | --columns INT number of grid columns  [:11] 42 | --rows INT number of grid rows  [:11] 43 | --scale FLOAT base scale factor of the grid elements [:10.0] 44 | --random-seed INT fixed initialization of the random number generator 45 | for predictable results 46 | 47 | Miscellaneous: 48 | --frame FLOAT increase or decrease spacing around the maze  [:20.0] 49 | --stroke-width FLOAT width of the generated strokes  [:2.0] 50 | --background-color COLOR 51 | SVG compliant color specification or identifier; adds 52 | a background to the SVG output 53 | --hue-shift FLOAT amount to rotate an imaginary color wheel before 54 | looking up new colors (in degrees)  [:15.0] 55 | --hue-shift-line FLOAT 56 | separate hue shift for continuous lines; if not passed 57 | `--hue-shift' applies too 58 | --best-path-width FLOAT 59 | show the best (aka the longest) path through the maze 60 | and set width of its marker line 61 | 62 | Schotter: 63 | --schotter-falloff {infinite,horizontal,vertical,radial,box,random} 64 | enable `George Nees'-style randomizing rotations and 65 | offsets of the maze's line segments 66 | --schotter-inverse flip the schotter mapping of the selected mode 67 | --schotter-rotation FLOAT 68 | rotational variance for schottering  [:0.5] 69 | --schotter-offset FLOAT 70 | positional variance for schottering  [:0.25] 71 | 72 | Output: 73 | -o FILENAME, --output FILENAME 74 | optionally rasterize the generated vector paths and 75 | write the result into a PNG file (requires the 76 | `svgcairo' Python module) 77 | --output-size INT force pixel width of the raster image, height is 78 | automatically calculated; if omitted the generated SVG 79 | viewbox dimensions are used 80 | ``` 81 | 82 | ### Usage Examples 83 | 84 | ``` shell 85 | # Generate a SVG file 86 | ./temo.py --columns=8 --rows=5 > output.svg 87 | 88 | # Rasterize directly into a PNG file (requires "cairosvg") 89 | ./temo.py --hue-shift=4 -o output.png 90 | ``` 91 | 92 | ``` shell 93 | # One Tiny Worm in its Home 94 | ./temo.py \ 95 | -o "tinyworm.png" \ 96 | --output-size=320 \ 97 | --stroke-width=1 \ 98 | --hue-shift=15 \ 99 | --hue-shift-line=0 \ 100 | --best-path-width=2 \ 101 | --rows=3 \ 102 | --columns=3 \ 103 | --background="#1a1b2c" 104 | ``` 105 | 106 | ``` shell 107 | # Preview output with ImageMagick's "convert" and Preview.app (Mac OS X) 108 | ./temo.py --random-seed=12345 | convert svg:- png:- | open -f -a Preview.app 109 | 110 | # Preview output with ImageMagick's "convert" and "display" (Linux/BSD/etc.) 111 | ./temo.py --random-seed=12345 | convert svg:- png:- | display 112 | ``` 113 | 114 | ## History 115 | 116 | 117 | 118 | 119 | 120 | 125 | 126 | 127 | 128 | 129 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
1.309-Jul-2020 121 |
    122 |
  • Added support for various 'schotter' modes 123 |
124 |
1.221-Jun-2020 130 |
    131 |
  • Added `--best-path-width` option to ignite a simple solver to find and visualize the longest path 132 | through the generated maze (aka "the worm") 133 |
  • Fixed missing handling of user input for hue shift in continuous lines and added `--hue-shift-line` option 134 | for further fine tuning of the effect 135 |
  • Various code improvements 136 |
137 |
1.119-Jun-2020Initial public source code release
-------------------------------------------------------------------------------- /temo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Temo 4 | Creates a colorful maze inspired by a famous one line C64 BASIC 5 | program. 6 | 7 | Copyright © 2020 Christian Rosentreter 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU Affero General Public License as published 11 | by the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU Affero General Public License for more details. 18 | 19 | You should have received a copy of the GNU Affero General Public License 20 | along with this program. If not, see . 21 | 22 | $Id: temo.py 164 2020-07-09 12:18:58Z tokai $ 23 | """ 24 | 25 | import random 26 | import argparse 27 | import math 28 | import sys 29 | import colorsys 30 | import logging 31 | from enum import Enum 32 | import xml.etree.ElementTree as xtree 33 | 34 | __author__ = 'Christian Rosentreter' 35 | __version__ = '1.3' 36 | __all__ = [] 37 | 38 | 39 | 40 | class Direction(Enum): 41 | """""" 42 | NORTH = 1 43 | SOUTH = 2 44 | EAST = 3 45 | WEST = 4 46 | 47 | 48 | class Slope(Enum): 49 | """""" 50 | UP = 0 # "/" 51 | DOWN = 1 # "\" 52 | 53 | 54 | 55 | class DLine(): 56 | """A diagonal line segment inside a square.""" 57 | 58 | def __init__(self, slope, hue, x1, y1, x2, y2, angle=0): 59 | self.slope = slope 60 | self.hue = hue 61 | 62 | if angle: 63 | xc = (x1 + x2) / 2.0 64 | yc = (y1 + y2) / 2.0 65 | r = math.sqrt(math.pow(x2 - x1, 2.0) + math.pow(y2 - y1, 2.0)) / 2.0 66 | a = -math.radians((-45.0 if slope == Slope.DOWN else 45) + angle) 67 | self.x1 = math.sin(a) * r + xc 68 | self.y1 = math.cos(a) * r + yc 69 | self.x2 = math.sin(a + math.pi) * r + xc 70 | self.y2 = math.cos(a + math.pi) * r + yc 71 | else: 72 | self.x1 = x1 if (slope == Slope.DOWN) else x2 73 | self.x2 = x2 if (slope == Slope.DOWN) else x1 74 | self.y1 = y1 75 | self.y2 = y2 76 | 77 | def __repr__(self): 78 | return '\\' if (self.slope == Slope.DOWN) else '/' 79 | 80 | 81 | def hue_blend(a, b): 82 | """Blends two angular hue values with linear interpolation.""" 83 | if a > b: 84 | a, b = b, a 85 | d = b - a 86 | if d > 180: 87 | a += 360 88 | return (a + ((b - a) / 2.0)) % 360 89 | return a + (d / 2.0) 90 | 91 | 92 | def lookup_hue(slope, x, y, rows, hue_shift_line): 93 | """Looks up a hue value or a pair of hue values from the already generated grid elements.""" 94 | hues = [] 95 | if y: 96 | if slope == Slope.DOWN: 97 | if x and (rows[y-1][x-1].slope == Slope.DOWN): 98 | hues.append(rows[y-1][x-1].hue) 99 | if rows[y-1][x].slope == Slope.UP: 100 | hues.append(rows[y-1][x].hue) 101 | else: # slope == Slope.UP 102 | if rows[y-1][x].slope == Slope.DOWN: 103 | hues.append(rows[y-1][x].hue) 104 | if (x < len(rows[y-1]) - 1) and (rows[y-1][x+1].slope == Slope.UP): 105 | hues.append(rows[y-1][x+1].hue) 106 | if hues: 107 | if len(hues) == 2: 108 | return hue_blend(hues[0], hues[1]) 109 | return (hues[0] + hue_shift_line) % 360 110 | return None 111 | 112 | 113 | def hls_to_hex(hue, lightness, saturation): 114 | """Converts a HLS color triplet into a SVG hex string.""" 115 | return '#{:02x}{:02x}{:02x}'.format(*(int(c*255) for c in list(colorsys.hls_to_rgb(hue / 360, lightness, saturation)))) 116 | 117 | 118 | 119 | def main(): 120 | """It's not just a single line of code, but what can we do? :)""" 121 | 122 | ap = argparse.ArgumentParser( 123 | description=('Creates a colorful maze inspired by a famous one line C64 BASIC program ' 124 | '(`10 PRINT CHR$(205.5+RND(1)); : GOTO 10\'). Output is generated as a set of vector ' 125 | 'shapes in Scalable Vector Graphics (SVG) format and printed on the standard output ' 126 | 'stream.'), 127 | epilog='Report bugs, request features, or provide suggestions via https://github.com/the-real-tokai/macuahuitl/issues', 128 | add_help=False, 129 | ) 130 | 131 | g = ap.add_argument_group('Startup') 132 | g.add_argument('-V', '--version', action='version', help="show version number and exit", version='%(prog)s {}'.format(__version__), ) 133 | g.add_argument('-h', '--help', action='help', help='show this help message and exit') 134 | 135 | g = ap.add_argument_group('Algorithm') 136 | g.add_argument('--columns', metavar='INT', type=int, help='number of grid columns  [:11]', default=40) 137 | g.add_argument('--rows', metavar='INT', type=int, help='number of grid rows  [:11]', default=30) 138 | g.add_argument('--scale', metavar='FLOAT', type=float, help='base scale factor of the grid elements [:10.0]', default=10.0) 139 | g.add_argument('--random-seed', metavar='INT', type=int, help='fixed initialization of the random number generator for predictable results') 140 | 141 | g = ap.add_argument_group('Miscellaneous') 142 | g.add_argument('--frame', metavar='FLOAT', type=float, help='increase or decrease spacing around the maze  [:20.0]', default=20.0) 143 | g.add_argument('--stroke-width', metavar='FLOAT', type=float, help='width of the generated strokes  [:2.0]', default=2.0) 144 | g.add_argument('--background-color', metavar='COLOR', type=str, help='SVG compliant color specification or identifier; adds a background to the SVG output') 145 | g.add_argument('--hue-shift', metavar='FLOAT', type=float, help='amount to rotate an imaginary color wheel before looking up new colors (in degrees)  [:15.0]', default=15.0) 146 | g.add_argument('--hue-shift-line', metavar='FLOAT', type=float, help='separate hue shift for continuous lines; if not passed `--hue-shift\' applies too') 147 | g.add_argument('--best-path-width', metavar='FLOAT', type=float, help='show the best (aka the longest) path through the maze and set width of its marker line') 148 | 149 | g = ap.add_argument_group('Schotter') 150 | g.add_argument('--schotter-falloff', choices=('infinite', 'horizontal', 'vertical', 'radial', 'box', 'random'), 151 | help='enable `George Nees\'-style randomizing rotations and offsets of the maze\'s line segments') 152 | g.add_argument('--schotter-inverse', action='store_true', help='flip the schotter mapping of the selected mode') 153 | g.add_argument('--schotter-rotation', metavar='FLOAT', type=float, help='rotational variance for schottering  [:0.5]', default=0.5) 154 | g.add_argument('--schotter-offset', metavar='FLOAT', type=float, help='positional variance for schottering  [:0.25]', default=0.25) 155 | 156 | g = ap.add_argument_group('Output') 157 | g.add_argument('-o', '--output', metavar='FILENAME', type=str, help='optionally rasterize the generated vector paths and write the result into a PNG file (requires the `svgcairo\' Python module)') 158 | g.add_argument('--output-size', metavar='INT', type=int, help='force pixel width of the raster image, height is automatically calculated; if omitted the generated SVG viewbox dimensions are used') 159 | 160 | user_input = ap.parse_args() 161 | 162 | # Generate data… 163 | # 164 | chaos = random.Random(user_input.random_seed) 165 | scale = user_input.scale 166 | frame = user_input.frame 167 | rows = [] 168 | master_hue = chaos.uniform(0,360) 169 | huesl = user_input.hue_shift if user_input.hue_shift_line is None else user_input.hue_shift_line 170 | 171 | for y in range(0, user_input.rows): 172 | # master_hue = (360 / user_input.rows * y) % 360 173 | rows.append([]) 174 | for x in range(0, user_input.columns): 175 | xoffset = 0 176 | yoffset = 0 177 | angle = 0 178 | slope = chaos.choice([Slope.UP, Slope.DOWN]) 179 | 180 | hue = lookup_hue(slope, x, y, rows, huesl) 181 | if hue is None: 182 | hue = master_hue 183 | master_hue = (master_hue + user_input.hue_shift) % 360 184 | 185 | if user_input.schotter_falloff == 'infinite': 186 | schotter_factor = 1.0 187 | elif user_input.schotter_falloff == 'random': 188 | schotter_factor = chaos.choice([0, 1.0]) 189 | elif user_input.schotter_falloff == 'vertical': 190 | schotter_factor = 1.0 / (user_input.rows - 1) * y 191 | elif user_input.schotter_falloff == 'horizontal': 192 | schotter_factor = 1.0 / (user_input.columns - 1) * x 193 | elif user_input.schotter_falloff == 'radial': 194 | xc = (user_input.columns - 1) / 2.0 195 | yc = (user_input.rows - 1) / 2.0 196 | d = math.sqrt(math.pow(xc - x, 2.0) + math.pow(yc - y, 2.0)) / 2.0 197 | schotter_factor = 1.0 / max(xc, yc) * d 198 | elif user_input.schotter_falloff == 'box': 199 | md = min(x, (user_input.columns - 1) - x, y, (user_input.rows - 1) - y) * 2.0 200 | schotter_factor = 1.0 / max(user_input.columns - 1, user_input.rows - 1) * md 201 | else: 202 | schotter_factor = 0 203 | 204 | #if schotter_factor > 1.0: 205 | # print('WARNING: schotter_factor too big: {}'.format(schotter_factor), file=sys.stderr) 206 | 207 | if user_input.schotter_inverse: 208 | schotter_factor = 1.0 - schotter_factor 209 | 210 | #schotter_factor = -(math.cos(math.pi * schotter_factor) - 1.0) / 2.0 # ease-in-out-sine 211 | schotter_factor = schotter_factor * schotter_factor # ease-in-quad 212 | 213 | if schotter_factor: 214 | xoffset = chaos.uniform(-scale, scale) * schotter_factor * user_input.schotter_offset 215 | yoffset = chaos.uniform(-scale, scale) * schotter_factor * user_input.schotter_offset 216 | angle = chaos.uniform( -90, 90) * schotter_factor * user_input.schotter_rotation 217 | 218 | rows[y].append(DLine(slope, hue, 219 | (x * scale + frame) + xoffset, 220 | (y * scale + frame) + yoffset, 221 | (x * scale + scale + frame) + xoffset, 222 | (y * scale + scale + frame) + yoffset, 223 | angle 224 | )) 225 | 226 | # Primitive path walking… 227 | # 228 | bestwalker = None 229 | circle_pos = None 230 | 231 | if user_input.best_path_width: 232 | coords = [] 233 | for x in range(0, user_input.columns): 234 | coords.append((x, -1, Direction.SOUTH)) 235 | coords.append((x, user_input.rows, Direction.NORTH)) 236 | for y in range(0, user_input.rows): 237 | coords.append((-1, y, Direction.EAST)) 238 | coords.append((user_input.columns, y, Direction.WEST)) 239 | chaos.shuffle(coords) 240 | 241 | offset = scale / 2.0 242 | 243 | for pos in coords: 244 | wx, wy, wd = pos 245 | cx = (wx * scale) + frame + offset 246 | cy = (wy * scale) + frame + offset 247 | tempwalker = ['M{} {}{}{}'.format(cx, cy, 248 | 'v' if wd in (Direction.SOUTH, Direction.NORTH) else 'h', 249 | offset if wd in (Direction.SOUTH, Direction.EAST) else -offset 250 | )] 251 | tx, ty = 1, 1 252 | 253 | while True: 254 | if wd == Direction.SOUTH: 255 | wy += 1 256 | if wy >= user_input.rows: 257 | break 258 | wd, tx, ty = (Direction.EAST, 1, 1) if (rows[wy][wx].slope == Slope.DOWN) else (Direction.WEST, -1, 1) 259 | elif wd == Direction.WEST: 260 | wx -= 1 261 | if wx < 0: 262 | break 263 | wd, tx, ty = (Direction.NORTH, -1, -1) if (rows[wy][wx].slope == Slope.DOWN) else (Direction.SOUTH, -1, 1) 264 | elif wd == Direction.EAST: 265 | wx += 1 266 | if wx >= user_input.columns: 267 | break 268 | wd, tx, ty = (Direction.SOUTH, 1, 1) if (rows[wy][wx].slope == Slope.DOWN) else (Direction.NORTH, 1, -1) 269 | else: # wd == Direction.NORTH 270 | wy -= 1 271 | if wy < 0: 272 | break 273 | wd, tx, ty = (Direction.WEST, -1, -1) if (rows[wy][wx].slope == Slope.DOWN) else (Direction.EAST, 1, -1) 274 | 275 | tempwalker.append('l{} {}'.format((offset * tx), (offset * ty))) 276 | logging.debug('New position <%u×%u>, direction <%s>', wx, wy, wd) 277 | 278 | logging.debug(tempwalker) 279 | if bestwalker is None or len(tempwalker) > len(bestwalker): 280 | bestwalker = tempwalker.copy() 281 | circle_pos = (cx, cy) 282 | 283 | # Generate SVG… 284 | # 285 | vbw = int((scale * user_input.columns) + (frame * 2.0)) 286 | vbh = int((scale * user_input.rows ) + (frame * 2.0)) 287 | 288 | svg = xtree.Element('svg', {'width':'100%', 'height':'100%', 'xmlns':'http://www.w3.org/2000/svg', 'viewBox':'0 0 {} {}'.format(vbw, vbh)}) 289 | title = xtree.SubElement(svg, 'title') 290 | title.text = 'A Temo Artwork' 291 | 292 | if user_input.background_color: 293 | xtree.SubElement(svg, 'rect', {'id':'background', 'x':'0', 'y':'0', 'width':str(vbw), 'height':str(vbh), 'fill':user_input.background_color}) 294 | svg_g = xtree.SubElement(svg, 'g', {'id':'goto10', 'stroke-width':str(user_input.stroke_width), 'stroke-linecap':'round'}) 295 | 296 | for row_id, row in enumerate(rows): 297 | for col_id, element in enumerate(row): 298 | xtree.SubElement(svg_g, 'line', { 299 | 'id': 'line-{}x{}'.format(col_id + 1, row_id + 1), 300 | 'x1': str(element.x1), 301 | 'y1': str(element.y1), 302 | 'x2': str(element.x2), 303 | 'y2': str(element.y2), 304 | 'stroke': hls_to_hex(element.hue, 0.6, 0.5), 305 | }) 306 | 307 | if bestwalker: 308 | svg_g = xtree.SubElement(svg, 'g', {'id':'best_walker'}) 309 | wcolor = hls_to_hex(chaos.uniform(0, 360), 0.5, 0.8) 310 | xtree.SubElement(svg_g, 'path', { 311 | 'd': ''.join(bestwalker), 312 | 'stroke-width': str(user_input.best_path_width), 313 | 'stroke': wcolor, 314 | 'stroke-linecap': 'round', 315 | 'stroke-linejoin': 'round', 316 | 'fill': 'none', 317 | }) 318 | xtree.SubElement(svg_g, 'circle', { 319 | 'id': 'start_point', 320 | 'cx': str(circle_pos[0]), 321 | 'cy': str(circle_pos[1]), 322 | 'r': str(user_input.best_path_width), 323 | 'fill': wcolor, 324 | }) 325 | 326 | rawxml = xtree.tostring(svg, encoding='unicode') 327 | 328 | # Output… 329 | # 330 | if not user_input.output: 331 | print(rawxml) 332 | else: 333 | try: 334 | import os 335 | from cairosvg import svg2png 336 | svg2png( 337 | bytestring = rawxml, 338 | write_to = os.path.realpath(os.path.expanduser(user_input.output)), 339 | output_width = user_input.output_size, 340 | output_height = int(user_input.output_size * vbh / vbw) if user_input.output_size is not None else None 341 | ) 342 | except ImportError as e: 343 | print('Couldn\'t rasterize nor write a PNG file. Required Python module \'cairosvg\' is not available: {}'.format(str(e)), file=sys.stderr) 344 | 345 | 346 | if __name__ == '__main__': 347 | main() 348 | -------------------------------------------------------------------------------- /teocuitlatl.md: -------------------------------------------------------------------------------- 1 | 2 | # Teocuitlatl 3 | 4 | `teocuitlatl` *noun* — Classical Nahuatl: [1] gold, lit. wonderful waste 5 | 6 | ## Synopsis 7 | 8 | Creates a grid of colored squares that are accentuated with smaller squares or discs inspired by works of *Victor Vasarely*. Additional 9 | color palettes of works of other op-art artists including *Bridget Riley* and *Ellsworth Kelly* are included for further experimentation 10 | with the theme. Output is generated as a set of vector shapes in Scalable Vector Graphics (SVG) format and printed on the standard 11 | output stream. 12 | 13 | ## Requirements 14 | 15 | An installation of `Python 3` (any version above v3.5 will do fine). For the optional `PNG` output support an installation of 16 | the `cairosvg` 3rd-party Python module is recommended. The module can be installed with Python's package manager: 17 | 18 | ``` shell 19 | pip --install cairosvg --user 20 | ``` 21 | 22 | ## Output Examples 23 | 24 | Figure 1 Figure 2 Figure 3 Figure 4 Figure 5 25 | 26 | ## Usage 27 | 28 | ``` 29 | usage: teocuitlatl.py [-V] [-h] [--columns INT] [--rows INT] [--no-inset] 30 | [--inset-offset INT] [--no-horizontal-flip] 31 | [--no-vertical-flip] [--color-bias INT] [--scale FLOAT] 32 | [--padding FLOAT] 33 | [--palette {shadowplay,spectrum9,binary,greyscale,rgb,yell}] 34 | [--random-seed INT] [--randomize] [-o FILENAME] 35 | [--output-size INT] 36 | 37 | Startup: 38 | -V, --version show version number and exit 39 | -h, --help show this help message and exit 40 | 41 | Algorithm: 42 | --columns INT number of grid columns  [:11] 43 | --rows INT number of grid rows  [:11] 44 | --no-inset disable the default accent shape inset 45 | --inset-offset INT manually force amount of frame tiles around the inset, 46 | else it's automatically calculated 47 | --no-horizontal-flip disable the default horizontal accent shape flip 48 | --no-vertical-flip disable the default vertical accent shape flip 49 | --color-bias INT increase amount of directional bias when choosing 50 | random colors  [:1] 51 | --scale INT base scale factor of the grid elements  [:74.0] 52 | --padding FLOAT manually force inner padding to control the frame 53 | around the accent shapes 54 | --palette {shadowplay,spectrum9,binary,greyscale,rgb,yell,owinja,folklore} 55 | choose random colors from the specified color scheme 56 |  [:default] 57 | --random-seed INT fixed initialization of the random number generator 58 | for predictable results 59 | --randomize generate truly random layouts; other algorithm values 60 | provided via command line parameters are utilized as 61 | limits 62 | 63 | Output: 64 | -o FILENAME, --output FILENAME 65 | optionally rasterize the generated vector paths and 66 | write the result into a PNG file (requires the 67 | `svgcairo' Python module) 68 | --output-size INT force pixel width of the raster image, height is 69 | automatically calculated; if omitted the generated SVG 70 | viewbox dimensions are used 71 | ``` 72 | 73 | ### Usage Examples 74 | 75 | ``` shell 76 | # Generate a SVG file 77 | ./teocuitlatl.py --columns=8 --rows=5 > output.svg 78 | 79 | # Rasterize directly into a PNG file (requires "cairosvg") 80 | ./teocuitlatl.py -o output.png --output-size=1024 81 | ``` 82 | 83 | ``` shell 84 | # Preview output with ImageMagick's "convert" and Preview.app (Mac OS X) 85 | ./teocuitlatl.py --random-seed=12345 | convert svg:- png:- | open -f -a Preview.app 86 | 87 | # Preview output with ImageMagick's "convert" and "display" (Linux/BSD/etc.) 88 | ./teocuitlatl.py --random-seed=12345 | convert svg:- png:- | display 89 | ``` 90 | 91 | ## History 92 | 93 | 94 | 95 | 96 | 97 | 107 | 108 | 109 | 110 | 111 | 117 | 118 | 119 | 120 | 121 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
1.416-Jul-2020 98 |
    99 |
  • Improved overlapping to avoid hairlines in some SVG renderers 100 |
  • Switched `--scale' from FLOAT to INT to avoid rounding issues: occasionally parts of the 101 | generated artwork was clipped off due to viewbox dimensions already truncated to integer ranges 102 |
  • Smaller SVG files due to improved output of float values (no trailing zeros) 103 |
  • `--inset', `--no-horizontal-flip', and `--no-vertical-flip' parameters were not properly randomized with `--randomize' due 104 | to incorrect randrange() usage. 105 |
106 |
1.37-Jul-2020 112 |
    113 |
  • Added new palettes `owinja` and `folklore` 114 |
  • Switched default palette to the actual Vasarely inspired `folklore` palette 115 |
116 |
1.24-Jul-2020 122 |
    123 |
  • Parameters `--no-horizontal-flip`, `--no-vertical-flip`, and `--no-inset` are now also randomized 124 | when `--randomize` is used 125 |
126 |
1.13-Jul-2020Initial public source code release
-------------------------------------------------------------------------------- /teocuitlatl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Teocuitlatl 4 | Creates a grid of colored squares that are accentuated with smaller squares 5 | or discs 6 | 7 | Copyright © 2020 Christian Rosentreter 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU Affero General Public License as published 11 | by the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU Affero General Public License for more details. 18 | 19 | You should have received a copy of the GNU Affero General Public License 20 | along with this program. If not, see . 21 | 22 | $Id: teocuitlatl.py 166 2020-07-16 20:39:42Z tokai $ 23 | """ 24 | 25 | import random 26 | import argparse 27 | import sys 28 | import xml.etree.ElementTree as xtree 29 | from collections import Counter 30 | 31 | __author__ = 'Christian Rosentreter' 32 | __version__ = '1.4' 33 | __all__ = [] 34 | 35 | 36 | 37 | def triangular_stronger_bias(chaos, low, high, bias, iterations): 38 | """Returns random values with a very strong bias.""" 39 | candidates = [int(chaos.triangular(low, high, bias)) for _ in range(iterations)] 40 | result_set = Counter(candidates).most_common(1) 41 | return chaos.choice(result_set)[0] 42 | 43 | 44 | def color_to_hex(color): 45 | """Converts a color tuple (r,g,b) into a SVG compatible hexadecimal color descriptor.""" 46 | return '#{:02x}{:02x}{:02x}'.format(*color) 47 | 48 | 49 | def float_to_svg(v): 50 | """Tries to generate smallest string representation of the supplied float value.""" 51 | return '{:g}'.format(round(v, 10)) 52 | 53 | 54 | 55 | def main(): 56 | """Yet another grid generator… and it probably won't be the last one either. :) """ 57 | 58 | palettes = { 59 | 'shadowplay': [ # Bridget Riley: "Shadowplay" 60 | ( 61, 85, 119), 61 | ( 48, 102, 208), 62 | ( 0, 141, 184), 63 | (112, 179, 113), 64 | (232, 98, 131), 65 | (112, 169, 236), 66 | (162, 124, 171), 67 | (197, 141, 211), 68 | (255, 164, 82), 69 | (248, 221, 143), 70 | (255, 224, 230), 71 | ], 72 | 'spectrum9': [ # Ellsworth Kelly: "Spectrum Ⅸ" 73 | (238, 225, 58), 74 | (143, 220, 67), 75 | (104, 209, 120), 76 | ( 42, 176, 186), 77 | ( 48, 138, 214), 78 | ( 97, 114, 197), 79 | (116, 95, 166), 80 | (138, 102, 152), 81 | (206, 105, 120), 82 | (241, 103 , 98), 83 | (250, 139, 0), 84 | (250, 196, 64), 85 | ], 86 | 'binary': [ 87 | ( 0, 0, 0), 88 | (255, 255, 255), 89 | ], 90 | 'greyscale': [ 91 | ( 0, 0, 0), 92 | (0x11, 0x11, 0x11), 93 | (0x22, 0x22, 0x22), 94 | (0x33, 0x33, 0x33), 95 | (0x44, 0x44, 0x44), 96 | (0x55, 0x55, 0x55), 97 | (0x66, 0x66, 0x66), 98 | (0x77, 0x77, 0x77), 99 | (0x88, 0x88, 0x88), 100 | (0x99, 0x99, 0x99), 101 | (0xAA, 0xAA, 0xAA), 102 | (0xBB, 0xBB, 0xBB), 103 | (0xCC, 0xCC, 0xCC), 104 | (0xDD, 0xDD, 0xDD), 105 | ( 255, 255, 255), 106 | ], 107 | 'rgb': [ 108 | (255, 0, 0), 109 | ( 0, 255, 0), 110 | ( 0, 0, 255), 111 | ], 112 | 'yell': [ # Unknown Artist: "Yell" (NHK asadora) marketing 113 | (0x00, 0x00, 0x00), 114 | (0x05, 0xae, 0xb0), 115 | (0xeb, 0x55, 0x75), 116 | (0xef, 0xba, 0x1f), 117 | (0xff, 0xff, 0xff), 118 | ], 119 | 'owinja': [ # Carla Thompson: "Turquoise & Orange Star Quilt" 120 | (195, 216, 227), 121 | (148, 209, 225), 122 | ( 0, 141, 171), 123 | (162, 37, 23), 124 | (231, 105, 83), 125 | (252, 117, 21), 126 | ], 127 | 'folklore': [ # Victor Vasarely: "Planetary Folklore Participations N° 1" (selection) 128 | (187, 248, 249), 129 | (252, 252, 4), 130 | (105, 222, 249), 131 | (252, 207, 10), 132 | (250, 126, 250), 133 | ( 35, 249, 66), 134 | ( 4, 159, 242), 135 | (251, 117, 13), 136 | ( 15, 114, 214), 137 | ( 6, 187, 82), 138 | (252, 62, 4), 139 | ( 36, 112, 178), 140 | (206, 76, 113), 141 | (105, 58, 162), 142 | ( 10, 131, 51), 143 | (135, 27, 65), 144 | ( 57, 34, 114), 145 | ( 17, 33, 13), 146 | ], 147 | # TODO: implement "original" special selection mode (separate array) 148 | } 149 | 150 | 151 | ap = argparse.ArgumentParser( 152 | description=('Creates a grid of colored squares that are accentuated with smaller squares ' 153 | 'or discs. Output is generated as a set of vector shapes in Scalable Vector Graphics (SVG) ' 154 | 'format and printed on the standard output stream.'), 155 | epilog='Report bugs, request features, or provide suggestions via https://github.com/the-real-tokai/macuahuitl/issues', 156 | add_help=False, 157 | ) 158 | 159 | g = ap.add_argument_group('Startup') 160 | g.add_argument('-V', '--version', action='version', help="show version number and exit", version='%(prog)s {}'.format(__version__), ) 161 | g.add_argument('-h', '--help', action='help', help='show this help message and exit') 162 | 163 | g = ap.add_argument_group('Algorithm') 164 | g.add_argument('--columns', metavar='INT', type=int, help='number of grid columns  [:11]', default=10) 165 | g.add_argument('--rows', metavar='INT', type=int, help='number of grid rows  [:11]', default=10) 166 | g.add_argument('--no-inset', action='store_true', help='disable the default accent shape inset') 167 | g.add_argument('--inset-offset', metavar='INT', type=int, help='manually force amount of frame tiles around the inset, else it\'s automatically calculated') 168 | g.add_argument('--no-horizontal-flip', action='store_true', help='disable the default horizontal accent shape flip') 169 | g.add_argument('--no-vertical-flip', action='store_true', help='disable the default vertical accent shape flip') 170 | g.add_argument('--color-bias', metavar='INT', type=int, help='increase amount of directional bias when choosing random colors  [:1]', default=1) 171 | g.add_argument('--scale', metavar='INT', type=int, help='base scale factor of the grid elements  [:74.0]', default=74.0) 172 | g.add_argument('--padding', metavar='FLOAT', type=float, help='manually force inner padding to control the frame around the accent shapes') 173 | g.add_argument('--palette', choices=list(palettes.keys()), help='choose random colors from the specified color scheme  [:default]', default='folklore') 174 | g.add_argument('--random-seed', metavar='INT', type=int, help='fixed initialization of the random number generator for predictable results') 175 | g.add_argument('--randomize', action='store_true', help='generate truly random layouts; other algorithm values provided via command line parameters are utilized as limits') 176 | 177 | g = ap.add_argument_group('Output') 178 | g.add_argument('-o', '--output', metavar='FILENAME', type=str, help='optionally rasterize the generated vector paths and write the result into a PNG file (requires the `svgcairo\' Python module)') 179 | g.add_argument('--output-size', metavar='INT', type=int, help='force pixel width of the raster image, height is automatically calculated; if omitted the generated SVG viewbox dimensions are used') 180 | 181 | user_input = ap.parse_args() 182 | 183 | 184 | # Generate data/ SVG… 185 | # 186 | chaos = random.Random(user_input.random_seed) 187 | 188 | tile_size = max(1, user_input.scale) 189 | tiles_x = max(1, user_input.columns) 190 | tiles_y = max(1, user_input.rows) 191 | tiles_ioff = user_input.inset_offset if user_input.inset_offset is not None else int(min(tiles_x, tiles_y)/2.0/2.0) 192 | tile_frame = user_input.padding if user_input.padding is not None else round(0.14 * tile_size, 2) 193 | palette = palettes[user_input.palette] # if chaos.uniform(0, 1) < 0.5 else list(reversed(palettes[user_input.palette])) 194 | color_iter = max(1, user_input.color_bias) 195 | flip_x = False if user_input.no_horizontal_flip else True 196 | flip_y = False if user_input.no_vertical_flip else True 197 | inset = False if user_input.no_inset else True 198 | 199 | if user_input.randomize: 200 | tile_size = chaos.randrange(0, tile_size) + 1 201 | tiles_x = chaos.randrange(0, tiles_x) + 1 202 | # TODO: we get a lot of silly pictures here; use some sane limits for now 203 | # tyles_y = chaos.randrange(0, tiles_y) + 1 204 | if tiles_x % 2: 205 | tiles_x += 1 206 | tiles_y = tiles_x 207 | # 208 | tiles_ioff = chaos.randrange(0, tiles_ioff + 1) 209 | tile_frame = chaos.uniform(0, tile_frame) 210 | palette = palettes[chaos.choice(list(palettes.keys()))] 211 | color_iter = int(max(1.0, triangular_stronger_bias(chaos, 0, color_iter, 0, 10))) 212 | flip_x = chaos.choice([0, 1]) 213 | flip_y = chaos.choice([0, 1]) 214 | inset = chaos.choice([0, 1]) 215 | 216 | if chaos.uniform(0, 1) < 0.5: 217 | palette.reverse() 218 | 219 | stile_size = tile_size - tile_frame - tile_frame 220 | stile_rad = stile_size / 2.0 221 | vbw = int(tile_size * tiles_x) 222 | vbh = int(tile_size * tiles_y) 223 | colors = len(palette) 224 | 225 | svg = xtree.Element('svg', {'width':'100%', 'height':'100%', 'xmlns':'http://www.w3.org/2000/svg', 'viewBox':'0 0 {} {}'.format(vbw, vbh)}) 226 | title = xtree.SubElement(svg, 'title') 227 | title.text = 'A Teocuitlatl Artwork' 228 | 229 | tile_backgrounds = [] 230 | init_shape = chaos.choice([0, 1]) # 1 == square, 2 == circle 231 | 232 | for y in range(0, tiles_y): 233 | for x in range(0, tiles_x): 234 | 235 | # Select inner shape 236 | shape = init_shape 237 | bias = x / tiles_x * colors 238 | if inset and (tiles_ioff <= x < (tiles_x - tiles_ioff)) and (tiles_ioff <= y < (tiles_y - tiles_ioff)): 239 | shape = 1 - shape # swap 240 | bias = colors - bias 241 | # TODO: reversing the bias direction should also change/ map the range and 242 | # take 'tiles_ioff' into account (so the inset uses the full range of 243 | # available colors), maybe something like this: 244 | # "colors - (x+tiles_ioff / tiles_x-tiles_ioff*2 * colors)" ? 245 | if flip_y and (y >= (tiles_y / 2)): 246 | shape = 1 - shape # swap 247 | if flip_x and (x >= (tiles_x / 2)): 248 | shape = 1 - shape # swap 249 | 250 | # Fetch background color 251 | loop_cnt = 0 252 | while loop_cnt < 100: 253 | loop_cnt += 1 254 | tile_color_bg = triangular_stronger_bias(chaos, 0, colors, bias, color_iter) 255 | if (x > 0) and (tile_color_bg is tile_backgrounds[-1]): # one to the left 256 | continue 257 | if (y > 0) and (tile_color_bg is tile_backgrounds[-tiles_x]): # one to the top 258 | continue 259 | break 260 | else: 261 | print('Warning: Couldn\'t get a non-colliding tile background color for tile "{}×{}", because the color bias is too high for the amount of available colors.'.format(x, y), file=sys.stderr) 262 | tile_backgrounds.append(tile_color_bg) 263 | 264 | # Fetch foreground color 265 | loop_cnt = 0 266 | while loop_cnt < 100: 267 | loop_cnt += 1 268 | tile_color_shape = triangular_stronger_bias(chaos, 0, colors, bias, color_iter) 269 | if tile_color_shape is not tile_color_bg: 270 | break 271 | else: 272 | print('Warning: Couldn\'t get a non-colliding accent shape color for tile "{}×{}", because the color bias is too high for the amount of available colors.'.format(x, y), file=sys.stderr) 273 | 274 | # Output the tile 275 | svg_tile_group = xtree.SubElement(svg, 'g', {'id': 'tile_{}x{}'.format(x+1, y+1)}) 276 | 277 | xtree.SubElement(svg_tile_group, 'rect', { 278 | 'x': float_to_svg(x * tile_size), 279 | 'y': float_to_svg(y * tile_size), 280 | # Note: overlap to avoid potential hairlines between the tiles in some SVG renderers 281 | 'width': float_to_svg(tile_size * (2 if ((x + 1) < tiles_x) else 1)), 282 | 'height': float_to_svg(tile_size * (2 if ((y + 1) < tiles_y) else 1)), 283 | 'fill': color_to_hex(palette[tile_color_bg]) 284 | }) 285 | 286 | if shape == 0: 287 | xtree.SubElement(svg_tile_group, 'rect', { 288 | 'x': float_to_svg((x * tile_size) + tile_frame), 289 | 'y': float_to_svg((y * tile_size) + tile_frame), 290 | 'width': float_to_svg(stile_size), 291 | 'height': float_to_svg(stile_size), 292 | 'fill': color_to_hex(palette[tile_color_shape]) 293 | }) 294 | else: 295 | xtree.SubElement(svg_tile_group, 'circle', { 296 | 'cx': float_to_svg((x * tile_size) + (tile_size / 2)), 297 | 'cy': float_to_svg((y * tile_size) + (tile_size / 2)), 298 | 'r': float_to_svg(stile_rad), 299 | 'fill': color_to_hex(palette[tile_color_shape]) 300 | }) 301 | 302 | rawxml = xtree.tostring(svg, encoding='unicode') 303 | 304 | 305 | # Output… 306 | # 307 | if not user_input.output: 308 | print(rawxml) 309 | else: 310 | try: 311 | import os 312 | from cairosvg import svg2png 313 | svg2png( 314 | bytestring = rawxml, 315 | write_to = os.path.realpath(os.path.expanduser(user_input.output)), 316 | output_width = user_input.output_size, 317 | output_height = int(user_input.output_size * vbh / vbw) if user_input.output_size is not None else None 318 | ) 319 | except ImportError as e: 320 | print('Couldn\'t rasterize nor write a PNG file. Required Python module \'cairosvg\' is not available: {}'.format(str(e)), file=sys.stderr) 321 | 322 | 323 | 324 | if __name__ == '__main__': 325 | main() 326 | --------------------------------------------------------------------------------