Responsive CSS Grid with persistent aspect ratio

后端 未结 3 1477
长发绾君心
长发绾君心 2021-02-07 18:26

My goal is to create a responsive grid with an unknown amount of items, that keep their aspect ratio at 16 : 9. Right now it looks like this:

3条回答
  •  滥情空心
    2021-02-07 18:36

    I needed this exact same thing for video layouts, but I couldn't use the other answers because I need to be bounded by width and height. Basically my use case was a container of a certain size, unknown item count, and a fixed aspect ratio of the items. Unfortunately this cannot be done in pure CSS, it needs some JS. I could not find a good bin packing algorithm so I wrote one myself (granted it might mimic existing ones).

    Basically what I did is took a max set of rows and found the fit with the best ratio. Then, I found the best item bounds retaining the aspect ratio, and then set that as auto-fit height and width for the CSS grid. The result is quite nice.

    Here's a full example showing how to use it with something like CSS custom properties. The first JS function is the main one that does the work of figuring out the best size. Add and remove items, resize browser to watch it reset to best use space (or you can see this CodePen version).

    // Get the best item bounds to fit in the container. Param object must have
    // width, height, itemCount, aspectRatio, maxRows, and minGap. The itemCount
    // must be greater than 0. Result is single object with rowCount, colCount,
    // itemWidth, and itemHeight.
    function getBestItemBounds(config) {
      const actualRatio = config.width / config.height
      // Just make up theoretical sizes, we just care about ratio
      const theoreticalHeight = 100
      const theoreticalWidth = theoreticalHeight * config.aspectRatio
      // Go over each row count find the row and col count with the closest
      // ratio.
      let best
      for (let rowCount = 1; rowCount <= config.maxRows; rowCount++) {
        // Row count can't be higher than item count
        if (rowCount > config.itemCount) continue
        const colCount = Math.ceil(config.itemCount / rowCount)
        // Get the width/height ratio
        const ratio = (theoreticalWidth * colCount) / (theoreticalHeight * rowCount)
        if (!best || Math.abs(ratio - actualRatio) < Math.abs(best.ratio - actualRatio)) {
          best = { rowCount, colCount, ratio }
        }
      }
      // Build item height and width. If the best ratio is less than the actual ratio,
      // it's the height that determines the width, otherwise vice versa.
      const result = { rowCount: best.rowCount, colCount: best.colCount }
      if (best.ratio < actualRatio) {
        result.itemHeight = (config.height - (config.minGap * best.rowCount)) / best.rowCount
        result.itemWidth = result.itemHeight * config.aspectRatio
      } else {
        result.itemWidth = (config.width - (config.minGap * best.colCount)) / best.colCount
        result.itemHeight = result.itemWidth / config.aspectRatio
      }
      return result
    }
    
    // Change the item size via CSS property
    function resetContainerItems() {
      const itemCount = document.querySelectorAll('.item').length
      if (!itemCount) return
      const container = document.getElementById('container')
      const rect = container.getBoundingClientRect()
      // Get best item bounds and apply property
      const { itemWidth, itemHeight } = getBestItemBounds({
        width: rect.width,
        height: rect.height,
        itemCount,
        aspectRatio: 16 / 9,
        maxRows: 5,
        minGap: 5
      })
      console.log('Item changes', itemWidth, itemHeight)
      container.style.setProperty('--item-width', itemWidth + 'px')
      container.style.setProperty('--item-height', itemHeight + 'px')
    }
    
    // Element resize support
    const resObs = new ResizeObserver(() => resetContainerItems())
    resObs.observe(document.getElementById('container'))
    
    // Add item support
    let counter = 0
    document.getElementById('add').onclick = () => {
      const elem = document.createElement('div')
      elem.className = 'item'
      const button = document.createElement('button')
      button.innerText = 'Delete Item #' + (++counter)
      button.onclick = () => {
        document.getElementById('container').removeChild(elem)
        resetContainerItems()
      }
      elem.appendChild(button)
      document.getElementById('container').appendChild(elem)
      resetContainerItems()
    }
    #container {
      display: inline-grid;
      grid-template-columns: repeat(auto-fit, var(--item-width));
      grid-template-rows: repeat(auto-fit, var(--item-height));
      place-content: space-evenly;
      width: 90vw;
      height: 90vh;
      background-color: green;
    }
    
    .item {
      background-color: blue;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    

提交回复
热议问题