Адаптивное свойство одной строкой

от автора

Задача. Описать изменение значения 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;       }    } }  

Из очевидных минусов данного подхода выделю один — код становится менее читабельным, из плюсов — медиа-запросы остаются исключительно для описания логики адаптива, структурных изменений, а не засоряются «техническими» строками изменения размеров шрифтов, отступов, и т.д.

Всем удачи и успехов.


ссылка на оригинал статьи https://habr.com/ru/post/646089/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *