Начинаем работать с CSS функцией calc()
Впервые я обнаружила функцию 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-position
c четырьмя значениями, я была не слишком заинтересована в использовании 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, и что мы используем широкоформатный монитор. Это означает, что слайды покрывают область просмотра по вертикали, а справа и слева еще есть место.
Покрытие области просмотра по вертикали означает высоту 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. В этом случае, слайды покрывают область просмотра по горизонтали с пустым местом сверху и снизу.
Покрытие области просмотра по горизонтали означает ширину 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()
?