Начинаем работать с CSS функцией calc()

Добавлено 15 декабря 2015 в 22:40

Впервые я обнаружила функцию calc() более четырех лет назад, спасибо CSS3 Click Chart, и была рада увидеть, что основные математические функции – сложение, вычитание, умножение и деление – нашли свое применение в CSS.

Множество людей думает, что препроцессоры полностью покрывают область логики и вычислений, но функция calc() может кое-что из того, что препроцессорам недоступно: смешивать любые типы единиц измерения. Препроцессоры могут смешивать только единицы, между которыми есть фиксированная связь, например, единицы измерения углов, единицы времени, единицы частоты и определенные единицы длины.

1turn всегда равен 360deg, 100grad всегда равны 90deg, а 3.14rad всегда равно 180deg. 1s это всегда 1000ms, а 1kHz это всегда 1000Hz. 1in всегда равен 2.54cm или 25.4mm, или 96px, а 1dppx всегда соответствует 96dpi. Вот почему препроцессоры могут конвертировать их между собой и смешивать их в вычислениях. Тем не менее, препроцессоры не могут решить, сколько в пикселях будут равны 1em или 1% или 1ch, так как им не хватает контекста.

Давайте посмотрим на несколько базовых примеров:

div {
   font-size: calc(3em + 5px);
   padding: calc(1vmax + -1vmin);
   transform: rotate(calc(1turn - 32deg));
   background: hsl(180, calc(2*25%), 65%); 
   line-height: calc(8/3);
   width: calc(23vmin - 2*3rem);
}

В некоторых случаях мы могли бы использовать переменные в функции calc(). Это вполне возможно с наиболее популярными препроцессорами.

Во-первых, Sass:

