Хорошие Округлости: Составные Фигуры в CSS
Я люблю сотрудничать с компаниями по вопросам дизайна, потому что это обычно заставляет меня пробовать новые вещи. Недавно одна дизайнерская компания дала мне следующее задание: составная фигура со скруглёнными углами, служащая фиксированным заголовком с текстурированным содержимым и внутренней тенью по кругу. Ну, я подумал, чего тут сложного?

Конечно, одно из решений было очень простым: я мог бы выбрать разрезанное изображение и использовать прозрачные файлы формата png для закруглений – тени и прочее – с «крышкой», которая бы позволила правой части растягиваться относительно контента (другая часть была бы фиксированной ширины).
Но я хотел большей гибкости в будущем, поэтому решил сделать это при помощи CSS. Вот подход, который я выбрал после долгих мучений. Ничего сверхъестественного, просто старый добрый CSS:
HTML
<!-- http://css-tricks.com/well-rounded-compound-shapes-css/ -->
<article id="top">
<!-- oveflow:hidden, чтобы обрезать тени -
:before добавляет белый «навес», отбрасывающий тень -
:after накладывает скруглённый угол внизу слева -->
<div class="header-content">
<div class="header-logo"></div>
<!-- oveflow:hidden, чтобы обрезать тени -
:after накладывает скруглённый угол -->
<div class="header-logo-patch"></div>
<div class="header-social"></div>
</div> <!-- end .header-content -->
<h2>Хорошие Округлости – Срезаем Углы</h2>
<p>Здесь приведён основной код для создания «заплатки», которая прячет внутреннюю тень шва двух перекрывающихся фигур (с наложенным на них внутренним углом).</p>
<img class="scale border-grey" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/23379/well-rounded-patch@2x.jpg" />
<p>Плюс бонус: вогнутый скруглённый угол слева внизу заголовка (<code>header-content:after</code>) Измените свойство border-color, чтобы проверить, как это работает, хотя эта картинка должна дать вам представление: </p>
<img class="border-grey" width="156px" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/23379/well-rounded-corner@2x.jpg" />
<p>Также на CodePen выложен <a href="http://codepen.io/parkerbennett/pen/EzAlj" target="_blank">полный код разметки</a> (правый клик, чтобы открыть в новой закладке).</p>
<a href="#top">Наверх</a>
</article>
CSS
/* ПЕРЕМЕННЫЕ *
* -------------------------- */
/* ЦВЕТА И ФОН */
$header-bg: #aed2cc;
$well-header-rgba: rgba(10,8,6,0.75); /* тёмно-грязно-серый */
$dropshadow-rgba: rgba(0,0,0,0.35);
$header-bg-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/23379/bg-map-bluegreen.jpg);
/* РАЗМЕРЫ */
/* я предпочитаю разместить расчёты здесь и использовать результаты ниже */
/* Устанавливаем ширину левой колонки, получаем размер логотипа ниже */
$hdr-side-width: 270px;
$hdr-big-btn-width: 72px; /* Блок SIGN UP */
/* чтобы меню имело больше одной строки, измените $min-width здесь на что-нибудь поменьше: например, * 1.5; */
$min-width: $hdr-side-width * 2;
$max-width: $hdr-side-width * 3;
$padding-small: 10px;
/* объявлено здесь для гибкости */
$padding-to-window: $padding-small;
$padding-to-top: $padding-small;
$padding-hdr: $padding-small;
$hdr-big-btn-margin: floor(.75 * $padding-hdr); /* округление вниз */
$border-radius-hdr: 10px;
$corner-border-width: $border-radius-hdr;
$corner-border-size: $border-radius-hdr * 2;
$shadow-size: 20px;
$shadow-size-deep: ceil(1.5 * $shadow-size);
$hdr-menu-height: 24px;
$hdr-logo-width: $hdr-side-width - $hdr-big-btn-width - 2 * $hdr-big-btn-margin;
$hdr-logo-height: ceil(0.75 * $hdr-logo-width); /* 4x3 */
$hdr-logo-margin-bottom: 12px;
/* высота большей левой части «навеса» без верхнего отступа */
$hdr-side-height: $hdr-logo-height + $hdr-logo-margin-bottom;
$hdr-side-total-height: $hdr-side-height + $padding-to-top;
$hdr-total-height: $hdr-side-total-height + $shadow-size-deep;
/* меньший зелёный фон сверху вниз */
/* можно указать, либо сосчитать: например, половину высоты логотипа */
$hdr-topwell-height: ceil(0.5 * $hdr-logo-height);
/* размер и расположение горизонтальной фоновой «заплатки»
продлеваем фон в отрицательное пространство на размер border-radius-hdr */
$hdr-logo-patch-height: $shadow-size + $border-radius-hdr;
$hdr-logo-patch-width: $hdr-logo-width + $border-radius-hdr;
$hdr-logo-patch-top: $padding-to-top + $hdr-topwell-height - $shadow-size;
$hdr-logo-patch-background-position: -1*($hdr-topwell-height - $shadow-size);
/* МИКСИНЫ
* -------------------------- */
@mixin dropshadow-header {
-webkit-box-shadow: 0 ceil(.67*$shadow-size) $shadow-size $dropshadow-rgba;
-moz-box-shadow: 0 ceil(.67*$shadow-size) $shadow-size $dropshadow-rgba;
box-shadow: 0 ceil(.67*$shadow-size) $shadow-size $dropshadow-rgba; }
@mixin well-header {
-webkit-box-shadow: inset 0 0 $shadow-size $well-header-rgba;
-moz-box-shadow: inset 0 0 $shadow-size $well-header-rgba;
box-shadow: inset 0 0 $shadow-size $well-header-rgba; }
/* использовано в «заплатке», чтобы совпадать с внутренней тенью */
@mixin well-header-outset {
/* устанавливаем свойства "opacify" и "transparent" на значение между 0 и 1
на .19 более непрозрачным для внешней тени, чтобы совпадать с внутренней */
$deepen-amount: .19;
-webkit-box-shadow: 0 0 $shadow-size opacify($well-header-rgba, $deepen-amount);
-moz-box-shadow: 0 0 $shadow-size opacify($well-header-rgba, $deepen-amount);
box-shadow: 0 0 $shadow-size opacify($well-header-rgba, $deepen-amount); }
/* использовано в «заплатке» для добавления тени только на левой стороне */
@mixin well-header-left {
-webkit-box-shadow: inset $shadow-size 0 $shadow-size -1*$shadow-size $well-header-rgba;
-moz-box-shadow: inset $shadow-size 0 $shadow-size -1*$shadow-size $well-header-rgba;
box-shadow: inset $shadow-size 0 $shadow-size -1*$shadow-size $well-header-rgba; }
/* CSS
* -------------------------- */
/* http://www.paulirish.com/2012/box-sizing-border-box-ftw */
*,
*:after,
*:before {
-webkit-box-sizing: border-box; /* Safari/Chrome, и прочие WebKit браузеры */
-moz-box-sizing: border-box; /* Firefox, и другие Gecko браузеры */
box-sizing: border-box; /* Opera/IE 8+ */
}
html {
overflow-y: scroll;
/* было бы странным прокручивать контент горизонтально с фиксированным заголовком */
overflow-x: hidden;
height: 100%;
margin: 0;
padding: 0;
}
body {
height: 100%;
margin: 0;
padding: 0;
background: white;
}
article {
display: block;
width: 90%;
max-width: 700px;
margin: 0 auto; /* 0 auto 2em */
padding: 0 20px 40px; /* 1.5em 3em 3em */
}
/* предполагаем, что в HTML5 только один элемент "header" */
.header-top {
/* прикрепляем к верхней границе - 0 по умолчанию */
position: fixed;
/* увеличиваем z-index, чтобы разместить поверх всего контента. Здесь z-index выше, чтобы разрешить z-indexы в прокручиваемом контенте */
z-index: 100;
width: 100%;
}
/* сопоставляем фоновые рисунки, чтобы они смешались */
/* ОПЦИОНАЛЬНО, продлеваем цвет фона заголовка по всей верхней части */
.header-top,
/* ОПЦИОНАЛЬНО дополнительный div, продлеваем больший фон заголовка слева: */
.header-bg:before,
.header-wrap,
/* заполнение до границы окна полосы с левой стороны */
.header-wrap:before,
/* «навес» слева */
.header-content:before {
background: white; /* белый */
}
/* сопоставляем ширину, отступы и заполнения */
.page-wrap, .header-wrap {
width: 90%;
min-width: $min-width;
max-width: $max-width;
/* центрируем контент */
margin: 0 auto;
/* отделяем контент от границ окна:
padding-top и -bottom определены ниже.
Здесь (не в теле), чтобы работало с фиксированным заголовком */
padding: 0 $padding-to-window;
}
.header-content {
/* absolute для дочерних элементов */
position: relative;
height: $hdr-total-height;
width: 100%;
padding-top: $padding-to-top;
/* обрезаем тени по краям и сверху */
overflow: hidden; }
/* добавляем больший фон «навеса» слева и тень */
.header-content:before {
content: "";
/* убираем из макета */
position: absolute;
/* заводим под .header-wrap и .nav-main ul, чтобы закрыть тень по правой границе */
z-index: -1;
height: $hdr-side-height;
width: $hdr-side-width;
padding: 0;
border-radius: 0 0 $border-radius-hdr 0;
@include dropshadow-header;
}
/* ВОГНУТЫЙ СКРУГЛЁННЫЙ УГОЛ */
.header-content:after {
content: "";
display: block;
position: absolute;
/* заводим под фон заголовка */
z-index: -1;
height: $border-radius-hdr * 2;
width: $border-radius-hdr * 2;
border-top: $border-radius-hdr solid white;
border-left: $border-radius-hdr solid white;
top: $hdr-side-total-height - $border-radius-hdr;
left: -1*$border-radius-hdr;
/* располагаем вверху слева */
border-radius: $border-radius-hdr * 2 0 0 0;
}
/* сопоставляем фоновые изображения */
.header-logo, .header-social {
background-color: $header-bg;
background-image: $header-bg-image;
@include well-header;
}
.header-logo {
position: absolute;
/* поверх .header-content-social */
/* z-index: 1; */
height: $hdr-logo-height;
width: $hdr-logo-width;
border-radius: $border-radius-hdr 0 $border-radius-hdr $border-radius-hdr;
}
/* «заплатка», чтобы закрыть шов перекрывающихся фигур -
здесь нельзя использовать вложенные псевдо-элементы, поэтому добавляем div
overflow:hidden, чтобы обрезать тень наложенного скруглённого угла (ниже) */
.header-logo-patch {
display: block;
overflow: hidden;
position: absolute;
z-index: 1;
top: $hdr-logo-patch-top;
height: $hdr-logo-patch-height;
width: $hdr-logo-patch-width;
background-color: $header-bg;
background-image: $header-bg-image;
background-position: 0 $hdr-logo-patch-background-position;
background-repeat: no-repeat;
@include well-header-left;
}
/* наложенный скруглённый угол (с внешней тенью) */
.header-logo-patch:after {
content: "";
display: block;
position: absolute;
bottom: -$border-radius-hdr;
right: -$border-radius-hdr;
height: $border-radius-hdr * 2;
width: $border-radius-hdr * 2;
background-color: white;
border-radius: $border-radius-hdr 0 0 0;
@include well-header-outset;
}
.header-social {
position: absolute;
top: $padding-to-top;
height: $hdr-topwell-height;
width: 100%;
border-radius: $border-radius-hdr $border-radius-hdr $border-radius-hdr 0;
}
/* Ниже представлен Презентационный код CSS */
/* принудительно добавляем вертикальную прокрутку, чтобы предотвратить прыганье страницы */
html {overflow-y: scroll;}
body {
line-height: 1.3125; }
h2 {
color: #777; font-weight:300;
}
.border-grey {
border: 8px solid #eaeaea;
}
/* соответственное масштабирование всех изображений */
img.scale, object.scale {
/* исправляет небольшую линию разрыва в нижней части содержащей div */
display: block;
/* странный баг Firefox: необходимо установить width:100%, иначе он разрушает разметку блока контекста (где display:table-cell заполняет оставшееся от фиксированной по ширине колонки пространство) */
width: 100%;
max-width: 100%;
/* на всякий случай, чтобы принудительно установить правильное соотношение сторон */
height: auto !important;
-ms-interpolation-mode: bicubic;
}
.lt-ie9 img.scale, .lt-ie9 object.scale {
width: auto9; /* ie8+9 – необходимо тестирование*/
}
Код примера на CodePen
CodePen – это круто!
CodePen оказался незаменимым как для всех моих экспериментов, так и для презентации клиентам. Огромное его преимущество в возможности написать код на SCSS и быстро видеть результат: я могу попробовать разную ширину, цвета, тени, радиусы границ и т.д., просто меняя значения переменных.
Я использовал переменные для определённых значений высоты и ширины, а также для теней и размеров радиусов границ, затем использовал их для расчёта дополнительных размеров, применяя коэффициенты, где это имело смысл, чтобы связать элементы дизайна вместе.

