CSS-live.ru

Еще раз про border-image

Хотя свойство border-image уже поддерживают более 96% браузеров (с оговорками, но всё же), популярным пока его не назовешь. Пожалуй, поначалу оно кажется неоправданно сложным: объединяет в себе 5 отдельных свойств, каждое с несколькими значениями, да еще неочевидные правила взаимодействия с обычным border — легко запутаться. Даже многие неплохие руководства (типа статьи Дадли Стори, которую мы переводили в прошлом году) грешат тем, что даже после них ощущение сложности не проходит.

Есть мнение, что его сложность преувеличена, а возможности недооценены. Попробуем исправить это упущение.

0. border-image = border + image

Название свойства состоит из двух слов: «рамка» и «картинка». Вокруг них всё и вертится. Интуитивно его действие можно представить в виде примерно такого алгоритма:

  1. Берем картинку.
  2. Вырезаем из этой картинки рамку.
  3. Заготавливаем «каркас» рамки по размерам нужного блока. Если надо, подгоняем ее толщину, положение краев и т.д.
  4. «Натягиваем» вырезанную часть картинки на этот «каркас».

В таком ракурсе мы его сейчас и рассмотрим.

1. Картинка: border-image-source

В теории, картинка может быть чем угодно, что относится к типу CSS-значения «image» (описано в модуле значений изображения и замещаемого содержимого 3 уровня). Это может быть растровая картинка (отдельный файл или data uri) и SVG-картинка (отдельный файл, base64 или прямо SVG-код с минимально заэкранированными спецсимволами!). Или CSS-градиент. И даже любой элемент страницы — благодаря функции element(). Конечно, не всё из этого поддерживается во всех браузерах, но с растровыми картинками, SVG и градиентами давно почти везде нет проблем, а этого для типичных задач хватает с избытком.

С векторными картинками и градиентами есть пара нюансов:

  1. Не у каждой картинки есть конкретные размеры. Соответственно, не из каждой картинки можно вырезать рамку, задавая ей размеры в пикселях. С градиентами вообще лучше всегда работать в процентах.
  2. Градиент может быть только один. Неприятный сюрприз по сравнению с фонами: если фоны у нас множественные, благодаря чему можно собирать целые паттерны из нескольких слоёв разного размера и положения, то здесь нам фактически доступен лишь один такой слой. Так что паттерны из градиентов в border-image использовать нельзя.

Точнее, было нельзя до недавних пор. Несколько недель назад CSS-волшебница @yoksel открыла для нас новый секретный уровень CSS. Если задать для border-image SVG-картинку с инлайновыми стилями, внутри них может быть много чего интересного, включая паттерны из нескольких градиентов. Но будьте внимательны, такая магия требует мастерства и глубокого понимания происходящего! Иначе можно сломать мозг себе и браузеру.

Я не волшебник, только учусь, так что меня пока хватило только на такую небольшую вариацию:

See the Pen три градиента в border-image by Ilya Streltsyn (@SelenIT) on CodePen.

2. Вырезка рамки: border-image-slice

Наша картинка разделяется на 9 «плиток». 8 внешних (4 угловых и 4 боковых) — по сути и есть рамка. А центральная «плитка» либо выбрасывается, либо (если задать ключевое слово fill) заполняет рамку изнутри, как фон.

«Линии разреза» задаются значениями свойства border-image-slice. Если присмотреться, оно очень похоже на обычный border-width! Те же 1–4 значения через пробел, тот же порядок (по часовой стрелке, верх-право-низ-лево), тот же смысл сокращенных записей (3 значения — верх, одинаковые бока и низ, 2 значения — верх-низ и бока, 1 значение — одинаковая толщина со всех 4 сторон). Только единицы измерения другие: либо проценты (от размеров картинки), либо безразмерные «единицы системы координат картинки». Для растровой картинки это ее «родные», исходные пиксели. Так что ни те, ни другие единицы никак не зависят от экрана, масштаба и т.п.

