В предыдущей статье мы создали классическую шестиугольную сетку. Это была реактивная реализация без использования медиа-запросов. Задача заключалась в том, чтобы улучшить подход пятилетней давности с помощью современного CSS.
Поддержка ограничена только Chrome, поскольку эта технология использует недавно выпущенные функции. corner-shape, sibling-index()и подразделение подразделений.
В этой статье мы рассмотрим другой тип сетки: пирамидальную. Мы по-прежнему работаем с шестиугольными формами, но организация элементов другая.
Демо из тысячи слов:
Для лучшей визуализации откройте полностраничный просмотр демо-версии, чтобы увидеть структуру пирамиды. При изменении размера экрана вы получаете адаптивное поведение: нижняя часть начинает вести себя аналогично сетке, которую мы создали в предыдущей статье!

Это здорово, не так ли? Все это было создано без единого медиа-запроса, JavaScript или большого количества хакерского CSS. Вы можете вырезать столько элементов, сколько захотите, и все идеально подойдет.
Прежде чем мы начнем, сделайте себе одолжение и прочтите предыдущую статью, если вы еще этого не сделали. Я опущу некоторые вещи, которые уже объяснял там, например, как создаются фигуры, а также некоторые формулы, которые я буду использовать здесь повторно. Как и в предыдущей статье, реализация Pyramid Grid является усовершенствованием подхода пятилетней давности, поэтому, если вы хотите провести сравнение между 2021 и 2026 годами, ознакомьтесь также с этой более старой статьей.
начальная конфигурация
На этот раз мы будем полагаться на CSS Grid вместо Flexbox. Благодаря такой структуре легче контролировать размещение элементов внутри столбцов и строк, а не регулировать поля.
.container {
--s: 40px; /* size */
--g: 5px; /* gap */
display: grid;
grid-template-columns: repeat(auto-fit, var(--s) var(--s));
justify-content: center;
gap: var(--g);
}
.container > * {
grid-column-end: span 2;
aspect-ratio: cos(30deg);
border-radius: 50% / 25%;
corner-shape: bevel;
margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg)));
}
Я использую классику снова и снова auto-fit Создать столько столбцов, сколько позволяет свободное пространство. Для предметов это тот же код из предыдущей статьи для создания шестиугольных фигур.
ты написал
var(--s)дважды. Это опечатка?
это! Я хочу, чтобы в моей сетке всегда было четное количество столбцов, где каждый элемент занимал два столбца (вот почему я использую grid-column-end: span 2). С помощью такой конфигурации я могу легко контролировать передачу между разными линиями.

Выше приведен снимок экрана DevTools, показывающий структуру сетки. Если, например, элемент 2 охватывает столбцы 3 и 4, элемент 4 должен охватывать столбцы 2 и 3, элемент 5 должен охватывать столбцы 4 и 5 и так далее.
Та же логика применима и к реактивной части. Каждый первый элемент каждой второй строки сдвигается на один столбец и начинается со второго столбца.

При такой конфигурации размер элемента будет равен 2*var(--s) + var(--g). По этой причине отрицательное нижнее поле отличается от предыдущего примера.
Итак, вместо:
margin-bottom: calc(var(--s)/(-4*cos(30deg)));
…Я использую:
margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg)));
Пока ничего особенного, но 80% кода у нас уже есть. Хотите верьте, хотите нет, но мы находимся всего в одном объекте от завершения всей сетки. нам просто нужно это настроить grid-column-start Определенные элементы необходимо разместить в правильном месте, и, как вы уже догадались, здесь начинается самая сложная часть, связанная со сложными расчетами.
пирамидальная сетка
Предположим, что контейнер достаточно велик, чтобы вместить пирамиду со всеми ее элементами. Другими словами, мы пока проигнорируем реактивную часть. Давайте проанализируем структуру и попробуем выявить закономерности:

