-
Notifications
You must be signed in to change notification settings - Fork 6
/
circles.py
158 lines (122 loc) · 5.72 KB
/
circles.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
import numpy as np
class Circle:
"""A little class representing an SVG circle."""
def __init__(self, cx, cy, r, icolour=None):
"""Initialize the circle with its centre, (cx,cy) and radius, r.
icolour is the index of the circle's colour.
"""
self.cx, self.cy, self.r = cx, cy, r
self.icolour = icolour
def overlap_with(self, cx, cy, r):
"""Does the circle overlap with another of radius r at (cx, cy)?"""
d = np.hypot(cx-self.cx, cy-self.cy)
return d < r + self.r
def draw_circle(self, fo):
"""Write the circle's SVG to the output stream, fo."""
print('<circle cx="{}" cy="{}" r="{}" class="c{}"/>'
.format(self.cx, self.cy, self.r, self.icolour), file=fo)
class Circles:
"""A class for drawing circles-inside-a-circle."""
def __init__(self, width=600, height=600, R=250, n=800, rho_min=0.005,
rho_max=0.05, colours=None):
"""Initialize the Circles object.
width, height are the SVG canvas dimensions
R is the radius of the large circle within which the small circles are
to fit.
n is the maximum number of circles to pack inside the large circle.
rho_min is rmin/R, giving the minimum packing circle radius.
rho_max is rmax/R, giving the maximum packing circle radius.
colours is a list of SVG fill colour specifiers to be referenced by
the class identifiers c<i>. If None, a default palette is set.
"""
self.width, self.height = width, height
self.R, self.n = R, n
# The centre of the canvas
self.CX, self.CY = self.width // 2, self.height // 2
self.rmin, self.rmax = R * rho_min, R * rho_max
self.colours = colours or ['#993300', '#a5c916', '#00AA66', '#FF9900']
self.circles = []
# The "guard number": we try to place any given circle this number of
# times before giving up.
self.guard = 500
def preamble(self):
"""The usual SVG preamble, including the image size."""
print('<?xml version="1.0" encoding="utf-8"?>\n'
'<svg xmlns="http://www.w3.org/2000/svg"\n' + ' '*5 +
'xmlns:xlink="http://www.w3.org/1999/xlink" width="{}" height="{}" >'
.format(self.width, self.height), file=self.fo)
def defs_decorator(func):
"""For convenience, wrap the CSS styles with the needed SVG tags."""
def wrapper(self):
print("""
<defs>
<style type="text/css"><![CDATA[""", file=self.fo)
func(self)
print("""]]></style>
</defs>""", file=self.fo)
return wrapper
@defs_decorator
def svg_styles(self):
"""Set the SVG styles: circles are coloured with no border."""
print('circle {stroke: none;}', file=self.fo)
for i, c in enumerate(self.colours):
print('.c{} {{fill: {};}}'.format(i, c), file=self.fo)
def make_svg(self, filename, *args, **kwargs):
"""Create the image as an SVG file with name filename."""
ncolours = len(self.colours)
with open(filename, 'w') as self.fo:
self.preamble()
self.svg_styles()
for circle in self.circles:
circle.draw_circle(self.fo)
print('</svg>', file=self.fo)
def _place_circle(self, r, c_idx=None):
"""Attempt to place a circle of radius r within the larger circle.
c_idx is a list of indexes into the self.colours list, from which
the circle's colour will be chosen. If None, use all colours.
"""
if not c_idx:
c_idx = range(len(self.colours))
# The guard number: if we don't place a circle within this number
# of trials, we give up.
guard = self.guard
while guard:
# Pick a random position, uniformly on the larger circle's interior
cr, cphi = ( self.R * np.sqrt(np.random.random()),
2*np.pi * np.random.random() )
cx, cy = cr * np.cos(cphi), cr * np.sin(cphi)
if cr+r < self.R:
# The circle fits inside the larger circle.
if not any(circle.overlap_with(self.CX+cx, self.CY+cy, r)
for circle in self.circles):
# The circle doesn't overlap any other circle: place it.
circle = Circle(cx+self.CX, cy+self.CY, r,
icolour=np.random.choice(c_idx))
self.circles.append(circle)
return True
guard -= 1
# Warn that we reached the guard number of attempts and gave up for
# for this circle.
print('guard reached.')
return False
def make_circles(self, c_idx=None):
"""Place the little circles inside the big one.
c_idx is a list of colour indexes (into the self.colours list) from
which to select random colours for the circles. If None, use all
the colours in self.colours.
"""
# First choose a set of n random radii and sort them. We use
# random.random() * random.random() to favour small circles.
r = self.rmin + (self.rmax - self.rmin) * np.random.random(
self.n) * np.random.random(self.n)
r[::-1].sort()
# Do our best to place the circles, larger ones first.
nplaced = 0
for i in range(self.n):
if self._place_circle(r[i], c_idx):
nplaced += 1
print('{}/{} circles placed successfully.'.format(nplaced, self.n))
if __name__ == '__main__':
circles = Circles(n=2000)
circles.make_circles()
circles.make_svg('circles.svg')