Не так интуитивно, когда суммарная толщина противоположных сторон рамки становится больше размера картинки. Тогда разные угловые «плитки» пересекутся — какая-то часть картинки окажется сразу на нескольких из них. Это легче представить как то, что исходной картинки у нас было 4 экземпляра, и из каждого щедро вырезали по углу. Плиток нулевого и отрицательного размера не бывает, поэтому при такой «нарезке» центральная «плитка» и пара боковых исчезают, остаются лишь угловые. В пределе, при border-image-slice:100% — странно, но это значение по умолчанию — этими оставшимися угловыми «плитками» станет вся картинка целиком.

Лучше увидеть и «пощупать» это вживую:

See the Pen LROoRZ by Ilya Streltsyn (@SelenIT) on CodePen.

3. Тонкая настройка: border-image-width и border-image-outset

Художественные эффекты, включая рисованные рамки, часто требуют настройки с точностью до пикселя. У border-image целых две «степени свободы» для этого.

Итоговая толщина рамки: border-image-width

С помощью border-image-width можно регулировать окончательную толщину рисованной рамки, совсем как с border-width — толщину обычной. Можно указывать толщину сторон рамки в обычных единицах длины (px, em, vh…), и эти стороны отмасштабируются до указанного значения (составляющие ее «плитки» сожмутся или растянутся поперек, угловые плитки масштабируются по обеим осям независимо). Но у него бывают еще три типа значений:

  • безразмерные коэффициенты — за единицу берется толщина соответствующей стороны обычного border-width.
  • проценты. Да-да, проценты для рамки! Чисто визуальной, но всё же. Считаются от общего размера рамки (с учетом того, что она может выступать за края блока, см. ниже).
  • ключевое слово auto — используется исходный размер соответствующих «плиток», т.е. соответствующее значение из border-image-slice.

Значение по умолчанию — как раз безразмерное 1: рисованная рамка масштабируется до толщины, заданной обычному border-у. Иногда, если нужно просто «залить рамку текстурой», это логично. Но часто удобнее задавать border и border-image-width по отдельности. Если же не указать ни того, ни другого, рамка не появится вообще (ее толщина будет нулевой).

Удобное значение auto: сколько пикселей «вырезали» из картинки, такую толщину рамки и получили, ничего не искажается. Есть нюанс: border-image-width считается в обычных CSS-пикселях, а border-image-slice — в исходных пикселях картинки. Поэтому на Retina-экранах при auto растровая картинка может «мылить». Чтобы сделать рамку двойной четкости из картинки двойного размера, придется явно указывать для border-image-width половины значений border-image-slice (т.е. вдвое уменьшать исходные «плитки»).

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

See the Pen Автомасштабирование border-image до размеров контейнера by Ilya Streltsyn (@SelenIT) on CodePen.

Примечание: работа над этим примером заставила меня осознать беспощадный факт, что составляющие border-image не анимируются. Выручил JS. Зато как минимум в Firefox внутри SVG-картинок в border-image работают SMIL-анимации!

Вынос рамки за габариты блока: border-image-outset

Это уже интереснее: рисованная рамка может выступать за края блока наружу, на внешние отступы и даже на соседние элементы! Редкая в CSS возможность (еще разве что тени да позиционированные псевдоэлементы так умеют). Бывает полезно для вычурных дизайнерских виньеток с веточками/лучиками/тентаклями/любыми др. выступающими деталями. Или для «хвостиков» от «балунов» прямой речи, которыми любят оформлять отзывы и комментарии. Причем выступает она чисто визуально, на блочную модель это не влияет (габариты блока по-прежнему считаются по краям обычного border-а).

По механизму border-image-outset похож на margin. Только наоборот: положительные значения — сдвиг наружу. Кроме обычных единиц длины, тоже можно указывать безразмерные множители для border-width. А вот проценты почему-то нельзя. Сдвигать края внутрь, к сожалению, тоже нельзя (отрицательные значения запрещены), но обычно и не нужно. По умолчанию значение 0 — без сдвига, край рамки совпадает с краем блока.

Эти два свойства отлично работают вместе, если надо точно состыковать с контентом дизайнерскую рамку типа такой:

See the Pen GjxVmp by Ilya Streltsyn (@SelenIT) on CodePen.

Или уголки нестандартной формы с тенью:

See the Pen
SVG as border-image for arbitrary corner shapes with shadow
by Ilya Streltsyn (@SelenIT)
on CodePen.

