tikz: Distribute evenly and randomly circles
Here's a macro that implements what you described. It takes four arguments: The width and height of the rectangle to be filled, the radius of the circles, and the number of attempts.
For each attempt, a random position is generated. The distances between this position and all existing circles are calculated, and if there's a collision, no circle is drawn. If the circle does not collide, it is drawn, and its coordinates are added to the list of existing circles.
This approach is far from efficient, but it works:
\fillrandomly{3}{2}{0.2}{50}
\pgfmathsetseed{2}
\fillrandomly{5}{5}{0.5}{300}
\documentclass{article}
\usepackage{tikz}
\begin{document}
\def\xlist{4}
\def\ylist{4}
\newcommand{\fillrandomly}[4]{
\pgfmathsetmacro\diameter{#3*2}
\draw (0,0) rectangle (#1,#2);
\foreach \i in {1,...,#4}{
\pgfmathsetmacro\x{rnd*#1}
\pgfmathsetmacro\y{rnd*#2}
\xdef\collision{0}
\foreach \element [count=\i] in \xlist{
\pgfmathtruncatemacro\j{\i-1}
\pgfmathsetmacro\checkdistance{ sqrt( ({\xlist}[\j]-(\x))^2 + ({\ylist}[\j]-(\y))^2 ) }
\ifdim\checkdistance pt<\diameter pt
\xdef\collision{1}
\breakforeach
\fi
}
\ifnum\collision=0
\xdef\xlist{\xlist,\x}
\xdef\ylist{\ylist,\y}
\draw [red, thick] (\x,\y) circle [radius=#3];
\fi
}
}
\begin{tikzpicture}
\pgfmathsetseed{2}
\fillrandomly{5}{5}{0.5}{300}
\end{tikzpicture}
\end{document}
Here's a different variation. The idea is to generate a bunch of circles, and then to cull those that intersect. For the culling phase, I used a halfway implementation of Sweep & Prune which speeds things up a bit as far as collision detection goes for large n
.
- Generate points randomly
- Sort their
x-coordinates
- Save pairs whose
x-coordinates
overlap for further testing, discard the rest - For saved pairs, determine whether
y-coordinates
overlap as well - If so, then they potentially collide, so do the actual collision test on them
- If they collide, then discard one of the pair
The macro is \circles{n}{rad}{width}{height}
and \circles{500}{.1}{10}{5}
gives:
\documentclass{article}
\usepackage{luacode}
\usepackage{tikz}
\usetikzlibrary{backgrounds}
\begin{luacode*}
local rand = math.random
local abs = math.abs
local pts = {}
local tstpairs = {}
-- generate the points
local function genpts(n,x,y)
for i = 1,n do
pts[i]={}
pts[i][1] = rand()*x
pts[i][2] = rand()*y
end
end
-- for sorted pairs, check if x-coords overlap
-- if so, store the pair in table tstpairs
local function getpairs(t,r)
for i = 1,#t do
tstpairs[i] = {}
for j = 1,#t-i do
if t[i+j][1]-t[i][1]<2*r then
tstpairs[i][#tstpairs[i]+1]=i+j
else
break
end
end
end
end
-- this is the actual collision test
-- it's less expensive to use x^2+y^2<d^2 than sqrt(x^2+y^2)<d
local function testcol(a,b,r)
local x = pts[b][1]-pts[a][1]
local y = pts[b][2]-pts[a][2]
if x*x+y*y<4*r*r then
return true
end
end
-- this is a bit of a mess, deleting pairs on the fly was causing some
-- problems, so I had to include some checks "if pts[k1]..." etc.
local function delpairs(r)
for k1,v1 in pairs(tstpairs) do
if pts[k1] then
for k2,v2 in pairs(v1) do
if pts[v2] then
if abs(pts[v2][2]-pts[k1][2])<2*r then
if testcol(k1,v2,r) then
pts[v2]=nil
end
end
end
end
end
end
end
-- quickSort helper
local function partition(array, p, r)
local x = array[r][1]
local i = p - 1
for j = p, r - 1 do
if array[j][1] <= x then
i = i + 1
local temp = array[i][1]
array[i][1] = array[j][1]
array[j][1] = temp
end
end
local temp = array[i + 1][1]
array[i + 1][1] = array[r][1]
array[r][1] = temp
return i + 1
end
-- quickSort to sort by x
-- taken from https://github.com/akosma/CodeaSort/blob/master/QuickSort.lua
local function quickSort(array, p, r)
p = p or 1
r = r or #array
if p < r then
q = partition(array, p, r)
quickSort(array, p, q - 1)
quickSort(array, q + 1, r)
end
end
-- draw output
local function showout(n,r,x,y)
tex.print("\\begin{tikzpicture}[radius="..r.."cm]")
tex.print("\\draw[red](0,0) rectangle ("..x..","..y..");")
for k,v in pairs(pts) do
tex.print("\\draw ("..pts[k][1]..","..pts[k][2]..") circle ;")
end
tex.print("\\end{tikzpicture}")
end
-- wrapper
function circles(n,r,x,y)
genpts(n,x,y)
quickSort(pts)
getpairs(pts,r)
delpairs(r)
showout(n,r,x,y)
end
\end{luacode*}
\def\circles#1#2#3#4{\directlua{circles(#1,#2,#3,#4)}}
\begin{document}
\circles{500}{.1}{10}{5}
\end{document}
Just discovered this old question, and for completeness I decided to add the following answer.
Poisson disc sampling seems to be the right way to create this kind of patterns. This question/answer provides an implementation of this algorithm in pure lualatex, allowing its use from pgf/tikz.
Provided that you have poisson.sty
and poisson.lua
(which you can get from the mentioned answer), and a working install of lualatex
, the solution will be:
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{backgrounds}
\usepackage{poisson}
\begin{document}
\edef\mylist{\poissonpointslist{5}{5}{1}{20}}
% 5 x 5 is the size of the area to fill
% 1 is the minimum distance between centers
\begin{tikzpicture}[framed,gridded,radius=0.5cm]
\draw[red](0,0) rectangle (5,5);
\foreach \x/\y in \mylist \draw (\x,\y) circle;
\end{tikzpicture}
\end{document}
And the result (compiled with lualatex
) is: