-
Notifications
You must be signed in to change notification settings - Fork 10
/
altepetl.py
executable file
·159 lines (124 loc) · 6.52 KB
/
altepetl.py
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/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 <https://www.gnu.org/licenses/>.
$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 <path> 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()