А заодно обеспечить им по-настоящему изящную деградацию.

4. «Натяжка» рамки на «каркас»: border-image-repeat

Теперь, когда известны окончательные размеры рамки, пора замостить эту область «плитками». Это работа свойства border-image-repeat. Общий принцип — угловые «плитки» просто разносятся по углам, а боковые заполняют оставшееся между углами пространство, для чего с ними что-то делается. Варианты, что именно делать, такие:

  1. stretch (по умолчанию) — растянуть (или сжать) боковые «плитки» до заполнения оставшегося места, с искажением пропорций. Как будто рамка, которую мы вырезали из картинки, была резиновая, и мы приклеиваем ее к «каркасу» за углы.
  2. repeat — размножить «плитку» и замостить ей это пространство. Как фон c background-repeat: repeatbackground-position по центру стороны). Пропорции сохранятся, но аккуратных стыков с углами никто не гарантирует.
  3. round — размножить и исказить пропорции чуть-чуть — настолько, чтобы в нужное пространство влезло целое число копий «плитки». Тогда стыки с углами будут такими же аккуратными, как на исходной картинке.
  4. space — не искажать пропорции, а взять столько копий, сколько поместится, а оставшееся свободное место поровну «раскидать» вокруг них. Увы, работает пока только в IE11/Edge и Safari 9.1+ (но вот-вот начнет в Firefox 50+).

Можно задать разные значения для горизонтальных и вертикальных сторон (напр. stretch round) или одно значение для всех 4-х. Центральная плитка по каждому измерению ведет себя так, как соответствующие боковые (например, может размножаться по вертикали и растягиваться по горизонтали).

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

See the Pen PGarao by Ilya Streltsyn (@SelenIT) on CodePen.

На мой взгляд, самые полезные значения — stretch (для сплошных, «монолитных» рамок) и round (для повторяющихся орнаментов).

5. Итого

Сокращенная запись свойства border-image, по спецификации, записывается практически как наш алгоритм:

border-image: <‘border-image-source’> || <‘border-image-slice’> [ / <‘border-image-width’> | / <‘border-image-width’>? / <‘border-image-outset’> ]? || <‘border-image-repeat’>

т.е., в переводе на человеческий: что за картинка — пробел — как ее резать — слеш — какой толщины делать рамку — слеш — насколько выдвигать ее за края — пробел — как натягивать «плитки». Части border-image-width и border-image-outset необязательны. Что именно из них пропущено, определяется по количеству слешей перед оставшимся. Например, в border-image: url(img.png) 50 / 25px round значение 25px — это толщина рамки (до него один слеш), а в border-image: url(img.png) 50 / / 25px stretch — это выступ за края (до него два слеша). Но «что резать», «как резать» и «как растягивать» указывать нужно (первое — по стандарту, остальное — по здравому смыслу).

Cледующий пример — набросок своего рода «песочницы» для этого свойства. Пробуйте загружать или задавать кодом свои картинки и градиенты, менять значения и единицы, смотрите на результат и… копируйте итоговое значение. Надеюсь, из этого получится неплохое дополнение к старому доброму border-image.com:)

See the Pen Конструктор border-image by Ilya Streltsyn (@SelenIT) on CodePen.

И несколько слов о поддержке браузерами. С ней всё хорошо: полностью выпадает лишь IE10 и ниже. Без значения space для border-image-repeat, по-моему, жить можно.

Правда, на CanIUse есть загадочное примечание (про WebKit и Edge 13), которое чуть не сбило меня с толку: «Есть баг, что border-image неправильно перекрывает border-style». Каково же было мое удивление, когда я обнаружил, что все браузеры «перекрывают» компоненты обычного borderпо-разному! Safari в iOS 10 не рисует картинку при border-width: 0, Edge 14 — при border-style: none, Хром (включая Canary 56) — при обоих. А вот Firefox (и IE11, что интересно) рисуют картинку несмотря ни на что, хотя о баге в них не сказано!

