#!/usr/bin/env python3 """ Altepetl Implements an artful grid-based layout of "U"-shapes; inspired by some of generative art pioneer Véra Molnar's artworks. Copyright © 2020 Christian Rosentreter This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . $Id: altepetl.py 144 2020-06-18 17:04:01Z tokai $ """ import random import argparse import sys import xml.etree.ElementTree as xtree __author__ = 'Christian Rosentreter' __version__ = '1.2' __all__ = ['USquare'] class USquare(): """SVG description for a square 'U' shape, optionally rotated by 90° and/ or flipped.""" dmod = {'n':['h', 'v', 1], 'e':['v', 'h', 1], 'w':['v', 'h', -1], 's':['h', 'v', -1]} def __init__(self, x, y, scale=1.0, direction='n', variation=0.0): self.x = x self.y = y self.scale = scale self.direction = direction self.variation = variation def __str__(self): mh, mv, m2 = self.dmod[self.direction] m2 *= self.scale v = 0.18 * min(self.variation, 1.0) return ''.join(str(s) for s in [ 'M', -0.5 * m2 + self.x, ' ', -0.5 * m2 + self.y, mh, (0.2 + v) * m2, mv, 0.8 * m2, mh, (0.6 - v) * m2, mv, -0.8 * m2, mh, 0.2 * m2, mv, 1.0 * m2, mh, -1.0 * m2, 'Z', '' ]) def main(): """Let's make a work of art.""" ap = argparse.ArgumentParser( description=('Implements an artful grid-based layout of "U"-shapes; inspired ' 'by some of generative art pioneer Véra Molnar\'s artworks.'), epilog='Report bugs, request features, or provide suggestions via https://github.com/the-real-tokai/macuahuitl/issues', add_help=False, ) g = ap.add_argument_group('Startup') g.add_argument('-V', '--version', action='version', help="show version number and exit", version='%(prog)s {}'.format(__version__), ) g.add_argument('-h', '--help', action='help', help='show this help message and exit') g = ap.add_argument_group('Algorithm') g.add_argument('--columns', metavar='INT', type=int, help='number of grid columns  [:11]', default=11) g.add_argument('--rows', metavar='INT', type=int, help='number of grid rows  [:11]', default=11) g.add_argument('--scale', metavar='FLOAT', type=float, help='base scale factor of the grid elements [:10.0]', default=10.0) g.add_argument('--gap', metavar='FLOAT', type=float, help='non-random base gap between grid elements [:5.0]', default=5.0) 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) 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) g.add_argument('--random-seed', metavar='INT', type=int, help='fixed initialization of the random number generator for predictable results') g = ap.add_argument_group('Miscellaneous') g.add_argument('--separate-paths', action='store_true', help='generate separate elements for each element') g.add_argument('--negative', action='store_true', help='inverse the output colors') 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) g = ap.add_argument_group('Output') 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)') 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') user_input = ap.parse_args() grid_x = user_input.columns grid_y = user_input.rows grid_size = user_input.scale grid_gap = user_input.gap variation = user_input.shape_variation jiggle = user_input.offset_jiggle frame = user_input.frame chaos = random.Random(user_input.random_seed) grid_offset = grid_size + grid_gap col1, col2 = 'white', 'black' if user_input.negative: col1, col2 = col2, col1 squares = [] for x in range(0, grid_x): for y in range(0, grid_y): dx = (x * grid_offset) + (grid_offset / 2.0) + frame + chaos.uniform(-jiggle, jiggle) dy = (y * grid_offset) + (grid_offset / 2.0) + frame + chaos.uniform(-jiggle, jiggle) squares.append(USquare(dx, dy, grid_size, chaos.choice('news'), chaos.uniform(0.0, variation))) vbw = int((grid_offset * grid_x) + (frame * 2.0)) vbh = int((grid_offset * grid_y) + (frame * 2.0)) svg = xtree.Element('svg', {'width':'100%', 'height':'100%', 'xmlns':'http://www.w3.org/2000/svg', 'viewBox':'0 0 {} {}'.format(vbw, vbh)}) title = xtree.SubElement(svg, 'title') title.text = 'An Altepetl Artwork' xtree.SubElement(svg, 'rect', {'id':'background', 'x':'0', 'y':'0', 'width':str(vbw), 'height':str(vbh), 'fill':col1}) if user_input.separate_paths: svg_g = xtree.SubElement(svg, 'g', {'id':'grid-of-us', 'stroke-width':'0', 'fill':col2}) for si, s in enumerate(squares): xtree.SubElement(svg_g, 'path', {'id':'element-{}'.format(si), 'd':str(s)}) else: xtree.SubElement(svg, 'path', {'id':'grid-of-us', 'stroke-width':'0', 'fill':col2, 'd':''.join(str(s) for s in squares)}) rawxml = xtree.tostring(svg, encoding='unicode') if not user_input.output: print(rawxml) else: try: import os from cairosvg import svg2png w = vbw if user_input.output_size is None else user_input.output_size svg2png(bytestring=rawxml, write_to=os.path.realpath(os.path.expanduser(user_input.output)), output_width=int(w), output_height=int(w * vbh / vbw) ) except ImportError as e: print('Couldn\'t rasterize nor write a PNG file. Required Python module \'cairosvg\' is not available: {}'.format(str(e)), file=sys.stderr) if __name__ == '__main__': main()