Как автоматически выводить изображения в строке в 2 или 3 колонки в зависимости от их пропорций (соотношения сторон)?

Здравствуйте. У меня есть простая галерея на React с динамически загружаемыми изображениями двух ориентировок (условно — горизонтальные и вертикальные). Требуется следующая логика раскладки:
  • 20 июля 2026 г. 19:00
Ответы на вопрос 1
Нужно уточнить только одно: какую именно «логику раскладки» вы хотите? Чаще всего задают одно из двух правил:

- Правило A (самое популярное): «горизонтальные» картинки занимают 2 колонки, «вертикальные» — 1 колонку; всего в ряду 3 колонки (т.е. 2+1 или 1+1+1 и т.д.).  
- Правило B: в узком экране — 2 колонки, в широком — 3 колонки (и ориентация влияет на то, сколько колонок занимает картинка).

Ниже дам два рабочих варианта реализации в React — короткий и надёжный через CSS Grid (проще) и более детерминированный (упаковка в строки) через JS + flex (если вам важен точный контроль, чтобы ряды всегда полностью заполнялись).

1) Вариант через CSS Grid (просто и быстро)
- Идея: узнаём соотношение сторон у каждой картинки (onLoad: naturalWidth / naturalHeight), назначаем span = 2 для landscape и span = 1 для portrait, рендерим в grid с grid-column: span N.

Пример:

JSX (React):
```
function Gallery({ images }) {
  const [items, setItems] = React.useState(
    images.map(src => ({ src, span: 1 }))
  );

  const onLoad = (index, e) => {
    const ar = e.target.naturalWidth / e.target.naturalHeight;
    const span = ar > 1.0 ? 2 : 1; // порог можно менять
    setItems(prev => {
      const copy = [...prev];
      copy[index] = { ...copy[index], span };
      return copy;
    });
  };

  return (
    <div className="grid">
      {items.map((it, i) => (
        <div key={i} className="item" style={{ gridColumn: `span ${it.span}` }}>
          <img src={it.src} onLoad={(e) => onLoad(i, e)} alt="" />
        </div>
      ))}
    </div>
  );
}
```

CSS:
```
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 3 колонки */
  gap: 8px;
  align-items: stretch;
}
.item img {
  width: 100%;
  height: 100%;
  object-fit: cover; /* или contain, в зависимости от желаемого кадрирования */
  display: block;
}
```

Пояснения:
- Горизонтальные картинки займут 2 из 3 колонок (span:2), вертикальные — 1.
- На мобильном можно менять сетку через медиазапросы (например repeat(2,1fr)) и соответственно подстраивать порог/логики.
- Этот вариант максимально прост и использует нативный поток grid. Минус: ряды могут быть «неплотными» (оставаться пустые ячейки), но визуально обычно выглядит нормально.

2) Вариант с «жёсткой» упаковкой (greedy packing) — если нужно, чтобы каждая строка всегда заполнялась комбинациями span=2/1
- Идея: заранее группируем элементы в строки (row) так, чтобы суммарные span в строке были равны колонкам (например 3). Алгоритм — жадный: добавляем элемент в текущую строку, если суммарный span <= cols, иначе стартуем новую строку.

Пример:

JS:
```
function buildRows(items, cols = 3) {
  const rows = [];
  let cur = [];
  let sum = 0;
  for (const it of items) {
    const span = it.span || 1;
    if (sum + span <= cols) {
      cur.push(it);
      sum += span;
    } else {
      // если элемент сам больше cols (маловероятно), можно принудительно положить в новую строку
      rows.push({ items: cur, sum });
      cur = [it];
      sum = span;
    }
  }
  if (cur.length) rows.push({ items: cur, sum });
  return rows;
}
```

Рендер (React):
```
function PackedGallery({ images }) {
  const [items, setItems] = React.useState(
    images.map(src => ({ src, span: 1 }))
  );
  const rows = buildRows(items, 3);

  const onLoad = (index, e) => {
    const ar = e.target.naturalWidth / e.target.naturalHeight;
    const span = ar > 1.0 ? 2 : 1;
    setItems(prev => {
      const copy = [...prev];
      copy[index] = { ...copy[index], span };
      return copy;
    });
  };

  return (
    <div className="packed">
      {rows.map((row, ri) => (
        <div className="row" key={ri}>
          {row.items.map((it, i) => {
            const span = it.span || 1;
            const widthPercent = (span / 3) * 100;
            return (
              <div className="cell" key={i} style={{ flex: `0 0 ${widthPercent}%` }}>
                <img src={it.src} onLoad={(e) => onLoad(/* index mapping needed */)} />
              </div>
            );
          })}
        </div>
      ))}
    </div>
  );
}
```

CSS:
```
.row {
  display: flex;
  gap: 8px;
}
.cell img { width: 100%; height: auto; display: block; }
```

Пояснения и тонкости:
- В варианте выше надо сопоставлять индекс картинки при onLoad — можно хранить индекс в item или замыкание.
- Если суммарные span в строке меньше cols (например остался 1 из 3), можно:
  - растянуть колонку(ы) — увеличить width остальных элементов в строке (пропорционально), либо
  - оставить пустое пространство.
- Для избежания «скачков» layout (CLS) лучше задавать width/height или атрибуты size для <img> заранее (или заранее знать aspect ratio и задавать контейнеру aspect-ratio), тогда grid/rows не перескакивают при загрузке.

Дополнительные советы
- Порог aspect-ratio можно варьировать (напр., >1.2 считать горизонтальной) если хотите отделять слегка квадратные изображения.
- Для сложных красивых шаблонов можно использовать библиотеки Masonry / CSS masonry (experimental) / Packery, но нативный grid + span/packing покрывают 95% задач.
- Для адаптивности используйте медиазапросы: на <600px сделайте grid-template-columns: repeat(2,1fr) и пересчитайте span (landscape = 2 -> займёт всю строку).

Если хотите, я могу:
- показать полный рабочий пример с корректным сопоставлением индексов onLoad -> items (с pasteable кодом),
- или реализовать именно ту логику, которую вы имели в виду (опишите правило: кто сколько колонок занимает и как должно выглядеть поведение при остатках).
Похожие вопросы