使用CSS transition和animation改变渐变状态

发布于 大漠

特别声明,本文根据@ANA TUDOR的《The State of Changing Gradients with CSS Transitions and Animations》一文所整理。

到目前为止,CSS的渐变属性linear-gradientradial-gradient已经是很成熟的CSS特性了,而且repeating-linear-gradientconic-gradient也越来越成熟。CSS渐变特性对于我们的帮助已经非常强大了,它们可以帮助我们绘图创建图片占位符制作环形进度条等等。另外还可以通过transitionanimation让渐变动起来。

但是给渐变添加动画效果目前还有很多极限性,如果不添加额外的元素或其他的渐变属性,有些效果是无法实现的,比如下面这个效果。

不过,在Edge浏览器,使用@keyframes就可以实现上图的效果,而且代码很简单:

html {
    background: linear-gradient(90deg, #f90 0%, #444 0) 50%/ 5em;
    animation: blinds 1s ease-in-out infinite alternate;
}

@keyframes blinds {
    to {
        background-image: linear-gradient(90deg, #f90 100%, #444 0);
    }
}

在些基础上,借助CSS的处理器,比如Sass,可以让上面的代码变得更为灵活:

@function blinds($open: 0) {
    @return linear-gradient(90deg, #f90 $open*100%, #444 0);
}

html {
    background: blinds() 50%/ 5em;
    animation: blinds 1s ease-in-out infinite alternate;
}

@keyframes blinds { 
    to { 
        background-image: blinds(1) 
    } 
}

虽然上面的代码实现了所需的效果,但使用CSS来维护和使用仍然还是需要编写代码,这是事实。动画效果也只是停留在0%100%之间,能达到我们所要的效果。不过,要是使用00px来替代0%的话,结果就会令人失望,动画效果失踪了。更不用说在Chrome和Firefox浏览器上了,能看到的仅仅就是#f90#444两个颜色之间的切换,根本没有停止位置的动效。

庆幸折是,现在我们有一个更好的选择:CSS自定义属性

虽然我们可以获得过transition效果(但不是animation效果),但是如果我们使用的属性是可动画化的,那么CSS自定义属性是不可动画化。比如,当在transfrom中使用时,我们可以在transition中使用transfrom属性。

让我们来做一个效果,复选框选中时,橙色正方形(.box)将会移动并且会被压扁的效果。我们在.box中定义了一个自定义属性--f,并且初始值设置为1

.box {
    --f: 1;
    transform: translate(calc((1 - var(--f)) * 100vw)) scalex(var(--f));
}

当复选框被选中时:checked.box的自定义属性--f的值变成.5

:checked ~ .box { 
    --f: .5 
}

.box中添加transition属性,我们可以让.box从一个状态到另一个状态时,整个过程是一种细腻的滑动效果。

.box {
    --f: 1;
    transform: translate(calc((1 - var(--f)) * 100vw)) scalex(var(--f));

    transition: transform .3s ease-in;
}

然而,CSS渐变是background-image,它只在Edge和IE10+中是可动画化的。因此,虽然我们可以让事情变得简单,并减少为transition生成的CSS代码量(如下面的代码所示),但是在扩展支持方面,一依没有取得任何进展。

.blinds {
    background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
    transition: .3s ease-in-out;
        
    :checked ~ & { 
        --pos: 100%; 
    }
}

值得我们庆幸的是,CSS Houdini的诞生,它允许我们注册自定义属性,然后让它们动起来。不过有点遗憾的是,目前这项强大的特性还只是实验性的特性,如果你想在浏览器中看到相应的效果,需要在实验Web平台特性标志(Experimental Web Platform features)中开启相应的特性。

接着回到我们的示例中来。先注册一个--pos自定义属性:

CSS.registerProperty({
    name: '--pos', 
    syntax: '<length-percentage>', 
    initialValue: '0%', 
    inherits: true
});

注意了。 这里syntax设置了<length-percentage>,也就意味着它不仅接受长度和百分比值,还接受它们的calc()组合。相比之下, <length> | <percentage>只接受长度和百分比值,而不接受它们的calc()组合。同时显式的指定了inherits(继承)是强制性的。

但这样做,在Chrome浏览器下依旧没有任何的效果,这可能是因为,在transition示例中,transition是属性,其值依赖于CSS自定义属性,而不是CSS自定义属性本身。众所周知,到目前为止,Chrome浏览器还不支持两个背景图片之间的transition效果。就这方面而言,Edge显得更为强大,就算是不注册自定义属性--pos,在Edge可以正常的工作。那是因为在Edge中允许我们在transition属性中运用渐变属性,从而实现两个渐变之间的过渡效果。

如果在Blink内核浏览器中,开启了实验性Web特性,就可以使用animation来替代transition属性。

html {
    background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
    animation: blinds .85s ease-in-out infinite alternate;
}

@keyframes blinds { 
    to { 
        --pos: 100%; 
    } 
}

这样一样,在Edge中又没有效果了,因为到现在为止Edge并不支持CSS自定义属性。因此,需要另外一种方法来处理。这个时候我们应该想起CSS的@supports,该他出场的时候到了。可以借助@supports来做一个条件判断:

@function grad($pos: 100%) {
    @return linear-gradient(90deg, #f90 $pos, #444 0);
}

html{
    background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
    animation: blinds .85s ease-in-out infinite alternate;
    
    @supports (-ms-user-select: none) {
        background-image: grad(0%);
        animation-name: blinds-alt;
    }
}

@keyframes blinds-alt { 
    to { 
        background-image: grad() 
    } 
}

这种方式并不是唯一的方式。我们可以对渐变的角度做同样的处理。它背后的原理可以说几乎是一样的,只是现在我们的动画不再是一个交替的,不过我们可以在动画中尝试使用easeInOutBack

@function grad($ang: 1turn) {
    @return linear-gradient($ang, #f90 50%, #444 0);
}

html {
    background: grad(var(--ang, 0deg));
    animation: rot 2s cubic-bezier(.68, -.57, .26, 1.65) infinite;

    @supports (-ms-user-select: none) {
        background-image: grad(0turn);
        animation-name: rot-alt;
    }
}

@keyframes rot { 
    to { 
        --ang: 1turn; 
    } 
}

@keyframes rot-alt { 
    to { 
        background-image: grad(); 
    } 
}

请记住,就像在停止位置的情况一样,我们只能在相同角度单位之间的渐变进行动画,所以在Sass中从grad(0deg)grad(0turn)替代是无法正常工作的。

当然,我们现在使用的CSS自定义属性来接受角度值,而不是长度和百分比:

CSS.registerProperty({
    name: '--ang', 
    syntax: '<angle>', 
    initialValue: '0deg', 
    inherits: true
});

以类似的方式,也可以把这个原理运用于径向渐变。

$p: 9%;

html {
    --x: #{$p};
    --y: #{$p};
    background: radial-gradient(circle at var(--x) var(--y), #f90, #444 $p);
}

这里我们注册了两个自定义属性--x--y

CSS.registerProperty({
    name: '--x', 
    syntax: '<length-percentage>', 
    initialValue: '0%', 
    inherits: true
});

CSS.registerProperty({
    name: '--y', 
    syntax: '<length-percentage>', 
    initialValue: '0%', 
    inherits: true
});

运用于动画如下:

$p: 9%;

html {
    --x: #{$p};
    --y: #{$p};
    background: radial-gradient(circle at var(--x) var(--y), #f90, #444 $p);

    animation: a 0s ease-in-out -2.3s alternate infinite;
    animation-name: x, y;
    animation-duration: 4.1s, 2.9s;
}
@keyframes x { 
    to { 
        --x: #{100% - $p} 
    } 
}
@keyframes y { 
    to { 
        --y: #{100% - $p} 
    } 
}

得到的效果如下:

我们可以使用这种技术,在渐变中使用不同的自定义属性,实现相应的动画效果。比如,我们要实现文章开头那种百叶窗的动效,我们可以引入另外两个自定义CSS属性:--c0--c1

$c: #f90 #444;

html {
    --c0: #{nth($c, 1)};
    --c1: #{nth($c, 2)};
    background: linear-gradient(90deg, var(--c0) var(--pos, 0%), var(--c1) 0) 50%/ 5em;
}

同样的注册需要的自定义属性:

CSS.registerProperty({
    name: '--pos', 
    syntax: '<length-percentage>', 
    initialValue: '0%', 
    inherits: true
});

CSS.registerProperty({
    name: '--c0', 
    syntax: '<color>', 
    initialValue: '#f90', 
    inherits: true
});

CSS.registerProperty({
    name: '--c0', 
    syntax: '<color>', 
    initialValue: '#444', 
    inherits: true
});

对于第一个停止位置,我们使用与之前动画中相同的自定义属性--pos,除此之外,我们为另外两个自定义属性引入动画的两个steps()中,每次完成第一个动画的迭代(改变--pos值):

    $t: 1s;

    html {
        /* 和前面代码相同 */

        animation: a 0s infinite;
        animation-name: c0, pos, c1;
        animation-duration: 2*$t, $t;
        animation-timing-function: steps(1), ease-in-out;
    }

    @keyframes pos { 
        to { 
            --pos: 100%; 
        } 
    }

    @keyframes c0 { 
        50% { 
            --c0: #{nth($c, 2)} 
        } 
    }
    @keyframes c1 { 
        50% { 
            --c1: #{nth($c, 1)} 
        } 
    }

得到的效果如下:

我们还可以将此应用到radial-gradient()中:

background: radial-gradient(circle, var(--c0) var(--pos, 0%), var(--c1) 0);

同样也可以运用于conic-gradient()

background: conic-gradient(var(--c0) var(--pos, 0%), var(--c1) 0);

如果在重复渐变(repeating-radial-gradient)中使用的话,可以得到一个类似水波涟漪的效果:

$p: 2em;

html {
    /* 和前面代码相同 */

    background: repeating-radial-gradient(circle, var(--c0) 0 var(--pos, 0px), var(--c1) 0 $p);
}

@keyframes pos { 
    90%, 100% { 
        --pos: #{$p} 
    } 
}

得到的效果如下:

如果运用在重复圆锥属性上,还可以绘制一些螺族或射线的动效:

$p: 5%;

html {
    /* 和前面的代码相同 */
    background: repeating-conic-gradient(var(--c0) 0 var(--pos, 0%), var(--c1) 0 $p);
}

@keyframes pos { 
    90%, 100% { 
        --pos: #{$p} 
    } 
}

我们还可以添加其他的CSS自定义属性,让动效变得更有意思:

$n: 20;

html {
    /* 和前面代码相同 */
    background: radial-gradient(circle at var(--o, 50% 50%), var(--c0) var(--pos, 0%), var(--c1) 0);
    animation: a 0s infinite;
    animation-name: c0, o, pos, c1;
    animation-duration: 2*$t, $n*$t, $t;
    animation-timing-function: steps(1), steps(1), ease-in-out;
}

@keyframes o {
    @for $i from 0 to $n {
        #{$i*100%/$n} { 
            --o: #{random(100)*1%} #{random(100)*1%} 
        }
    }
}

同样的方式注册新增加的自定义属性:

CSS.registerProperty({
    name: '--o', 
    syntax: '<length-percentage>+', 
    initialValue: '50%', 
    inherits: true
});

效果如下:

我认为使用@keyframes动画改变渐变的效果看起来很酷。但与此同时,对于跨浏览器的解决方案,JavaScript方案仍然是一个有效的方法。nike air max 1 soccer