$a: 4em
height: calc(#{$a} + 7px)

LESS:

@a: 4em;
height: ~"calc(@{a} + 7px)";

И Stylus:

a = 4em
height: "calc(%s + 7px)" % a

Также мы можем использовать собственные переменные CSS, но обратите внимание, что на данный момент это работает только в Firefox 31+, остальные браузеры пока не поддерживают переменные CSS.

--a: 4em;
height: calc(var(--a) + 7px);

Мы должны иметь в виду несколько вещей, чтобы убедиться, что функция calc() работает. Во-первых, деление на ноль, очевидно, не будет работать. Пробелы между именем функции и круглыми скобками не допускаются. И операторы плюс и минус должны быть окружены пробелами. Это означает, что следующие выражения неправильны:

calc(50% / 0)
calc (1em + 7px)
calc(2rem+2vmin)
calc(2vw-2vh)

Функция calc() должна работать в качестве значения во всем местах, где требуется числовое значение с заданием единиц измерения или без него. Тем не менее, хотя основная поддержка и обеспечивается, но мы можем столкнуться с проблемами, зависящими от того, где мы используем данную функцию. Давайте посмотрим на несколько примеров, с какими проблемами в поддержке они сталкиваются (если таковые имеются) и являются ли они лучшими решениями.

Понимание вычисляемых значений

Предположим, нам нужен радужный градиент. CSS для этого довольно прост:

background: linear-gradient(#f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);

Но HEX значения не достаточно осмысленны. Использование hsl() и calc(), хоть и более многословно, делает всё гораздо яснее:

background: linear-gradient(hsl(calc(0*60), 100%, 50%), 
                            hsl(calc(1*60), 100%, 50%), 
                            hsl(calc(2*60), 100%, 50%), 
                            hsl(calc(3*60), 100%, 50%), 
                            hsl(calc(4*60), 100%, 50%), 
                            hsl(calc(5*60), 100%, 50%), 
                            hsl(calc(6*60), 100%, 50%));

К сожалению, использование calc() в hsl(), hsla() или rgba() на данный момент не работает в Firefox и Internet Explorer (IE), что означает, что данный код сейчас будет работать только в браузерах на основе WebKit. Поэтому на практике, возможно, лучше пока позволить препроцессору обработать весь этот код, включая и вычисления. И лучший способ использования препроцессора в данном случае – это сформировать список в цикле:

$n: 6;
$l: ();

@for $i from 0 through $n {
   $l: append($l, hsl($i*360/$n, 100%, 50%), comma);
}

background: linear-gradient($l);

Более эффективный градиентный фон для эластичных элементов

Предположим, нам нужен фон с фиксированными полосами высотой 1em и сверху, и снизу. Одна проблема, мы не знаем высоту элемента. Одно из возможных решений могло бы использовать два градиента:

background: 
   linear-gradient(#e53b2c 1em, transparent 1em),
   linear-gradient(0deg, #e53b2c 1em, #f9f9f9 1em);

Но если воспользоваться calc(), то нам понадобится только один градиент:

background: 
   linear-gradient(#e53b2c 1em, #f9f9f9 1em, 
                   #f9f9f9 calc(100% - 1em), 
                   #e53b2c calc(100% - 1em));

Это должно работать нормально во всех браузерах, которые поддерживают calc() и градиенты, а так как здесь используется смешивание единиц измерения, то у препроцессоров нет эквивалента для этого выражения. Тем не менее, мы можем сделать код более удобным для сопровождения, использовав переменные:

$s: 1em;
$c: #e53b2c;
$bg: #f9f9f9;

background: 
   linear-gradient($c $s, 
                   $bg $s, 
                   $bg calc(100% - #{$s}), 
                   $c calc(100% - #{$s}));

Примечание: по каким-то причинам в Chrome и Opera одна из полос будет слегка размыта и уже по сравнению с другой.

Диагональные градиентные линии

Предположим, нам нужен элемент с толстой полосой, проходящей по диагонали. Мы могли бы сделать это с помощью остановок, выставленных с помощью процентов:

background: 
   linear-gradient(to right bottom, 
                   transparent 42%, #000 0, #000 58%, 
                   transparent 0);

В этом случае ширина полосы будет зависеть от размеров элемента. Иногда это именно то, что нам нужно. Например, если бы мы хотели воспроизвести флаг с помощью CSS. Добавление немного зеленого, желтого и синего к градиенту, показанному выше, даст нам флаг, который, возможно, узнают любители шоколада, – флаг Танзании.

background: 
   linear-gradient(to right bottom, 
                   #1eb53a 38%, #fcd116 0, 
                   #fcd116 42%, #000 0, 
                   #000 58%, #fcd116 0, 
                   #fcd116 62%, #00a3dd 0);
Флаг Танзании
Флаг Танзании

Но что, если мы хотим, чтобы наша диагональ была фиксированной ширины, которая не зависела бы от размеров элемента? Тогда мы должны использовать calc() и выставить остановки в 50% минус половина фиксированной ширины линии и в 50% плюс половина фиксированной ширины линии. Если мы хотим, чтобы ширина линии была 4em, то должны сделать так:

background: 
   linear-gradient(to right bottom, 
                   transparent calc(50% - 2em), 
                   #000 0, 
                   #000 calc(50% + 2em), 
                   transparent 0);

Вы можете проверить это вживую, изменив размеры окна. Размеры элемента выражены единицах области просмотра, но полоса по диагонали всегда остается одной ширины.

Позиционирование дочерних элементов с известными размерами в центре родительского элемента

Вероятно, вы видели маленький трюк с абсолютным позиционированием элемента в центре родительского элемента намертво:

position: absolute;
top: 50%; 
left: 50%;
margin: -2em -2.5em;
width: 5em; 
height: 4em;

С calc() мы можем избавиться от свойства margin:

position: absolute;
top: calc(50% - 2em); 
left: calc(50% - 2.5em);
width: 5em; 
height: 4em;

И мы можем сделать код более удобным для поддержки, используя переменные для ширины и высоты:

$w: 5em;
$h: 4em;

position: absolute;
top: calc(50% - #{.5*$h});
left: calc(50% - #{.5*$w});
width: $w; 
height: $h;

Обратите внимание, что использование смещения (top, left) для начального позиционирования является безопасным, если вы планируете анимировать положение элемента, для чего вам следует использовать transform. Это потому, что изменения transformтребуют только изменение компоновки изображений, тогда как изменение смещения вызывает еще и перекомпоновку, и перерисовку, ухудшая, таким образом, производительность.

Система координат и сетка с началом отсчета в центре

После обнаружения background-positionc четырьмя значениями, я была не слишком заинтересована в использовании calc() для позиционирования фона относительно правой или нижней стороны элемента. На calc() оказалась отличным решением для позиционирования определенной точки фона относительно центра элемента.

Пару лет назад я захотела создать фон, который представляет собой систему координат с сеткой, начало отсчета которой было бы намертво прикреплено к середине экрана.

система координат и сетка
Система координат и сетка

Система координат и сетка были легко реализованы:

background-image: 
   linear-gradient(#e53b2c .5em, transparent .5em) /* горизонтальная ось */,
   linear-gradient(90deg, #e53b2c .5em, transparent .5em) /* вертикальная ось */, 
   linear-gradient(#333 .25em, transparent .25em) /* основные горизонтальные линии сетки */, 
   linear-gradient(90deg, #333 .25em, transparent .25em) /* основные вертикальные линии сетки */, 
   linear-gradient(#777 .125em, transparent .125em) /* второстепенные горизонтальные линии сетки */, 
   linear-gradient(90deg, #777 .125em, transparent .125em) /* второстепенные вертикальные линии сетки */;

background-size: 
   100vw 100vh, 100vw 100vh, 
   10em 10em, 10em 10em, 
   1em 1em, 1em 1em;

Но как нам реализовать начало координат фона, прикрепленным намертво к середине, а не в верхнем левом углу? Во-первых, background-position: 50% 50% не работает, так как это заставляет точку 50% 50% градиента совпадать с точкой 50% 50% элемента, но линии находятся на верхней и левой стороне градиента соответственно. Решение заключается в использовании calc() и позиционирование градиентов так, что их верхняя и левая стороны находились бы почти в середине области просмотра, просто сместив верхнюю и левую стороны на половину ширины оси или линии сетки:

background-position: 
    0 calc(50vh - .25em), calc(50vw - .25em), 
    0 calc(50vh - .125em), calc(50vw - .125em), 
    0 calc(50vh - .0625em), calc(50vw - .0625em);

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

Сохранение соотношения сторон и покрытие размеров области просмотра

При создании HTML слайдов я всегда хотела, чтобы для каждого слайда существовал бы контейнер с определенным фиксированным соотношением сторон, который всегда покрывал бы, по крайней мере, одно из измерений области просмотра (ширину или высоту), и который, конечно, находился бы в середине, вдоль другой оси.

Давайте начнем, предположив, что нужное для слайдов соотношение сторон равно 4:3, и что мы используем широкоформатный монитор. Это означает, что слайды покрывают область просмотра по вертикали, а справа и слева еще есть место.

контейнер
Пропорциональный контейнер: случай 1

Покрытие области просмотра по вертикали означает высоту 100vh. Зная высоту и соотношение сторон, мы можем получить ширину, которая равна 4/3*100vh. И чтобы получить контейнер в центре, мы должны сдвинуть его от левого края на половину ширины области (100vw/2) минус половина ширины слайда (4/3*100vh/2). Здесь мы должны использовать функцию calc(), так как смешиваем единицы измерения.

.slide {
   position: absolute;
   left: calc(100vw/2 - 4/3*100vh/2);
   width: calc(4/3*100vh);
   height: 100vh;
}

Теперь рассмотрим вариант, когда соотношение сторон области просмотра меньше, чем 4:3. В этом случае, слайды покрывают область просмотра по горизонтали с пустым местом сверху и снизу.

Пропорциональный контейнер: случай 2

Покрытие области просмотра по горизонтали означает ширину 100vw. Знание этого и соотношения сторон дает нам высоту, которая равна 3/4*100vw. И, наконец, смещение сверху равно половине высоты области просмотра минус половина высоты слайда, то есть 100vh/2 - 3/4*100vw/2.

@media (max-aspect-ratio: 4/3) {
   .slide {
      top: calc(100vh/2 - 3/4*100vw/2);
      left: auto; /* Undo style set outside media query  */
      width: 100vw;
      height: calc(3/4*100vh);
   }
}

Мы, конечно, можем сделать код более гибким, не прописывая фиксированные пропорции и используя две переменных, а не одну (ширину или высоту). Вот Sass версия, которую вы также можете проверить, изменяя размер окна:

$a: 4;
$b: 3;

.slide {
   position: absolute;
   top: 0; 
   left: calc(50vw - #{$a/$b/2*100vh});
   width: $a/$b*100vh; 
   height: 100vh;
  
   @media (max-aspect-ratio: #{$a}/#{$b}) {
      top: calc(50vh - #{$b/$a/2*100vw}); 
      left: 0;
      width: 100vw; 
      height: $b/$a*100vw;
   }
}

Даже лучше, мы могли бы превратить это миксин, который, как правило, лучше, чем практика использования глобальных переменных:

@mixin proportional-box($a: 1, $b: $a) {
   position: absolute;
   top: 0; 
   left: calc(50vw - #{$a/$b/2*100vh});
   width: $a/$b*100vh; 
   height: 100vh;
  
   @media (max-aspect-ratio: #{$a}/#{$b}) {
      top: calc(50vh - #{$b/$a/2*100vw}); left: 0;
      width: 100vw; height: $b/$a*100vw;
   }
}

.slide {
   @include proportional-box(4, 3);
}

Обратите внимание, что $a и $bдолжны быть целыми числами, чтобы медиа запрос работал.

Это поддерживается во всех текущих версиях основных браузеров. Тем не менее, WebKit браузеры до недавнего времени не поддерживали единицы измерения области просмотра в функции calc(). Это было исправлено в Safari 8 и Chrome 34, соответственно, и в последней версии Opera.

Короткий заголовок слайда в центре

Мне хотелось еще пару вещей для слайд-презентаций.

Первое, слайды не должны полностью покрывать всю область просмотра, так как края могут получиться обрезанными. Это было легко исправить. Я просто установила им свойство box-sizing в значение border-box и установила для них границу.

Второе, обозначить разделы, каждый из которых начинается со слайда, на котором нет ничего, кроме короткого и незабываемого заголовка в середине.

Желаемый результат

Я не хотела использовать абсолютное позиционирование и поэтому решила установить подходящее значение line-height.

В случае, если высота слайда (вместе с границей) покрывает всю высоту области просмотра, необходимое мне значение line-height составляет 100vh минус удвоенное значение border-width слайда:

$slide-border-width: 5vmin;

.slide {
   /* другие стили */
   box-sizing: border-box;
   border: solid $slide-border-width dimgrey;
    
   h1 {
      line-height: calc(100vh - #{2*$slide-border-width});
   }
}

В случае, когда слайд вместе с границами покрывает область просмотра по горизонтали (и находится в центре по вертикали), его высота должна составлять $b/$a*100vw. Таким образом, значение line-height для заголовка будет равно значению высоты слайда минус удвоенное значение border-width:

line-height: calc(#{$b/$a*100vw} - #{2*$slide-border-width});

Это было моей первой идеей, которая, в теории, должна была работать нормально. Всё так и есть в WebKit браузерах и IE. Тем не менее, оказывается, что значения calc() для line-height (и некоторых других свойств) не работают в Firefox, поэтому calc() здесь не самое лучшее решение. К счастью есть много других способов решить эту проблему (flexbox, абсолютное позиционирование и т.д.).

Вид с фиксированной точки

Еще одно, мне интересен CSS 3D и создание геометрических 3D фигур с помощью CSS, в частности. Если я создаю только одну фигуру, то обычно располагаю ее в центре сцены, в которой она находится. Сцена – это элемент, на котором я устанавливаю свойство perspective, а также родительский элемент элемента фигуры. У элемента фигуры есть свои собственные потомки, которые являются сторонами фигуры, но здесь мы не будем вдаваться в это подробно; если вы хотите узнать, как они позиционируются, то посмотрите мою гостевую статью на CSS-Tricks.

Установка perspective на сцену гарантирует, что мы будем видеть всё, что находится ближе в большем размере, и всё, что дальше в меньшем. Свойство perspective принимает значения длины, и чем меньше эти значения, тем больше разница между тем, что ближе к нам, и тем, что дальше.

Теперь предположим, что у нас есть очень простая 3D фигура, например, куб, прямо в центре нашей сцены. Это выглядит не совсем 3D: слишком симметрично, и если стороны полностью непрозрачны, то мы можем видеть только переднюю грань.

куб
Куб

Мы могли бы повернуть его немного, скажем на 30°, вокруг своей оси y (т.е. вертикальной оси, проходящей через середину куба) или вокруг своей оси x. Это выглядит лучше, но мы можем видеть только две грани. Кроме того, куб теперь заметно повернут, а это не было целью.

повернутый куб
Повернутый куб

Мы можем еще кое-что сделать, чтобы изменить нашу точку обзора. Сделаем это с помощью свойства perspective-origin. Его начальное значение 50% 50% относительно сцены. Мы знаем, что точка сцены 50% 50% – это центральная точка размещенной фигуры. Теперь предположим, что переместиться вверх и вправо. Простейший способ сделать это – установить свойство perspective-origin: 100% 0. Но это создаст проблему: то, как теперь мы видим куб, зависит от размеров сцены (вы можете проверить это вживую изменяя размер области просмотра).

Изменение размеров сцены изменяет то, как мы видим куб

perspective-origin 100% 0 отсчитывается от верхнего правого угла сцены, в то время как куб всегда в центре сцены. Из-за этого, изменение размеров сцены изменит расстояние между точкой 50% 50% (где размещен куб) и точкой 100% 0 (где мы установили perspective-origin).

Решение для этого – использование calc() для perspective-origin, чтобы просто добавить или вычесть фиксированное значение из первоначальных 50%:

perspective-origin: calc(50% + 15em) calc(50% - 10em);
Решение

Вы можете проверить это вживую, изменяя размеры области просмотра.

А для чего вы используете calc()?


На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.


Сообщить об ошибке