CSS如何实现内凹角效果

编辑推荐:诚征广告商金主入驻此广告位置,如有感兴趣的金主,欢迎邮件至:airenliao@gmail.com咨询相关合作事宜!!!(^_^)

特别声明:此篇文章内容来源于@ANA TUDOR的《Scooped Corners in 2018》一文。

记得@Lea Verou的《CSS Secrets》一书和前几天@Chris Coyier刚发的帖子都介绍了CSS怎么实现元素斜切口的效果。我也尝试着借助Vue的能力,把这种效果构建成一个Vue组件。我把这种效果定义为外切口。而今天将要聊的是与其刚好相反的一个效果:CSS如何实现内凹角的效果

上图展示的效果就是接下来所要聊的内凹角的效果。也就是说,通过下文的介绍,我们可以知道这种效果是如何做的,而且如何在多个元素上实现这样的内凹角效果。在实现这样的效果当中,将会遇到些什么棘手的问题,又是怎么绕过这些问题的。

最初的想法:box-shadow

对于box-shadow的属性,想必大家已经非常了解了,如果你从未接触过box-shadow属性,那么强烈建议您花一点时间去了解一下box-shadow相关的知识。这样能帮助你更好的理解后续的内容。

我先假设你对box-shadow有了一定的了解。就算你不了解,也没有关系。你也可以继续后面的内容。假设我们有一个div的元素。给这个元素添加了一个.box的类名:

<div class="box"></div>

我们可以显式的给这个.box元素设置大小或者通过其自己的内容来决定大小,不管是哪种方式,都并不很重要。这里为了简单起见,给其设置了max-widthmin-height(也是用来设置其大小的)。另外为了能在浏览器中看到效果,其添加了一个outline的效果,让其看起来有边框的样子。或许你会问,为什么不直接使用border呢?这个问题留给大家去思考吧,因为不是这篇文章要探讨的内容。

.box {
    outline: solid 2px;
    max-width: 15em;
    min-height: 10em;
}

接下来,通过伪元素::before来创建一个正方形,其边长等于圆角的直径(或者半径--r的两倍),而且对这个伪元素使用绝对定位。另外为了能在浏览器中看到效果,给这个伪元素添加了一个box-shadowbackground属性。这只是用来辅助大家理解的,后续会删除的。

:root {
    --r: 2em;
}

.box {
    position: relative;

    &::before {
        content: '';
        position: absolute;
        padding: var(--r);
        box-shadow: 0 0 7px #b53;
        background: #95a;
    }
}

特别声明:本文的实例代码都来自于@ANA TUDOR的《Scooped Corners in 2018》一文。不同的是我把文章中的Sass变量换成了CSS自定义变量。后续内容如无特别说明,都将类似的做了修改。

这个时候看到的效果如下:

效果如你所期望的一样。接下来对伪元素::beforeborder-radius值设置为50%,让它成为一个圆形,并且给它设置一个margin的负值,值等于它的半径--r。伪元素的中心点和它的父容器.box的左上角(0,0)重合。为了让溢出的.box的伪元素能隐藏起来,需要在.box中添加一个overflow:hidden

:root {
    --r: 2em;
}

.box {
    position: relative;
    overflow: hidden;

    &::before {
        content: '';
        position: absolute;
        padding: var(--r);
        box-shadow: 0 0 7px #b53;
        background: #95a;
        margin: calc(var(--r) * (-1));
        border-radius: 50%;
    }
}

现在的结果是这样的:

但这样的效果仍然不是我们想要的。为了达到我们想要的效果,我们需要使用box-shadow的第四个参数值:阴影扩展半径。如果你想了解box-shadow添加第四个参数值的效果,可以看下面这个Demo:

你可能已经猜到我们下一步要做什么了。把backgroundbox-shadow前三个值(xy轴的偏移值以及模糊半径)设置为0,并给box-shadow的扩展半径设置为一个较大的值。

box-shadow: 0 0 0 300px;

下面的这个示例演示了box-shadow的扩展半径如何让阴影效果覆盖容器更多的面积。

这里用到的一个技巧是让box-shadow有足够大的扩展半径,这样让伪元素的阴影能覆盖其容器更多的面积。这是非常有意思的一点,给.box设置box-shadow以及给其伪元素添加一个半透明的阴影效果。