Независимо от количества объектов, структура в некоторой степени устойчива. Элементы слева (т. е. первый элемент каждой строки) всегда одни и те же (1, 2, 4, 7, 11 и т. д.). Тривиальное решение — нацелиться на них, используя :nth-child() Селектор.
:nth-child(1) { grid-column-start: ?? }
:nth-child(2) { grid-column-start: ?? }
:nth-child(4) { grid-column-start: ?? }
:nth-child(7) { grid-column-start: ?? }
:nth-child(11) { grid-column-start: ?? }
/* etc. */
Все эти посты взаимосвязаны. Если элемент 1 помещен в столбец xПоэтому пункт 2 следует поместить в столбец x - 1позиция в графе 4 x - 2и т. д.
:nth-child(1) { grid-column-start: x - 0 } /* 0 is not need but useful to see the pattern*/
:nth-child(2) { grid-column-start: x - 1 }
:nth-child(4) { grid-column-start: x - 2 }
:nth-child(7) { grid-column-start: x - 3 }
:nth-child(11) { grid-column-start: x - 4 }
/* etc. */
Элемент 1 логически располагается посередине, поэтому если наша сетка содержит N столбец, затем x равно N/2: :
:nth-child(1) { grid-column-start: N/2 - 0 }
:nth-child(2) { grid-column-start: N/2 - 1 }
:nth-child(4) { grid-column-start: N/2 - 2 }
:nth-child(7) { grid-column-start: N/2 - 3 }
:nth-child(11){ grid-column-start: N/2 - 4 }
А поскольку каждый элемент занимает два столбца, N/2 Его также можно рассматривать как количество предметов, помещающихся в контейнер. Итак, давайте обновим нашу логику и рассмотрим N Наличие количества элементов вместо количества столбцов.
:nth-child(1) { grid-column-start: N - 0 }
:nth-child(2) { grid-column-start: N - 1 }
:nth-child(4) { grid-column-start: N - 2 }
:nth-child(7) { grid-column-start: N - 3 }
:nth-child(11){ grid-column-start: N - 4 }
/* etc. */
Для расчета количества предметов я буду использовать ту же формулу, что и в предыдущей статье:
N = round(down, (container_size + gap)/ (item_size + gap));
Единственное отличие состоит в том, что форма объекта больше не является var(--s)Но 2*var(--s) + var(--g)Это дает нам следующий CSS:
.container {
--s: 40px; /* size */
--g: 5px; /* gap */
container-type: inline-size; /* we make it a container to use 100cqw */
}
.container > * {
--_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
}
.container > *:nth-child(1) { grid-column-start: calc(var(--_n) - 0) }
.container > *:nth-child(2) { grid-column-start: calc(var(--_n) - 1) }
.container > *:nth-child(4) { grid-column-start: calc(var(--_n) - 2) }
.container > *:nth-child(7) { grid-column-start: calc(var(--_n) - 3) }
.container > *:nth-child(11){ grid-column-start: calc(var(--_n) - 4) }
/* etc. */
Оно работает! У нас пирамидальная структура. Он еще не реагирует, но мы доберемся до этого. Кстати, если ваша цель — создать такую структуру с фиксированным количеством объектов и вам не нужно реактивное поведение, то вышеописанное идеально, и все готово!
Как все предметы были размещены правильно? Мы определили столбцы только для некоторых элементов и не указали ни одной строки!
В этом сила алгоритма автоматического размещения CSS Grid. Когда вы определяете столбец для элемента, следующий элемент автоматически размещается после него! Нам не нужно вручную указывать набор столбцов и строк для всех элементов.
улучшение реализации
Тебе не нравится это словоблудие :nth-child() Селектор, да? Я тоже, так что давайте удалим их и сделаем лучшую реализацию. Такая пирамида хорошо известна в мире математики, и у нас есть нечто, называемое треугольными числами, которые я собираюсь использовать. Не волнуйтесь, я не буду начинать курс математики, поэтому вот формула, которую я буду использовать:
j*(j + 1)/2 + 1 = index
…Где j является положительным целым числом (включая ноль).
В принципе все :nth-child Может быть сгенерировано с использованием следующего псевдокода:
for(j = 0; j< ?? ;j++) {
:nth-child(j*(j + 1)/2 + 1) { grid-column-start: N - j }
}
В CSS нет циклов, поэтому я буду следовать той же логике, что и в предыдущей статье (надеюсь, вы прочитали, иначе немного запутаетесь). я выражаю j Использование индекса. Я решил предыдущую формулу, которая представляет собой квадратное уравнение, но я уверен, что вы не хотите углубляться в эту математику.
j = sqrt(2*index - 1.75) - .5
Мы можем получить индекс, используя sibling-index() Праздник. Логика заключается в проверке каждого элемента, если sqrt(2*index - 1.75) - .5 является положительным целым числом.
.container {
--s: 40px; /* size */
--g: 5px; /* gap */
container-type: inline-size; /* we make it a container to use 100cqw */
}
.container > * {
--_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
--_j: calc(sqrt(2*sibling-index() - 1.75) - .5);
--_d: mod(var(--_j),1);
grid-column-start: if(style(--_d: 0): calc(var(--_n) - var(--_j)););
}
Когда --_d равно переменной 0это означает, что --_j является целым числом; И когда это происходит, я устанавливаю столбец на N - j. Если мне не нужно тестировать --_j Позитивное, потому что оно всегда позитивное. Наименьшее значение индекса равно 1, поэтому наименьшее значение равно --_j Является 0.
Тада! мы изменили все :nth-child() Селектор с тремя строками CSS, охватывающий любое количество элементов. Давайте сделаем его отзывчивым прямо сейчас!
ответственное поведение
В своей статье 2021 года я переключался между сеткой пирамиды и классической сеткой в зависимости от размера экрана. На этот раз я сделаю что-то другое. Я буду продолжать создавать пирамиды, пока это не станет возможным, а затем это изменится на классическую сетку.

