CSS 形状入门

围绕自定义路径环绕内容

Razvan Caliman
Razvan Caliman

长期以来,网页设计师一直被迫在矩形的限制内进行创作。网络上的大多数内容仍然局限于简单的方框中,因为大多数尝试非矩形布局的创意最终都以失败告终。随着 CSS 形状的引入,这种情况即将发生变化,从 Chrome 37 开始提供。 CSS 形状允许网页设计师围绕自定义路径(如圆形、椭圆形和多边形)封装内容,从而摆脱矩形的限制。

形状可以手动定义,也可以从图片中推断。

我们来看一个非常简单的示例。

也许您也曾像我一样天真,在第一次浮动带有透明部分的图片时,期望内容能够环绕并填补空白,结果却失望地发现元素周围仍然保持着矩形的环绕形状。CSS 形状可用于解决此问题。

从图片中提取形状
<img class="element" src="image.png" />
<p>Lorem ipsum…</p>

<style>
.element{
  shape-outside: url(image.png);
  shape-image-threshold: 0.5;
  float: left;
}
</style>

shape-outside: url(image.png) CSS 声明会告知浏览器从图片中提取形状。

shape-image-threshold 属性定义了用于创建形状的像素的最低不透明度级别。其值必须介于 0.0(完全透明)和 1.0(完全不透明)之间。因此,shape-image-threshold: 0.5 表示只有不透明度为 50% 及以上的像素才会用于创建形状。

float 属性是关键。虽然 shape-outside 属性定义了内容环绕区域的形状,但如果没有浮动,您将看不到形状的效果。

元素在其 float 值的另一侧有一个浮动区域。例如,如果包含咖啡杯图片的元素向左浮动,则浮动区域将创建在咖啡杯的右侧。即使您可以在图片两侧都留出空隙,内容也只会围绕由 float 属性指定的另一侧(左侧或右侧,绝不会同时出现在两侧)的形状环绕。

未来,随着 CSS 排除项的引入,shape-outside 将可用于未浮动的元素。

手动创建形状

除了从图片中提取形状外,您还可以手动编写代码。您可以从几个函数值中进行选择来创建形状:circle()、ellipse()、inset() 和 polygon()。每个形状函数都接受一组坐标,并与一个参考框配对,该参考框用于建立坐标系。稍后我们将详细介绍参考框。

circle() 函数

circle() 形状值的示意图

圆形值的完整表示法为 circle(r at cx cy),其中 r 是圆形的半径,cx 和 cy 是圆心在 X 轴和 Y 轴上的坐标。圆心坐标为可选参数。如果您省略这些值,系统会默认使用元素的中心(其对角线的交点)。

.element{
  shape-outside: circle(50%);
  width: 300px;
  height: 300px;
  float: left;
}

在上面的示例中,内容将环绕在圆形路径的外侧。单个参数 50% 用于指定圆形的半径,在本例中,该半径相当于元素宽度或高度的一半。更改元素的尺寸会影响圆形形状的半径。这是一个关于 CSS 形状如何实现自适应的基本示例。

在继续之前,请注意:请务必记住,CSS 形状只会影响元素周围浮动区域的形状。如果元素有背景,则背景不会被形状裁剪。若要实现该效果,您必须使用 CSS 遮罩中的属性,即 clip-path 或 mask-image。clip-path 属性非常方便,因为它遵循与 CSS 形状相同的表示法,因此您可以重复使用值。

`circle()` 形状 + clip-path 的图示

本文档中的插图使用剪裁来突出显示形状,并帮助您了解效果。

回到圆形。

如果使用百分比来表示圆半径,则实际值会通过稍微复杂一些的公式计算得出:sqrt(width^2 + height^2) / sqrt(2)。了解这一点很有用,因为这有助于您想象如果元素的尺寸不相等,最终的圆形形状会是什么样子。

所有 CSS 单位类型都可用于形状函数坐标,例如 px、em、rem、vw、vh 等。您可以根据需要选择灵活或严格的模式。

您可以通过为圆心坐标设置明确的值来调整圆的位置。

.element{
  shape-outside: circle(50% at 0 0);
}

这会将圆心定位在坐标系的原点。什么是坐标系?这就是我们要引入参考框的原因。

CSS 形状的参考框

参考框是元素周围的虚拟框,用于建立绘制和定位形状的坐标系。坐标系的原点位于其左上角,X 轴指向右侧,Y 轴指向下方。

CSS 形状的坐标系

请注意,shape-outside 会改变内容环绕的浮动区域的形状。浮动区域会延伸到由 margin 属性定义的框的外边缘。这称为 margin-box,如果未明确提及,则它是形状的默认参考框。

以下两个 CSS 声明的结果相同:

.element{
  shape-outside: circle(50% at 0 0);
  /* identical to: */
  shape-outside: circle(50% at 0 0) margin-box;
}

