Spread out images randomly without overlapping inside container

I'm trying to figure out how to solve this.

Say I 10 pictures, each picture is 95x95 - then I create a container (position relative) that has a height of 300px and width of 100%.

Now I want to take these 10 pictures (position absolute) and place them randomly both using top and left inside the container, however I never want them to overlap each other.

I create the picture tags

 for (let person of people) {
  let personBubble = document.createElement('a')

  personBubble.style.backgroundImage = "url(" + person.picture_url + ")";
  personBubble.style.display = 'block';
  personBubble.style.width = '100%';
  personBubble.style.height = '100%';
  personBubble.style.backgroundSize = '100% 100%';
  personBubble.style.position = 'absolute';
  personBubble.style.top = '0';
  personBubble.style.left = '0';
  personBubble.style.borderRadius = '50%';
  personBubble.style.zIndex = '10';
 }

now basically I want to change the top and left properties in such a way that this can be solved (instead of having all 10 of them at top 0 and left 0)

this is kinda the feel I'm going for

Upvotes: 1

Views: 2251

Answers (1)

Gershom Maes
Gershom Maes

Reputation: 8170

Ok I'm gonna try to do something fancy over here.

The idea: Start with a single image which we'll call "ready", near the center. Now for each remaining image, pick an image that's "ready", and align against one of its randomly selected 4 edges. If, after this alignment, the newly aligned image overlaps another previously positioned image, retry this alignment mechanic.

Working example:

let global = window;

global.params = { sep: 1, randOff: 0 };

let randInt = (min, max) => Math.floor(min + Math.random() * (max - min));

let getBound = (elem, isRoot) => {
  let { width, height } = elem.getBoundingClientRect();
  return {
    x: isRoot ? width >> 1 : (parseInt(elem.style.left) || 0),
    y: isRoot ? height >> 1 : (parseInt(elem.style.top) || 0),
    w: width,
    h: height,
    hw: width >> 1, // half-width
    hh: height >> 1 // half-height
  };
};
let boxCollides = (box, boxes, parBound, sep) => {
  
  let b1 = getBound(box);
  
  // Make sure `box` doesn't stick outside its `parBound`
  if (parBound) {
    if ((b1.x - b1.hw) < (parBound.x - parBound.hw)
      || (b1.x + b1.hw) > (parBound.x + parBound.hw)
      || (b1.y - b1.hh) < (parBound.y - parBound.hh)
      || (b1.y + b1.hh) > (parBound.y + parBound.hh))
      return true;
  }
  
  // Make sure `box` doesn't overlap any other box
  for (let box2 of boxes) {
    
    if (box === box2) continue;
    
    let b2 = getBound(box2);
    
    let sepX = Math.max(b1.x, b2.x) - Math.min(b1.x, b2.x);
    let sepY = Math.max(b1.y, b2.y) - Math.min(b1.y, b2.y);
    
    // If there isn't sufficient separation on either axis there's a collision
    if (sepX < (b1.hw + b2.hw + sep) && sepY < (b1.hh + b2.hh + sep)) return true;
    
  }
  
  return false;
  
};
let reposition = (boxes, { sep=1, randOff=0 }=global.params) => {
  
  // Awesome (and terrible) `shuffle` implementation
  boxes = [ ...boxes ].sort(function() { return Math.random() - 0.5; });
  
  let parBound = getBound(boxes[0].parentNode, true);
  
  // Consider the 1st box "ready"; position it in the center
  Object.assign(boxes[0].style, { left: `${parBound.x}px`, top: `${parBound.y}px` });
  
  // Start counting at 1 (since 1 box is initially "ready")
  for (let numReady = 1; numReady < boxes.length; numReady++) {
    
    let box = boxes[numReady];
    let b = getBound(box);
    
    // Use a counting loop to prevent too many attempts
    for (let attempts = 0; attempts < 500; attempts++) {
      
      // The bound of a random ready box
      let b2 = getBound( boxes[ randInt(0, numReady) ] );
      
      let side = randInt(0, 4);             // Randomly pick side to align to
      let off = randInt(-randOff, randOff); // Calculate random offset for `box`...
      let [ x, y ] = [ 0, 0 ];              // We'll calculate `x` and `y` next...
            
      // Align left
      if (side === 0) [ x, y ] = [ b2.x - (b2.hw + b.hw + sep), b2.y + off ];
      
      // Align right
      if (side === 1) [ x, y ] = [ b2.x + (b2.hw + b.hw + sep), b2.y + off ];
      
      // Align top
      if (side === 2) [ x, y ] = [ b2.x + off, b2.y - (b2.hh + b.hh + sep) ];
      
      // Align bottom
      if (side === 3) [ x, y ] = [ b2.x + off, b2.y + (b2.hh + b.hh + sep) ];
      
      Object.assign(box.style, { left: `${x}px`, top: `${y}px` });
      
      // Check if `box` now collides any of the ready boxes. If it doesn't,
      // we've successfully positioned it and `box` is ready!
      if (!boxCollides(box, boxes.slice(0, numReady), parBound, sep)) break;
      
    }
    
  }
  
};

