"""2025-11-15
Distributing circles
Exercício de distribuição de círculos dentro de um círculo maior, em tons pastéis
ericof.com
png
Sketch,py5,CreativeCoding
"""
from collections import deque
from sketches.utils.draw import canvas
from sketches.utils.draw.cores.paletas import gera_paleta
from sketches.utils.helpers import sketches as helpers
import numpy as np
import py5
sketch = helpers.info_for_sketch(__file__, __doc__)
class Circle:
"""A little class representing an SVG circle."""
def __init__(self, cx, cy, r, color=None):
"""Initialize the circle with its centre, (cx,cy) and radius, r.
icolor is the index of the circle's color.
"""
self.cx, self.cy, self.r = cx, cy, r
self.color = color
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
class Circles:
"""A class for drawing circles-inside-a-circle."""
def __init__(
self,
colors: deque[py5.Py5Color],
width=800,
height=800,
R=400,
n=800,
rho_min=0.005,
rho_max=0.05,
guard=1000,
):
"""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.
colors is a list of SVG fill color 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.guard = guard
self.colors = colors
self.img = np.ones((height, width))
def _circle_fits(self, icx, icy, r):
"""If I fits, I sits."""
if icx - r < 0 or icy - r < 0:
return False
if icx + r >= self.width or icy + r >= self.height:
return False
return all((
self.img[icx - r, icy],
self.img[icx + r, icy],
self.img[icx, icy - r],
self.img[icx, icy + r],
))
def apply_circle_mask(self, icx, icy, r):
"""Zero all elements of self.img in circle at (icx, icy), radius r."""
x, y = np.ogrid[0 : self.width, 0 : self.height]
r2 = (r + 1) ** 2
mask = (x - icx) ** 2 + (y - icy) ** 2 <= r2
self.img[mask] = 0
def _place_circle(self, r, c_idx=None):
"""Attempt to place a circle of radius r within the image figure.
c_idx is a list of indexes into the self.colors list, from which
the circle's colour will be chosen. If None, use all colors.
"""
if not c_idx:
c_idx = range(len(self.colors))
# Get the coordinates of all non-zero image pixels
img_coords = np.nonzero(self.img)
if not img_coords:
return False
# The guard number: if we don't place a circle within this number
# of trials, we give up.
guard = self.guard
# For this method, r must be an integer. Ensure that it's at least 1.
r = max(1, int(r))
while guard:
# Pick a random candidate pixel...
i = np.random.randint(len(img_coords[0]))
icx, icy = img_coords[0][i], img_coords[1][i]
# ... and see if the circle fits there
if self._circle_fits(icx, icy, r):
self.apply_circle_mask(icx, icy, r)
circle = Circle(icx, icy, r, color=self.colors[np.random.choice(c_idx)])
self.circles.append(circle)
return True
guard -= 1
return False
def __call__(self) -> list[Circle]:
"""Place the little circles inside the big one."""
# First choose a set of n random radii and sort them. We use
# random.random() * random.random() to favour small circles.
self.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.
for i in range(self.n):
self._place_circle(r[i])
return self.circles
def setup():
py5.size(*helpers.DIMENSOES.external, py5.P3D)
cor_fundo = py5.color("#000")
py5.background(cor_fundo)
paleta = gera_paleta("pastel", True)
with py5.push():
py5.translate(*helpers.DIMENSOES.pos_interno)
with py5.push():
py5.fill("#FFF")
py5.stroke("#FFF")
py5.rect(0, 0, *helpers.DIMENSOES.internal)
circles = Circles(
colors=paleta,
width=800,
height=800,
R=300,
n=6000,
guard=800,
rho_min=0.001,
rho_max=0.09,
)
for circle in circles():
x = circle.cx
y = circle.cy
r = circle.r
color = circle.color
with py5.push():
py5.stroke("#000")
py5.stroke_weight(1)
py5.fill(color)
py5.circle(x, y, r)
# Credits and go
canvas.sketch_frame(
sketch, cor_fundo, "large_transparent_white", "transparent_white"
)
def key_pressed():
key = py5.key
if key == " ":
save_and_close()
def save_and_close():
py5.no_loop()
canvas.save_sketch_image(sketch)
py5.exit_sketch()
if __name__ == "__main__":
py5.run_sketch()