【问题标题】:Space filling with circles of unequal size用大小不等的圆圈填充空间
【发布时间】:2020-09-27 01:17:19
【问题描述】:

这是我的问题:

  • 我需要在画布内显示一堆圆圈。
  • 有任意数量的圆,每个圆都有一个预定义的半径。
  • 圆圈的总面积总是小于画布的面积。

我想定位圆圈,使它们占据画布内的最大可用空间,而不会相互接触。我的目标是实现视觉上令人愉悦的效果,使圆圈在画布内均匀分布。我不知道这是否真的是“空间填充”,因为我的目标不是最小化元素之间的距离,而是最大化它。

这是我想要实现的一个示例:

我的第一个“蛮力”想法如下:

  1. 对于每个圆:计算其边界与其他圆的边界之间的最短距离;将所有这些距离相加,称为 X。
  2. 计算所有 X 的总和。
  3. 随机改变圆圈之间的距离。
  4. 按照预设的迭代次数重做 1-3,并取步骤 (2) 中获得的最大值。

但是,这似乎并不优雅;我确信有更好的方法来做到这一点。有没有现有的算法来实现这样的布局?我可以使用任何现有的库(JavaScript 或 Ruby)来实现这一点吗?

编辑

这是已接受答案的Javascript version,它使用拉斐尔绘制圆圈。

【问题讨论】:

  • 不是圈子,而是openprocessing.org/sketch/1811
  • louism,看看克里斯的回答!这些图像看起来很棒——我认为你应该接受那个。 @KrisVanBael,将代码发布到 GitHub 上怎么样?

标签: javascript ruby layout geometry


【解决方案1】:

我会尝试在球体之后插入球体(最大的在前)。每一个都被添加到最大的可用空间中,并带有一些随机抖动。

找到(或多或少)最大可用空间的一种相对简单的方法是在您的视图上想象一个点网格,并为每个网格点(在二维数组中)存储到任何项目的最近距离:边缘或球体,以最接近者为准。这个数组随着每个新球体的添加而更新。

