尝试通过迭代循环创建x个不重叠的随机矩形

3
这个问题让我困惑了很久。这更多是一个数学/逻辑问题,而不是一个具体的JS问题,但我正在使用JS并且在某些时候需要将结果逻辑转换为代码...
我正在尝试在画布上创建X个随机大小的非重叠矩形和/或正方形,以实现类似于此示例图像的效果:

enter image description here

我希望创建的方格数量可能从10到100个左右。显然,需要更多方格时,它们都必须变得更小。

我在JS Fiddle上尝试了不同的想法,但这个问题的逻辑一直困扰着我...

我不想要经典的斐波那契螺旋框图案。我希望随机性能在某种程度上解决这个问题,但我也有一个想法,即每次在存储的线条数组中查找最长的线条,并从该线条随机点开始。

我正在尝试将画布在随机点处切成两半,然后将该线添加到数组中。然后根据第一条线的坐标绘制另一条线,然后再存储该线,以此类推...... 我将坐标存储在对象数组中,如下所示:

function storeLine(startX, startY, endX, endY, array) {
        array.push({
                start : {
                         x: startX,
                         y: startY
                        },
                end : {
                         x: endX,
                         y: endY
                        }
                });
}

但是,我很快遇到了一个问题,那就是我画在整个x轴上的第一条线总是最长的线,结果我只得到了许多细长的盒子。
在完美的世界里,我的最终结果将接受像盒子的总数和最小x/y比这样的变量,这样我就可以(以某种方式)倾向于更多的肖像或更多的风景盒子,这样我就可以调整并不断重新生成,直到我喜欢的结果出现为止。
无论如何,我卡在了如何继续下去,甚至不知道我是否在正确的道路上。如果有人对如何继续沿着我目前的道路或更好的方法有想法,我将永远感激你!
注意:检查我的问题后,我想到斐波那契盒图案可能是一个不错的起点,但我仍然需要使较大的初始盒子也被分割,这样当我想要更多的盒子时,我就不会只得到越来越小的盒子...无论如何,这只是一个随意的想法,如果给别人一些灵感,那就好了。
另外的想法:沃罗诺伊图案也很棒,但我的数学技能还不够,甚至不知道从哪里开始。

“我喜欢的结果”是一个相当模糊的编码规范。 - Scott Hunter
我同意。我的意思是,由于数字是随机的,我可能想要重新生成,直到它产生一个符合我所需盒子数量的结果。许多软件中的随机生成器通常会有一个名为“种子”的选项,可以改变结果。作为一名VFX艺术家,当您不喜欢随机生成的噪声模式时,这是一个很好的选择,因此您可以调整种子直到获得“您喜欢的结果”。 - Different Gravity
也许以下内容对您的种子问题有用:https://dev59.com/QHRB5IYBdhLWcg3wxZ1K - MauriceNino
@MauriceNino:我认为问题更多地涉及到如何使用随机值而不是如何生成它们。 - Scott Hunter
可以在谷歌上找到很多沃罗诺伊生成器。 - Scott Hunter
2个回答

2

这是一个很酷的想法!

你可以将其看作盒子中的盒子(也像一棵树)。这些盒子有自己的坐标和大小。盒子可以嵌套在盒子内,因此您需要选择一个维度进行分割(水平或垂直),然后根据需要分成多少个盒子。然后,在每个盒子中,您可以添加更多的盒子,以此类推。最后,为了绘制线条,您只需使盒子具备自我绘制的能力(并告诉它们的盒子自己绘制)。

以下是一些执行此操作的JS代码。您可以轻松地调整要进行的嵌套量。可能需要一些调整的是如何将空间分成大致相等的盒子(稍微随机地不同)。为了将空间分成 n 个盒子,我开始时将大小设为可用空间的 1/n,然后稍微随机调整一下。如果您只使用 remaining*Math.random(),则大多数情况下会得到非常窄的盒子。