После раскопок в спецификациях и консультаций с умными людьми я выяснил, что поведение FIrefox (и IE11) правильное. Это подтверждают официальные тесты к спецификации. По стандарту, составляющие обычного border не должны влиять на border-image чем-либо еще, кроме как через дефолтное значение border-image-width (причем его легко «отвязать», задав конкретное значение). Неразбериха возникла из-за двусмысленной фразы в спецификации, что «при нулевом border-width рамка считается отсутствующей» (без уточнения, идет ли речь только об обычной или о картиночной тоже), а также из-за проблем совместимости со старыми префикснутыми реализациями и гугловским календарем:). Ради совместимости с Хромом, видимо, сломали и Edge. Впрочем, «лекарство» — явно указать, например, border-style: solid и ненулевой border-width — элементарно. К тому же они наверняка всё равно понадобятся для изящной деградации.

И еще две хорошие новости и одна плохая. Хорошая №1 — border-image работает в Опере Мини! Так что его поддержка чуть ли не лучше, чем у border-radius). Хорошая №2 — на сегодня это единственный браузер, которому нужен префикс. И чуть ли не единственный случай, где это префикс -o-. Даже префикс -webkit- уже не актуален! А плохая новость в том, что Опера Мини поддерживает только сокращенное свойство целиком (нельзя задавать, скажем, border-image-slice и border-image-width по раздельности) и не понимает в border-image-repeat не только странного space, но и полезного round.

P.S. Это тоже может быть интересно:

12 комментариев

  1. По статье скажу позже, а пока сообщаю, что border-width периодически приходится менять, когда изменишь, например, значение fill или border-image-width или border-image-outset. border-width, похоже, в этих случаях, скидывается на дефолтные 20px, при этом значение в поле остаётся то, которое выставил вручную. Для того, чтоб «вернуть» это значение, приходится сделать что-то вроде +/-1 от этого значения, тогда песочница «вспоминает» про свои куличики :)

  2. А почему, на рисунке «рисование угловых плиток», в некоторых случаях, когда размер плитки растёт в ширину(по-моему это 3е и 5е увеличение), на результирующей картинке, идёт уменьшение высоты картинки?

    1. Картинка масштабируется пропорционально. Увеличить большую сторону уже нельзя — она упирается в предел, заданный общим размером «каркаса» рамки (обе стороны рамки должны вписаться в него). Поэтому при изменении пропорций картинки остается ужимать короткую сторону.

  3. А можно ли сделать так, чтоб эта рисованная граница, появлялась/исчезала постепенно? Интересует, прежде всего, граница, сделанная через линейный градиент.

    1. Вот в спецификации, в частности, сказано:

      gradient: interpolated via the positions and colors of each stop. They must have the same type (radial or linear) and same number of stops in order to be animated. Note: [CSS3-IMAGES] may extend this definition.

      Непонятно только как это осуществить на практике: делаешь полностью аналогичный градиент, меняешь только значения альфа-канала — переход мгновенный, а не плавный. (смотрел в Хроме, но если в нём не работает это уже достаточный повод отказаться)

      1. Насколько я в курсе, интерполяция градиентов, и вообще значений для любых свойств — достаточно новая вещь в CSS, до этого свойства жестко делились на анимируемые и неанимируемые, и border-image с его подсвойствами, увы, попадал во вторые:(. Как ни забавно, для background-image интерполяция градиентов работает в Edge, но с border-image, к сожалению, это не проходит(. Остается только JS…

        1. Ну поскольку «рисовать» полоски можно не только с помощью градиента, но и с помощью, как ни странно, теней(впрочем, при желании, пожалуй, можно найти ещё пару способов), относительное понимание, как это можно заанимировать, не без помощи коллективного разума, у меня появилось.
          В любом случае, спасибо за ответ!

  4. А сейчас по прежнему можно использовать только один градиент? А то, магия SVG-картинки с инлайновыми стилями, требует мастерства и глубокого понимания происходящего! У меня тут, пожалуй, только глубина непонимания :-)

    1. Насколько я понимаю, да: border-image-source принимает только одно значение типа <image>, а тот по-прежнему допускает только один градиент. Есть надежды на функцию cross-fade(), которая вроде как умеет смешивать несколько картинок, но я пока не смог увидеть ее в действии.

  5. Добрый день, возникли вопросы по оптимизации border-image

    Вопрос на хабре https://qna.habr.com/q/1197356 — можете подсказть?

Оставить комментарий

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

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