我正在尝试生成一个包含随机分布和随机旋转的文本的页面,并且代码这里做得很好。
但即使在空间充足的情况下(我可以看到很多空白),它也会导致文本重叠(我使用的是天城文字母 --- 有超过 48 个)。
避免重叠的一种方法是继续生成页面,希望随机性对我们有利并生成没有重叠的页面。
但这要求的随机性太高了。我尝试了近百次才放弃。:)
如何获得随机分布、随机旋转且不重叠?
答案1
这个答案几乎完全取自这个很好的答案. 它需要 lualatex。
\documentclass[tikz,border=3.14mm]{standalone}
\usepackage{filecontents}
% poisson.sty and poisson.lua are from https://tex.stackexchange.com/a/185423/121799
\begin{filecontents*}{poisson.sty}
\directlua{dofile("poisson.lua")}
\newcommand{\poissonpointslist}[4]{
\directlua{poisson_points_list(#1,#2,#3,#4)}
}
\newcommand{\poissonpointslistordered}[4]{
\directlua{poisson_points_list_ordered(#1,#2,#3,#4)}
}
\end{filecontents*}
\begin{filecontents*}{poisson.lua}
-- This is a lua implementation of the algorithm described in
-- http://devmag.org.za/2009/05/03/poisson-disk-sampling/
--
-- The structure of the algorithm is exactly the same than in
-- the mentioned article. Its pseudo-code snippets were translated
-- to Lua.
--
-- One detail worths an explanation, though. The article uses a 2D matrix
-- called grid to store coordinates of points. In the article, it is
-- assumed that grid elements can be accesed via grid[point], being point
-- some structure with a pair of x and y integers, so grid[point] should
-- be equivalent to grid[x,y] or grid[x][y]. This grid is assumed to be
-- initially dimensioned and filled by nils.
--
-- In my implementation the grid is dynamic, and it is an associative array
-- indexed by string keys in the form grid["(x,y)"]. The function gridToString()
-- can be used to convert a Point to its string form, so the grid is indeed
-- accesed like this: grid[gridToString(p)] being p a Point with integer
-- coordinates (which in fact is found via imageToGrid, like in the article)
-- UTILITY FUNCTIONS (used in the article, without giving implementation)
-- =====================================================================
-- RandomQueue stores values and gives them in random order
local RandomQueue = {}
function RandomQueue.new ()
return {last=-1}
end
function RandomQueue.push(q, item)
local last = q.last + 1
q.last = last
q[last] = item
end
function RandomQueue.pop(q)
if (RandomQueue.empty(q)) then
return nil
end
local index = math.random(0,q.last)
-- A random index is generated. The last element
-- is swaped with this random item, and the new
-- last item is popped.
local last = q.last
item = q[index]
q[index] = q[last]
q[last] = nil
q.last = last -1
return item
end
function RandomQueue.empty(q)
return q.last==-1
end
function RandomQueue.print(q)
-- For debugging. Not used
local t = {}
for i=0, q.last do
table.insert(t, string.format("(%f,%f)", q[i].x, q[i].y))
end
print (string.format("RandomQueue %d elem: %s", q.last+1, table.concat(t, " ")))
end
-- Point stores a coordinate pair
local Point = {}
function Point.new(x,y)
return {x=x, y=y}
end
-- Determines if a point is inside the rendered rectangle
local function inRectangle(point, width, height)
return (point.x>0 and point.y>0 and point.x<width and point.y<height)
end
-- Converts a point to a string representation, to be used as index in the grid
local function gridToString(gridPoint)
return string.format("(%d,%d)", gridPoint.x, gridPoint.y)
end
-- Computes the distance between two points
local function distance(p1, p2)
return math.sqrt(math.pow(p2.x-p1.x,2) + math.pow(p2.y-p1.y,2))
end
-- Prints the grid. For debugging. Not used
local function printGrid(grid)
print "==========="
for k,v in pairs(grid) do
print (string.format("%s: %f, %f", k, v.x, v.y))
end
end
-- THE FUNCTIONS GIVEN IN THE ARTICLE
local function imageToGrid(point, cellSize)
local gridX = math.floor(point.x/cellSize)
local gridY = math.floor(point.y/cellSize)
return Point.new(gridX, gridY)
end
local function generateRandomPointAround(point, mindist)
local r1 = math.random()
local r2 = math.random()
local radius = mindist * (r1+1)
local angle = 2 * math.pi * r2
newX = point.x + radius * math.cos(angle)
newY = point.y + radius * math.sin(angle)
return Point.new(newX, newY)
end
-- This one is not given in the article. It returns the
-- values of several cells around the give gridPoint
-- We are using string indexes for the grid, but if we
-- try to access to a key which is not stored, lua gives
-- nil instead of an exception, so it works as expected
-- because we get nils for cells which have no dot inside
local function squareAroundPoint(grid, gridPoint, n)
local extreme = math.floor(n/2)
local result = {}
for i=-extreme,extreme do
for j=-extreme,extreme do
ii = i + gridPoint.x
jj = j + gridPoint.y
data = grid[gridToString(Point.new(ii,jj))]
if data == nil then
repr = "nil"
else
repr = string.format("(%f,%f)", data.x, data.y)
end
table.insert(result, data)
end
end
return result
end
local function inNeighbourhood(grid, point, mindist, cellSize)
local gridPoint = imageToGrid(point, cellSize)
cellsAroundPoint = squareAroundPoint(grid, gridPoint, 5)
for k,cell in pairs(cellsAroundPoint) do
if not (cell==nil) then
local d = distance(cell, point)
if distance(cell, point) < mindist then
return true
end
end
end
return false
end
-- This is the lua implementation of the pseudocode in the article
function generate_poisson(width, height, min_dist, new_points_count)
local cellSize = min_dist/math.sqrt(2)
local grid = {} -- Point.new(math.ceil(width/cellSize), math.ceil(height/cellSize))}
local processList = RandomQueue.new()
local samplePoints = {}; -- Empty list
-- Generate the first point
local firstPoint = Point.new(math.random()*width, math.random()*height)
-- print (string.format("newPoint: [%f, %f]", firstPoint.x, firstPoint.y))
RandomQueue.push(processList, firstPoint)
table.insert(samplePoints, firstPoint)
grid[gridToString(imageToGrid(firstPoint, cellSize))] = firstPoint
-- Generate other points from points in queue
while (not RandomQueue.empty(processList)) do
-- RandomQueue.print(processList)
-- printGrid(grid)
local point = RandomQueue.pop(processList)
for i=0,new_points_count do
local newPoint = generateRandomPointAround(point, min_dist)
-- print (string.format("newPoint: [%f, %f]", newPoint.x, newPoint.y))
-- Check the point is in the region and not too close
-- to other points
if inRectangle(newPoint, width, height) and
not inNeighbourhood(grid, newPoint, min_dist, cellSize) then
-- In this case, the point is accepted
RandomQueue.push(processList, newPoint)
table.insert(samplePoints, newPoint)
grid[gridToString(imageToGrid(newPoint, cellSize))] = newPoint;
end
end
end
return samplePoints
end
-- Initialize random seed
math.randomseed(os.time())
-- Function to generate the list of dots in a tikz's foreach compatible syntax
function poisson_points_list(width, height, mindist, add_points)
local data = generate_poisson(width, height, mindist, add_points)
local str = {}
for k,v in ipairs(data) do
table.insert(str, string.format("%f/%f", v.x, v.y))
end
tex.print(table.concat(str, ", "))
end
-- Function similar to the above, but the returned list is "ordered"
-- so that the points are more or less in the left-to-right,
-- top-to-down order
function poisson_points_list_ordered(width, height, mindist, add_points)
local cellSize = mindist/math.sqrt(2)
local function compare_coords(a,b)
aa = imageToGrid(a, cellSize);
bb = imageToGrid(b, cellSize);
if (aa.y == bb.y) then -- If they are around the same row
return a.x<b.x; -- the x coord orders them
else -- if not
return a.y>b.y; -- the y coord orders them
end
end
local data = generate_poisson(width, height, mindist, add_points)
table.sort(data, compare_coords);
local str = {}
for k,v in ipairs(data) do
table.insert(str, string.format("%f/%f", v.x, v.y))
end
tex.print(table.concat(str, ", "))
end
\end{filecontents*}
\usepackage{tikz}
\usetikzlibrary{backgrounds}
\usepackage{poisson}
\begin{document}
\xdef\LstTxt{"duck","koala","marmot","honey","tiger","lion","bear","pineapple",
"pizza","sun","Mercure","Venus","Earth","Mars","Jupiter","Saturn","Uranus"}
\xdef\MaxWidth{0}
\foreach \X [count=\Y] in \LstTxt
{\pgfmathsetmacro{\mywidth}{max(\MaxWidth,width(\X))}
\xdef\MaxWidth{\mywidth}
\xdef\NumEntries{\Y}}
\pgfmathsetmacro{\GraceDist}{2*\MaxWidth*1pt/1cm}
\edef\mylist{\poissonpointslist{14}{14}{\GraceDist}{\NumEntries}}
% 14 x 14 is the size of the area to fill
% \GraceDist is the minimum distance between centers
\typeout{\mylist}
\begin{tikzpicture}[framed,gridded,radius=0.5cm]
\draw[red](-1,-1) rectangle (15,15);
\foreach \x/\y [count=\n starting from 0] in \mylist
{\pgfmathsetmacro{\RndOne}{255*rnd}
\pgfmathsetmacro{\RndTwo}{255*rnd}
\pgfmathsetmacro{\RndThree}{255*rnd}
\pgfmathsetmacro{\Scale}{0.5+1.5*rnd}
\pgfmathsetmacro{\Rot}{360*rnd}
\pgfmathsetmacro{\String}{{\LstTxt}[\n]}
\definecolor{mycolor}{RGB}{\RndOne,\RndTwo,\RndThree}
\draw[color=mycolor] (\x,\y) node[scale=\Scale,rotate=\Rot] {\String};}
\end{tikzpicture}
\end{document}