一個HTML元素和五個CSS屬性的魔力

Tiara3450 6年前發布 | 50K 次閱讀 CSS 前端技術

假設我告訴你,我可以使用一個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://flags 或 opera://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} 
    } 
}

在這種情況下, 效果更完美了

但是我們仍然可以通過在每次迭代之間水平地翻轉整個東西來增加它的趣味性,這樣它就會一直翻轉到相反的部分。這意味著,當 --p 從 0% 到 $p 時和當 --p 從 $p 到 0 時,它是不會翻轉。

在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 是我們來回動畫的(從 0 到 1 ,然后再回到 0 ),并使用一點原生的JavaScript來實現這個效果。我們首先設置動畫發生的幀數 NF ,當前幀索引 f 和當前動畫方向 dir :

const NF = 50;

let f = 0, dir = 1;

在 update() 函數中,我們更新當前幀索引 f ,然后將當前的進度值( f/NF )設置為當前的 alpha 值 --a 。如果 f 已經達到 NF 的 0 位置,我們就需要改變方向。然后在下次刷新時再次調用 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);

因為當前幀索引 f 從 0 開始,向正方向走,在第一次點擊時指向 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-in 和 ease-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)
})();

最終的效果如下:

 

來自:https://www.w3cplus.com/css/1-html-element-5-css-properties-magic.html

 

 本文由用戶 Tiara3450 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!