// probably play around with this to get better splitting
// maybe split near a mouse click
let splitDimension = (l, n) => {
    let splits = [];
    let remaining = l;
    let x = 0;
    for (let i=0; i<n-1; i++) {
        let r = Math.random();
        let seg = remaining * (1/n);
        let s = seg + 0.75*(0.5-r)*seg
        splits.push([x, s]);
        x += s;
        remaining -= s;
    }
    // add the last bit
    splits.push([x, remaining])
    return splits;
};
// the main idea
class Box {
    constructor(x, y, w, h) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.boxes = [];
        
    }

    draw(ctx) {
        ctx.beginPath();
        ctx.rect(this.x, this.y, this.w, this.h);
        ctx.stroke();
        this.boxes.forEach(box => {
            box.draw(ctx)
        });
    }

    addBoxes(n, dim) {
        let splits;
        if (dim == "x") {
            // split on width
            splits = splitDimension(this.w, n)
            // turn the splits into new boxes
            this.boxes = splits.map(([x,w]) => {
                return new Box(this.x+x, this.y, w, this.h)
            });
        } else {
            // split over height
            splits = splitDimension(this.h, n);
            this.boxes = splits.map(([y,h]) => {
                return new Box(this.x, this.y+y, this.w, h);
            })
        }
    }
}
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
// let's make some boxes!
let bigBox = new Box(0,0,canvas.width,canvas.height);
bigBox.addBoxes(2, "y");
// now add boxes on boxes on boxes
bigBox.boxes.forEach(box => {
    box.addBoxes(3, "x");
    // also more boxes
    box.boxes.forEach(boxBox => {
        boxBox.addBoxes(2, "y");
    });
});
// now draw the boxes!
bigBox.draw(ctx);

这太棒了,我要将其标记为答案。对于我的问题,它确切地产生了我所期望的结果......现在我看到了它,我意识到它永远不会看起来像我需要的那样。主要问题仍然是任何开始时的分割线都永远不会与其他框“融合”,因为有些框会跨越。但是,完全归功于@g23,因为它实现了我想要的99%。我添加了一个简陋的界面,以使调整变得容易。但我认为我可能仍然需要继续寻找。 - Different Gravity
1
太棒了,很高兴我能帮上忙。我刚才正在玩它,我认为让它看起来更好的一件事是在forEach中加深盒子时,随机决定一个给定的盒子是否会有子盒子。这使它看起来更加整洁。 - g23
1
关于消除“分割线”,也许这个方法会有所帮助:可以尝试添加一个较小的中间框,并使用它来放置周围的4个框,这样就不会有分割线了。就像把纸箱合起来(哈哈),不用胶带,你必须将4个翻盖对折,这样就能在中间产生4个矩形和一个小矩形洞。因此,在某些步骤中可能需要进行“中间添加”,这会添加总共5个框,而不是“分裂维度”。 - g23
1
我一直在思考你的“纸板盒盖子”想法,我想我可以尝试反转它,并将其与你的原始答案结合起来。因此,我制作了4个较大的矩形(中间有孔),但是正是这些4个矩形使用了你的原始系统进行划分。 - Different Gravity
1
原来当您更新设置时,clickbox 的宽度和高度会变成字符串。您需要用 parseInt(clickBoxElem.value) || 200 来包装它,这样就可以解决问题了。 - g23
显示剩余3条评论

1

@g23的回答是这个系统的基础,但为了让任何人都能获得完整的工作系统,我想发布我得到的最终结果。

最终结果通过在画布上创建一个盒子,用户单击该盒子后,在其周围创建4个盒子来实现。然后,那4个盒子使用@g23的原始答案将其随机分成更小的盒子。中间的盒子是解决第一个分割线通过整个图像并因此使其看起来像两个随机分割的盒子贴在一起的方案。使用这个新系统,永远不会有一条线穿过整个画布。我还添加了一个保存按钮以下载结果,以及滑块来控制所有设置和尺寸:

Working Fiddle here

var ImageWidth = 960
var ImageHeight = 540

var direction1 = "x";
var direction2 = "y";
var vert = true;

var LargeVerticalBoxes = parseInt(document.getElementById("lvb").value);
var inner = parseInt(document.getElementById("inner").value);
var smallest = parseInt(document.getElementById("smallest").value);
var totalboxes = "";

var clickBoxWidth = 200;
var clickBoxHeight = 100;
var lineWidth = 5;

var clickBox_xStart = 0;
var clickBox_yStart = 0;

var minSize = 0.1
var maxSize = 0.9

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');


ctx.canvas.width = ImageWidth;
ctx.canvas.height = ImageHeight;

