一个HTML元素和五个CSS属性的魔力

发布于 大漠

特别声明:此篇文章内容来源于@ANA TUDOR的《1 HTML Element + 5 CSS Properties = Magic!》一文。

假设我告诉你,我可以使用一个HTML元素和五个CSS属性实现下图的效果。而且这个效果没有使用任何一行SVG代码,也没有使用图像(只是在html元素上使用了background设置了一个背景图片,只是为了表明这个元素有一些透明的部分),同样也没有使用JavaScript代码。你一定会觉得很神奇,对吧!有好奇之心,对于我们做前端的同学而言,应该一直都有,只有这样才能做出很多我们一直以为实现不了的效果,比如接下来要介绍的内容。

这篇文章将解释如何实现这个效果,然后展示如何通过添加一些动画来让效果变得更有趣。

CSS中的渐变射线

假设在HTML中刚好有一个<div>元素:

<div class='rays'></div>

在CSS中,给这个元素设置一个尺寸,并且给它添加一个background,以便我们能看到它。同时使用border-radius把这个元素变成一个圆形。

.rays {
    width: 80vmin; 
    height: 80vmin;
    border-radius: 50%;
    background: linear-gradient(#b53, #f90);
}

看到上面的代码,可能你会纳闷了,不是说五个属性和一个元素能实现文章开头的效果吗?现在都已经用掉四个属性了,只不过得到如下的一个效果。

那么第五个属性是什么呢?其实就是带有repeating-conic-gradient()值的一个mask

假设我们想要20条射线。这意味着我们需要把圆分成20份,并且把这这个值赋值给一个变量:$p: 100% / 20,这个值包含了射线和射线间的间距。如下图所示:

在这个示例中,我们让射线和射线间的间距相等,也就是射线和间距都是$p / 2的大小(也就是$p的一半),但我们完全可以根据自己所需,将其中任意一个变得更宽或更窄。我们希望在不透明部分(射线)的结束位置就是透明部分地起始位置。如果射线的停止位置是.5 * $p,那么这个间隙的起始位置就不会更大。但是,它可以是小的,它可以帮助我们保持简单,意味着我们可以把间距的起始位置设置为0

$nr: 20;          // 射线数量
$p: 100% / $nr;   // 射线和间距所占圆的百分比

.rays {
    /* 和前面相同的几个样式 */
    mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);
}

注意:与线性渐变和径向渐变不同的是,圆锥渐变的停止位置不能是无单位的。它们要么是百分比,要么是角度值。这意味着像使用transparent 0 $p是不起作用的,我们需要使用transparent 0% $p(我们也可以使用0deg替换0%,不管使用哪一个都不重要,重要的是不能是一个无单位的)。

上面的效果在Edge是无效的

当聊到浏览器支持的时候,有几点需要注意:

Edge还不支持在HTML元素上使用mask,尽管Edge已把这个功能列入到开发系列当中,而且它已经出现在about:flags中,但是到目前为止还没有做何事情。

conic-gradient()仅在Blink内核的浏览器中得到了支持,而且也仅是实验性的特性,如果想要在Blink内核的浏览器中查看到效果,同样需要通过chrome://flagsopera://flags中开启Experimental Web Platform features。Safari也有支持,但到目前为止,Safari仍然依赖于Polyfill,就像Firefox一样。

Webkit内核浏览器中的mask仍然需要添加-webkit-前缀。你会认为这没问题,因为我们使用的是Polyfill,而它依赖于-prefix-free,所以,如果我们使用Polyfill,我们需要在它之前引入-prefix-free。不幸的是,这比我们想的要复杂一点。主要是因为-prefix-free的运行需要通过特性检测,而在这种情况下常会失败,这是因为所有的浏览器都支持SVG不带前缀的mask属性。但是我们在HTML元素上使用了mask,而Wekit内核浏览器又需要-webkit-前缀的情部下,-prefix-free又不会添加,所以需要手动去添加:

$nr: 20; 
$p: 100%/$nr; 
$m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); 

.rays {
    -webkit-mask: $m;
        mask: $m;
}

我想我们也可以使用Autoprefixeer,就算是我们需要使用-prefix-free,但总感觉使用这两种方法有点像是在用猎枪打死一只苍蝇一样。

添加动画

