几乎所有 Web 应用(或网站)都需要裁剪图片这一功能。许多 Web 应用依赖于服务端( 使用像 PHP 、Node 和其他编程语言来裁剪图片)。不过,Web 开发者有时候可能无法从服务端拿到裁剪好的图片,因此你需要在客户端(比如,浏览器)中进行裁剪。
庆幸的是,你可以使用简单的 HTML5 和 CSS 代码对 Web 上的图片进行裁剪,而无需使用 JavaScript 或任何其他脚本语言。
在这节课中,我将向大家展示实现这一目标的几种技术(CSS 裁剪图片的技术),其中大多数技术利用了 CSS 的属性,比如 width
、height
、overflow
、object-fit
、object-position
、aspect-ratio
、padding-top
、transform
、clip()
、clip-path
、mask
和 object-view-box
等。
裁剪图片的好处是什么?
裁剪的图片肯定是对 Web 的呈现有一定好处的,所以在 Web 上使用图片时,总是需要对图片进行裁剪,不管是在服务端裁剪,还是在客户端裁剪。裁剪后的图片会有以下几个优势。
专注于一个主题
裁剪图片使你可以放大或缩小主题。你可以使用这种技术使图片只表达一个主题,这样可以使用户的焦点转移到该主题上。
删除过多的视觉信息
太多的视觉信息会使你的用户分散注意力,结果,用户反而容易忽略了图片。裁剪图片可以帮助你将焦点转移到主要主题,删除不必要的信息和不适当的位置,并防止随机物体干扰用户的注意力。
合成的裁剪
可以使用图片裁剪技术进行图片合成。例如,你可以将图像分割为 3×3
网格,在网格线上对齐元素,并根据三分法进行裁剪。在用户头像中,你可以将眼睛与网格线匹配,在其他图像中,你可以将焦点转移到主题上。
不寻常的形状
理想情况下,用户应该在浏览网页时遵循消息和行动呼吁(CTA)提示。他们不应该停下来分析设计。不寻常的形状,比如钻石和星星,会让观众停下来思考形状背后的原因,把注意力从信息转移到设计上。你可以把这些形状裁剪成一个圆圈或任何形状,让你的用户对你的信息感兴趣。
CSS 是如何处理 Web 上图片的?
CSS 不允许处理文件、数据格式或数据转换。换句话说,CSS 并不能在深层次上改变 Web 上的图片,但是使用 CSS,你可以决定 Web 图片在浏览器上如何呈现。
事实上,客户端(浏览器)上裁剪图片,它也只是隐藏图片的一部分,而不是实际裁剪图片。使用 CSS 技术裁剪过的图片,使用右键单击选项下载图片时,保存的图片将始终是原始图片文件的副本。
到目前为止,CSS 中用于裁剪图片的技术方案有很多种,只需要将 width
、height
、overflow
、object-fit
、object-position
、clip-path
、mask
、clip()
和 object-view-box
等特性用好,就可以在客户端向用户呈现更好的 Web 图片,表达更聚焦的主题。
设计软件中如何裁剪图片?
在深入研究 CSS 裁剪术之前,我想向你展示我们在设计软件中是如何对图片进行裁剪的。拿 Figma 软件为例吧:
在 Figma 设计软件中,一般会采用蒙板技术,对图片进行裁剪:
在蒙板层中,将图片位置调整到最佳位置。
现在我们了解了它是如何工作的,接下来就来看看它在浏览器中是如何工作的。
CSS 裁剪术
CSS 中裁剪图片(也可以是其他元素)有很多种技术方案。接下来,我们从古老的技术开始,逐步将大家带入到现代裁剪术中。
使用 clip 裁剪
CSS 2.0 就有了 clip
属性,配合其属性关键字 rect
可以实现元素的矩形裁剪效果。它更像设计软件中的裁剪工具:
简单地说,我们可以在 clip
的 rect()
中传入四个值,即 top
、right
、bottom
和 left
,它们指的都是距离原图左上角的位置,这和设计软件中的裁剪是有所差异的:
不过,使用 clip
时有一个前提条件,它只能作用于绝对定位(position: absolute
)的元素上 。通过下面这个示例来演示 clip
如何使用。
<div class="card">
<figure>
<img src="card-thumbnail.jpg" alt="Card Thumbnail" />
</figure>
<h3> Card Ttitle</h3>
<p> Card Description</p>
</div>
.card figure {
position: relative;
aspect-ratio: 4 / 3;
}
.card img {
--top: 420px;
--left: 220px;
--width: 480px;
--height: calc(var(--width) * 3 / 4);
--bottom: calc(var(--top) + var(--height));
--right: calc(var(--left) + var(--width));
position: absolute;
clip: rect(var(--top) var(--right) var(--bottom) var(--left));
top: calc(-1 * var(--top));
left: calc(-1 * var(--left));
}
该技术方案,最显著的缺陷是:
- 适配性差,不能自动随容器尺寸变化调整裁剪位置;
- 大图放在小容器中时,很难裁剪到所需要的焦点位置。
使用 clip-path 裁剪
CSS Masking Module Level 1 中的 clip-path 有点类似于 clip
,但它比 clip
要强大得多,因为有多个函数可用于 clip-path
,比如 inset()
、circle()
、ellipse()
、polygon()
和 url()
等。clip-path
可以根据这些函数绘制的图形来对图片(或其他元素)进行裁剪:
/* 矩形 inset() */
.card {
clip-path: inset(20px 20px 50px);
}
/* 带圆角的矩形 inset() */
.card {
clip-path: inset(20px 20px 50px round 15px);
}
/* 圆形 circle() */
.card {
clip-path: circle(80px at 50% 50%);
}
/* 椭圆形 ellipse() */
.card {
clip-path: ellipse(100px 80px at center);
}
/* 多边形 polygon */
.card {
clip-path: polygon(5% 5%, 95% 5%, 95% 95%, 5% 95%);
}
/* SVG Path: url() */
.card {
clip-path: url("#triangle");
}
其中,clip-path
的 inset()
函数和 Figma 设计软件中的裁剪工具是相似的:
你也可以使用类似 Clippy 在线工具来获取所需要的 clip-path
值:
Clippy 在线地址:https://bennettfeely.com/clippy/
clip-path
的一个好处是它允许你指定裁剪的形状和位置。虽然 clip-path
能裁剪出更多的形状,但它所存在的缺陷和 clip
几乎是相似的。不过,有意思的是,clip-path
可以结合 CSS 的 transition
或 animation
制作出比较有创意的动画效果。比如将一张图片(或元素)从不可见过渡到完全可见。
img {
aspect-ratio: 16 / 9;
max-width: 400px;
border-radius: 8px;
display: block;
animation: clip-path 6s linear alternate infinite;
}
@keyframes clip-path {
from {
clip-path: inset(50% round 8px);
}
to {
clip-path: inset(0 round 8px)
}
}
其原理很简单,当 clip-path
的值是 inset(50%)
时,整个图片是不可见的;反过来,inset(0)
时,整个图片又可见。
注意,通过应用 inset(50%)
,蓝色矩形可以完全不可见。使它不可见的值是 50%
,因为我们从蓝色矩形的四面向其中心裁剪。
基于该原理,我们还可以控制方向,即元素从单边向相对方向扩展。要做到这一点,我们只需要使用四个边中的一个值。例如,如果我们想要一个从上到下的过渡效果,只需要将底部的值设置为 100%
过渡到 0
。下面这张图可以解释这一点:
<figure class="zoom-in">
<img src="https://picsum.photos/800/600?random=1" alt="">
<span>Zoom In</span>
</figure>
figure {
display: grid;
border-radius: 8px;
aspect-ratio: 16 / 9;
}
figure > *,
figure::after {
grid-area: 1 / 1 / -1 / -1;
}
figure img {
z-index: 1;
}
figure::after {
content: "";
z-index: 2;
clip-path: inset(0% 0% 0% 0%);
transition: all .2s linear;
}
/* Zoom In */
.zoom-in:hover::after {
clip-path: inset(50% 50% 50% 50%);
}
/* Zoom Out */
.zoom-out::after {
clip-path: inset(50%);
}
.zoom-out:hover::after {
clip-path: inset(0);
}
/* Top To Bottom */
.top-to-bottom:hover::after {
clip-path: inset(100% 0 0 0);
}
/* Bottom To Top */
.bottom-to-top:hover::after {
clip-path: inset(0 0 100% 0);
}
/* Left To Right */
.left-to-right:hover::after {
clip-path: inset(0 0 0 100%);
}
/* Right To Left */
.right-to-left:hover::after {
clip-path: inset(0 100% 0 0);
}
同理,clip-path
的 circle()
、ellipse()
和 polygon()
也可以像 inset()
一样,为裁剪元素添加动效。不过,在使用 polygon()
制作动效时,要保持节点数量相同。比如我们希望形状从一个五角形过渡到一个正方形。一般情况下,五角形有十个节点位置,而正方形只有四个节点位置:
两个图形的节点数要是不一致的话,做出来的动画效果就会让人感到不协调:
.polygon {
clip-path: polygon(
50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 91%,
50% 70%,
21% 91%,
32% 57%,
2% 35%,
39% 35%
);
transition: clip-path 0.2s linear;
}
.polygon:hover {
clip-path: inset(0 0 0 0);
}
你已经看到了,示例中的矩形使用的是 inset()
函数绘制的。其实,我们也可以使用 polygon()
,也用十个点来绘制矩形:
.polygon {
clip-path: polygon(
50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 91%,
50% 70%,
21% 91%,
32% 57%,
2% 35%,
39% 35%
);
transition: clip-path 0.2s linear;
}
.polygon:hover {
clip-path:polygon(
0 0,
50% 0,
100% 0,
100% 34%,
100% 68%,
100% 100%,
50% 100%,
0 100%,
0 68%,
0 34%
);
}
只要你有足够的创意,使用 clip-path
就可以制作出很多有创意的 Web 动效,比如 @argyleink 的 transition.css 就是一个典型的案例:
使用 object-view-box 裁剪
在 CSS Images Module 下一版本(Level 5)将会新增一个 object-view-box
属性。它有点类似于 clip-path
中的 inset()
,可以用来裁剪图片(或其他元素)。
The object-view-box property specifies a “view box” over an element, similar to the
<svg viewBox>
attribute, zooming or panning over the element’s contents.
大致的意思是:“object-view-box
可以用来指定元素上的视图框(View Box),该视图框类似于 <svg>
元素的 viewBox
属性,可以使元素的内容进行缩放或平移。”
注意,
object-view-box
属性相关的规范是 @Jake Archibald 提出的。
object-view-box
除了接受 inset()
函数值之外,还可以接受 rect()
和 xywh()
函数值。但在这里,我们只介绍 inset()
的用法。
让我们回到这节课的主题上来。
使用 object-view-box
,我们能够使用 inset()
从四个边(top
、right
、bottom
和 left
)绘制一个矩形,从而达到裁剪图片(或其他可替换元素,比如 <video>
)的目的。
img {
aspect-ratio: 1;
width: 300px;
object-view-box: inset(25% 20% 15% 0%);
}
从《响应式图片:防止图片的拉伸或挤压》一节中,我们可以知道,运用于 Web 上的任何图片都会有一个原始尺寸(也称为内在尺寸 ),即图片的默认尺寸,也就是设计师导出时的图片尺寸。如果我们在一张 1194px × 1194px
原始图片上使用下面这段 CSS 代码,它将获得一个 300px × 300px
的图片(在浏览器呈现给用户的图片):
<img src="thumbnail-1194-1194.jpg" />
img {
display: block;
aspect-ratio: 1;
width: 300px;
}
而我们这里的目标是,使用 object-view-box
的 inset()
在原图上裁剪出更适合展示图片主题的矩形区域。
其实很简单,因为 object-view-box
的 inset()
和 clip-path
的 inset()
非常的相似。它将基于原始图像的宽度(width
)和高度(height
),形成一个裁剪过的图像。它将帮助我们绘制一个嵌入的矩形并控制它的四个边缘(top
、right
、bottom
和 left
):
注意,clip-path
和 object-view-box
的 inset()
仅仅是使用方式相同,但两者在浏览器中呈现出来的效果有天壤之别:
clip-path
的insiet()
直接对原图的四边向内进行裁剪,图片在视觉上会变得更小;object-view-box
的inset()
进行裁剪之后会放大且平移,图片继续填充整个容器。
我们通过一个录屏效果来进一步向大家展示 object-view-box
的效果:
object-view-box
、clip-path
和 clip
有点相似,都可用于图片的裁剪,但不足之处是,要是图片的尺寸是正方形的,经 object-view-box
裁剪后的图片会变形。
庆幸的是,我们可以使用 CSS 的 object-fit
来解决这个问题:
img {
width: 300px;
aspect-ratio: 1;
object-view-box: inset(25% 20% 15% 0%);
object-fit: cover;
}
是不是很神奇。这样做,能够在 <img>
上进行艺术指导(达到 HTML5 的 <picture>
等同的效果),对于 Web 响应式设计非常有用。
注意,响应式 Web 图片是 CSS 中复杂的一部分,其中
<picture>
就可以实现艺术指导的图片效果,但这部分内容已超出这节课的范畴,在这里不做相关的阐述。
另外,object-view-box
和 clip-path
还有一点不同,clip-path
可以很好地制作有创意的 Web 动效;可 object-view-box
却不能用于 Web 动效。换句话说,object-view-box
是不可以用于 transition
和 animation
的。不过,object-view-box
可以用正负值来实现图片的缩小和放大的效果:
/* Zoomed In*/
.zoomed-in {
object-view-box: inset(10%);
object-fit: cover;
}
/* Zoomed Out */
.zoomed-out {
object-view-box: inset(-10%);
}
想象一下,这对于能够缩放图像是多么有用,而不需要用一个额外的元素来包装它。
需要注意的是,object-view-box
是一个较新的 CSS 特性,到目前为止(写这节课的时候)还没有得到所有现代主流浏览器的支持,具体可以从 Caniuse.com 上查看浏览器的兼容性:
使用 object-fit 裁剪
在介绍 object-view-box
裁剪图片的时候,有说过,可以使用 object-fit
来避免图片的拉伸变形和失真。其实,对于 Web 图片的裁剪还可以使用 object-fit
来控制。
避免开 object-view-box
这个新属性不说,对于可替换元素 img
或 video
等,使用 object-fit
对其进行裁剪是最佳的方案。object-fit
定义了 HTML 可替换元素(比如 img
或 video
)的内容应该如何调整大小以适应其容器。该属性可以接受 cover
、contain
、fill
(其默认值) 、scale-down
和 none
。它们的具体表现如下:
<div>
<img src="https://picsum.photos/500/372?random=1" alt=" " class="landscape" width="500" height="372" />
</div>
<div>
<img src="https://picsum.photos/500/372?random=1" alt=" " class="square" width="500" height="372" />
</div>
<div>
<img src="https://picsum.photos/500/372?random=1" alt=" " class="portrait" width="500" height="372" />
</div>
img {
display: block;
object-fit: var(--object-fit, cover);
object-position: center;
}
.portrait{
width: 250px;
height: 350px;
}
.landscape{
width: 300px;
height: 200px;
}
.square{
width: 250px;
height: 250px;
}
你可以尝试着调整 Demo 参数,可以看到 object-fit
裁剪图片之后的不同效果:
另外,CSS 的 background-size
和 mask-size
取值为 cover
或 contain
时,它的表现形式和 object-fit
的 cover
或 contain
相同:
注意,object-fit
、background-size
和 mask-size
属性的 cover
和 contain
的计算是一套数学公式的,有关于这方面详细的介绍,可以参阅读前面的《响应式图片:防止图片的拉伸或挤压》课程。在课程中,以 background-size
为例,向大家阐述了它的 cover
和 contain
是如何计算的。
image() 函数裁剪
image()
函数可以像 url()
函数,给 background-image
引入一张背景图片。我们都知道,clip
、clip-path
、object-view-box
和 object-fit
都只能运用于元素上,无法直接对 background-image
引入的图片做裁剪相关的操作。
不过,除了 background-size
可以用于背景图片的裁剪之外,还可以使用 image()
的图片片段功能对其进行裁剪。简单地说,你可以给 image()
函数引入的图像中添加一段媒体标识符(沿着 x
和 y
轴的起点以及宽度 w
和高度 h
),只在元素背景层中显示背景图像中的一部分。
.element {
background-image: image('sprites.png#xywh=338,324,360,390')
}
其中 sprites.png
是引入的背景图像名称和相应的路径,和 url()
函数引入背景图像是一样的; #xywh=xVal,yVal,wVal,hVal
指的是媒体标识符,也就是需要显示的图像对应的 x
和 y
坐标以及宽度和高度:
正如上图所示,它的使用和以前 CSS Sprites 是相似的,不同的只是使用媒体标识符来指定要显示的图像的坐标位置和大小。这也意味着,你可以通过 CSS 裁剪和加载图像的一部分。
注意,媒体标识符
#xywh=xVal,yVal,wVal,hVal
是向后兼容的,如果浏览器不理解媒体标识符将会被忽略,image()
函数引入的图片将会被视为无效。另外,url()
函数中也可以使用媒体标识符,只不过浏览器不理解时会显示整张图片。
CSS 的 image()
函数除了具备图片片段功能之外,还具备双向感知 、颜色回退和图片类型查询等功能。我们在《Web 图片:你不应该遗忘的 CSS 技巧》详细阐述过 image()
函数的相关特性,所以在这里就不做重复性的阐述了。
mask 裁剪
前面我们有说过,在设计软件(比如 Figma)中,常常通过添加一个蒙板来对图片进行裁剪:
蒙板的功能同样存在于 CSS 中,即 mask
。它和 CSS 的 clip-path
都可以用来裁剪图片(或其他元素),只不过,它们有一定的差异,如下图所示:
- 剪切需要一个剪切路径,剪切路径可以是一个闭合矢量路径、形状或多边形;剪切路径是一个区域,该区域内部的所有内容都可以显示出来,外部的所有内容将被剪切掉,在页面上不可见;
- 遮罩需要一个高亮或 Alpha 遮罩层,将源和遮罩层合在一起会创建一个缓冲区域,在合层阶段之前,亮度和 Alpha 遮罩会影响这个缓冲区的透明度,从而实现完全或部分遮罩源的部分
根据遮罩层的不同,CSS 遮罩分为高亮和 Alpha 两种模式:
- Alpha 模式带有 Alpha 通道的图像(遮罩图层),Alpha 通道包含在每个像素数据中的透明信息。上图左侧就是带有黑色和透明区域的 PNG 图像,其中黑色部分将会显示,透明区域内容将会被隐藏;
- 高亮模式使用图像的亮度值作为遮罩值,上图右侧的遮罩层白色区域将会显示出来,透明区域将会被隐藏。
来看一个简单的示例:
.mask {
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
}
.mask--alpha {
mask-image: url("alpha-mask.png");
}
.mask--luminance {
mask-image: url("luminance-mask.png");
}
使用 mask
时,有一个细节需要注意,url()
引入的遮罩层图片必须是同域的(要解决 图片 CROS 兼容性),否则 mask-image
会被视为是 none
。
可以说,mask
用于裁剪图片(或其他元素)是最接近于设计软件中裁剪图片功能的。你可以通过 mask-position
来调整遮罩图的位置,还可以通过 mask-size
来调整遮罩图的尺寸,它们的使用和 background-position
以及 background-size
是相同的。
这种裁剪术,非常适用于制作一些镂空(挖洞)的效果:
来看一个实际案例,使用 CSS 渐变当作遮罩层图片,实现一个类似信封边框的卡片效果。关键代码如下:
.ticket-visual_visual::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
inset: -10px;
border-radius: 20px;
transition: all 300ms cubic-bezier(0.03, 0.98, 0.53, 0.99) 0s;
/* 制作渐变边框 */
padding: 10px;
background: linear-gradient(to right, var(--background), var(--background)), linear-gradient( to right, var(--color1), var(--color2), var(--color3), var(--color4) );
background-origin: content-box, padding-box;
background-clip: content-box, padding-box;
/* 蒙板效果 */
mask-image: repeating-linear-gradient(125deg , black, black 30px, transparent 30px, transparent 48px);
mask-size: cover;
mask-repeat: no-repeat;
}
mask
和 clip-path
一样,可以用在 transition
和 animation
中,来制作有创意的 Web 动画效果。比如 @Ricardo Soto 在 Codepen 上使用 mask 制作的一个动效:
小结
正如你所看到的,CSS 中的裁剪术有很多种,这些技术和 Web 布局技术一样,没有哪一种技术是最好的,只有最合适的。换句话说,它们各有利弊,比如说:
clip
和clip-path
裁剪出之后的图片(或其他元素)的适配性较差,反之object-fit
、object-view-box
和mask
适配性就要更强;clip-path
和mask
可以裁剪出各式各样的形状,还可以配合transition
和animation
制作一些有创意的 Web 动效,就这方面而言,其他的裁剪术就无法做到;object-view-box
更像svg
的viewBox
特性,它可以将裁剪后的内容做拉伸和位移,并且能填充整个容器,只是会出现变形和失真等现象,需要结合object-fit
来修复。
其实,在 CSS 中还有其他的一些裁剪术,比如 position:absolute
和 overflow:hidden
相结合(position
做位移,overflow
将移出的内容裁剪掉);transform
和 overfow
结合等。
既然 CSS 裁剪术有这么多种,所以大家在使用的时候,需要根据自己的业务场景,选择最适合的一种方式。