updateSettings();
canvas.addEventListener('click', function(evt) {
  var mousePos = getMousePos(canvas, evt);
  //console.log(mousePos.x + ',' + mousePos.y);

  clearCanvas();
  ctx.beginPath();
  calcclickBox(mousePos.x, mousePos.y)
  ctx.rect(clickBox_xStart, clickBox_yStart, clickBoxWidth, clickBoxHeight);
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = 'black';
  ctx.stroke();

  ctx.closePath();
  reDraw();
}, false);


download_img = function(el) {
  var image = canvas.toDataURL("image/png");
  el.href = image;
};


function updateSettings() {
  lineWidth = parseInt(document.getElementById("linewidth").value,10);
  clickBoxWidth = parseInt(document.getElementById("boxWidth").value,10);
  clickBoxHeight = parseInt(document.getElementById("boxHeight").value,10);
  canvas.width = parseInt(document.getElementById("canWidth").value,10);
  canvas.height = parseInt(document.getElementById("canHeight").value,10);
  document.getElementById("dispW").innerText = "Width: " + canvas.width;
  document.getElementById("dispH").innerText = "Height: " + canvas.height;
  document.getElementById("canW").innerText = "Width: " + clickBoxWidth;
  document.getElementById("canH").innerText = "Height: " + clickBoxHeight;
}


function clearCanvas() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}


function calcclickBox(x, y) {
  clickBox_xStart = x - clickBoxWidth / 2;
  clickBox_yStart = y - clickBoxHeight / 2;
  clickBoxWidth = clickBoxWidth;
  clickBoxHeight = clickBoxHeight;
}



function getMousePos(canvas, evt) {
  var rect = canvas.getBoundingClientRect();
  return {
    x: evt.clientX - rect.left,
    y: evt.clientY - rect.top
  };
}

function toggle() {
  vert = !vert;
  if (vert) {
    direction1 = "x";
    direction2 = "y";
  } else {
    direction1 = "y";
    direction2 = "x";
  }
}

function getTotal() {
  LargeVerticalBoxes = parseInt(document.getElementById("lvb").value);
  inner = parseInt(document.getElementById("inner").value);
  smallest = parseInt(document.getElementById("smallest").value);
  totalboxes = LargeVerticalBoxes * inner * smallest * 4 + 1
  document.getElementById("total").innerText = totalboxes
}


function getRandomArbitrary(min, max) {
  return Math.random() * (max - min) + min;
}


/////// big long function that does most of the work //////////
function reDraw() {

  getTotal();

  // probably play around with this to get better splitting
  // maybe split near a mouse click
  let splitDimension = (l, n) => {
    let splits = [];
    let remaining = l;
    let x = 0;
    for (let i = 0; i < n - 1; i++) {
      let r = Math.random();
      let seg = remaining * (1 / n);
      let s = seg + 0.75 * (0.5 - r) * seg
      splits.push([x, s]);
      x += s;
      remaining -= s;
    }
    // add the last bit
    splits.push([x, remaining])
    return splits;
  };
  // the main idea
  class Box {
    constructor(x, y, w, h) {
      this.x = x;
      this.y = y;
      this.w = w;
      this.h = h;
      this.boxes = [];

    }



    draw(ctx) {
      ctx.beginPath();
      ctx.rect(this.x, this.y, this.w, this.h);
      ctx.stroke();
      this.boxes.forEach(box => {
        box.draw(ctx)
      });
    }

    addBoxes(n, dim) {
      let splits;
      if (dim == "x") {
        // split on width
        splits = splitDimension(this.w, n)
        // turn the splits into new boxes
        this.boxes = splits.map(([x, w]) => {
          return new Box(this.x + x, this.y, w, this.h)
        });
      } else {
        // split over height
        splits = splitDimension(this.h, n);
        // turn the splits into new boxes
        this.boxes = splits.map(([y, h]) => {
          return new Box(this.x, this.y + y, this.w, h);
        })
      }
    }
  }


  // let's make some boxes!
  let TopRightBox = new Box(clickBox_xStart,
    clickBox_yStart,
    canvas.width - clickBox_xStart,
    -clickBox_yStart);

  let BottomRight = new Box(clickBox_xStart + clickBoxWidth,
    clickBox_yStart,
    canvas.width - clickBox_xStart - clickBoxWidth,
    canvas.height - clickBox_yStart);

  let BottomLeft = new Box(clickBox_xStart + clickBoxWidth,
    clickBox_yStart + clickBoxHeight,
    -clickBox_xStart - clickBoxWidth,
    canvas.height - clickBox_yStart - clickBoxHeight);

  let TopLeft = new Box(0, 0, clickBox_xStart, clickBox_yStart + clickBoxHeight);
  TopRightBox.addBoxes(LargeVerticalBoxes, direction1);
  BottomRight.addBoxes(LargeVerticalBoxes, direction1);
  BottomLeft.addBoxes(LargeVerticalBoxes, direction1);
  TopLeft.addBoxes(LargeVerticalBoxes, direction1);

  // now add boxes on boxes on boxes
  TopRightBox.boxes.forEach(box => {
    box.addBoxes(inner, direction2);

    // also more boxes
    box.boxes.forEach(boxBox => {
      boxBox.addBoxes(smallest, direction1);
    });
  });
  BottomRight.boxes.forEach(box => {
    box.addBoxes(inner, direction2);

    // also more boxes
    box.boxes.forEach(boxBox => {
      boxBox.addBoxes(smallest, direction1);
    });
  });

  BottomLeft.boxes.forEach(box => {
    box.addBoxes(inner, direction2);

    // also more boxes
    box.boxes.forEach(boxBox => {
      boxBox.addBoxes(smallest, direction1);
    });
  });
  TopLeft.boxes.forEach(box => {
    box.addBoxes(inner, direction2);

    // also more boxes
    box.boxes.forEach(boxBox => {
      boxBox.addBoxes(smallest, direction1);
    });
  });


  // now draw the boxes!
  TopRightBox.draw(ctx);
  BottomRight.draw(ctx);
  BottomLeft.draw(ctx);
  TopLeft.draw(ctx);
  document.getElementById("total").innerText = totalboxes
}
<html>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<style>
canvas {
}
</style>
<body>
<div class="fluid-container px-3 border">
     <strong>click</strong> on the canvas to create start box and draw new layout


