Хорошие Округлости: Составные Фигуры в 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 и быстро видеть результат: я могу попробовать разную ширину, цвета, тени, радиусы границ и т.д., просто меняя значения переменных.

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

CodePen – это круто!

Переменные рулят!

Фиксированные элементы

Для большинства проектов в качестве контейнера контента я использовал отцентрированный элемент .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), чтобы она гармонировала с внутренней тенью блока.

Скругляем Углы

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

Скругляем Углы - 2

Внутреннее скругление

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

Как это всё сочетается

Вот как я собрал все части вместе:

Как это всё сочетается
Как это всё сочетается - 2

Полностью законченный код на CodePen

Заключение

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

РедакцияПеревод статьи «Well Rounded: Compound Shapes in CSS»