.box {
    overflow: hidden;
    position: relative;
    margin: .25em auto;
    min-width: 15em;
    max-width: 15em;
    min-height: 10em;
    border-radius: 1em;

    &:before {
        position: absolute;
        margin: calc(var(--r) * -1);
        padding: var(--r);
        border-radius: 50%;
        box-shadow: 0 0 0 300px rgba(#95a, .75);
        content: ''
    }
}

其实这是很关键的一步,如果你不仔细看,你或许会认为,那个凹角的效果是box-shadow实现的。或许你和我一样会纳闷,box-shadow是如何实现透明凹角的效果。事实并非如此,透明凹角部分是伪元素::beforebackground-colortransparent,而整个紫色部分是由::beforebox-shadow实现的(就是阴影扩散半径有足够大的值,能铺满.box的容器大小)。我录一个视频给大家看看,或许能比文字更好的说明一切原理:

是不是一图胜过千言万语呀。

上面看到的效果,不难发现,凹角的大小是固定的。好在我们这里使用了CSS的自定义属性。因为使用CSS自定义属性之后,可以很容易的通过JavaScript来修改这个属性。这样一来,就可以很好的控制凹角的大小。比如:

:root { --r: 50px } 

.box {
    padding: var(--r);

    &:before {
        margin: calc(-1*var(--r));
        padding: inherit;
    }
}

这是实现凹角效果的关键样式。具体的不多说了,能只要仔细阅读上面的内容,你就能明白为什么。

值得一提的是,我们前面看到的效果都是.box中没有任何内容。也就是说.box里有内容的时候,我们是需要在样式上做一定的调整的。为什么这么说呢?先来看一个效果:

要解决这个问题,很简单,咱们只需要在.box的伪元素::before上添加z-index属性,并且给其设置值为-1

另外通过.setProperty()来修改--r的值。这需要一些JavaScript代码来支持:

// 获取id为r的input元素 和 output元素
const _R = document.getElementById('r'), 
    _O = _R.nextElementSibling.querySelector('output'); 

// 设置一个变量v
let v;

// 创建一个update函数,更新--r的值
function update() {
    if(v !== +_R.value) {
        document.body.style.setProperty(`--r`, `${_O.value = v = +_R.value}px`)
    }
};

update();

_R.addEventListener('change', update, false);
_R.addEventListener('input', update, false);

最终效果如下:

现在离我们想要的效果越来越近了。我们已经知道如何通过box-shadow给单个.box设置单个凹角的效果。那么如果我们想要给一个元素添加四个凹角效果,怎么实现呢?想想,如果你想得出来,可以立马动手试试,就算你想不出来,也并不要紧,后面我们会介绍怎么给.box盒子的每个角添加凹角的效果。

那么到这一步,咱们先暂停一下。上面我们看到的是CSS的自定义属性和JavaScript来实现想要的凹角效果。那么咱们先暂停一步,来看看怎么通过Vue来实现上面示例的效果。

有关于Vue的代码这里就不展示了。详细的可以查看上面Demo的代码,其实你还可以添加其他的参数,比如除了给scooped-corners组件传凹角半径值之外,还可以传border-radiusbackground-color之类。感兴趣的可以尝试一下,并且欢迎在下面的评论中分享您的成果。

就用这种技术

接下来,咱们再深入一点,看看怎么运用这种技术来实现文章开头展示的效果。这里有一点不一样,伪元素的中心点与盒子不致,但他们都有一个共同点,伪元素的中心点,在每个盒子的顶点处。

使用的HTML结构非常简单,这里使用了4<article>元素(相当于前面所讲的.box元素),在<article>元素中包含了一些文本内容:

<article>
    <h3>Yogi Bear</h3>
    <section>
        <p>Smaaaarter than the average bear!</p><a href="#">go</a>
    </section>
</article>
...

<body>包含了四个<article>元素,还有一个<header>元素,整个布局效果采用的是Flexbox。从文章开头的效果上来看,<header>宽度非常宽,然后每行有一个个或两个<article>元素。具体每行展示一个还是两个,这取决于浏览器视窗的宽度。

如果我们每一行只有一个<article>时,那么元素上就不会有凹角的效果,这个时候需要把凹角的半径设置为0。否则我们就要设置一个非零的半径,也就是说--r的值不为0

:root {
    --minW: 15rem; /* 每个article元素的最小宽度 */
    --m: 1rem;     /* 每个article元素的margin值 */
    --r: 0px;      /* 每个article元素上凹角的半径 */
}

article {
    margin: var(--m);
    min-width: var(--minW);
    width: 21em;
}

@media (min-width: 2*($min-w + 2*$m)) {
    html { 
        --r: 4rem; 
    }

    article { 
        width: 40%; 
    }
}

特别注意,CSS自定义属性不能用于媒体查询的条件中,但可以用于媒体查询的区块内。

现在我们考虑一下,每行有两个<article>元素(当然,每个元素都有一个内凹角,因为这个才是我们感兴趣的东东)。

第一个元素中,凹角的圆形在它的父元素的最右边边,也就是left:100%。为了将凹角圆中心点x坐标移到其父元素的右边缘,我们就需要减去圆的半径--r,那么left的值就变成了calc(100% - var(--r))。但我们不想让它出现在右边,而是希望它在<article>元素向右移--m。这样我们就可以算出我们最终想要的一个值:

left: calc(100% - var(--r) + var(--m));

同样的沿着y轴,我们给凹角设置top: 100%,可以把整个凹角圆移到盒子的底部边缘,同样让圆心能和边缘重合,需要把凹角上移凹角的半径--r。如此一来,top的值就变成了calc(100% - var(--r))。最后,类似于left一样,为了让凹角中心点在其父元素底线边缘下,需要加上一定的偏移量--m

top: calc(100% - var(--r) + var(--m));

对于第二个<article>元素(同一行的第二部分),其垂直方向的偏移量具有相同的值:

然而,对于水平方向,凹角是从左边界开始,也就是left0%开始,如此要把凹角放到元素左边缘上,需要向左移动一个--r,如此得到left:calc(0% - var(--r))。然而,最后的位置,咱们同样还需要向左偏移--m。最终left的值:

left: calc(0% - var(--r) - var(--m));

对于第三个<article>元素,在x轴上的偏移量与第一个<article>相同:

第三个元素的垂直方向,凹角的顶部沿其容器顶部边缘开始,也就是top: 0%。如果把凹角圆中心点放在父容器的顶部缘,同样要向上移动一个半径--r,如此得到top: calc(0% - var(--r))。但要把凹角的圆心高于父容器的顶部边缘,那么还需要向上移动--m。这样可以得到top的值:

top: calc(0% - var(--r) - var(--m));

对于最后一个,其水平方向的偏移量和第二个具有相同的值,垂直方向的偏移量和第三个具有相同的值。

所以四个元素对应的lefttop的偏移量如下:

article:nth-of-type(1) { /* 1st */
    left: calc(100% - var(--r) + var(--m));
    top:  calc(100% - var(--r) + var(--m));
}

article:nth-of-type(2) { /* 2nd */
    left: calc(  0% - var(--r) - var(--m));
    top:  calc(100% - var(--r) + var(--m));
}

article:nth-of-type(3) { /* 3rd */
    left: calc(100% - var(--r) + var(--m));
    top:  calc(  0% - var(--r) - var(--m));
}

article:nth-of-type(4) { /* 4th */
    left: calc(  0% - var(--r) - var(--m));
    top:  calc(  0% - var(--r) - var(--m));
}

这意味着凹角圆的中心位置取决于<article>元素之间的间距(这个间距是我们设置的margin: var(--m)的两倍),凹角的半径是--r。在一对水平和垂直的乘数因子分别是--i--j,而且他们的最初的值都是-1

对于第一行的两个<article>元素(第一行是一个2 x 2的网格),我们需要改变垂直方向的乘数因子--j1,这样就可以让凹角的圆心在y轴上低于父容器底部边缘;而对于奇数的<article>(第一列),需要改变水平方向的乘数因子--i1,这样就可以让凹角的圆心在x轴上位于父容器的右侧边缘。

/* multipliers initially set to -1 */
html { 
    --i: -1; 
    --j: -1 
} 

h3, section {
    &:before {
        /* set generic offsets */
        top:  calc((1 + var(--j)) * 50% - var(--r) + var(--j) * var(--m));
        left: calc((1 + var(--i)) * 50% - var(--r) + var(--i) * var(--m));
    }
}

@media (min-width: 2*($min-w + 2*$m)) {
    article {
        /* change vertical multiplier for first two (on 1st row of 2x2 grid) */
        &:nth-of-type(-n + 2) { 
            --j: 1 
        }

        /* change horizontal multiplier for odd ones (on 1st column) */
        &:nth-of-type(odd) { 
            --i: 1 
        }
    }
}

注意,第一行的的两个凹角位置位于<article>中的<section>元素上,所以这两个元素的凹角使用的是<section>的伪元素::before;另第二行的两个凹角位置位于<article>中的<h3>元素上,因此这两个凹角用的是<h3>的伪元素::before。前两个元素的<h3>元素的::before的半径--r设置为0,后两个元素中<section>::before--r设置为0

@media (min-width: 2*($min-w + 2*$m)) {
    article {
        &:nth-of-type(-n + 2) h3, 
        &:nth-of-type(n + 3) section { 
            &:before { 
                --r: 0 ; 
            } 
        }
    }
}

以类似的方式,我们为<article>元素的子元素添加不同的样式:

h3, section { 
    --p: .5rem;
    padding: $p; 
}

@media (min-width: 2*($min-w + 2*$m)) {
    article {
        &:nth-of-type(-n + 2) section, 
        &:nth-of-type(n + 3) h3 {
            padding-right: calc(.5*(1 + var(--i))*(var(--r) - var(--m)) + var(--p));
            padding-left: calc(.5*(1 - var(--i))*(var(--r) - var(--m)) + var(--p));
        }
    }
}

最终效果如下:

特别声明:今天使用CSS自定义属性,在媒体查询的条件中使用自定义属性踩了一个坑。那是因为我想在代码中统一使用CSS自定义属性来替代Sass这样处理器的变量。一直以为在CSS的媒体查询的条件中使用CSS自定义属性是OK的,结果实测代码的时候才发现不支持。最后查找了一下原因:

The var() function can be used in place of any part of a value in any property on an element. The var() function can not be used as property names, selectors, or anything else besides property values. (Doing so usually produces invalid syntax, or else a value whose meaning has no connection to the variable.) —— From the spec

值得庆達的是,你可以使用PostCSS插件postcss-media-variables来做处理。感兴趣的可以自己试试。说实话,再一次感叹PostCSS的神奇之处和无所不能。

潜在的问题

上面的示例看上去完美,方法简单而又能跪浏览器兼容。或许你已经发现了,上例是在一个特定情况下想的结果,但很多时候我们总不是这么的幸运。哪一天需求一变,是不是还能如此轻易而又完美的实现呢?

首先,我们需要用一个伪元素来做这个凹角,当你只需要一个(比如上面看到的示例)或者两个的时候,都不是问题,但有的时候元素的四个角都需要这样的凹角时,那么我们就需要引入一个额外的元素。另外当你的伪元素被其他功能(比如Icon)占用时,你也不得不为此效果添加一个额外的标签元素。蛋疼了吧!

其次上面示例中的background是一个纯色,但我们不可能总是在使用纯色背景的场景中。如果我们想要一个半透明的或者渐变的背景,或者在一张背景图片之下,那么凹角将会成为我们的一个痛点,甚至会说,这个没法实现。

因此,我们需要探索其他更可靠的方案,并且也能让它得到众多浏览器的支持。

灵活性和良好的浏览器支持?是SVG?

想到SVG并不奇怪,但是如果我们想要灵活一点,浏览器兼容性全面一点,SVG可以说是一个最好的解决方案。在.box容器中包含了一个<svg>元素,而且放置在内容的前面。SVG中包含了一个<circle>元素,在这个元素上设置了r属性。

<div class='box'>
    <svg>
        <circle r='50'/>
    </svg>
    TEXT CONTENT OF BOX GOES HERE
</div>

svg相对于.box元素做相对定位,将将其大小设置为能完全覆盖父容器:

.box { 
    position: relative; 
}

svg {
    position: absolute;
    width: 100%;
    height: 100%;
}

到目前为止,没有什么有趣的东西,所以给<circle>添加一个id属性,并且使用SVG的<use>元素来复制多个id相同的<circle>

<circle id='c' r='50'/> 
<use xlink:href='#c' x='100%'/> 
<use xlink:href='#c' y='100%'/> 
<use xlink:href='#c' x='100%' y='100%'/> 

看到这里,是不是会觉得比使用::before伪元素要来得简便,而且也非常方便,就算你要移去一个或多个凹角(示例效果的紫色部分),你只需要少使用几个<use>去克隆就行了。

从上例效果中可以看到,.box的四个角落都圆圈在那了,但这并不是我们想要的凹角,对吧!不要纳闷了,我们的做法是对的。请接着往下看。接下来要做的就是把这些圆圈放到一个<mask>中,给这个<mask>设置一个white的填充色(fill属性来搞定)。同时在其里面使用<rect>元素设置一个和SVG元素一样大小的矩形,主要用来覆盖整个SVG。然后我们在另一个再次使用<use>来调用这个已创建好的<mask>

<mask id='m' fill='#fff'>
    <rect id='r' width='100%' height='100%'/>
    <circle id='c' r='50' fill='#000'/>
    <use xlink:href='#c' x='100%'/>
    <use xlink:href='#c' y='100%'/>
    <use xlink:href='#c' x='100%' y='100%'/>
</mask>
<use xlink:href='#r' fill='#f90' mask='url(#m)'/>

最终效果如下:

同样的,我录制了一个动图来演示效果中的每一个元素:

特别注意:如果.box中有内容,建议放置在svg元素之后。当然也可以放置在其前面,如果放置在前面,svg在做定位时,需要显式的设置toprightbottomleft之类的值。至于为什么,这里不做过多的阐述,感兴趣的同学可以自己去深究其中的为什么。

如果.box有文本,需要把.boxpadding的值和圆角的半径设置相同,同样的,如果使用了CSS自定义属性,可以使用JavaScript来控制它。最好把圆的半径和.boxpadding使用同一个CSS自定义属性--r。这样会更好的控制一点:

当然,我们的背景不再局限于使用一个纯色了。它可以是半透的(就比如上面演示的一样),或者可以使用SVG的渐变和图案填充来实现它。后者也允许我们使用一个或多个背景图像。

我喜欢的就是CSS

或许你会说,我不懂SVG,我就是想使用CSS来实现。其实很高兴你能这样的深究与思考。事实上我们的确可以使用CSS来实现这样的效果。

遗憾的是,使用CSS的方案目前为止并不是所有浏览器都能支持,但使用CSS让我们把事情变得更简化,而且在不远的将来,它们肯定是能得到众多浏览器支持的。

在HTML元素上使用CSS的mask

这里我们移除SVG所有的东西,然后使用CSS,可以在.box元素上设置一个background(可以是一个纯色、半透明、渐变、图像或者多背景,甚至是你任何你想要的CSS)和mask属性。

.box {
    /* any kind of background we wish */
    mask: url(#m);
}

注意,在HTML元素上使用一个svg元素,这个元素里有我们前面使用的mask元素。不幸的是,到目前为止只在Firefox浏览器可以看到效果。

使用CSS设置圆半径

这意味着,需要把<circle>中的r属性删除,然后在CSS中给其设置半径的大小,这里设置的半径大小与.box容器的padding值一样:

.box { 
    padding: var(--r); 
}

[id='c'] { 
    r: var(--r); 
}

这样一来,如果我们改变半么--r的值,凹角的大小和.boxpadding也会随着更新。

注意,在CSS中给SVG元素设置几何属性只在Blink浏览器中有效!

结合前两种方法

虽然这很酷,但遗憾的是目前在任何浏览器中都看不到效果。但值得庆幸的是我们可以做得更好!

使用渐变来做朦层

Note that CSS masking on HTML elements doesn't work at all in Edge at this point, though it's listed as "In Development" and a flag for it (that doesn't do anything for now) has already shown up in about:flags.

由于我们需要完全抛弃SVG,所以我们需要使用CSS的渐变为mask做些事情。这里将使用CSS径向渐变来画圆,下面就是CSS绘制的一个半径为--r的圆,并且这个圆位于.box的左上角。

.box {
    background: radial-gradient(circle at 0 0, #000 var(--r, 50px), transparent 0);
}

如果您对CSS的渐变不太了解,建议您花点时间阅读这几篇文章:《再说CSS3渐变:线性渐变》、《再说CSS3渐变:径向渐变》、《为什么要使用repeating-linear-gradient》、《你真的理解CSS的linear-gradient》。

这个时候你可以看到像上面这样的一个效果:

接下来在mask使用相同的渐变:

.box {
    /* same as before */
    /* any CSS background we wish */
    mask: radial-gradient(circle at 0 0, #000 var(--r, 50px), transparent 0);
}

注意,Webkit浏览器仍然需要给mask属性添加-webkit-前缀。如果你不知道mask怎么使用,建议你花点时间阅读《如何在CSS中使用遮罩》一文。因为后面很多内容都会涉及到这个属性,这样能帮助更好的理解后续的内容。

.box每个角落都添加渐变绘制的圆:

$grad-list: radial-gradient(circle at   0    0 , #000 var(--r, 50px), transparent 0), 
            radial-gradient(circle at 100%   0 , #000 var(--r, 50px), transparent 0), 
            radial-gradient(circle at   0  100%, #000 var(--r, 50px), transparent 0), 
            radial-gradient(circle at 100% 100%, #000 var(--r, 50px), transparent 0);
.box {
    /* same as before */
    /* any CSS background we wish */
    mask: $grad-list
}

看到上面的代码是不是感觉要崩溃了,有太多重复的代码要写,其实我们使用一个CSS自定义属性--stop-list可以让我们把事情简化不少:

$grad-list: radial-gradient(circle at   0    0 , var(--stop-list)), 
            radial-gradient(circle at 100%   0 , var(--stop-list)), 
            radial-gradient(circle at   0  100%, var(--stop-list)), 
            radial-gradient(circle at 100% 100%, var(--stop-list));

.box {
    /* same as before */
    /* any CSS background we wish */
    --stop-list: #000 var(--r, 50px), transparent 0;
    mask: $grad-list;
}

上面这样做还不是很好,可以借助CSS处理器的循环特性来做,会更好一些:

$grad-list: ();

@for $i from 0 to 4 {
    $grad-list: $grad-list, 
        radial-gradient(circle at ($i%2)*100% floor($i/2)*100%, var(--stop-list));
}

.box {
    /* same as before */
    /* any CSS background we wish */
    --stop-list: #000 var(--r, 50px), transparent 0;
    mask: $grad-list;
}

就代码而言,这样写已经很完美了。因为我们不需要多次编写,并且以后在任何地方使用都不需要做任何更改。但到目前为止的结果并不是我们想要的:

从上面的示例中可以看出,我们除了凹角部分之外的东西都剪切掉了,这正好和我们想要的东西相反。要得到我们想要的效果,咱们只需要做一件事情,把渐变反过来。让凹角的圆变成透明,剩余的部分全部是黑色

--stop-list: transparent var(--r, 50px), #000 0;

需要注意的是,如果我们只使用一个渐变的时候,那么上面的代码就帮我们解决了问题:

但是,当我们把所有的四个圆圈(甚至两个)都堆起来的时候,就会得到一个黑色的矩形,这个矩形的大小相当于我们的mask的大小,这意味着没有任何东西会被掩盖掉。

因此,我们需要把每个渐变的大小限制在盒子的四分之处(width50%height50%),从而得到25%的面积:

这个意思就是,我们需要设置mask-size的值为50% 50%,同时mask-repeat的值为no-repeat以及每个mask-image自身的位置。

$grad-list: ();

@for $i from 0 to 4 {
    $x: ($i%2)*100%;
    $y: floor($i/2)*100%;
    $grad-list: $grad-list 
                radial-gradient(circle at $x $y, var(--stop-list)) /* mask image */
                $x $y; /* mask position */
}

.box {
    /* same as before */
    /* any CSS background we wish */
    --stop-list: transparent var(--r, 50px), #000 0;
    mask: $grad-list;
    mask-size: 50% 50%;
    mask-repeat: no-repeat;
}

但这里有一个大问题,一般情况下,我们的四分之一计算的每个部分会经过四舍五入,那么这四个部分重新组合在一起的时候,widthheight都有可能产生间距。如下图所示:

好吧,我们不能用linear-gradient()来做这个线条或者说把mask-size的尺寸增加到51%。比如下面的这个示例,增加了mask-size的尺寸来处理四个渐变区载之间的间距。

但是,难道没有更优雅的方式来处理这个间距?不是的,可以使用mask-composite属性来帮我们处理。当我们返回全部渐变的全尺寸时,可以把mask-composite的值设置为intersect

$grad-list: ();

@for $i from 0 to 4 {
    $grad-list: $grad-list, 
            radial-gradient(circle at ($i%2)*100% floor($i/2)*100%, var(--stop-list));
}

.box {
    /* same as before */
    /* any CSS background we wish */
    --stop-list: transparent var(--r, 50px), #000 0;
    mask: $grad-list;
    mask-composite: exclude;
}

这非常酷,因为它是纯CSS的解决方案,没有使用任何SVG代码,但不幸的是,目前得能看到效果的也仅限于Firefox53+。

corner-shape

大约在五年前,@Lea Verou提出了一个想法,甚至还为它创建了一个预览页面。遗憾的是,它不仅没有被任何浏览器实现,而且在此期规范还没有得到很大的提高。对于未来,它仍然是值得期待的,因为它提供了很多灵活性,而且代码非常少。比如说,实现我们前面所说的效果,只需要以下几行代码:

padding: var(--r);
corner-shape: scoop;
border-radius: var(--r);

就是一个非常简单的CSS。是不是值得期待,但最终还是要看浏览器什么时候会对其支持。

CSS Houdini

CSS Houdini慢慢的开始进入大家的世界当中,试问一下,我们使用CSS Houdini是不是可以更方便的实现这个内凹角的效果呢?比如像下面这样的一个效果,它就是使用CSS Houdini实现的:

咱们不仿尝试一下使用Paint Worklet或者CSS Paint API来实现呢?请开动你的大脑,动手撸一撸。希望您能把你的成果在下面的评论中与大家一起分享?如果你感兴趣,也可以在下面的评论中留言,我们后续可以专门花一点时间来看看CSS Houdini可以实现内凹角的效果,甚至是前面所讲的斜外切口的效果

构建一个内凹角的Vue组件

记得在《使用Vue制作切口盒子组件》一文中,咱们就尝试使用Vue构建了一个斜外切口的Vue组件c-noth

那么我们来看看怎么使用Vue来构建一个内凹角的组件,具体代码如下:

特别声明,如果您的浏览器没有看到任何效果,请使用Firefox 53+浏览器查阅。具体原因,前面文章已经介绍过来了。

为了照顾其他同学查看最终的效果,我录了一个屏:

这就是最终的效果。由于我自己是Vue的初学者,现在有一个病,看到什么东西都想用Vue来写,而且想封装成一个组件。如果写得不好,或者有更好的方案,欢迎大家指点,并且希望能看到您的分享的成果。如果你和我一样,也是Vue的一个初学者,可以和我一起来学习Vue。整理了一些有关于Vue的学习笔记,希望大家能喜欢,更希望能帮助到初学者,同时也希望不会误人子弟。

总结

文章开头抛出了怎么实现内凹角的一个效果。首先从CSS的box-shadow着手,使用CSS的box-shadow可以轻易的实现内凹角的效果,但这个方案有一定的局限性,比如要多个内凹角时,需要通过增加元素标签来实现,特别是在面对渐变,或者有背景图像和半透明的情景之下,这个方案基本上无法来满足我们的需求。

接着探索了SVG的方案,通过SVG的maskuse之类的一些独有的特性,可能灵活的帮助我们实现想要的效果,而且能做到box-shadow无法做到的事情。特别是通过CSS自定义属性来修改SVG的属性,让事情变得更具灵活性,只不过部分浏览器还不支持CSS来修改SVG的属性,这算是其中的一个坑吧。不过我们还是可以规避掉的。

虽然SVG能实现想要的效果,但对于一位CSS执着者而言,总是希望不借助其他的外力,通过纯CSS来实现这个效果,事实上也是可以的,使用CSS的径向渐变和mask相关的知识,可以实现我们想要的效果。遗憾的是,目前众多浏览器对mask还是有所保留,未能全面支持。比如文中提到的,很多mask相关的特性,仅能在Firefox 53+上看到。包括咱们写的示例,有些仅能在Firefox上看到。

随着CSS Houdini技术越来越成熟,我在试想,是否可以通过CSS Houdini来实现。正如@Lea Verou五年前提出的corner-shape属性。我想是可以的,后面可以尝试动手写写。当然,CSS Houdini虽然还没有得到所有浏览器支持,但这并不防碍我们去尝试着写各种效果。有兴趣的一起动手写写,看看这个想法是否能成真。

最后为了能练习Vue相关的知识,尝试使用Vue封装了一个简单的凹角组件。写得比较拙逼,希望能得到大神的指点。

最后的最后,需要特别感谢@ANA TUDOR写了这么优秀的教程。我在原作者的基础上做过一些调整,如果你觉得这里整理和不好,可以查阅原文。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/css/scooped-corners.html

返回顶部