要添加一个新球体,只需选取距离最远的网格点并应用一些随机抖动(您实际上知道可以抖动多少,因为您知道到最近项目的距离)。 (我会随机化不超过 (d-r)/2,其中 d 是数组中的距离,r 是要添加的球体的半径。

在添加另一个圆之后更新此数组不是火箭科学:您为每个网格点计算到新添加的球体的距离,如果更大,则替换存储的值。

可能是您的网格太粗了,您不能再添加任何圆(当二维数组不包含大于要添加的圆半径的距离时)。然后你必须在继续之前增加(例如加倍)网格分辨率。

这是这个实现的一些结果(我花了大约 100 行代码)

  • 100 个不同大小的圆圈

  • 500 个不同大小的圆圈

  • 100 个相同大小的圆圈

这里是一些粗略的 C++ 代码(只是算法,别指望它会编译)

    // INITIALIZATION

    // Dimension of canvas
    float width = 768;
    float height = 1004;

    // The algorithm creates a grid on the canvas
    float gridSize=10;

    int gridColumns, gridRows;
    float *dist;

    void initDistances()
    {
      // Determine grid dimensions and allocate array
      gridColumns = width/gridSize;
      gridRows = height/gridSize;

      // We store a 2D array as a 1D array:
      dist = new float[ gridColumns * gridRows ];

      // Init dist array with shortest distances to the edges
      float y = gridSize/2.0;
      for (int row=0; row<gridRows; row++)
      {
        float distanceFromTop = y;
        float distanceFromBottom = height-y;
        for (int col=0; col<gridColumns; col++)
        {
          int i = row*gridColumns+col;
          dist[i]=(distanceFromTop<distanceFromBottom?distanceFromTop:distanceFromBottom);
        }
        y+=gridSize;
      }
      float x = gridSize/2.0;
      for (int col=0; col<gridColumns; col++)
      {
        float distanceFromLeft = x;
        float distanceFromRight = width-x;
        for (int row=0; row<gridRows; row++)
        {
          int i = row*gridColumns+col;
          if (dist[i]>distanceFromLeft) dist[i] = distanceFromLeft;
          if (dist[i]>distanceFromRight) dist[i] = distanceFromRight;
        }
        x+=gridSize;
      }
    }

    void drawCircles()
    {
      for (int circle = 0; circle<getNrOfCircles(); circle++)
      {
        // We assume circles are sorted large to small!
        float radius = getRadiusOfCircle( circle ); 

        // Find gridpoint with largest distance from anything
        int i=0;
        int maxR = 0;
        int maxC = 0;
        float maxDist = dist[0];

        for (int r=0; r<gridRows; r++) 
          for (int c=0; c<gridColumns; c++)
          {
            if (maxDist<dist[i]) {
              maxR= r; maxC= c; maxDist = dist[i];
            }
            i++;
          }

        // Calculate position of grid point
        float x = gridSize/2.0 + maxC*gridSize;
        float y = gridSize/2.0 + maxR*gridSize;

        // Apply some random Jitter
        float offset = (maxDist-radius)/2.0;
        x += (rand()/(float)RAND_MAX - 0.5) * 2 * offset;
        y += (rand()/(float)RAND_MAX - 0.5) * 2 * offset;


        drawCircle(x,y,radius);


        // Update Distance array with new circle;
        i=0;
        float yy = gridSize/2.0;
        for (int r=0; r<gridRows; r++)
        {
          float xx = gridSize/2.0;
          for (int c=0; c<gridColumns; c++)
          {
            float d2 = (xx-x)*(xx-x)+(yy-y)*(yy-y);

            // Naive implementation
            // float d = sqrt(d2) - radius;
            // if (dist[i]>d) dist[i] = d;

            // Optimized implementation (no unnecessary sqrt)
            float prev2 = dist[i]+radius;
            prev2 *= prev2;
            if (prev2 > d2)
            {
              float d = sqrt(d2) - radius;
              if (dist[i]>d) dist[i] = d;
            }



            xx += gridSize;
            i++;
          }
          yy += gridSize;
        }
      }
    }

【讨论】:

  • 我喜欢,+1。我认为这可能是最好的解决方案。
  • 谢谢亚历克斯。但是由于 Louism 还没有接受我的回答,所以我就跟着去实现了。
  • 非常好的克里斯,太棒了!你介意在 Git 上发布代码吗?
  • 我发布了我的 C++ 项目的相关部分。这是一个相当快速和肮脏的算法概念证明,所以它不是很干净。
  • 这非常有帮助,克里斯。我会在 SA lets me do it 时尽快奖励你。
【解决方案2】:

也许force-directed layout 的一些应用会很有用。

【讨论】:

  • 此解决方案最适合动画布局。
【解决方案3】:

由于您的目标只是“达到令人愉悦的效果”,而不是解决数学问题,您应该先尝试最简单的算法,看看它是否看起来不错。应该不需要使用非常复杂的数学。

我了解您希望球体“填充”可用空间,而不是在其他区域拥挤时留下大片空白区域。您还希望布局看起来是随机的——而不是排列在网格或类似的东西上。

实现这一点的明显且极其简单的方法就是将球体一个一个地放置在随机位置。如果一个落在已经放置的球体顶部,则生成另一个随机位置,直到找到适合的位置。

显示的图像中似乎有大约 40 个球体。 40 个球体全部落在图像的同一区域,而图像的其余部分为空的可能性非常非常小。随着球体数量的增加,得到非常不平衡的布局的机会将接近于零。

先尝试一下,看看它是否满足您的需求。如果它不够“均匀”,您应该能够使用一些非常简单的数学来使随机选择的位置偏向于选择空白区域。应该不需要使用复杂的算法。

【讨论】:

  • +1 开始简单。但是随机真的不好看。你确实激励我编辑我的答案。
  • 是的,这是一个很好的开始建议。我只是尝试了随机不重叠的位置,它已经比手动定位它们要好,但距离我想要达到的视觉效果还很远。我会在尝试其他解决方案时发布一些更新。
猜你喜欢
  • 2020-09-07
  • 1970-01-01
  • 2013-02-19
  • 1970-01-01
  • 2019-01-24
  • 1970-01-01
  • 1970-01-01
  • 2021-09-23
  • 1970-01-01
相关资源
最近更新 更多