我们尚未在该元素上设置外边距。此时,可以安全地假设坐标系的原点和圆心位于元素内容区域的左上角。如果您设置了边距,情况就会发生变化:

.element{
  shape-outside: circle(50% at 0 0) margin-box;
  margin: 100px;
}

现在,坐标系的原点位于元素内容区域之外(向上 100 像素,向左 100 像素),圆心也是如此。圆半径的计算值也会增加,以适应 margin-box 引用框建立的坐标系表面积的增加。

有外边距和无外边距的外边距框坐标系
有几种参考框选项可供选择:`margin-box`、`border-box`、`padding-box` 和 `content-box`。它们的名称暗示了其边界。我们之前解释过 `margin-box`。`border-box` 受元素边框外边缘的约束,`padding-box` 受元素内边距的约束,而 `content-box` 与元素内内容所使用的实际表面积相同。
所有参考框的插图

在 shape-outside 声明中,一次只能使用一个参考框。每个参考框都会以不同的方式(有时是细微的方式)影响形状。还有另一篇文章深入探讨了这一主题,可帮助您了解 CSS 形状的参考框。

ellipse() 函数

ellipse() 形状值的插图

椭圆看起来像被压扁的圆。它们定义为 ellipse(rx ry at cx cy),其中 rx 和 ry 是椭圆在 X 轴和 Y 轴上的半径,而 cx 和 cy 是椭圆中心的坐标。

.element{
  shape-outside: ellipse(150px 300px at 50% 50%);
  width: 300px;
  height: 600px;
}

百分比值将根据坐标系的尺寸计算得出。这里不需要复杂的数学计算。您可以省略椭圆中心的坐标,系统会根据坐标系的中心推断出这些坐标。

X 轴和 Y 轴上的半径也可以使用关键字定义:farthest-side 表示半径等于椭圆中心与参考框最远一侧之间的距离,而 closest-side 表示完全相反的意思,即使用中心与一侧之间的最短距离。

.element{
  shape-outside: ellipse(closest-side farthest-side at 50% 50%);
  /* identical to: */
  shape-outside: ellipse(150px 300px at 50% 50%);
  width: 300px;
  height: 600px;
}

当元素的尺寸(或参考框)可能以不可预测的方式发生变化,但您希望椭圆形状能够自适应时,这可能会派上用场。

circle() 形状函数中的半径也可以使用相同的 farthest-side 和 closest-side 关键字。

polygon() 函数

polygon() 形状值的插图

如果圆形和椭圆形过于局限,多边形形状函数可提供更多选择。格式为 polygon(x1 y1, x2 y2, ...),您可以在其中为多边形的每个顶点(点)指定 x y 坐标对。指定多边形所需的坐标对数量下限为 3,即三角形。

.element{
  shape-outside: polygon(0 0, 0 300px, 300px 600px);
  width: 300px;
  height: 600px;
}

顶点放置在坐标系中。对于自适应多边形,您可以为部分或全部坐标使用百分比值。

.element{
  /* polygon responsive to font-size*/
  shape-outside: polygon(0 0, 0 100%, 100% 100%);
  width: 20em;
  height: 40em;
}

有一个可选的 fill-rule 参数,从 SVG 导入,用于指示浏览器在路径自相交或形状封闭的情况下如何考虑多边形的“内部”。Joni Trythall 很好地解释了 fill-rule 属性在 SVG 中的工作原理。如果未定义,fill-rule 默认为 nonzero。

.element{
  shape-outside: polygon(0 0, 0 100%, 100% 100%);
  /* identical to: */
  shape-outside: polygon(nonzero, 0 0, 0 100%, 100% 100%);
}

inset() 函数

inset() 形状函数可让您创建矩形形状,以便在其中封装内容。考虑到最初的前提是 CSS 形状可以从简单的框中释放 Web 内容,这听起来可能有些违反直觉。很有可能。我还没有找到任何一个无法通过浮动和外边距或 polygon() 实现的 inset() 用例。不过,inset() 确实为矩形形状提供了比 polygon() 更易读的表达式。

inset 形状函数的完整表示法为 inset(top right bottom left border-radius)。前四个位置参数是元素边缘向内的偏移量。最后一个参数是矩形的边框半径。此参数为可选参数,因此您可以将其省略。它遵循您在 CSS 中已使用的 border-radius 简写表示法。

.element{
  shape-outside: inset(100px 100px 100px 100px);
  /* yields a rectangular shape which is 100px inset on all sides */
  float: left;
}

根据参考框创建形状

如果您未为 shape-outside 属性指定形状函数,则可以允许浏览器从元素的参考框派生形状。默认参考框为 margin-box。到目前为止,一切都很平常,浮动元素就是这样工作的。但应用此技术后,您可以重复使用元素的几何图形。我们来看看 border-radius 属性。

