├── .gitignore ├── LICENSE.md ├── README.md ├── main.py └── tile.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Tiling 2 | 3 | Quickly construct tilings of regular polygons and their dual tilings using a 4 | simple API. 5 | 6 | Scroll down for a tutorial. Here are some examples. 7 | 8 | ![Sample](http://i.imgur.com/gyoQnuG.gif) 9 | 10 | ### Links 11 | 12 | http://en.wikipedia.org/wiki/Tiling_by_regular_polygons 13 | 14 | http://en.wikipedia.org/wiki/List_of_uniform_tilings 15 | 16 | ### Motivation 17 | 18 | 1. Write polygon tiling code. 19 | 2. ??? 20 | 3. Profit! 21 | 22 | ### Wallpaper 23 | 24 | * [16x9 Wallpaper](http://i.imgur.com/oerkmDS.png) 25 | * [16x10 Wallpaper](http://i.imgur.com/H28k39a.png) 26 | 27 | ### How To 28 | 29 | [pycairo](http://cairographics.org/pycairo/) is used for rendering. 30 | Installation on OS X is easy using Homebrew. 31 | 32 | brew install py2cairo 33 | 34 | Before creating a new pattern, set these flags to zoom in and label the 35 | polygons and their edges. 36 | 37 | SCALE = 128 38 | SHOW_LABELS = True 39 | 40 | The first step is to create a Model that will hold our polygons. 41 | 42 | model = Model() 43 | 44 | Next, we will place our first polygon at the origin. We need only specify its 45 | number of sides. Let's add a hexagon. 46 | 47 | model.append(Shape(6)) 48 | 49 | At this point we can run the following code to render the model. 50 | 51 | surface = model.render() 52 | surface.write_to_png('output.png') 53 | 54 | ![Image](http://i.imgur.com/OjV0HTb.png) 55 | 56 | Now, let's add squares adjacent to all of the hexagon's edges. 57 | 58 | a = model.add(0, range(6), 4) 59 | 60 | The first parameter, `0`, specifies which shape(s) we're attaching to. Here, 61 | we're only attaching to one shape (the hexagon) and it was the first one 62 | created, so it's referred to by zero. 63 | 64 | The second parameter, `range(6)`, specifies the edges we're attaching to. In this 65 | case we want to attach to all six sides of the hexagon. You can see the edges 66 | labeled in the output image. 67 | 68 | The third parameter, `4`, specifies the number of sides for the new shapes. In 69 | this case, squares. 70 | 71 | The return value of `add` tracks the indexes of the newly created squares 72 | so we can refer to them later. 73 | 74 | ![Image](http://i.imgur.com/D0zqHkA.png) 75 | 76 | Next comes the cool part. We can attach triangles to all of the squares we just 77 | created in one fell swoop by using the previous return value. Here, we are 78 | adding triangles to edge number 1 of each of those squares. 79 | 80 | b = model.add(a, 1, 3) 81 | 82 | ![Image](http://i.imgur.com/lfyfaC0.png) 83 | 84 | Now we'll add more hexagons which will represent the repeating positions of 85 | our template. 86 | 87 | c = model.add(a, 2, 6) 88 | 89 | ![Image](http://i.imgur.com/2HgeMRd.png) 90 | 91 | Now that we have positions for repeating the pattern, we can use the 92 | repeat function to automatically fill in the rest of the surface 93 | with our pattern. 94 | 95 | model.repeat(c) 96 | 97 | ![Image](http://i.imgur.com/JC2MSwH.png) 98 | 99 | Here's all the code needed for this pattern: 100 | 101 | from tile import Model, Shape 102 | 103 | BLUE = 0x477984 104 | ORANGE = 0xEEAA4D 105 | RED = 0xC03C44 106 | 107 | model = Model() 108 | model.append(Shape(6, fill=RED)) 109 | a = model.add(0, range(6), 4, fill=ORANGE) 110 | b = model.add(a, 1, 3, fill=BLUE) 111 | c = model.add(a, 2, 6, fill=RED) 112 | model.repeat(c) 113 | surface = model.render() 114 | surface.write_to_png('output.png') 115 | 116 | Once finished, you can turn off the helper labels and adjust the scale as 117 | desired. 118 | 119 | ![Image](http://i.imgur.com/cOrQsXW.png) 120 | 121 | Dual tilings can be created with `model.render(dual=True)`. This setting 122 | renders polygons such that the vertices of the original tiling correspond to 123 | the faces of the dual tiling and vice-versa. 124 | 125 | Here is the dual of the above pattern. 126 | 127 | ![Image](http://i.imgur.com/BnIdKV2.png) 128 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from tile import Model, Shape 2 | 3 | BLUE = 0x477984 4 | ORANGE = 0xEEAA4D 5 | RED = 0xC03C44 6 | WHITE = 0xFEF5EB 7 | 8 | def render(pattern, dual): 9 | if pattern == 0: # 3.6.3.6 10 | model = Model() 11 | model.append(Shape(6)) 12 | a = model.add(0, range(6), 3) 13 | b = model.add(a, 1, 6) 14 | model.repeat(b) 15 | return model.render(dual) 16 | 17 | if pattern == 1: # 4.6.12 18 | model = Model() 19 | model.append(Shape(12)) 20 | a = model.add(0, range(0, 12, 2), 6) 21 | b = model.add(0, range(1, 12, 2), 4) 22 | c = model.add(b, 2, 12) 23 | model.repeat(c) 24 | return model.render(dual) 25 | 26 | if pattern == 2: # 3.3.4.3.4 27 | model = Model() 28 | model.append(Shape(4)) 29 | a = model.add(0, range(4), 3) 30 | b = model.add(a, 1, 4) 31 | c = model.add(b, [2, 3], 3) 32 | d = model.add(c, 2, 4) 33 | model.repeat(d) 34 | return model.render(dual) 35 | 36 | if pattern == 3: # 3.3.3.3.6 37 | model = Model() 38 | model.append(Shape(6)) 39 | a = model.add(0, range(6), 3) 40 | b = model.add(a, 1, 3) 41 | c = model.add(a, 2, 3) 42 | d = model.add(c, 1, 6) 43 | model.repeat(d) 44 | return model.render(dual) 45 | 46 | if pattern == 4: # 4.8.8 47 | model = Model() 48 | model.append(Shape(8)) 49 | a = model.add(0, range(1, 8, 2), 4) 50 | b = model.add(a, 1, 8) 51 | model.repeat(b) 52 | return model.render(dual) 53 | 54 | if pattern == 5: # 3.3.4.12 / 3.3.3.3.3.3 55 | model = Model() 56 | model.append(Shape(12)) 57 | a = model.add(0, range(0, 12, 2), 3) 58 | b = model.add(0, range(1, 12, 2), 4) 59 | c = model.add(b, [1, 3], 3) 60 | d = model.add(b, 2, 12) 61 | model.repeat(d) 62 | return model.render(dual) 63 | 64 | if pattern == 6: # 3.4.6.4 65 | model = Model() 66 | model.append(Shape(6)) 67 | a = model.add(0, range(6), 4) 68 | b = model.add(a, 1, 3) 69 | c = model.add(a, 2, 6) 70 | model.repeat(c) 71 | return model.render(dual) 72 | 73 | if pattern == 7: # 3.3.4.4 74 | model = Model() 75 | model.append(Shape(4)) 76 | a = model.add(0, [0, 2], 4) + [0] 77 | b = model.add(a, [1, 3], 3) 78 | c = model.add(b, 1, 3) 79 | d = model.add(c, 2, 4) 80 | model.repeat(d) 81 | return model.render(dual) 82 | 83 | if pattern == 8: # 3.3.3.3.3.3 84 | model = Model() 85 | model.append(Shape(3)) 86 | a = model.add(0, range(3), 3) 87 | b = model.add(a, [1, 2], 3) 88 | model.repeat(b) 89 | return model.render(dual) 90 | 91 | if pattern == 9: 92 | model = Model(scale=50) 93 | model.append(Shape(5)) 94 | a = model.add(0, range(5), 4) 95 | for i in range(8): 96 | b = model.add(a, 2, 5) 97 | a = model.add(b, 2, 4) 98 | return model.render(dual) 99 | 100 | if pattern == 10: 101 | model = Model() 102 | model.append(Shape(6)) 103 | a = model.add(0, range(6), 4) 104 | b = model.add(a, 2, 3) 105 | c = model.add(b, 1, 4) 106 | d = model.add(b, 2, 4) 107 | e = model.add(d, 2, 6) 108 | model.add(a, 1, 3) 109 | model.add(c, 1, 3) 110 | model.repeat(e) 111 | return model.render(dual) 112 | 113 | if pattern == 11: 114 | model = Model() 115 | model.append(Shape(8)) 116 | a = model.add(0, range(0, 8, 2), 6) 117 | b = model.add(a, 3, 8) 118 | model.repeat(b) 119 | return model.render(dual) 120 | 121 | if pattern == 12: # 3.12.12 122 | model = Model() 123 | model.append(Shape(12)) 124 | a = model.add(0, range(0, 12, 2), 3) 125 | b = model.add(0, range(1, 12, 2), 12) 126 | model.repeat(b) 127 | return model.render(dual) 128 | 129 | def main(): 130 | for pattern in range(13): 131 | surface = render(pattern, False) 132 | surface.write_to_png('output%02da.png' % pattern) 133 | surface = render(pattern, True) 134 | surface.write_to_png('output%02db.png' % pattern) 135 | 136 | if __name__ == '__main__': 137 | main() 138 | -------------------------------------------------------------------------------- /tile.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, tan, pi, atan2 2 | import cairo 3 | 4 | # Model() defaults 5 | WIDTH = 1024 6 | HEIGHT = 1024 7 | SCALE = 64 8 | 9 | # Model.render() defaults 10 | BACKGROUND_COLOR = 0x000000 11 | LINE_WIDTH = 0.1 12 | MARGIN = 0.1 13 | SHOW_LABELS = False 14 | 15 | # Shape() defaults 16 | FILL_COLOR = 0x477984 17 | STROKE_COLOR = 0x313E4A 18 | 19 | def color(value): 20 | r = ((value >> (8 * 2)) & 255) / 255.0 21 | g = ((value >> (8 * 1)) & 255) / 255.0 22 | b = ((value >> (8 * 0)) & 255) / 255.0 23 | return (r, g, b) 24 | 25 | def normalize(x, y): 26 | return (round(x, 6), round(y, 6)) 27 | 28 | def inset_corner(p1, p2, p3, margin): 29 | (x1, y1), (x2, y2), (x3, y3) = (p1, p2, p3) 30 | a1 = atan2(y2 - y1, x2 - x1) - pi / 2 31 | a2 = atan2(y3 - y2, x3 - x2) - pi / 2 32 | ax1, ay1 = x1 + cos(a1) * margin, y1 + sin(a1) * margin 33 | ax2, ay2 = x2 + cos(a1) * margin, y2 + sin(a1) * margin 34 | bx1, by1 = x2 + cos(a2) * margin, y2 + sin(a2) * margin 35 | bx2, by2 = x3 + cos(a2) * margin, y3 + sin(a2) * margin 36 | ady, adx = ay2 - ay1, ax1 - ax2 37 | bdy, bdx = by2 - by1, bx1 - bx2 38 | c1 = ady * ax1 + adx * ay1 39 | c2 = bdy * bx1 + bdx * by1 40 | d = ady * bdx - bdy * adx 41 | x = (bdx * c1 - adx * c2) / d 42 | y = (ady * c2 - bdy * c1) / d 43 | return (x, y) 44 | 45 | def inset_polygon(points, margin): 46 | result = [] 47 | points = list(points) 48 | points.insert(0, points[-2]) 49 | for p1, p2, p3 in zip(points, points[1:], points[2:]): 50 | point = inset_corner(p1, p2, p3, margin) 51 | result.append(point) 52 | result.append(result[0]) 53 | return result 54 | 55 | class Shape(object): 56 | def __init__(self, sides, x=0, y=0, rotation=0, **kwargs): 57 | self.sides = sides 58 | self.x = x 59 | self.y = y 60 | self.rotation = rotation 61 | self.fill = FILL_COLOR 62 | self.stroke = STROKE_COLOR 63 | for key, value in kwargs.items(): 64 | setattr(self, key, value) 65 | def copy(self, x, y): 66 | return Shape( 67 | self.sides, x, y, self.rotation, 68 | fill=self.fill, stroke=self.stroke 69 | ) 70 | def points(self, margin=0): 71 | angle = 2 * pi / self.sides 72 | rotation = self.rotation - pi / 2 73 | if self.sides % 2 == 0: 74 | rotation += angle / 2 75 | angles = [angle * i + rotation for i in range(self.sides)] 76 | angles.append(angles[0]) 77 | d = 0.5 / sin(angle / 2) - margin / cos(angle / 2) 78 | return [(self.x + cos(a) * d, self.y + sin(a) * d) for a in angles] 79 | def adjacent(self, sides, edge, **kwargs): 80 | (x1, y1), (x2, y2) = self.points()[edge:edge + 2] 81 | angle = 2 * pi / sides 82 | a = atan2(y2 - y1, x2 - x1) 83 | b = a - pi / 2 84 | d = 0.5 / tan(angle / 2) 85 | x = x1 + (x2 - x1) / 2.0 + cos(b) * d 86 | y = y1 + (y2 - y1) / 2.0 + sin(b) * d 87 | a += angle * ((sides - 1) / 2) 88 | return Shape(sides, x, y, a, **kwargs) 89 | def render(self, dc, margin): 90 | points = self.points(margin) 91 | dc.move_to(*points[0]) 92 | for point in points[1:]: 93 | dc.line_to(*point) 94 | dc.set_source_rgb(*color(self.fill)) 95 | dc.fill_preserve() 96 | dc.set_source_rgb(*color(self.stroke)) 97 | dc.stroke() 98 | def render_edge_labels(self, dc, margin): 99 | points = self.points(margin) 100 | for edge in range(self.sides): 101 | (x1, y1), (x2, y2) = points[edge:edge + 2] 102 | text = str(edge) 103 | tw, th = dc.text_extents(text)[2:4] 104 | x = x1 + (x2 - x1) / 2.0 - tw / 2.0 105 | y = y1 + (y2 - y1) / 2.0 + th / 2.0 106 | dc.set_source_rgb(1, 1, 1) 107 | dc.move_to(x, y) 108 | dc.show_text(text) 109 | def render_label(self, dc, text): 110 | text = str(text) 111 | tw, th = dc.text_extents(text)[2:4] 112 | x = self.x - tw / 2.0 113 | y = self.y + th / 2.0 114 | dc.set_source_rgb(1, 1, 1) 115 | dc.move_to(x, y) 116 | dc.show_text(text) 117 | 118 | class DualShape(Shape): 119 | def __init__(self, points): 120 | super(DualShape, self).__init__(len(points) - 1) 121 | self.data = points 122 | def points(self, margin=0): 123 | if margin == 0: 124 | return self.data 125 | else: 126 | return inset_polygon(self.data, margin) 127 | 128 | class Model(object): 129 | def __init__(self, width=WIDTH, height=HEIGHT, scale=SCALE): 130 | self.width = width 131 | self.height = height 132 | self.scale = scale 133 | self.shapes = [] 134 | self.lookup = {} 135 | def append(self, shape): 136 | self.shapes.append(shape) 137 | key = normalize(shape.x, shape.y) 138 | self.lookup[key] = shape 139 | def _add(self, index, edge, sides, **kwargs): 140 | parent = self.shapes[index] 141 | shape = parent.adjacent(sides, edge, **kwargs) 142 | self.append(shape) 143 | def add(self, indexes, edges, sides, **kwargs): 144 | if isinstance(indexes, int): 145 | indexes = [indexes] 146 | if isinstance(edges, int): 147 | edges = [edges] 148 | start = len(self.shapes) 149 | for index in indexes: 150 | for edge in edges: 151 | self._add(index, edge, sides, **kwargs) 152 | end = len(self.shapes) 153 | return range(start, end) 154 | def add_repeats(self, x, y): 155 | for shape in self.shapes: 156 | key = normalize(x + shape.x, y + shape.y) 157 | if key in self.lookup: 158 | continue 159 | self.lookup[key] = shape.copy(x + shape.x, y + shape.y) 160 | def _repeat(self, indexes, x, y, depth, memo): 161 | if depth < 0: 162 | return 163 | key = normalize(x, y) 164 | previous_depth = memo.get(key, -1) 165 | if previous_depth >= depth: 166 | return 167 | memo[key] = depth 168 | if previous_depth == -1: 169 | self.add_repeats(x, y) 170 | for index in indexes: 171 | shape = self.shapes[index] 172 | self._repeat( 173 | indexes, x + shape.x, y + shape.y, depth - 1, memo) 174 | def repeat(self, indexes): 175 | memo = {} 176 | depth = 0 177 | while True: 178 | self._repeat(indexes, 0, 0, depth, memo) 179 | w = self.width / 2.0 / self.scale 180 | h = self.height / 2.0 / self.scale 181 | tl = any(x < -w and y < -h for x, y in memo) 182 | tr = any(x > w and y < -h for x, y in memo) 183 | bl = any(x < -w and y > h for x, y in memo) 184 | br = any(x > w and y > h for x, y in memo) 185 | if tl and tr and bl and br: 186 | break 187 | depth += 1 188 | def dual(self): 189 | vertexes = {} 190 | for shape in self.lookup.values(): 191 | for (x, y) in shape.points()[:-1]: 192 | key = normalize(x, y) 193 | vertexes.setdefault(key, []).append(shape) 194 | result = [] 195 | for (x, y), shapes in vertexes.items(): 196 | if len(shapes) < 3: 197 | continue 198 | def angle(shape): 199 | return atan2(shape.y - y, shape.x - x) 200 | shapes.sort(key=angle, reverse=True) 201 | points = [(shape.x, shape.y) for shape in shapes] 202 | points.append(points[0]) 203 | result.append(DualShape(points)) 204 | return result 205 | def render( 206 | self, dual=False, background_color=BACKGROUND_COLOR, margin=MARGIN, 207 | show_labels=SHOW_LABELS, line_width=LINE_WIDTH): 208 | surface = cairo.ImageSurface( 209 | cairo.FORMAT_RGB24, self.width, self.height) 210 | dc = cairo.Context(surface) 211 | dc.set_line_cap(cairo.LINE_CAP_ROUND) 212 | dc.set_line_join(cairo.LINE_JOIN_ROUND) 213 | dc.set_line_width(line_width) 214 | dc.set_font_size(18.0 / self.scale) 215 | dc.translate(self.width / 2, self.height / 2) 216 | dc.scale(self.scale, self.scale) 217 | dc.set_source_rgb(*color(background_color)) 218 | dc.paint() 219 | shapes = self.dual() if dual else self.lookup.values() 220 | if show_labels: 221 | for shape in shapes: 222 | shape.render_edge_labels(dc, margin - 0.25) 223 | for shape in shapes: 224 | shape.render(dc, margin) 225 | if show_labels: 226 | for index, shape in enumerate(self.shapes): 227 | if shape in shapes: 228 | shape.render_label(dc, index) 229 | return surface 230 | --------------------------------------------------------------------------------