Как сделать имитацию background-size: contain на чистом js?

  1. есть элемент с размером 200px по ширине и 100 по высоте
  2. есть контейнер с динамичным размером (можно менять высоту и ширину, например, как в textarea)

Как сделать так,

  1. чтобы элемент сохранял соотношение сторон?
  2. и если размер контейнера 500х500, то чтобы элемент растянулся на всю ширину (500px) и на всю возможную высоту (250px), то есть с сохранением соотношения сторон

Необходима реализация именно на JS, так как размеры и элемента и контейнера приходят из json и я не могу применить к этим элементам стили.

Вот иллюстрация: Красный - контейнер Зелёный - элемент, который должен динамично подстраиваться под размеры родителя с сохранением соотношения сторон. введите сюда описание изображения


Ответы (1 шт):

Автор решения: ΝNL993

Начнём с самого начала. Во-первых, вот вам разница между

background-size: cover

#block {
  width: 500px;
  height: 250px;
  outline: 1px solid;
  background-image: url('https://i.bcow.xyz/fMGVpco_d.webp');
  background-size: cover;
}
<div id="block"><div>

И background-size: contain

#block {
  width: 500px;
  height: 250px;
  outline: 1px solid;
  background-image: url('https://i.bcow.xyz/fMGVpco_d.webp');
  background-size: contain;
}
<div id="block"><div>

И если вы всё-таки хотите contain, тогда это будет очень ресурсозатратно и легче тогда будет что-нибудь намудрить CSS, но всё-таки сделать на нём.


А теперь вот рассмотрим решение вашей проблемы на JS:

class BackgroundCover {
  constructor(selector) {
    this.el = document.querySelector(selector)
  }

  __setProperties(image) {
    let bcr = this.el.getBoundingClientRect()
    image.style.cssText = `
    position: absolute;
    left: ${bcr.left}px;
    top: ${bcr.top}px;
    width: ${bcr.width}px;
    height: ${bcr.height}px;
    z-index: -1;`
  }

  init(src) {
    let img = document.createElement('img')

    img.src = src
    this.__setProperties(img)
    document.querySelector('body').appendChild(img)

    let obs = new MutationObserver(() => {
      this.__setProperties(img)
    })

    obs.observe(this.el ,{
      attributes: true,
      subtree: true,
      childList: true,
    })

    for (let i = 0; i < 2; i++) {
      (i < 1 ? window : this.el).addEventListener('resize', () => {this.__setProperties(img)})
    }
  }
}

let selectors = ['#actual', '#left1', '#left2', '#nav']
let images = [
  'https://i.bcow.xyz/fMGVpco_d.webp',
  'https://i.bcow.xyz/6vy9VVS_d.webp',
  'https://i.bcow.xyz/lumjqf8_d.webp',
  'https://i.bcow.xyz/6lhW59S_d.webp',
]

for (let i = 0; i < images.length; i++) {
  new BackgroundCover(selectors[i] + ' .green')
    .init(images[i])
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.red {
  padding: 2px;
  outline: 2px solid #ea0505;
}

.green {
  padding: 2px;
  outline: 2px solid #05ea05;
}

.green, .red {
  filter: drop-shadow(rgb(0, 0, 0) 0 0 3px);
  display: block;
}

body {
  padding: 5px;
}

#nav {
  width: calc(30% - 6px);
  height: 20px;
  margin-left: 5px;
}

#nav .green {
  width: 40%;
  height: 16px;
}

#content {
  display: flex;
}

#content > div {
  margin: 10px 5px;
}

#content > :first-child {
  width: 30%;
}

#content > :last-child {
  width: 70%;
}

#left1 {
  height: 40px;
  margin-bottom: 10px;
}

#left2 {
  height: 80px;
}

#actual {
  height: 130px;
}

#left1 .green {
  height: 20px;
}

#left2 .green {
  height: 40px;
}