如果您使用它来对浮动元素的角进行圆角处理,则会获得剪裁效果,但浮动区域仍为矩形。添加 shape-outside: border-box 以封装由 border-radius 创建的轮廓。

使用 border-box 参考框从元素的 border-radius 中提取形状
.element{
  border-radius: 50%;
  shape-outside: border-box;
  float: left;
}

当然,您也可以通过这种方式使用所有参考框。下面是派生形状的另一种用途:偏移引用。

使用 content-box 参考框创建偏移的引用

仅使用 float 和 margin 属性即可实现偏移引用效果。但这就需要您将引用元素放置在 HTML 树中您希望其呈现的位置。

以下是实现相同偏移引用效果的方法,且灵活性更高:

.pull-quote{
  shape-outside: content-box;
  margin-top: 200px;
  float: left;
}

我们为形状的坐标系明确设置了 content-box 参照框。在这种情况下,旁注中的内容量决定了外部内容将围绕其换行的形状。此处使用 margin-top 属性来定位(偏移)摘录,无论其在 HTML 树中的位置如何。

形状边距

您会注意到,让内容环绕形状可能会导致内容与元素过于贴近。您可以使用 shape-margin 属性在形状周围添加间距。

.element{
  shape-outside: circle(40%);
  shape-margin: 1em;
  float: left;
}

其效果与您习惯使用的常规 margin 属性类似,但 shape-margin 仅影响 shape-outside 值周围的空间。只有在坐标系中有空间的情况下,它才会在形状周围添加间距。这就是为什么在上面的示例中,圆圈半径设置为 40%,而不是 50%。如果半径设为 50%,圆圈会占据坐标系中的所有空间,导致 shape-margin 无法发挥作用。请注意,形状最终会受限于元素的 margin-box(元素及其周围的 margin)。如果形状较大且溢出,则会被裁剪到 margin-box,最终会呈现矩形形状。

请务必注意,shape-margin 仅接受单个正值。它没有长格式表示法。那么,圆形的 shape-margin-top 是什么?

为形状添加动画效果

您可以将 CSS 形状与许多其他 CSS 功能(例如过渡和动画)结合使用。不过,我必须强调,用户在阅读时,如果文本布局发生变化,他们会感到非常烦恼。如果您决定使用动画形状,请密切关注用户体验。

您可以为 circle() 和 ellipse() 形状的半径和中心添加动画效果,只要它们是以浏览器可以插值的值定义的。可以从 circle(30%) 升级到 circle(50%)。但是,在 circle(closest-side) 到 circle(farthest-side) 之间添加动画效果会导致浏览器卡顿。

.element{
  shape-outside: circle(30%);
  transition: shape-outside 1s;
  float: left;
}

.element:hover{
  shape-outside: circle(50%);
}
GIF 动画圆圈

为 polygon() 形状添加动画效果时,可以实现更有趣的效果,但需要注意的是,多边形在两个动画状态之间必须具有相同数量的顶点。如果您添加或移除顶点,浏览器将无法进行插值。

一个技巧是添加所需的最大顶点数量,并在动画状态下将它们聚集在一起,这样形状的边缘看起来就会更少。

.element{
  /* four vertices (looks like rectangle) */
  shape-outside: polygon(0 0, 100% 0, 100% 100%, 0 100%);
  transition: shape-outside 1s;
}

.element:hover{
  /* four vertices, but second and third overlap (looks like triangle) */
  shape-outside: polygon(0 0, 100% 50%, 100% 50%, 0 100%);
}
三角形动画 GIF

在形状内换行内容

《爱丽丝梦游仙境》演示的屏幕截图,其中使用 CSS 形状来封装内容

CSS 形状规范的初始草稿包含一个 shape-inside 属性,可让您将内容封装在形状内。甚至在 Chrome 和 Webkit 中也曾实现过一段时间。但是,将任意位置的内容封装在自定义路径中需要付出更多努力并进行更多研究,才能涵盖所有可能的情况并避免 bug。因此,shape-inside 属性已推迟到 CSS 形状级别 2,并且其实现已被撤消。

不过,只要稍加努力并做出一些妥协,您仍然可以实现将内容封装在自定义形状内的效果。解决方法是使用两个具有 shape-outside 的浮动元素,它们位于容器的两侧。折衷方案是,您必须使用一两个没有语义含义的空元素,但它们可以作为支柱,在内部营造出形状的错觉。

<div>
  <div class='left-shape'></div>
  <div class='right-shape'></div>

  Lorem ipsum...
</div>

.left-shape 和 .right-shape 支柱元素在容器顶部的位置很重要,因为它们会向左和向右浮动,以便位于内容的两侧。

.left-shape{
  shape-outside: polygon(0 0, ...);
  float: left;
  width: 50%;
  height: 100%;
}