Переменные рулят!
Фиксированные элементы
Для большинства проектов в качестве контейнера контента я использовал отцентрированный элемент .page-wrap, шириной в процентах от окна браузера. Но когда вы прикалываете ваш заголовок к вершине, используя position: fixed, он больше не является потомком .page-wrap.
Одно из решений – расположить page-wrap после фиксированного заголовка и добавить header-wrap, который дублирует ширину, отступы и дополнения page-wrap (подробности в CodePen).
Чтобы поддерживать иллюзию прокручивания контента, находящегося в «вырезанной» секции под заголовком, я использовал overflow: hidden, чтобы обрезать тени заголовка по бокам. Это вынудило создать оборачивающий блок div, чтобы поддержать небольшой отступ между контентом и рамкой окна при маленькой ширине окна (да, я настолько дотошный).
z-index: Выравнивание
Обычно я стараюсь поддерживать минимальный код, при котором разметка не зависит от порядка расположения блоков кода. Я использовал псевдо-элементы там, где их применение имело смысл, и полагался на множество элементов с абсолютным позиционированием, чтобы поставить элементы на свои места, а также z-index, чтобы контролировать их наложения друг на друга.
Из-за того, что абсолютное позиционирование извлекает элемент из основного потока разметки, я должен был время от времени компенсировать это, например, добавляя padding к другим элементам, чтобы заполнить это пространство.
По мере того, как я работал с позиционированием, я узнал, что могу завести себя в «z-index тупик», благодаря особенностям порядка наложения.
Z-index подразумевает наличие позиционирования: первыми прорисовываются элементы без позиционирования, и легко потерять контроль над тем, какие элементы ограничены «контекстом наложения», например, псевдо-элементы не могут иметь z-index выше, чем их родители (см. пример на CodePen).
Я также поменял z-index теней, сделав их отдельным псевдо-элементом. Это позволило мне завести тени под объекты, которые должны их скрывать или, как по бокам прокручиваемого контента в элементе .main, поднять их наверх, чтобы закрыть всё, что расположено у краёв, и не сломать иллюзию глубины.
/* элементы, расположенные у краёв блока .main должны попадать под
Тень, чтобы поддерживать иллюзию вырезанного блока */
.well-sides {
/* потомки элемента с абсолютным позиционированием */
position: relative;
overflow: hidden; }
.well-sides:before, .well-sides:after {
content: "";
display: block;
position: absolute;
/* выше, чем любой z-index в .main, ниже, чем у заголовка */
z-index: 99;
/* размер тени */
top: -20px;
height: 120%;
/* размер тени */
width: 20px;
background: transparent; }
.well-sides:before {
left: 0;
/* распространяется в отрицательную сторону */
-webkit-box-shadow: inset 20px -20px 20px -20px rgba(0,0,0,0.35);
-moz-box-shadow: inset 20px -20px 20px -20px rgba(0,0,0,0.35);
box-shadow: inset 20px -20px 20px -20px rgba(0,0,0,0.35); }
.well-sides:after {
right: 0;
/* распространяется в отрицательную сторону */
-webkit-box-shadow: inset -20px 20px 20px -20px rgba(0,0,0,0.35);
-moz-box-shadow: inset -20px 20px 20px -20px rgba(0,0,0,0.35);
box-shadow: inset -20px 20px 20px -20px rgba(0,0,0,0.35); }
Скругляем Углы
К сожалению, не существует способа создать объект с вогнутым углом в CSS. В месте, где соединяются два прямоугольника, я использовал «заплатку», чтобы закрыть внутренние тени на «шве» и продлить текстурированный фон в область вогнутого угла.
Затем я накрыл его скруглённым белым углом, используя отрицательное смещение как положительное. В заключение, я добавил обычную тень к скруглённому углу (обрезанную с помощью overflow: hidden), чтобы она гармонировала с внутренней тенью блока.

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

Внутреннее скругление
Для других внутренних скруглённых углов я расположил прозрачный квадрат с одним скруглённым углом и толстой белой границей, чтобы завершить иллюзию. Я использовал размеры border-radius и box-shadow в виде переменных, чтобы определить размер и расположение углов. Пример на CodePen
Как это всё сочетается
Вот как я собрал все части вместе:












Полностью законченный код на CodePen
Заключение
Это правда, я люблю хорошие головоломки, но реализация этого проекта предложенным способом дала мне большую гибкость в процессе разработки, и преимущество мобильной и отзывчивой разметки (исправление скруглённой нижней части при маленькой ширине окна, усечение меню, и прочее). В любом случае, я надеюсь, вы найдёте некоторые полезные элементы в моём слишком тщательном исследовании.