一个HTML元素和五个CSS属性的魔力
Tiara3450
7年前
<p>假设我告诉你,我可以使用一个HTML元素和五个CSS属性实现下图的效果。而且这个效果没有使用任何一行SVG代码,也没有使用图像(只是在 html 元素上使用了 background 设置了一个背景图片,只是为了表明这个元素有一些透明的部分),同样也没有使用JavaScript代码。你一定会觉得很神奇,对吧!有好奇之心,对于我们做前端的同学而言,应该一直都有,只有这样才能做出很多我们一直以为实现不了的效果,比如接下来要介绍的内容。</p> <p><img src="https://simg.open-open.com/show/688dd0838cebaa23432225459f72e16d.png"></p> <p>这篇文章将解释如何实现这个效果,然后展示如何通过添加一些动画来让效果变得更有趣。</p> <h2>CSS中的渐变射线</h2> <p>假设在HTML中刚好有一个 <div> 元素:</p> <pre> <code class="language-css"><div class='rays'></div></code></pre> <p>在CSS中,给这个元素设置一个尺寸,并且给它添加一个 background ,以便我们能看到它。同时使用 border-radius 把这个元素变成一个圆形。</p> <pre> <code class="language-css">.rays { width: 80vmin; height: 80vmin; border-radius: 50%; background: linear-gradient(#b53, #f90); }</code></pre> <p>看到上面的代码,可能你会纳闷了,不是说五个属性和一个元素能实现文章开头的效果吗?现在都已经用掉四个属性了,只不过得到如下的一个效果。</p> <p>那么第五个属性是什么呢?其实就是带有 repeating-conic-gradient() 值的一个 mask 。</p> <p>假设我们想要 20 条射线。这意味着我们需要把圆分成 20 份,并且把这这个值赋值给一个变量: $p: 100% / 20 ,这个值包含了射线和射线间的间距。如下图所示:</p> <p><img src="https://simg.open-open.com/show/039d78395909cc8de4c532b52ac7383f.jpg"></p> <p>在这个示例中,我们让射线和射线间的间距相等,也就是射线和间距都是 $p / 2 的大小(也就是 $p 的一半),但我们完全可以根据自己所需,将其中任意一个变得更宽或更窄。我们希望在不透明部分(射线)的结束位置就是透明部分地起始位置。如果射线的停止位置是 .5 * $p ,那么这个间隙的起始位置就不会更大。但是,它可以是小的,它可以帮助我们保持简单,意味着我们可以把间距的起始位置设置为 0 。</p> <p><img src="https://simg.open-open.com/show/47e138087dc984ab696dbcc56de2b2bf.jpg"></p> <pre> <code class="language-css">$nr: 20; // 射线数量 $p: 100% / $nr; // 射线和间距所占圆的百分比 .rays { /* 和前面相同的几个样式 */ mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); }</code></pre> <p>注意:与线性渐变和径向渐变不同的是,圆锥渐变的停止位置不能是无单位的。它们要么是百分比,要么是角度值。这意味着像使用 transparent 0 $p 是不起作用的,我们需要使用 transparent 0% $p (我们也可以使用 0deg 替换 0% ,不管使用哪一个都不重要,重要的是不能是一个无单位的)。</p> <p><em>上面的效果在Edge是无效的</em> 。</p> <p>当聊到浏览器支持的时候,有几点需要注意:</p> <p>Edge还不支持在HTML元素上使用 mask ,尽管Edge已把这个功能列入到 <a href="/misc/goto?guid=4959757686510373500" rel="nofollow,noindex">开发系列</a> 当中,而且它已经出现在 about:flags 中,但是到目前为止还没有做何事情。</p> <p><img src="https://simg.open-open.com/show/1f3f06b67a4d33fa06e9c304d7b03d39.png"></p> <p><a href="/misc/goto?guid=4959757686601527707" rel="nofollow,noindex">conic-gradient() </a> 仅在Blink内核的浏览器中得到了支持,而且也仅是实验性的特性,如果想要在Blink内核的浏览器中查看到效果,同样需要通过 chrome://flags 或 opera://flags 中开启 <strong>Experimental Web Platform features</strong> 。Safari也有支持,但到目前为止,Safari仍然依赖于 <a href="/misc/goto?guid=4959750404151030849" rel="nofollow,noindex">Polyfill</a> ,就像Firefox一样。</p> <p><img src="https://simg.open-open.com/show/f852442706bcde8cadb63514e219591e.png"></p> <p>Webkit内核浏览器中的 mask 仍然需要添加 -webkit- 前缀。你会认为这没问题,因为我们使用的是Polyfill,而它依赖于 <a href="/misc/goto?guid=4959624534623420029" rel="nofollow,noindex"> -prefix-free </a> ,所以,如果我们使用Polyfill,我们需要在它之前引入 -prefix-free 。不幸的是,这比我们想的要复杂一点。主要是因为 -prefix-free 的运行需要通过特性检测,而在这种情况下常会失败,这是因为所有的浏览器都支持SVG不带前缀的 mask 属性。但是我们在HTML元素上使用了 mask ,而Wekit内核浏览器又需要 -webkit- 前缀的情部下, -prefix-free 又不会添加,所以需要手动去添加:</p> <pre> <code class="language-css">$nr: 20; $p: 100%/$nr; $m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); .rays { -webkit-mask: $m; mask: $m; }</code></pre> <p>我想我们也可以使用Autoprefixeer,就算是我们需要使用 -prefix-free ,但总感觉使用这两种方法有点像是在用猎枪打死一只苍蝇一样。</p> <h3>添加动画</h3> <p>在Blink浏览器中已支持了了 conic-gradient() ,这样一来咱们就可以使用CSS自定义属性来替代Sass的变量(如果使用了Polyfill,那是不可以使用CSS自定义属性的)。在Blink内核的浏览器中使用Houdini可以让CSS自定义属性动态变化。</p> <p>为了添加动画部分的代码,我改变了 mask 中的渐变,给 alpha 值使用了CSS自定义属性。</p> <pre> <code class="language-css">$m: repeating-conic-gradient( rgba(#000, var(--a)) 0% .5*$p, rgba(#000, calc(1 - var(--a))) 0% $p);</code></pre> <p>然后我们通过 CSS.registerProperty 注册一个自定义属性 --a :</p> <pre> <code class="language-css">CSS.registerProperty({ name: '--a', syntax: '<number>', initialValue: 1; })</code></pre> <p>最后在CSS中添加一个 animation :</p> <pre> <code class="language-css">.rays { animation: a 2s linear infinite alternate; } @keyframes a { to { --a: 0 } }</code></pre> <p><a href="/misc/goto?guid=4959757686735716862" rel="nofollow,noindex">最后的效果</a> 如下:</p> <p><img src="https://simg.open-open.com/show/419aa14e4833a1c13d97fa201c863cdc.gif"></p> <p>效果看起来还不太好。但是,我们可以通过使用多个 alpha 值来让效果更好一些:</p> <pre> <code class="language-css">$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);</code></pre> <p>下一步是注册这些自定义属性:</p> <pre> <code class="language-css">for(let i = 0; i < 4; i++) { CSS.registerProperty({ name: `--a${i}`, syntax: '<number>', initialValue: 1 - ~~(i/2) }) }</code></pre> <p>最后,在CSS中调整 animation :</p> <pre> <code class="language-css">.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)} } } }</code></pre> <p>注意,由于我们将值设置为自定义属性,所以我们需要插入 <a href="/misc/goto?guid=4959757686821166939" rel="nofollow,noindex"> floor() </a> 函数。</p> <p>这个时候你 <a href="/misc/goto?guid=4959757686899477926" rel="nofollow,noindex">看到的效果</a> 如下:</p> <p><img src="https://simg.open-open.com/show/d8c29e25920e194cd2edbe479633a326.gif"></p> <p>现在效果看起来蛮不错了,但我们肯定还能做得更好,不是吗?</p> <p>让我们试着用CSS自定义属性来表示射线和间距的停止位置:</p> <pre> <code class="language-css">$m: repeating-conic-gradient(#000 0% var(--p), transparent 0% $p);</code></pre> <p>接来注册另一个自定义属性 --p :</p> <pre> <code class="language-css">CSS.registerProperty({ name: '--p', syntax: '<percentage>', initialValue: '0%' })</code></pre> <p>我们在CSS的 keyframe 中使用这个自定义属性:</p> <pre> <code class="language-css">.rays { animation: p .5s linear infinite alternate } @keyframes p { to { --p: #{$p} } }</code></pre> <p>在这种情况下, <a href="/misc/goto?guid=4959757686975881448" rel="nofollow,noindex">效果更完美了</a> 。</p> <p><img src="https://simg.open-open.com/show/7b538ffcd4cafd913908f77add893c90.gif"></p> <p>但是我们仍然可以通过在每次迭代之间水平地翻转整个东西来增加它的趣味性,这样它就会一直翻转到相反的部分。这意味着,当 --p 从 0% 到 $p 时和当 --p 从 $p 到 0 时,它是不会翻转。</p> <p>在CSS中可以通过 transform: scaleX(-1) 可以让一个元素进行水平翻转。由于我们希望在第一次迭代结束时应用这个翻转,然后在第二(反转)结束时删除它。这样我们可以在一个关键帧动画中使用它,并且配合 steps() 时间函数和两倍的 animation-duration 。</p> <pre> <code class="language-css">$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); } }</code></pre> <p>现在我们终于有一个看起来 <a href="/misc/goto?guid=4959757687060806877" rel="nofollow,noindex">非常酷的效果</a> 了:</p> <p><img src="https://simg.open-open.com/show/e1bcf16023ce0ea221192a8c5638565e.gif"></p> <h2>渐变射线和波纹</h2> <p>为了得到光线和波纹(涟漪)的效果,我们需要在 mask 上添加第二个渐变属性: repeating-radial-gradient() :</p> <p><img src="https://simg.open-open.com/show/4c8a959eaa76933861870098da321933.jpg"></p> <pre> <code class="language-css">$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; }</code></pre> <p>遗憾的是,使用 <a href="/misc/goto?guid=4959757687136294677" rel="nofollow,noindex">多个停止位置</a> 只在Blink内核浏览器中可用,并且是要开启了实验Web平台特性标记。虽然在HTML元素中使用 mask 时, conic-gradient() 的Polyfill会覆盖 repeating-conic-gradient() ,但是不支持原生的 conic-gradient() (Firefox、Safari、Blink浏览器没有启有标记),但在这些浏览器中,对于 repeating-radial-gradient() 部分到目前没有相应的解决方案。</p> <p>这意味着,我们不得不在代码中做一些重复的事情:</p> <pre> <code class="language-css">$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; }</code></pre> <p>虽然接近我们 <a href="/misc/goto?guid=4959757687211608516" rel="nofollow,noindex">想要的效果</a> ,但还是没到那一步:</p> <p><img src="https://simg.open-open.com/show/52b9e7c0fc2259d17358d466191c6f45.png"></p> <p>为了得到我们想要的效果,需要使用 <a href="/misc/goto?guid=4959757687295417153" rel="nofollow,noindex"> mask-composite </a> 属性,并且将其设置 exclude :</p> <pre> <code class="language-css">$m: repeating-conic-gradient($stop-list) exclude, repeating-radial-gradient(closest-side, $stop-list);</code></pre> <p>注意,目前只有Firefox 53+ 支持了 mask-composite ,但是当它最终支持HTML元素的 mask 时,Edge应该 <a href="/misc/goto?guid=4959757687377009730" rel="nofollow,noindex">加入进来了</a> 。</p> <p><img src="https://simg.open-open.com/show/d79ca895f427cd3edc3a25da14f7f55e.png"></p> <p>如果你认为它看起来射线之间的间隙不相等,那是对的。这主要是由于 <a href="/misc/goto?guid=4959757687457045395" rel="nofollow,noindex">Polyfill引起的问题</a> 。</p> <h3>添加动画</h3> <p>由于 mask-composite 现在只有 Firefox 53+中才能运行,而Firefox还不支持 conic-gradient() ,因此不能将CSS自定义属性用于 repeating-conic-gradient() 中(因为Fiefox仍然要借助于Polyfill,而有Polyfill的时候是不能使用CSS自定义属性)。但是可以在 repeating-conic-gradient() 中使用CSS自定义属性,即使我们不能使用CSS关键帧来控制动画,我们也可以使用JavaScript来控制。</p> <p>因为我们现在把CSS自定义属性用于 repeating-radial-gradient() ,但不能用于 repeating-conic-gradient() (正如XOR效应也同样用于 mask-composite ,目前只有Firefox支持 mask-composite ,而Firefox又不支持原生的 conic-gradient ,所以会用到Polyfill来做降级处理,但Polyfill又不支持CSS自定义属性)。因此我们不能在 mask 的渐变中使用相同的 $stop-list 。</p> <p>但是,如果在没有一个通用的 $stop-list 的情况下要重写 mask ,那我们就可以利用这个机会使用不同的停止位置来实现两个渐变:</p> <pre> <code class="language-css">// for conic gradient $nc: 20; $pc: 100%/$nc; // for radial gradient $nr: 10; $pr: 100%/$nr;</code></pre> <p>在 animation 中有一个CSS自定义属性 --a ,就像第一射线动画的示例。我们还引入了 --c0 和 --c1 两个CSS自定义属性,这是因为我们不能有多个停止位置,以及我们想尽量避免重复:</p> <pre> <code class="language-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; }</code></pre> <p>alpha 自定义属性 --a 是我们来回动画的(从 0 到 1 ,然后再回到 0 ),并使用一点原生的JavaScript来实现这个效果。我们首先设置动画发生的帧数 NF ,当前帧索引 f 和当前动画方向 dir :</p> <pre> <code class="language-css">const NF = 50; let f = 0, dir = 1;</code></pre> <p>在 update() 函数中,我们更新当前帧索引 f ,然后将当前的进度值( f/NF )设置为当前的 alpha 值 --a 。如果 f 已经达到 NF 的 0 位置,我们就需要改变方向。然后在下次刷新时再次调用 update() 函数。</p> <pre> <code class="language-css">(function update() { f += dir; document.body.style.setProperty('--a', (f/NF).toFixed(2)); if(!(f%NF)) dir *= -1; requestAnimationFrame(update) })();</code></pre> <p>这就是JavaScript全部内容。现在可以看到一个 <a href="/misc/goto?guid=4959757687535517129" rel="nofollow,noindex">生动的效果</a> :</p> <p><img src="https://simg.open-open.com/show/7da3720ce3f5df35c03607ea0d541f9f.gif"></p> <p>这是一个线性动画, alpha 值 --a 被设置为 f / NF 。但是,我们可以将时间函数更改变其他的,正如我在前面的文章中所解释的那样,使用JavaScript来 <a href="/misc/goto?guid=4959757687618228212" rel="nofollow,noindex">模拟CSS的时间函数</a> 。</p> <p>例如,如查我们想要一个 ease-in 的时间函数,将 alpha 值设置为 easeIn(f / NF) 而不是 f / NF ,下面就是 easeIn() 函数:</p> <pre> <code class="language-css">function easeIn(k, e = 1.675) { return Math.pow(k, e) }</code></pre> <p>可以在 <a href="/misc/goto?guid=4959757687693440612" rel="nofollow,noindex">Codepen中的这个示例</a> 中看到使用 ease-in 时间函数的效果(只在Firefox 53+浏览器中可以看到效果)。如果你对我们如何得到这个函数感兴趣,那么可以查阅 <a href="/misc/goto?guid=4959757687618228212" rel="nofollow,noindex">以前整理的相关文章</a> 。</p> <p>同样的方法也适用于 easeOut() 或 easeInOut() :</p> <pre> <code class="language-css">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) }</code></pre> <p>因为我们使用的是JavaScript,所以我们可以添加一些交互事件,比如只有在点击或触摸时让动画动起来。</p> <p>为了做到这一点,我们添加了一个 ID 请求的变量( rID ),它最初的值是 null ,然后在 update() 函数中获取 requestAnimationFrame() 返回的值。这使用我们可以在任何时候使用 stopAni() 函数来停止动画:</p> <pre> <code class="language-css">let rID = null; function stopAni() { cancelAnimationFrame(rID); rID = null }; function update() { if(!(f%NF)) { stopAni(); return } rID = requestAnimationFrame(update) };</code></pre> <p>可以添加 click 事件,停止任何可能正在运行的动画,反转动画方向 dir 和调用 update() 函数:</p> <pre> <code class="language-css">addEventListener('click', e => { if(rID) stopAni(); dir *= -1; update() }, false);</code></pre> <p>因为当前帧索引 f 从 0 开始,向正方向走,在第一次点击时指向 NF 。因为我们在每次点击的时候都会改变方向,所以它的初始值必须是 -1 ,所以在第一次点击时,它会被反转为 +1 。</p> <p>最终的效果可以在 <a href="/misc/goto?guid=4959757687787062778" rel="nofollow,noindex">Codepen中查看</a> 。我们也可以给每个停止值使用不同的 alpha 的自定义属性,就像我们在射线的情况下一样:</p> <pre> <code class="language-css">$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);</code></pre> <p>在JavaScript中,我们使用 ease-in 和 ease-out 的时间函数:</p> <pre> <code class="language-css">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) } };</code></pre> <p>在 update() 函数中,与第一个动效效果唯一的区别是,我们不只改变一个CSS自定义属性,我们现在要改变四个CSS自定义属性: --a0 、 --a1 、 --a2 和 --a3 。可以在一个循环中使用 ease-in ,然后在偶数时使用 ease-out 函数。在前两项中,进度是 f / NF 给出,而在前两荐中,进度是 1 - f / NF 。这样就可以得到像下面这样的一个公式:</p> <pre> <code class="language-css">(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) })();</code></pre> <p>最终的 <a href="/misc/goto?guid=4959757687867446215" rel="nofollow,noindex">效果</a> 如下所示:</p> <p><img src="https://simg.open-open.com/show/fe4edc820d139f39caaf7ef1f2458ec5.gif"></p> <p>就像圆锥渐变一样,我们也可以使不透明部分和 mask 中径向渐变的透明部分之间的停止位置产生动画效果。为些,我们使用一个CSS自定义属性 --p ,来表示这个停止位置 的进度:</p> <pre> <code class="language-css">$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);</code></pre> <p>JavaScript和第一个 alpha 动画几乎是一样的,除了我们不更新 --a 自定义属性,还会更新 --p 自定义属性,以及使用一个 ease-in-out 函数:</p> <pre> <code class="language-css">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)); })();</code></pre> <p><img src="https://simg.open-open.com/show/b725e8ee23e906f21ce5f260db53786d.gif"></p> <p>我们可以让效果更好一些,如果我们在不透明条前面加上一条透明条,就需要新增一个新的停止位置的CSS自定义属性 --p0 。这个透明条会变成不透明条:</p> <pre> <code class="language-css">$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);</code></pre> <p>在JavaScript中,我们现在需要激活两个CSS自定义属性: --p0 和 --p1 。第一个使用 ease-in 时间函数,第二个使用 ease-out 时间函数。我们也不再改变动画的方向:</p> <pre> <code class="language-css">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) })();</code></pre> <p>最终的效果如下:</p> <p><img src="https://simg.open-open.com/show/f4af730f6c552205c2861f73dfe80031.gif"></p> <p><img src="https://simg.open-open.com/show/9fc48efe50d68f0d2bde11b73075ba51.jpg"></p> <h3> </h3> <p>来自:https://www.w3cplus.com/css/1-html-element-5-css-properties-magic.html</p> <p> </p>