.right-shape{
  shape-outside: polygon(50% 0, ...);
  float: right;
  width: 50%;
  height: 100%;
}
爱丽丝演示中 shape-inside 属性的解决方法示意图

这种样式设置使两个浮动支柱占据元素内的所有空间,但 shape-outside 属性会为其余内容留出空间。

如果浏览器不支持 CSS 形状,则会将所有内容向下推,从而产生不美观的效果。因此,请务必以渐进增强的方式使用该功能。

在前面的形状动画示例中,您会注意到文本移动可能会让人感到不适。并非所有用例都适合使用动画形状。但您可以为与 CSS 形状互动的其他属性添加动画效果,以便在适当的位置添加效果。

在 CSS 形状的爱丽丝梦游仙境演示中,我们使用滚动位置来更改内容的上外边距。文本被挤在两个浮动元素之间。当它向下移动时,必须根据两个浮动元素的 shape-outside 重新布局。这会给人一种文字正在掉入兔子洞的感觉,从而增强讲故事的体验。擦边球内容?或许可以。但看起来确实很酷。

由于文本布局由浏览器原生完成,因此性能优于使用 JavaScript 解决方案。但在滚动时更改 margin-top 确实会触发大量重新布局和绘制事件,这可能会明显降低性能。请谨慎使用!不过,如果使用 CSS 形状而不为其添加动画效果,则不会对性能产生明显影响。

采用渐进增强的方式

首先假设浏览器不支持 CSS 形状,并在检测到该功能时在此基础上进行构建。Modernizr 是进行功能检测的理想解决方案,“非核心检测”部分中提供了 CSS 形状测试。

某些浏览器通过 @supports 规则在 CSS 中提供功能检测,而无需外部库。Google Chrome 也支持 CSS 形状,因此能够理解 @supports 规则。以下是使用它进行渐进增强的方法:

.element{
  /* styles for all browsers */
}

@supports (shape-outside: circle(50%)){
  /* styles only for browsers which support CSS Shapes */
  .element{
    shape-outside: circle(50%);
  }
}

Lea Verou 撰写了更多关于如何使用 CSS @supports 规则的文章。

与 CSS 排除项的区分

我们现在所说的 CSS 形状在规范早期被称为 CSS 排除和形状。命名上的转变看似细微,但实际上非常重要。CSS 排除现在是一项单独的规范,可让内容环绕任意定位的元素,而无需使用 float 属性。想象一下,让内容环绕绝对定位的元素,这就是 CSS 排除项的一个应用场景。CSS 形状仅定义内容环绕的路径。

因此,形状和排除对象并不相同,但它们确实相辅相成。CSS 形状目前已在浏览器中提供,但 CSS 排除项尚未通过形状交互实现。

用于处理 CSS 形状的工具

您可以在传统图片制作工具中创建路径,但在撰写本文时,这些工具都无法导出 CSS 形状值所需的语法。即使能,这种工作方式也不太实用。

CSS 形状旨在用于浏览器,它们会与页面上的其他元素发生互动。直观呈现形状编辑对周围内容的影响非常有用。以下几种工具可帮助您完成此工作流程:

Brackets:Brackets 的 CSS 形状编辑器扩展程序使用代码编辑器的实时预览模式,叠加了一个用于修改形状值的交互式编辑器。

Google Chrome:适用于 Google Chrome 的 CSS 形状编辑器扩展程序通过添加用于创建和修改形状的控件,扩展了浏览器的开发者工具。它会在所选元素上方放置一个交互式编辑器。

Google Chrome 中的检查器内置了对突出显示形状的支持。将鼠标悬停在具有 shape-outside 属性的元素上,该元素会亮起以显示形状。

从图片中提取形状:如果您更喜欢生成图片并让浏览器从中提取形状,Rebecca Hauck 撰写了一篇不错的 Photoshop 教程。

Polyfill:Google Chrome 是首个支持 CSS 形状的主流浏览器。Apple 的 iOS 8 和 Safari 8 即将支持此功能。其他浏览器供应商未来可能会考虑支持该功能。在此之前,您可以使用 CSS 形状 polyfill 来提供基本支持。

总结

在内容大多局限于简单方框的网页中,CSS 形状提供了一种创建富有表现力的布局的方法,从而弥合了网页设计与印刷设计之间的保真度差距。当然,形状也可能被滥用,从而分散用户的注意力。不过,如果运用得当,形状可以增强内容呈现效果,并以独特的方式吸引用户的注意力。

最后,我将向您展示其他人的作品集,其中大部分来自印刷品,展示了非矩形布局的有趣用途。希望这能激发您尝试 CSS 形状并尝试新的设计理念。

非常感谢 Pearl Chen、Alan Stearns 和 Zoltan Horvath 审阅本文并提供宝贵意见。