shape(): фигуры для всех браузеров
13.03.2026 • css
Вступление
Вот и вышел 148й Файерфокс с поддержкой shape().
Функция позволяет описывать “путь” для свойств clip-path и offset-path. Раньше максимум из того, что мы могли сделать – полигон через прямые отрезки и путь из SVG. И то и то ограничивало разработчиков: полигоны не поддерживали кривые, а пути из SVG позволяли задать координаты только в пикселях (но не в относительных единицах, например, относительно ширины).
Более того, shape() предлагает абсолютно новый синтаксис: если раньше пути в SVG было тяжело прочитать человеческим глазом (да и программно тоже, там есть нюансы), то новый синтаксис, хоть и более многословен, призван быть куда более читабельным.
Посмотрим, что такое smooth(), в чём он похож на существующие инструменты и как решает старые проблемы.
Текущее применение
Прежде чем рассматривать все подробности новых фигур, вкратце вспомним, что такое clip-ath / offset-path и зачем они нужны.
Что такое clip-path
clip-path позволяет сделать обрезку элемента по произвольной фигуре:
Что такое offset-path
offset-path (вместе с другими свойствами offset-position, offset-rotate и прочими) позволяет перемещать элемент вдоль определённого пути:
Вернёмся к shape()
shape() позволяет задать путь в виде набора команд, который очень похож на путь в SVG:
offset-path: shape(from 10px 10px, move by 10px 5px, line by 20px 40%, close);from <coordinate-pair> задаёт стартовую точку, откуда будет выполнена последовательность команд. Отсчёт начинается от левого верхнего угла элемента.
<coordinate-pair> используется как в from, так и в командах после него. Это значение проще всего сравнить с background-position: можно просто задать два значения через пробел, можно использовать значения по типу top left, можно и вовсе одним словом обозначить центр элемента: center.
Также перед from можно задать ключевые слова nonzero / evenodd, которые работают аналогично свойству fill-rule из SVG. С их помощью можно задать алгоритм, который решит, является ли точка внутри объекта или нет (недоступно для offset-path по очевидным причинам).
nonzero(по умолчанию). Выпускает из точки луч и считает количество пересечений фигур с обходом слева направо как +1, а справа налево как -1. Если итоговый результат 0, то точка за пределами фигуры (нет заливки). В ином случае точка считается внутри (заливка есть).evenodd. Аналогичным способом считает количество пересечений. Если нечётное – то точка внутри, если чётное – то снаружи.
Пример того, что свойство реально влияет на отображение (но это не значит, что nonzero не позволяет делать “вырезы” внутри фигуры – это просто два разных способа):
Описываются nonzero / evenodd так:
shape(evenodd from 0 0, ...)В любом случае после from идёт последовательность команд, и команды следующие:
movelinehline/vlinecurvesmootharcclose
Для большинства команд есть указание by или to. Первое включает режим относительного смещения, второе – абсолютные координаты. И то и то влияет на координаты внутри команды.
Важное отличие от SVG видно уже на этом этапе: все значения с привычными единицами измерения CSS. Также можно использовать calc() и другие фичи по типу max().
Все перечисленные команды будут знакомы тем, кто знает про команды пути в SVG: по сути, это они есть, только записанные в другом виде.
move
move [to | by] <coordinate-pair>Классический способ объяснить концепцию этих команд – представить ручку, которая расположена над листом бумаги. Команда move позволяет “переместить” ручку над бумагой, не отрисовывая ничего. Другие команды будут отрисовывать ту или иную линию, кривую или дугу, тем самым в них ручка будет “прижата” к листу бумаги.
line
line [to | by] <coordinate-pair>Прямая линия из прошлой позиции в указанную точку.
hline / vline
hline [to | by] x
vline [to | by] yТакая же прямая линия, но либо горизонтальная, либо вертикальная. Задаётся через одну координату, а не две, как в line.
curve
curve [to | by] <end-point> with <control-point> [/ <control-point>]Рисует кривую Безье, используя одну или две контрольные точки (квадратичную или кубическую кривую Безье). <end-point> задаётся через <coordinate-pair> и обозначает координаты, которыми закончится кривая и от которых начнёт работу следующая команда.
<control-point> может принимать значение как просто в виде <coordinate-pair>, так и позволяет задать смещение относительно одной из нескольких точек, используя ключевое слово from: start, end и origin. Ключевые слова значат начальную точку кривой, конечную и левый верхний угол фигуры соответственно. Если from не задан, то start является значением по умолчанию. Смещение можно использовать только в режиме by.
Пример с относительными контрольными точками:
curve by 10px 10px with 5px 5px from end / 5px 0px from originsmooth
smooth [to | by] <end-point> [with <control-point>]Рисует кривую Безье, аналогичную curve. В случае, если не используется with, то используется квадратичная кривая Безье с одной контрольной точкой, посчитанной из предыдущей команды. Если используется with, то рисуется кубическая кривая с двумя контрольными точками: одной, посчитанной из предыдущей команды, и второй, которая передана явно.
Контрольная точка из предыдущей команды считается по следующей логике:
- Если предыдущая команда была кубической кривой Безье с двумя контрольными точками, то берётся вторая контрольная точка и зеркально отражается относительно стартовой точки текущей команды
- Иначе берётся стартовая точка текущей команды
На самом деле, эта команда тоже есть в SVG, если вдруг у кого-то закрались сомнения.
arc
arc [to | by] <coordinate-pair> of <radius> [<radius>] [<arc-sweep> [<arc-size> [rotate <arc-rotate>]]]Рисует дугу эллипса от начальной точки до конечной. Конечная задаётся аналогично предыдущим командам с помощью to / by и последующих значений. Радиус эллипса задаётся одним (в этом случае он круг) или двумя значениями.
Дальше становится интереснее, так как имея конкретный эллипс и точки, через которые он должен пройти, можно получить до 4х кривых. Конкретную дугу помогают определить следующие значения.
<arc-sweep> задаёт порядок отрисовки эллипса: по часовой (clockwise, cw) или против часовой (counterclockwise , ccw, значение по умолчанию). Если указан порядок “по часовой”, то стартовая точка на эллипсе будет “раньше” конечной, аналогично ходу часовой стрелки. Если используется порядок “против часовой”, то позже.
<arc-size> задаёт один из двух вариантов: small (по умолчанию) или large. Первое выбирает меньший вариант дуги, второе – больший.
<arc-rotate> позволяет задать поворот эллипса. Можно задать в градусах deg, в радианах rad или в других единицах измерения.
Если радиус получившегося эллипса не позволяет провести дугу от одной точки до другой (так как он меньше необходимого), по эллипс масштабируется, чтобы такую дугу провести было можно.
close
closeПроводит прямую линию до “стартовой точки”. Стартовая точка – или точка, переданная через from, или координаты команды move, следующей сразу после предыдущего close.
Примеры
Пример со разными видами команд в одном (кнопки кликабельны):
Пример “вкладок” с вырезами:
Хитрости и подводные камни shape()
Из хитростей можно подсветить, что часть команд можно вынести с помощью var:
.block {
--part: curve by 100px 100px with 0 -50px;
clip-path: shape(from 0% 0%, var(--part));
}С помощью подобной конструкции можно и повторять куски, если это необходимо.
И тут же вынесу минус: как и во многих частях веба, подобные текстовые конструкции с определённой длинной становится очень сложно разобрать, а любая ошибка приводит к тому, что они перестают работать целиком.
Поэтому, собирая подобный shape() по частям, важно убедиться, что нет никаких ошибок во всех вариантах (например, прописаны фолбечные значения для CSS переменных). Для clip-path / offset-path отсутствие свойства в каких-то случаях может привести к разительно другому внешнему виду.
Ещё один подводный камень при использовании shape() в clip-path() – нет гарантии того, что кривые не выйдут за пределы элемента. В этом случае вместо желаемой гладкой линии можно получить “обрубленную” фигуру. Если ваши кривые содержат вычисляемые значения, вероятно, стоит их перепроверить на разных размерах элемента и на разных внешних условиях.
Плюсы над polygon()
polygon() тоже позволяет задать сложную многоугольную фигуру и тоже позволяет использовать разные единицы измерения, но есть и минусы.
Первое – polygon() позволяет использовать только прямые линии, в то время как shape() позволяет задать несколько видов кривых.
Второе – polygon() описывает замкнутую фигуру без прерываний. А в shape() можно не использовать close (особенно актуально для offset-path).
Итого
shape(), по сути, является новым кирпичиком для будущего функционала. Вполне возможно, что поддержка этой функции будет реализована во всех остальных местах, где может пригодиться, например, в shape-outside.
Появление это функции очень похоже на то, как сейчас развивается весь веб целиком: новые, более продвинутые апи приходят на смену старым, предлагая новые возможности и решая проблемы. Хотя старые апи при своём появлении часто решали задачи, которые без них было невозможно решить, новые апи уверенно закрывают оставшиеся дыры по функционалу и предлагают свежий взгляд.
Остаётся дождаться широкой поддержки в браузерах (пока реализована только в недавних стабильных версиях), а пока посмотрим, как ещё разовьётся shape().