Пункты с 1 по 28 образуют пирамиду. После этого у нас получится та самая классическая сетка, которую мы создали в предыдущей статье. Нам нужно выбрать первый элемент определенных строк (29, 42 и т. д.) и переместить их. На этот раз мы не собираемся устанавливать поля слева, но нам нужно установить их поля. grid-column-start значение для 2.
Как обычно, мы определяем формулу объектов, выражаем ее с помощью индексов, а затем проверяем, является ли результат положительным целым числом:
N*i + (N - 1)*(i - 1) + 1 + N*(N - 1)/2 = index
так:
i = (index - 2 + N*(3 - N)/2)/(2*N - 1)
Когда? i является положительным целым числом (кроме нуля), мы устанавливаем столбец в начало 2.
.container {
--s: 40px; /* size */
--g: 5px; /* gap */
container-type: inline-size; /* we make it a container to use 100cqw */
}
.container > * {
--_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
/* code for the pyramidal grid */
--_j: calc(sqrt(2*sibling-index() - 1.75) - .5);
--_d: mod(var(--_j),1);
grid-column-start: if(style(--_d: 0): calc(var(--_n) - var(--_j)););
/* code for the responsive grid */
--_i: calc((sibling-index() - 2 + (var(--_n)*(3 - var(--_n)))/2)/(2*var(--_n) - 1));
--_c: mod(var(--_i),1);
grid-column-start: if(style((--_i > 0) and (--_c: 0)): 2;);
}
Против --_j Переменная, мне нужно проверить, если --_i Это положительное значение, поскольку для некоторых значений индекса оно может быть отрицательным. По этой причине у меня есть дополнительное условие по сравнению с первым.
Но подождите! Это совсем нехорошо. мы объявляем grid-column-start Дважды, поэтому будет использоваться только один из них. У нас должно быть только одно объявление, и для этого мы можем объединить оба условия, используя if() заявление:
grid-column-start:
if(
style((--_i > 0) and (--_c: 0)): 2; /* first condition */
style(--_d: 0): calc(var(--_n) - var(--_j)); /* second condition */
);
Если первое условие истинно (адаптивная сетка), мы устанавливаем значение 2; В противном случае, если второе условие истинно (пирамидальная сетка), устанавливаем значение calc(var(--_n) - var(--_j)); В противном случае мы ничего не делаем.
Почему такой особый порядок?
Потому что адаптивные сетки должны иметь более высокий приоритет. Проверьте изображение ниже:

