![如何在 tikz 中用随机圆圈填充随机空间?](https://linux22.com/image/296444/%E5%A6%82%E4%BD%95%E5%9C%A8%20tikz%20%E4%B8%AD%E7%94%A8%E9%9A%8F%E6%9C%BA%E5%9C%86%E5%9C%88%E5%A1%AB%E5%85%85%E9%9A%8F%E6%9C%BA%E7%A9%BA%E9%97%B4%EF%BC%9F.png)
有人知道如何在 tikz 中有效地绘制以下任一幅图片吗?
我特别感兴趣的是“等级良好”,因为人们必须用不同的随机小圆圈(多种尺寸)来填充圆圈之间的随机空间。
\documentclass{standalone}
% Possibly some code here
\begin{document}
% Possibly some code here
\begin{tikzpicture}
% Definitely some code here
\end{tikzpicture}
\end{document}
谢谢您的帮助和关注。
答案1
开场白
在D3js图书馆,参见例如mbostock 的积木或者这个画廊。这非常鼓舞人心。这些示例非常快,如果需要,我们可以在 JavaScript 级别获取 SVG 文件(轻松转换为 PDF、TikZ 等)。
让我重点介绍一下这些类型的图表。我相信这些示例属于具有/不具有各个圆圈/物体的保留距离的碰撞检测领域。
在以下实验中,我将测试螺旋线在创建词云中的应用,请参阅 Jason Davies 的网页。为了绘制/模拟螺旋线,我们需要一个固定点、一个角度和一个与固定点之间的距离。接下来我们需要一个已定义圆的数据库。我使用的是 Lua 及其数据表。
我将示例分成一系列单独的文件,以便于操作和实验。每个 Lua 文件都会生成自己的 TeX 文件,最后会加载到主 TeX 文件中。
等级较差的类型,第一个变体
我将角度和距离转换为一个点。在该点附近,我随机选择另一个点作为新圆的中心点候选。我们测试是否与已创建的圆没有碰撞,如果没有(变量发生变化yes
),我们将该候选添加到 Lua 表中。达到要求的圆数(steps
)后,我们停止算法,代码片段生成 TeX 文件。
-- Poorly Graded type, mal-circles-a.lua
io.write("Creating a poorly graded type... ")
steps=70 -- number of circles
angles=25 -- angle of the spiral
distances=.01 -- distance from the center of the spiral
circles={ } -- Lua table with (x,y),r of the circles
r=1.8 -- diameter of the circle
dx=2.5 -- a window for random number generation (x axis)
dy=2.5 -- the same for the y axis
-- a distance between two points (circle centers)
function dist(xz,yz,nx,ny)
return math.sqrt( (nx-xz)^2 + (ny-yz)^2 )
end
for step=1,steps do -- how many circles do we want
angle=0 -- a starting angle for each new spiral
distance=0 -- a starting distance for each new spiral
while true do -- do spiral as long a circle cannot be inserted
angle=angle+angles -- increase an angle
distance=distance+distances -- insce a distance
y=distance*math.sin(math.rad(angle)) -- convert an angle to a point
x=distance*math.cos(math.rad(angle)) -- the same for the y axis
newx=math.random()*dx-dx/2+x -- find a new possible location nearby spiral
newy=math.random()*dy-dy/2+y -- the same for the y axis
yes=1
for l=1,#circles do -- test if there is no intersection among circles
d=dist(circles[l][1],circles[l][2],newx,newy)
m=circles[l][3]/2+.05*circles[l][4]+r/2 -- there is a reserse
if d<m then yes=nil; break end -- circles intersect! skip this try
end -- for, l
if yes then table.insert(circles, {newx,newy,r}); break end -- circles don't intersect, use that new one
end -- while, spin spiral as long as it is necessary
end -- no. of circles we need
-- generate a TeX file for later loading
writeme=io.open("mal-a.tex","w")
for circle=1,#circles do
writeme:write("\\node[c, minimum width="..circles[circle][5].."cm] at ("..circles[circle][6]..", "..circles[circle][7]..") {};\n")
end
writeme:close()
print("I'm done!")
等级较差的类型,第二种变体
我喜欢另一种方法,即用接触圆和重力效果(尽可能靠近中心点)。我尝试了相当耗时的算法,但它确实有效。该算法尝试在现有圆周围绘制许多圆,并只选取最接近图形中心点(零,零)且与其他圆没有交点的圆。(也许我可以在按距离排序后选取更多点?但它们自己相交了……)
如果我们需要精确接触圆圈,则需要对线宽/2 进行校正。有一种更快的方法,我们可以在 TikZ 级别设置draw=none, fill=black
。
-- Touching circles type, mal-circles-a-alt.lua
io.write("Creating a touching circles type... ")
steps=150
d0=1.0
steps=steps-1
coef=0.3
circles={ {0,0,d0+coef*math.random()} }
--print("")
function dist(xz,yz,nx,ny)
return math.sqrt( (nx-xz)^2 + (ny-yz)^2 )
end
for step=1,steps do -- number of touching circles to be generated
--print(step)
r=d0+coef*math.random() -- to be randomized;
tosaver=r;
dmin=nil
for l=1,#circles do
m=circles[l][9]
distance=(r+m)/2
--print(l,distance)
for angle=0,360,5 do
y=distance*math.sin(math.rad(angle))
x=distance*math.cos(math.rad(angle))
newx=circles[l][10]+x
newy=circles[l][11]+y
crossing=nil
for k=1,#circles do
mnew=circles[k][12]
distancenew=(r+mnew)/2
d=dist(circles[k][13],circles[k][14],newx,newy)
--if step==steps then print(l,k,d,distance,newx,newy) end
if d<distancenew then crossing=true --do nothing; circles intersect
end
end -- k
if not crossing then
value=dist(newx,newy,0,0)
if not dmin or dmin>value then tosavex=newx; tosavey=newy;
--print(step,l,angle,dmin,value); --newx,newy,
dmin=value end
end
--m=circles[l][15]/2+.25*circles[l][16]+r/2
-- if step>21 then m=circles[l][17]/2+r/2 end
--if d<m then yes=nil; break end
end -- for, angle
--if yes then table.insert(circles, {x,y,r}); break end
end -- for, l
--print(tosavex,tosavey,r)
if dmin then table.insert(circles, {tosavex, tosavey, tosaver}) end
--[[for circle=1,#circles do
print(circles[circle][18], circles[circle][19], circles[circle][20])
end]]
end -- for, steps, no . of circles
-- Printing results to the TeX document...
writeme=io.open("mal-a-alt.tex","w")
for circle=1,#circles do
towrite="\\node[c, minimum width="..circles[circle][21].."cm] at ("..circles[circle][22]..", "..circles[circle][23]..") {};\n" -- "..circle.."
--print(towrite)
writeme:write(towrite)
end
writeme:close()
print("I'm done!")
间隙分级类型
一旦我们走到这一步,我们就可以在算法运行过程中开始更改参数。在下面的例子中,我更改了剩余圆的直径。一个小技巧是,代码片段为大圆设置了一个保留距离(直径的 25%)(m
变量),而小圆的保留距离设置为零。
-- Gap Graded type, mal-circles-b.lua
io.write("Creating a gap graded type... ")
steps=600
bigones=20
angles=25
distances=.02
circles={ }
rfix=1.8
r=rfix
dx=2.5
dy=2.5
function dist(xz,yz,nx,ny)
return math.sqrt( (nx-xz)^2 + (ny-yz)^2 )
end
for step=1,steps do
--print(step,angle,distance)
--tex.print("\\node at ("..angle..":"..distance..") {a};")
--print(x,y)
--tex.print("\\node at ("..x..","..y..") {b};")
angle=0
distance=0
if step>bigones then r=.3 end -- .4*math.random()*rfix+
--if step>100 then r=.2*math.random()*rfix+.2 end
while true do
angle=angle+angles
distance=distance+distances
y=distance*math.sin(math.rad(angle))
x=distance*math.cos(math.rad(angle))
newx=math.random()*dx-dx/2+x
newy=math.random()*dy-dy/2+y
yes=1
for l=1,#circles do
d=dist(circles[l][25],circles[l][26],newx,newy)
m=circles[l][27]/2+.25*circles[l][28]+r/2
if step>bigones then m=circles[l][29]/2+r/2 end
if d<m then yes=nil; break end
end -- for, l
if yes then table.insert(circles, {newx,newy,r}); break end
end -- while
end -- No. of circles
writeme=io.open("mal-b.tex","w")
for circle=1,#circles do
writeme:write("\\node[c, minimum width="..circles[circle][30].."cm] at ("..circles[circle][31]..", "..circles[circle][32]..") {};\n")
end
writeme:close()
print("I'm done!")
等级良好的类型
一旦我们知道了这些技巧,我们就可以更改其他参数。在下一个示例中,我随机更改圆圈的大小。我设置了三个级别,最大级别(圆圈 1-20),中等级别(圆圈编号 21-100)和小级别(100+)。第二组和第三组可以有共同的大小。原因是我在处理时没有设置最小数量math.random()
。我们无法识别它,直到我们使用一些样式,每个样式针对单独的组。
-- Well Graded type, mal-circles-c.lua
io.write("Creating a well graded type... ")
steps=555
angles=25
distances=.01
circles={ }
rfix=1.8
r=rfix
dx=2.5
dy=2.5
function dist(xz,yz,nx,ny)
return math.sqrt( (nx-xz)^2 + (ny-yz)^2 )
end
for step=1,steps do
--print(step,angle,distance)
--tex.print("\\node at ("..angle..":"..distance..") {a};")
--print(x,y)
--tex.print("\\node at ("..x..","..y..") {b};")
angle=0
distance=0
if step>20 then r=.4*math.random()*rfix+.2 end
if step>100 then r=.2*math.random()*rfix+.2 end
while true do
angle=angle+angles
distance=distance+distances
y=distance*math.sin(math.rad(angle))
x=distance*math.cos(math.rad(angle))
newx=math.random()*dx-dx/2+x
newy=math.random()*dy-dy/2+y
yes=1
for l=1,#circles do
d=dist(circles[l][34],circles[l][35],newx,newy)
m=circles[l][36]/2+r/2 -- +.25*circles[l][37]
-- if step>21 then m=circles[l][38]/2+r/2 end
if d<m then yes=nil; break end
end -- for, l
if yes then table.insert(circles, {newx,newy,r}); break end
end -- while
end -- No. of circles
writeme=io.open("mal-c.tex","w")
for circle=1,#circles do
writeme:write("\\node[c, minimum width="..circles[circle][39].."cm] at ("..circles[circle][40]..", "..circles[circle][41]..") {};\n")
end
writeme:close()
print("I'm done!")
岛屿类型,无颜色的变体
当我们开始改变距离储备时,不同类型的尺寸,我们可以得到不同的图片。我把这个称为岛屿类型。每个圆圈都有自己的距离储备,尺寸在预定义的组中随机变化,如上例所示。这就是我们得到的。
-- Island type without colors, mal-circles-d.lua
io.write("Creating an island type... ")
steps=555
angles=25
distances=.01
circles={ }
rfix=1.8
r=rfix
dx=2.5
dy=2.5
function dist(xz,yz,nx,ny)
return math.sqrt( (nx-xz)^2 + (ny-yz)^2 )
end
for step=1,steps do
--print(step,angle,distance)
--tex.print("\\node at ("..angle..":"..distance..") {a};")
--print(x,y)
--tex.print("\\node at ("..x..","..y..") {b};")
angle=0
distance=0
if step>20 then r=.4*math.random()*rfix+.2 end
if step>100 then r=.2*math.random()*rfix+.2 end
while true do
angle=angle+angles
distance=distance+distances
y=distance*math.sin(math.rad(angle))
x=distance*math.cos(math.rad(angle))
newx=math.random()*dx-dx/2+x
newy=math.random()*dy-dy/2+y
yes=1
for l=1,#circles do
d=dist(circles[l][43],circles[l][44],newx,newy)
m=circles[l][45]/2+.25*circles[l][46]+r/2
-- if step>21 then m=circles[l][47]/2+r/2 end
if d<m then yes=nil; break end
end -- for, l
if yes then table.insert(circles, {newx,newy,r}); break end
end -- while
end -- No. of circles
writeme=io.open("mal-d.tex","w")
for circle=1,#circles do
writeme:write("\\node[c, minimum width="..circles[circle][48].."cm] at ("..circles[circle][49]..", "..circles[circle][50]..") {};\n")
end
writeme:close()
print("I'm done!")
岛屿类型,具有颜色的变体
我们可以改变不同的东西,在最后一个例子中,我改变了一种样式。它的圆大小不同(直径小于 0.45,大于 0.45,小于 1 和大于 1)。作为证明,我在 TeX/TikZ 级别准备了几种样式。
-- Island type with colors, mal-circles-d-alt.lua
io.write("Creating an island type in color... ")
steps=555
angles=25
distances=.01
circles={ }
rfix=1.8
r=rfix
dx=2.5
dy=2.5
function dist(xz,yz,nx,ny)
return math.sqrt( (nx-xz)^2 + (ny-yz)^2 )
end
for step=1,steps do
--print(step,angle,distance)
--tex.print("\\node at ("..angle..":"..distance..") {a};")
--print(x,y)
--tex.print("\\node at ("..x..","..y..") {b};")
angle=0
distance=0
malstyle="style1"
if step>20 then r=.4*math.random()*rfix+.2 end
if step>100 then r=.2*math.random()*rfix+.2 end
if r<1 then malstyle="style2" end
if r<0.45 then malstyle="style3" end
while true do
angle=angle+angles
distance=distance+distances
y=distance*math.sin(math.rad(angle))
x=distance*math.cos(math.rad(angle))
newx=math.random()*dx-dx/2+x
newy=math.random()*dy-dy/2+y
yes=1
for l=1,#circles do
d=dist(circles[l][52],circles[l][53],newx,newy)
m=circles[l][54]/2+.25*circles[l][55]+r/2
-- if step>21 then m=circles[l][56]/2+r/2 end
if d<m then yes=nil; break end
end -- for, l
if yes then table.insert(circles, {newx,newy,r,malstyle}); break end
end -- while
end -- No. of circles
writeme=io.open("mal-d-alt.tex","w")
for circle=1,#circles do
writeme:write("\\node[c, minimum width="..circles[circle][57].."cm, "..circles[circle][58].."] at ("..circles[circle][59]..", "..circles[circle][60]..") {};\n")
end
writeme:close()
print("I'm done!")
结束语:使用 Lua 代码片段
将代码片段保存到单独的 Lua 文件后,我们在 TeX 引擎之外运行 Lua,如下所示(第二个代码片段相当慢):
texlua mal-circles-a.lua
texlua mal-circles-a-alt.lua
texlua mal-circles-b.lua
texlua mal-circles-c.lua
texlua mal-circles-d.lua
texlua mal-circles-d-alt.lua
如果一切正常,我们会在终端中发现几条消息:
Creating a poorly graded type... I'm done!
Creating a touching circles type... I'm done!
Creating a gap graded type... I'm done!
Creating a well graded type... I'm done!
Creating an island type... I'm done!
Creating an island type in color... I'm done!
这些代码片段生成一系列 TeX 文件,我们通过主 TeX 文件加载这些文件。我们运行任何 LaTeX 引擎,例如lualatex mal-circles.tex
。
% *latex mal-circles.tex
\documentclass[a4paper]{article}
\pagestyle{empty}
\usepackage{tikz}
% small improvements to the mirror
\addtolength{\hoffset}{-1in}
\paperwidth=1.4\paperwidth
\pdfpagewidth=\paperwidth
\textwidth=2\textwidth
\begin{document}
% preparing styles for nodes
\tikzset{inner sep=0pt, outer sep=0pt,
every node/.style={circle, draw=black, thick},
c/.style={fill=none},
style1/.style={fill=green, line width=1pt},
style2/.style={fill=red},
style3/.style={fill=yellow},
}
% definition for loading generated TeX files
\def\malinput#1 {%
\newpage % one example per page
\begin{tikzpicture}
%\draw (-5,-2) grid (5,2); % fast grid drawing, if needed
\input #1.tex % loading a generated file
\end{tikzpicture}
}
\malinput mal-a
\malinput mal-a-alt
\malinput mal-b
\malinput mal-c
\malinput mal-d
\malinput mal-d-alt
\end{document}