Задача. Описать изменение значения CSS-свойства как функцию от ширины вьюпорта без использования медиа-запросов. Результатом работы миксина должна быть единственная строка вида <свойство>: <функция от ширины вьюпорта >. В качестве входных данных имеются заданные (табулированные) точки (ширина вьюпорта, значение свойства). Поведение CSS-свойства от точки к точке аппроксимируется прямой линией.
В сети достаточно много разных способов решений для частных случаев (см., например, https://habr.com/ru/post/501392/). Здесь же предлагается общее решение задачи.
Например, нужно описать такое «мифическое» поведение свойства margin-left, как приведено на рисунке:
Как видно, имеется несколько диапазонов, где значение может то линейно увеличиваться, то уменьшаться, то изменяться скачкообразно или оставаться постоянным. Для описания такого сложного поведения вызывается SCSS-миксин (код приведен ниже) такой строкой:
@include adaptiveValueModified("margin-left", 320, 5, 575, 10, 576, -10, 767, -10, 768, 20, 992, 40,
1199, 30, 1200, 50, 1920, 60);
а в файле стилей «на выходе» будет всего одна (хоть и длинная) строка:
margin-left: calc(min(0.625rem, -0.07966rem + 1.96078vw)
+ clamp(-0.625rem, 719.375rem + -2000vw, 0.625rem)
+ clamp(-0.625rem, -1438.75rem + 3000vw, 1.25rem)
+ clamp(1.25rem, -3.03571rem + 8.92857vw, 2.5rem)
+ clamp(1.875rem, 5.49517rem + -4.83092vw, 2.5rem)
+ clamp(1.875rem, -1496.875rem + 2000vw, 3.125rem)
+ max(3.125rem, 2.08333rem + 1.38889vw) - 8.75rem);
Очевидно, что чем сложнее (больше диапазонов) поведение свойства, тем длиннее будет и функция-результат, и наоборот.
Ограничения. Таким образом (на данный момент) можно адаптировать только те свойства, значения которых задаются в единицах длины. Приведенная реализация миксина «принимает» на входе значения, заданные в пикселях (без указания “px”), а результат выводит в относительных единицах rem.
Ниже приведен код миксина и вспомогательных функций.
// Вспомогательная функция для миксина adaptiveValueModified
@function defineLineParameters($breakPoints, $i) {
// Определяет параметры линии, заданной парой точек {x1, y1} и {x2, y2}
// Точки берутся из списка $breakPoints, который имеет вид (x1, y1, x2, y2, ..., xN, yN)
// Индекс $i задает выбор начальной точки - $x1
// Результат - список параметров (наклон, точка пересечения оси Y, формула прямой)
$x1: nth($breakPoints, 2 * $i - 1);
$x2: nth($breakPoints, 2 * $i + 1);
$y1: nth($breakPoints, 2 * $i);
$y2: nth($breakPoints, 2 * ($i + 1));
$slope: ($y2 - $y1) / ($x2 - $x1);
$yIntersection: -($x1 * $slope) + $y1;
$flyValue: 0; // Оптимизация $flyValue - избавление от члена вида 0 * vw
@if ($slope == 0) {
$flyValue: #{rem($yIntersection)};
} @else {
@if ($yIntersection != 0) {
$flyValue: #{rem($yIntersection)} + #{$slope * 100}vw;
} @else {
$flyValue: #{$slope * 100}vw;
}
}
$lineParameters: ();
$lineParameters: append($lineParameters, $slope);
$lineParameters: append($lineParameters, $yIntersection);
$lineParameters: append($lineParameters, $flyValue);
@return $lineParameters;
}
@function rem($px) {
$result: $px / 16 + rem;
@return $result;
}
// Миксин, основной блок
@mixin adaptiveValueModified($property, $breakPoints...) {
// Миксин, который воспроизводит произвольное заданное пользователем поведение (изменение) значения свойства.
//
// Поведение задается (табулируется) контрольными точками ($breakpoints).
// Вывод в CSS -- в относительных единицах rem.
//
// Параметры:
// $property - свойство, которое нужно адаптировать ("font-size", "margin-top", "padding", ...)
//
// $breakPoints -- список точек определяющих поведение свойства $property в формате
// $значениеШириныВьюпорта1, $значениеСвойства1, ..., $значениеШириныВьюпортаN, $значениеСвойстваN.
// Величины задаются в пикселях без указания единицы измерения.
// $значениеШириныВьюпорта задаются строго в возрастающем порядке (положительные числа),
// равных значений быть не должно:
// $значениеШириныВьюпорта1 < $значениеШириныВьюпорта2 < ... < $значениеШириныВьюпортаN
// Ограничений на $значениеСвойства нет -- любое положительное или отрицательное число, и ноль.
//
// Примеры:
// adaptiveValueModified("font-size", 320, 20, 900, 30, 1600, 40) --
// на 320px значение "font-size" равно 20px, на 900px - увеличивается до 30px, на 1600px - до 40px,
// больше 1600px - продолжает расти (как и на участке от 900 до 1600px), меньше 320px -- продолжает падать
// (как и на участке от 900 до 320px).
//
// adaptiveValueModified("margin-top", 320, 5, 991, 5, 992, 20, 1600, 40, 1700, 40) --
// на 320px значение "margin-top" равно 5px, до 991px - остается равным 5px, при 992px - скачкообразно возрастает
// до 20px, на 1600px - увеличивается до 40px, больше 1600px - остается постоянным и равно 40px.
//
// Если ширина вьюпорта меньше, чем $значениеШириныВьюпорта1, то поведение свойства аппроксимируется
// линией с отрезка [$значениеШириныВьюпорта1, $значениеШириныВьюпорта2]; если больше, чем $значениеШириныВьюпортаN,
// то линией с отрезка [$значениеШириныВьюпорта(N-1), $значениеШириныВьюпортаN].
$breakPointsLength: length($breakPoints); // Длина аргумента $breakPoints (должна быть парным числом)
$breakPointsPair: $breakPointsLength / 2; // Количество точек, задающих поведение свойства $property
$breakPointsEven: $breakPointsPair - round($breakPointsPair);
@if ($breakPointsEven != 0) {
// Проверка на парность длины аргумента $breakPoints
@error "Список параметров слишком краток -- возможно задано непарное количество точек.";
}
@if ($breakPointsLength < 4) {
// Проверяем, чтоб имелось минимальное количество аргументов при вызове миксина. Если нет --
// выводим ошибку
@error "Список параметров слишком краток, не хватает точек для определения поведения свойства!";
} @else {
@if ($breakPointsPair == 2) {
// Если заданы только две пары точек - моментальный вывод в CSS
$slope: (nth($breakPoints, 4) - nth($breakPoints, 2)) / (nth($breakPoints, 3) - nth($breakPoints, 1));
$yIntersection: -(nth($breakPoints, 1) * $slope) + nth($breakPoints, 2);
$flyValue: #{rem($yIntersection)} + #{$slope * 100}vw;
$propertyValue: "";
@if ($slope == 0) {
$propertyValue: rem($yIntersection);
} @else {
@if ($yIntersection == 0) {
$propertyValue: #{$slope * 100}vw;
} @else {
$propertyValue: #{"calc(" + $flyValue + ")"};
}
}
#{$property}: $propertyValue;
} @else if ($breakPointsPair == 3) {
// Если заданы только три пары точек - оптимизируем результат, вывод в CSS
$slopeList: ();
$flyValueList: ();
@for $i from 1 through 2 {
$lineParameters: defineLineParameters($breakPoints, $i);
$slope: nth($lineParameters, 1);
$yIntersectionList: nth($lineParameters, 2);
$flyValue: nth($lineParameters, 3);
$slopeList: append($slopeList, $slope);
$flyValueList: append($flyValueList, $flyValue);
}
$propertyValue: #{nth($flyValueList, 1)};
@if (nth($slopeList, 2) > nth($slopeList, 1)) {
$propertyValue: #{"max(" + $propertyValue + ", " + nth($flyValueList, 2) + ")"};
} @else {
$propertyValue: #{"min(" + $propertyValue + ", " + nth($flyValueList, 2) + ")"};
}
#{$property}: $propertyValue;
} @else {
// Количество пар данных больше трех
// Вычисление константы
$cumul: 0;
@for $i from 2 through ($breakPointsPair - 1) {
$cumul: $cumul + nth($breakPoints, 2 * $i);
}
// Основной блок
$propertyValue: "";
$propertyValueList: ();
@for $i from 1 through ($breakPointsPair - 1) {
$lineParameters: defineLineParameters($breakPoints, $i);
$y1: nth($breakPoints, 2 * $i);
$y2: nth($breakPoints, 2 * ($i + 1));
$slope: nth($lineParameters, 1);
$yIntersection: nth($lineParameters, 2);
$flyValue: nth($lineParameters, 3);
@if ($slope == 0) {
// Значение свойства НЕ изменяется на участке $x1, $x2
$cumul: $cumul - $yIntersection;
} @else {
// Значение свойства изменяется на участке $x1, $x2
@if ($i == 1) {
// Обработка первой записи в результат
@if ($slope > 0) {
$propertyValue: #{"min(" + rem($y2) + ", " + $flyValue + ")"};
} @else {
$propertyValue: #{"max(" + rem($y2) + ", " + $flyValue + ")"};
}
$propertyValueList: append($propertyValueList, $propertyValue);
} @else if ($i == ($breakPointsPair - 1)) {
// Обработка последней записи в результат
@if ($slope > 0) {
$propertyValue: #{"max(" + rem($y1) + ", " + $flyValue + ")"};
} @else {
$propertyValue: #{"min(" + rem($y1) + ", " + $flyValue + ")"};
}
$propertyValueList: append($propertyValueList, $propertyValue);
} @else {
// Последующие записи в результат
$propertyValue: #{"clamp(" + rem(min($y1, $y2)) + ", " + $flyValue + ", " + rem(max($y1, $y2)) + ")"};
$propertyValueList: append($propertyValueList, $propertyValue);
}
}
}
// Формирование результата работы
$propertyValue: nth($propertyValueList, 1);
@if (length($propertyValueList) != 1) {
@for $i from 2 through length($propertyValueList) {
$propertyValue: #{$propertyValue + " + " + nth($propertyValueList, $i)};
}
}
@if ($cumul > 0) {
$propertyValue: #{"calc(" + $propertyValue + " - " + rem(max($cumul, -$cumul)) + ")"};
} @else if($cumul < 0) {
$propertyValue: #{"calc(" + $propertyValue + " + " + rem(max($cumul, -$cumul)) + ")"};
}
#{$property}: $propertyValue;
}
}
}
Из очевидных минусов данного подхода выделю один - код становится менее читабельным, из плюсов - медиа-запросы остаются исключительно для описания логики адаптива, структурных изменений, а не засоряются "техническими" строками изменения размеров шрифтов, отступов, и т.д.
Всем удачи и успехов.
Jack_Rabbit
Это все чудесно, но зачем? Качество кода определяется не количеством строчек, а в первую очередь его читаемостью и простотой.
В процессе написания вам может казаться, что тут все логично и понятно, но другой разработчик, который будет править этот код через пару месяцев, вероятно, будет весьма сильно огорчен и захочет узнать какой же прекрасный человек это написал. И да, этим разработчиком можете оказаться вы.
Что касается самой задачи, то можно использовать для этого CSS переменные (CSS custom properties) и менять их значение в зависимости от ширины экрана или любых других условий. Таким образом вы можете более гибко настраивать поведение кучи свойств в одном месте и вам не придется каждый раз писать длинную строку со значениями.
ryevych Автор
Минусы понятны, я об этом писал и полностью согласен с вами. Хотелось показать, что такое "в принципе" возможно.
А насчёт решения с помошью CSS- переменных, разве в таком случае медиа-запросы не возникнут (хотя бы на этапе инициализации переменных)? Плюс проблемы с анимацией свойств. Здесь, лично для меня, фишка именно в том, что поведение реализуется без медиа-запросов вообще.