Элемент 29 является частью пирамидальной сетки, поскольку он является первым элементом в своей строке. Это означает, что условие пирамиды всегда будет выполняться для этого объекта. Но когда сетка становится реактивной, этот элемент становится частью реактивной сетки, и второе условие также верно. Когда оба условия истинны, ответственное условие должно победить; Вот почему это первое условие, которое мы проверяем.
Давайте посмотрим на это в игре:
Упс! Пирамида выглядит хорошо, но дальше все становится не так.
Чтобы понять, что происходит, давайте посмотрим конкретно на пункт 37. Если вы рассмотрите предыдущий рисунок, то увидите, что он является частью пирамидальной структуры. Таким образом, даже если сетка становится реактивной, ее условие остается истинным, и она получает значение столбца из формулы. calc(var(--_n) - var(--_j)) Это нехорошо, потому что мы хотим сохранить значение по умолчанию для автоматического размещения. Так происходит со многими вещами, поэтому нам нужно их исправить.
Чтобы найти решение, давайте посмотрим, как ведут себя ценности в пирамиде. Все они следуют этой формуле N - jГде? j является положительным целым числом. Если, например, N равно 10, получаем:
10, 9, 8, 7, ... ,0, -1 , -2
В некоторые моменты значения становятся отрицательными, и, поскольку отрицательные значения допустимы, эти объекты будут размещаться случайным образом, нарушая сетку. Нам нужно убедиться, что отрицательные значения игнорируются и вместо них используется значение по умолчанию.
Мы используем следующее, чтобы сохранить только положительные значения и преобразовать все отрицательные значения в ноль:
max(0, var(--_n) - var(--_j))
мы устанавливаем 0 В качестве минимального порога (подробнее об этом здесь) значения становятся:
10, 9, 8, 7, ... , 0, 0, 0, 0
Мы либо получаем положительное значение для столбца, либо получаем 0.
Но вы сказали, что значение должно быть по умолчанию, нет.
0.
да, но 0 имеет недопустимое значение для grid-column-startтак что используя 0 Это означает, что браузер проигнорирует его и вернется к значению по умолчанию!
Наш новый код:
grid-column-start:
if(
style((--_i > 0) and (--_c: 0)): 2; /* first condition */
style(--_d: 0): max(0,var(--_n) - var(--_j)); /* second condition */
);
И это работает!
Вы можете добавить столько элементов, сколько захотите, изменить размер экрана, и все будет идеально!
больше примеров
Хватит кода и математики! Давайте насладимся новыми вариациями, используя разные формы. Я позволю вам проанализировать код в качестве домашнего задания.
ромбическая сетка
В следующих трех демонстрациях вы увидите немного разные подходы к определению расстояния между элементами.
восьмиугольная сетка
круговая сетка
И еще одна шестиугольная сетка:
заключение
Помните, я говорил вам, что нам остался один объект до завершения сетки? Он собственность(grid-column-start) Нам буквально понадобилась целая статья для обсуждения! Это показывает, что CSS эволюционировал и требует нового мышления для работы с ним. CSS больше не является языком, в котором вы просто устанавливаете постоянные значения. color: red, margin: 10px, display: flexи т. д.
Теперь мы можем определить динамическое поведение посредством сложных вычислений. Это полный процесс мышления, поиска формул, определения переменных, создания условий и т. д. В этом нет ничего нового, поскольку я смог сделать это и в 2021 году. Однако теперь у нас есть надежные функции, которые позволяют нам использовать меньше хакерского кода и более гибкие реализации.