<div class="row">
    <div class="col">
        <div class="row">
        
            <div class="col-12">
            Canvas:
        </div>
            
        </div>
        
        <div class="row">
        
            <div class="col">
                    <span id="dispW">Width: </span>  <input class="form-control" type="range" min="16" max="1920" value=480 id="canWidth" onchange="updateSettings()" />
            </div>
        
            <div class="col">
                <span id="dispH">Height: </span><input class="form-control" type="range" min="9" max="1080" value=270 id="canHeight" onchange="updateSettings()" /> 
            </div> 
            
            
    </div>
    
    <div class="row">
        <div class="col-12">
                Start Box:
        </div>
    </div>
    <div class="row">
        <div class="col">
        <span id="canW">Width: </span> <input class="form-control" type="range" min="1" max="200" value=50 id="boxWidth" onchange="updateSettings()"> 
        </div>
        <div class="col">
            <span id="canH">Height: </span> <input class="form-control" type="range" min="1" max="200" value=100 id="boxHeight" onchange="updateSettings()">
            </div>
</div>
    <div class="row">
        <div class="col-12">
            Line Width:    (changing these settings breaks stuff)
        <input class="form-control" type="range" min="1" max="20" value="5" id="linewidth" onchange="updateSettings()"> </div>
        </div>
        
        <div class="row">
        <div class="col-6">
    <p>Large Vertical Boxes: 
  <input type="range" min="1" max="10" value="1" id="lvb" onchange="getTotal()"></p>
      <p>Medium inner Boxes: 
  <input type="range" min="1" max="10" value="1" id="inner" onchange="getTotal()"></p>
    <p>Smallest inner Boxes: 
  <input type="range" min="1" max="10" value="1" id="smallest" onchange="getTotal()">
    </p>
    </div>
        <div class="col-6">
     toggle horizontal/vertical
<label class="switch">
  <input type="checkbox" id="toggle" onchange="toggle()">
  <span class="slider round"></span>
</label>
    <p>Total number of boxes: <span id="total"> </span></p>
    
    <a  id="download" download="GridLayout.png" href="" onclick="download_img(this);"><button>
    Save Layout
    </button></a>
    </div>
</div>
<br>
<div class="row">
    <div class="col">
        <canvas id="myCanvas" width="578" height="200" style="border:1px solid #000000;" ></canvas>
    </div>
 </div>
</div>
</div>
</div>
<br><br><br><br><br>
</body>
</html>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接