在Blink浏览器中已支持了了conic-gradient(),这样一来咱们就可以使用CSS自定义属性来替代Sass的变量(如果使用了Polyfill,那是不可以使用CSS自定义属性的)。在Blink内核的浏览器中使用Houdini可以让CSS自定义属性动态变化。

为了添加动画部分的代码,我改变了mask中的渐变,给alpha值使用了CSS自定义属性。

$m: repeating-conic-gradient(
    rgba(#000, var(--a)) 0% .5*$p, 
    rgba(#000, calc(1 - var(--a))) 0% $p);

然后我们通过CSS.registerProperty注册一个自定义属性--a

CSS.registerProperty({
    name: '--a', 
    syntax: '<number>', 
    initialValue: 1;
})

最后在CSS中添加一个animation

.rays {
    animation: a 2s linear infinite alternate;
}

@keyframes a { 
    to { 
        --a: 0 
    } 
}

最后的效果如下:

效果看起来还不太好。但是,我们可以通过使用多个alpha值来让效果更好一些:

$m: repeating-conic-gradient(
    rgba(#000, var(--a0)) 0%, rgba(#000, var(--a1)) .5*$p, 
    rgba(#000, var(--a2)) 0%, rgba(#000, var(--a3)) $p);

下一步是注册这些自定义属性:

for(let i = 0; i < 4; i++) {
    CSS.registerProperty({
        name: `--a${i}`, 
        syntax: '<number>', 
        initialValue: 1 - ~~(i/2)
    })
}

最后,在CSS中调整animation

.rays {
    animation: a 2s infinite alternate;
    animation-name: a0, a1, a2, a3;
    animation-timing-function: 
        cubic-bezier(.57, .05, .67, .19) /* easeInCubic */, 
        cubic-bezier(.21, .61, .35, 1); /* easeOutCubic */
}

@for $i from 0 to 4 {
    @keyframes a#{$i} { 
        to { 
            --a#{$i}: #{floor($i/2)} 
        } 
    }
}

注意,由于我们将值设置为自定义属性,所以我们需要插入floor()函数。

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

现在效果看起来蛮不错了,但我们肯定还能做得更好,不是吗?

让我们试着用CSS自定义属性来表示射线和间距的停止位置:

$m: repeating-conic-gradient(#000 0% var(--p), transparent 0% $p);

接来注册另一个自定义属性--p

CSS.registerProperty({
    name: '--p', 
    syntax: '<percentage>', 
    initialValue: '0%'
})

我们在CSS的keyframe中使用这个自定义属性:

.rays {
    animation: p .5s linear infinite alternate
}

@keyframes p { 
    to { 
        --p: #{$p} 
    } 
}

在这种情况下,效果更完美了

但是我们仍然可以通过在每次迭代之间水平地翻转整个东西来增加它的趣味性,这样它就会一直翻转到相反的部分。这意味着,当--p0%$p时和当--p$p0时,它是不会翻转。

在CSS中可以通过transform: scaleX(-1)可以让一个元素进行水平翻转。由于我们希望在第一次迭代结束时应用这个翻转,然后在第二(反转)结束时删除它。这样我们可以在一个关键帧动画中使用它,并且配合steps()时间函数和两倍的animation-duration

$t: .5s;

.rays {
    animation: p $t linear infinite alternate, s 2*$t steps(1) infinite;
}

@keyframes p { 
    to { 
        --p: #{$p} 
    } 
}

@keyframes s { 
    50% { 
        transform: scalex(-1); 
    } 
}

现在我们终于有一个看起来非常酷的效果了:

渐变射线和波纹

为了得到光线和波纹(涟漪)的效果,我们需要在mask上添加第二个渐变属性:repeating-radial-gradient()

$nr: 20;
$p: 100% / $nr;
$stop-list: #000 0% .5*$p, transparent 0% $p;
$m: repeating-conic-gradient($stop-list), 
    repeating-radial-gradient(closest-side, $stop-list);

.rays-ripples {
    mask: $m;
}

遗憾的是,使用多个停止位置只在Blink内核浏览器中可用,并且是要开启了实验Web平台特性标记。虽然在HTML元素中使用mask时,conic-gradient()的Polyfill会覆盖repeating-conic-gradient(),但是不支持原生的conic-gradient()(Firefox、Safari、Blink浏览器没有启有标记),但在这些浏览器中,对于repeating-radial-gradient()部分到目前没有相应的解决方案。

这意味着,我们不得不在代码中做一些重复的事情:

$nr: 20;
$p: 100% / $nr;
$stop-list: #000, #000 .5*$p, transparent 0%, transparent $p;
$m: repeating-conic-gradient($stop-list), 
    repeating-radial-gradient(closest-side, $stop-list);

.rays-ripples {
    mask: $m;
}

虽然接近我们想要的效果,但还是没到那一步:

为了得到我们想要的效果,需要使用mask-composite属性,并且将其设置exclude

$m: repeating-conic-gradient($stop-list) exclude, 
    repeating-radial-gradient(closest-side, $stop-list);

注意,目前只有Firefox 53+ 支持了mask-composite,但是当它最终支持HTML元素的mask时,Edge应该加入进来了

如果你认为它看起来射线之间的间隙不相等,那是对的。这主要是由于Polyfill引起的问题

添加动画

由于mask-composite现在只有 Firefox 53+中才能运行,而Firefox还不支持conic-gradient(),因此不能将CSS自定义属性用于repeating-conic-gradient()中(因为Fiefox仍然要借助于Polyfill,而有Polyfill的时候是不能使用CSS自定义属性)。但是可以在repeating-conic-gradient()中使用CSS自定义属性,即使我们不能使用CSS关键帧来控制动画,我们也可以使用JavaScript来控制。

因为我们现在把CSS自定义属性用于repeating-radial-gradient(),但不能用于repeating-conic-gradient()(正如XOR效应也同样用于mask-composite,目前只有Firefox支持mask-composite,而Firefox又不支持原生的conic-gradient,所以会用到Polyfill来做降级处理,但Polyfill又不支持CSS自定义属性)。因此我们不能在mask的渐变中使用相同的$stop-list

但是,如果在没有一个通用的$stop-list的情况下要重写mask,那我们就可以利用这个机会使用不同的停止位置来实现两个渐变:

// for conic gradient
$nc: 20;
$pc: 100%/$nc;

// for radial gradient
$nr: 10;
$pr: 100%/$nr;

animation中有一个CSS自定义属性--a,就像第一射线动画的示例。我们还引入了--c0--c1两个CSS自定义属性,这是因为我们不能有多个停止位置,以及我们想尽量避免重复:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, 
    repeating-radial-gradient(closest-side, 
        var(--c0), var(--c0) .5*$pr, 
        var(--c1) 0, var(--c1) $pr);

body {
    --a: 0;
}

.xor {
    --c0: #{rgba(#000, var(--a))};
    --c1: #{rgba(#000, calc(1 - var(--a)))};
    mask: $m;
}

alpha自定义属性--a是我们来回动画的(从01,然后再回到0),并使用一点原生的JavaScript来实现这个效果。我们首先设置动画发生的帧数NF,当前帧索引f和当前动画方向dir

const NF = 50;

let f = 0, dir = 1;

update()函数中,我们更新当前帧索引f,然后将当前的进度值(f/NF)设置为当前的alpha--a。如果f已经达到NF0位置,我们就需要改变方向。然后在下次刷新时再次调用update()函数。

(function update() {
    f += dir;
    
    document.body.style.setProperty('--a', (f/NF).toFixed(2));
        
    if(!(f%NF)) dir *= -1;
    
    requestAnimationFrame(update)
})();

这就是JavaScript全部内容。现在可以看到一个生动的效果

这是一个线性动画,alpha--a被设置为f / NF。但是,我们可以将时间函数更改变其他的,正如我在前面的文章中所解释的那样,使用JavaScript来模拟CSS的时间函数

例如,如查我们想要一个ease-in的时间函数,将alpha值设置为easeIn(f / NF)而不是f / NF,下面就是easeIn()函数:

function easeIn(k, e = 1.675) {
    return Math.pow(k, e)
}

可以在Codepen中的这个示例中看到使用ease-in时间函数的效果(只在Firefox 53+浏览器中可以看到效果)。如果你对我们如何得到这个函数感兴趣,那么可以查阅以前整理的相关文章

同样的方法也适用于easeOut()easeInOut()

function easeOut(k, e = 1.675) {
    return 1 - Math.pow(1 - k, e)
};

function easeInOut(k) {
    return .5*(Math.sin((k - .5)*Math.PI) + 1)
}

因为我们使用的是JavaScript,所以我们可以添加一些交互事件,比如只有在点击或触摸时让动画动起来。

为了做到这一点,我们添加了一个ID请求的变量(rID),它最初的值是null,然后在update()函数中获取requestAnimationFrame()返回的值。这使用我们可以在任何时候使用stopAni()函数来停止动画:

let rID = null;

function stopAni() {
    cancelAnimationFrame(rID);
    rID = null
};

function update() {

    if(!(f%NF)) {
        stopAni();
        return
    }
    
    rID = requestAnimationFrame(update)
};

可以添加click事件,停止任何可能正在运行的动画,反转动画方向dir和调用update()函数:

addEventListener('click', e => {
    if(rID) stopAni();
    dir *= -1;
    update()
}, false);

因为当前帧索引f0开始,向正方向走,在第一次点击时指向NF。因为我们在每次点击的时候都会改变方向,所以它的初始值必须是-1,所以在第一次点击时,它会被反转为+1

最终的效果可以在Codepen中查看。我们也可以给每个停止值使用不同的alpha的自定义属性,就像我们在射线的情况下一样:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, 
    repeating-radial-gradient(closest-side, 
        rgba(#000, var(--a0)), rgba(#000, var(--a1)) .5*$pr, 
        rgba(#000, var(--a2)) 0, rgba(#000, var(--a3)) $pr);

在JavaScript中,我们使用ease-inease-out的时间函数:

const TFN = {
    'ease-in': function(k, e = 1.675) {
        return Math.pow(k, e)
    }, 
    'ease-out': function(k, e = 1.675) {
        return 1 - Math.pow(1 - k, e)
    }
};

update()函数中,与第一个动效效果唯一的区别是,我们不只改变一个CSS自定义属性,我们现在要改变四个CSS自定义属性:--a0--a1--a2--a3。可以在一个循环中使用ease-in,然后在偶数时使用ease-out函数。在前两项中,进度是f / NF给出,而在前两荐中,进度是1 - f / NF。这样就可以得到像下面这样的一个公式:

(function update() {
    f += dir;
    
    for(var i = 0; i < 4; i++) {
        let j = ~~(i/2);
            
        document.body.style.setProperty(
            `--a${i}`, 
            TFN[i%2 ? 'ease-out' : 'ease-in'](j + Math.pow(-1, j)*f/NF).toFixed(2)
        )
    }
        
    if(!(f%NF)) dir *= -1;
    
    requestAnimationFrame(update)
})();

最终的效果如下所示:

就像圆锥渐变一样,我们也可以使不透明部分和mask中径向渐变的透明部分之间的停止位置产生动画效果。为些,我们使用一个CSS自定义属性--p,来表示这个停止位置 的进度:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, 
    repeating-radial-gradient(closest-side, 
        #000, #000 calc(var(--p)*#{$pr}), 
        transparent 0, transparent $pr);

JavaScript和第一个alpha动画几乎是一样的,除了我们不更新--a自定义属性,还会更新--p自定义属性,以及使用一个ease-in-out函数:

function easeInOut(k) {
    return .5*(Math.sin((k - .5)*Math.PI) + 1)
};

(function update() {
    f += dir;
    
    document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2));
    
})();

我们可以让效果更好一些,如果我们在不透明条前面加上一条透明条,就需要新增一个新的停止位置的CSS自定义属性--p0。这个透明条会变成不透明条:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, 
    repeating-radial-gradient(closest-side, 
        transparent, transparent calc(var(--p0)*#{$pr}), 
        #000, #000 calc(var(--p1)*#{$pr}), 
        transparent 0, transparent $pr);

在JavaScript中,我们现在需要激活两个CSS自定义属性:--p0--p1。第一个使用ease-in时间函数,第二个使用ease-out时间函数。我们也不再改变动画的方向:

const NF = 120, 
    TFN = {
        'ease-in': function(k, e = 1.675) {
            return Math.pow(k, e)
        }, 
        'ease-out': function(k, e = 1.675) {
            return 1 - Math.pow(1 - k, e)
        }
    };

let f = 0;

(function update() {
    f = (f + 1)%NF;
        
    for(var i = 0; i < 2; i++)
        document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF);
    
    requestAnimationFrame(update)
})();

最终的效果如下:

大漠

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

如需转载,烦请注明出处:https://www.fedev.cn/css/1-html-element-5-css-properties-magic.htmljordan shoes for sale outlet size