使用 CSS clip-path 绘制粗略头像

Avatar of Chris Coyier
Chris Coyier 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

以下是 Ryan Scherf 的客座文章。Ryan 找到了一种巧妙的方法,可以使头像呈现出粗糙、不规则、多变的边缘。有点像它们是用剪刀剪出来的,而剪刀使用者不太擅长使用剪刀。好处在于,这本身就是一种渐进增强技术,并且可以通过纯 CSS 来实现。

对于像 Quirky 这样创意十足且充满活力的品牌,我们一直在思考如何将这种氛围带到网络上。在整个网站中,某些元素都具有“手绘”的外观。如果不使用大量图片,很难获得这种手绘效果。通过一些简单的三角函数和对 CSS clip-path 的基本了解,我们能够相对轻松且高效地实现这一点。

我们正在构建的内容。请注意每个头像上不规则且多变的边缘。

为什么不使用图像蒙版?

例如,在 SVG 中定义的蒙版

img {
  mask: url(mask.svg) top left / cover;
}

mask 属性可以引用外部 SVG 或通过 ID 在文档中定义的 SVG。

但是,如果您希望每个显示的头像都有一个唯一的形状,而不是相同的形状怎么办?您可以以编程方式生成许多不同的 SVG 形状来应用。但是,我们可以通过使用 (S)CSS 生成 clip-path 来实现相同的效果并获得这种数学生成。

浏览器的支持情况如何?

当与形状值(如 polygon())一起使用时,clip-path 的浏览器支持情况如下:Chrome 24+、Safari 7+、Opera 25+、iOS 7.1+、Android 4.4+。Firefox 仅支持使用在 SVG 中定义的路径的 clip-path(我们将介绍这一点)。IE 目前尚不支持。

您需要使用 -webkit-clip-path,因为这是目前唯一支持的方式,但最好也添加 clip-path。如果 IE 或 Firefox 开始以这种方式支持它,它可能会取消前缀。

简而言之,剪辑路径

您可以为 CSS 剪辑使用一些不同的形状值,但在我们的案例中,polygon 形状是最好的,因为它为我们提供了最多的点和灵活性来创建手绘效果。

您为 polygon() 提供一个 X、Y 点值的列表,例如:<x0> <y0>, <x1> <y1>, ... <xn> <yn>。这将按照顺序围绕您的点绘制一条路径,并裁剪新创建形状**外部**的任何内容。

/* 
  This will create a Hexagon, with the first 
  point being the top tip of the shape 
*/

.hexagon {
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}

这是一个简单的示例

查看 CodePen 上 Chris Coyier (@chriscoyier) 编写的 使用 clip-path 绘制六边形

不那么可怕的数学

我们的六边形非常酷,但它还没有真正达到粗略的效果。它非常僵硬——线条太少。将手绘形状视为一系列连接两个点的短线是最好的方法。我们拥有的点越多,创建的短线就越多。实际上,如果点数足够多,我们可以使 polygon 形状变得非常平滑,从而模拟 circle

这是一个使用 200 个点的示例

查看 CodePen 上 Chris Coyier (@chriscoyier) 编写的 200 个点

这些点从哪里来?

这里需要用到一点数学知识。也许你在高中时学过三角函数?在这个课程中,你学习到的一个基本概念是关于**单位圆**的。基本上,有一组公式(给定 pi)可以生成圆周上的任意数量的点。

单位圆(来自 维基百科

如果我们将这些线段连接起来,我们会得到一个看起来像这样的形状

连接这些点!

仍然有点僵硬,但看起来更像手绘了。

更多点!

我们知道如何使用 clip-path: polygon() 创建六边形和圆形,那么我们如何使它看起来像手绘的呢?

  • 调整点数(点数越多,线段长度越短)
  • 添加一些 X 和 Y 偏差(因此线段不是均匀的)

让我们在 SCSS 中引入它,并创建一个函数来为我们完成繁重的工作。我们将使用

  • random()
  • cos()
  • sin()

最相关的数学公式是

/* 
  To generate an arbitrary points on 
  the unit circle at angle t 
*/
  
$x: cos(t);
$y: sin(t);

将其放入正确的语法中看起来像这样

$w: 160px    // Avatar width
$n: 60;      // Number of points on the circle

@function sketchAvatar() {
  $points: ();

  @for $i from 0 through $n {
    $points: append($points, ($w / 2) * (1 + cos((2 * pi() * $i / $n))) ($w / 2) * (1 + sin((2 * pi() * $i / $n))), comma);
  } 
  
  @return $points;
}

这有点复杂。发生的情况是我们从形状的顶部中间开始,并为 60 个等间距的点生成围绕圆圈的一系列点集。

将所有内容与偏差结合起来

上面的代码仍然生成相当平淡且统一的多边形,因此我们必须添加偏差。我们只需在任何方向调整点,即可获得我们正在寻找的偏移效果。$lower$upper 偏差数字可以是任何值,具体取决于您要实现的外观。

$w:     120px;   // Overall width

@function sketchAvatar() {
  $n: 	  60;     // Number of points
  $lower: -80;    // Lower variance
  $upper: 80;     // Upper variance

  $points: ();

  @for $i from 0 through $n {
    $points: append($points, ($w / 2) * (1 + cos((2 * pi() * $i / $n))) + (rand($lower, $upper) / 100) ($w / 2) * (1 + sin((2 * pi() * $i / $n))), comma);
  } 
  
  @return $points;
}

我们做到了!使用 CSS clip-path: polygon() 创建粗略、独特的头像。

查看 CodePen 上 Chris Coyier (@chriscoyier) 编写的 粗略头像

在 Firefox 中使其生效

Chris 在这里!我认为既然 Firefox 不支持这种方式,但支持 SVG 语法,我们也许可以对其进行某种程度的 polyfill。

.avatar {
  clip-path: polygon( ... ) /* Firefox: nope */
  clip-path: url(#clip); /* Firefox: yep */
}

因此,对于每个头像,我…

  1. 在 CSS 中的伪元素(具有有效伪元素的元素(如父 div)的 content 属性)中输出多边形点。
  2. 使用 JavaScript 提取该值。
  3. 重新格式化点以匹配 SVG 格式(例如,不带“px”)。
  4. 在路径上注入一个新的 <svg>,并准备好 <clipPath>
$(".user").each(function(i) {
 
  var path = window.getComputedStyle(this, ':after').getPropertyValue('content');
  
  // clean house
  svgPolygonPoints = 
    path
      .replace(/px/g, "")
      .replace(/polygon/, "")
      .replace(/\(/, "")
      .replace(/\)/, "")
      .replace(/\;/g, "")
      .replace(/"/g, "")
      .replace(/\'/g, "");
    
  // To get this to actually work, create a <div> instead with this inside, see below.
  var svg = $("<svg width='0' height='0'>")
    .append("<defs><clipPath id='clip-" + (i+1) +"'><polygon points='" + svgPolygonPoints +"' /></clipPath></defs>");
  
  $("body").append(svg);
    
});

它不起作用!哈哈。即使您强制重新绘制头像,由于某些原因,它也不喜欢注入的 SVG。 查看 Amelia 的解决方案

它基本上就像

.user:nth-child(1)  {
  clip-path: polygon(120.04px 60px ...);
}

变成

<svg width="0" height="0">
  <defs>
    <clippath id="clip-1">
      <polygon points="120.04 60, ... "></polygon>
    </clippath>
  </defs>
</svg>