window.addEventListener('load', () => {
  
  let boxesElem = document.querySelector('.boxes');
  for (let i = 0; i < 45; i++) {
    let boxElem = document.createElement('box');
    boxElem.classList.add('box', `dim${(i % 3)}`);
    boxesElem.appendChild(boxElem);
  }
  
  let boxes = [ ...boxesElem.childNodes ];
  
  reposition(boxes);
  setInterval(() => reposition(boxes), 2500);
  
});

for (let controlElem of document.querySelectorAll('.controls > .control')) {
  controlElem.addEventListener('click', evt => {
    
    evt.stopPropagation();
    evt.preventDefault();
        
    Object.assign(global.params, eval(`(${controlElem.textContent})`));
    
    document.getElementById('activeControl').setAttribute('id', '');
    controlElem.setAttribute('id', 'activeControl');
    
  });
}
.boxes {
  position: absolute;
  left: 0; top: 0;
  width: 100%; height: 100%;
  box-shadow: inset 0 0 0 2px #404040;
}
.boxes > .box {
  position: absolute;
  left: 50%; top: 50%;
  background-color: #ffffff;
  transition: left 400ms ease-in-out, top 400ms ease-in-out;
}
.boxes > .box.dim0 {
  width: 40px; height: 30px;
  margin-left: -20px; margin-top: -15px;
  background-color: #804000;
  box-shadow: inset 0 0 0 2px #c08000;
}
.boxes > .box.dim1 {
  width: 14px; height: 48px;
  margin-left: -7px; margin-top: -24px;
  background-color: #400080;
  box-shadow: inset 0 0 0 2px #a000f0;
}
.boxes > .box.dim2 {
  width: 22px; height: 22px;
  margin-left: -11px; margin-top: -11px;
  background-color: #600060;
  box-shadow: inset 0 0 0 2px #c000d0;
}
.controls {
  position: fixed;
  left: 0; top: 0;
  width: 130px; height: 100%;
  background-color: rgba(0, 0, 0, 0.3);
  overflow-y: auto;
}
.controls > .control {
  white-space: pre-wrap;
  font-family: monospace;
  font-size: 10px;
  cursor: pointer;
}
.controls > .control:hover,
.controls > .control#activeControl {
  background-color: rgba(0, 0, 0, 0.5);
  color: #ffffff;
}
<div class="boxes"></div>
<div class="controls">
  <div class="control" id="activeControl">
  {
    sep: 1,
    randOff: 0
  }
  </div>
  <div class="control">
  {
    sep: 10,
    randOff: 0
  }
  </div>
  <div class="control">
  {
    sep: 5,
    randOff: 10
  }
  </div>
  <div class="control">
  {
    sep: 0,
    randOff: 20
  }
  </div>
</div>

Upvotes: 7

Related Questions