#actual .green {
  height: 50%;
  resize: both;
  overflow: auto;
}
<div id="nav" class="red">
  <div class="green"></div>
</div>

<div id="content">
  <div>
    <div id="left1" class="red">
      <div class="green"></div>
    </div>
    <div id="left2" class="red">
      <div class="green"></div>
    </div>
  </div>
  <div>
    <div id="actual" class="red">
      <div class="green"></div>
    </div>
  </div>
</div>

В данном случае решение просто покрывает весь задний фон элемента и динамически изменяется, есть ещё одно решение в котором изображение не искажается, но при этом уже задний фон будет замениться чёрным цветом:

class BackgroundCover {
  constructor(selector) {
    this.el = document.querySelector(selector)
  }

  __setProperties(element) {
    let bcr = this.el.getBoundingClientRect()
    let img = element.children[0]
    element.style.cssText = `
    position: absolute;
    background-color: rgb(0, 0, 0);
    left: ${bcr.left}px;
    top: ${bcr.top}px;
    width: ${bcr.width}px;
    height: ${bcr.height}px;
    z-index: -1;
    overflow: hidden;`
    img.style.left = 'calc(50% - ' + img.offsetWidth / 2 + 'px)'
  }

  init(src) {
    let bg = document.createElement('div')
    let img = document.createElement('img')

    img.src = src
    img.style.height = '100%'
    img.style.position = 'relative'
    img.onload = function() {
      img.style.left = 'calc(50% - ' + img.offsetWidth / 2 + 'px)'
    }
    bg.appendChild(img)

    this.__setProperties(bg)

    document.querySelector('body').appendChild(bg)

    let obs = new MutationObserver(() => {
      this.__setProperties(bg)
    })

    obs.observe(this.el ,{
      attributes: true,
      subtree: true,
      childList: true,
    })

    for (let i = 0; i < 2; i++) {
      (i < 1 ? window : this.el).addEventListener('resize', () => {this.__setProperties(bg)})
    }
  }
}

let selectors = ['#actual', '#left1', '#left2', '#nav']
let images = [
  'https://i.bcow.xyz/fMGVpco_d.webp',
  'https://i.bcow.xyz/6vy9VVS_d.webp',
  'https://i.bcow.xyz/lumjqf8_d.webp',
  'https://i.bcow.xyz/6lhW59S_d.webp',
]

for (let i = 0; i < images.length; i++) {
  new BackgroundCover(selectors[i] + ' .green')
    .init(images[i])
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.red {
  padding: 2px;
  outline: 2px solid #ea0505;
}

.green {
  padding: 2px;
  outline: 2px solid #05ea05;
}

.green, .red {
  filter: drop-shadow(rgb(0, 0, 0) 0 0 3px);
  display: block;
}

body {
  padding: 5px;
}

#nav {
  width: calc(30% - 6px);
  height: 20px;
  margin-left: 5px;
}

#nav .green {
  width: 40%;
  height: 16px;
}

#content {
  display: flex;
}

#content > div {
  margin: 10px 5px;
}

#content > :first-child {
  width: 30%;
}

#content > :last-child {
  width: 70%;
}

#left1 {
  height: 40px;
  margin-bottom: 10px;
}

#left2 {
  height: 80px;
}

#actual {
  height: 130px;
}

#left1 .green {
  height: 20px;
}

#left2 .green {
  height: 40px;
}

#actual .green {
  height: 50%;
  resize: both;
  overflow: auto;
}
<div id="nav" class="red">
  <div class="green"></div>
</div>

<div id="content">
  <div>
    <div id="left1" class="red">
      <div class="green"></div>
    </div>
    <div id="left2" class="red">
      <div class="green"></div>
    </div>
  </div>
  <div>
    <div id="actual" class="red">
      <div class="green"></div>
    </div>
  </div>
</div>

Очень надеюсь что я вас правильно понял и не писал всё зря)

Если возникли вопросы по коду, обязательно пишите их в комментариях.

→ Ссылка