【Web動畫】科技感十足的暗黑字元雨動畫

ChokCoco發表於2021-08-02

本文將使用純 CSS,帶大家一步一步實現一個這樣的科幻字元跳動背景動畫。類似於這樣的字元雨動畫:

Digital Char Rain Animation

或者是類似於這樣的:

CodePen HomeMatrix digital rain (animated version) By yuanchuan

運用在一些類似科技主題的背景之上,非常的添彩。

文字的豎排

首先第一步,就是需要實現文字的豎向排列:

這一步非常的簡單,可能方法也很多,這裡我簡單羅列一下:

  1. 使用控制文字排列的屬性 writing-mode 進行控制,可以通過 writing-mode: vertical-lr 等將文字進行豎向排列,但是對於數字和英文,將會旋轉 90° 展示:
<p>1234567890ABC</p>
<p>中文或其他字元ォヶ</p>
p {
    writing-mode: vertical-lr; 
}

當然這種情況下,英文字元的展示不太滿足我們的需求。

  1. 控制容器的寬度,控制每行只能展示 1 箇中文字元。

這個方法算是最簡單便捷的方法了,但是由於英文的特殊性,要讓連續的長字串自然的換行,我們還需要配合 word-break: break-all

p {
    width: 12px;
    font-size: 10px;
    word-break: break-all;
}

效果如下,滿足需求:

使用 CSS 實現隨機字串的選取

為了讓我們的效果更加自然。每一行的字元的選取最好是隨機的。

但是要讓 CSS 實現隨機生成每一行的字元可太難了。所以這裡我們請出 CSS 前處理器 SASS/LESS 。

而且由於不太可能利用 CSS 給單個標籤內,譬如 <p> 標籤插入字元,所以我們把標籤內的字元展示,放在每個 <p> 元素的偽元素 ::beforecontent 當中。

我們可以提前設定好一組字串,然後利用 SASS function 隨機生成每一次元素內的 content,虛擬碼如下:

<div>
    <p></p>
    <p></p>
    <p></p>
</div>
$str: 'ぁぃぅぇぉかきくけこんさしすせそた◁▣▤▥▦▧♂♀♥☻►◄▧▨♦ちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎをァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥabcdefghigklmnopqrstuvwxyz123456789%@#$<>^&*_+';
$length: str-length($str);

@function randomChar() {
    $r: random($length);
    @return str-slice($str, $r, $r);
}

@function randomChars($number) {
    $value: '';

    @if $number > 0 {
        @for $i from 1 through $number {
            $value: $value + randomChar();
        }
    }
    @return $value;
}

p:nth-child(1)::before {
    content: randomChars(25);
}
p:nth-child(2)::before {
    content: randomChars(25);
}
p:nth-child(3)::before {
    content: randomChars(25);
}

簡單解釋下上面的程式碼:

  1. $str 定義了一串隨機字串,$length 表示字串的長度
  2. randomChar() 中利用了 SASS 的 random() 方法,每次隨機選取一個 0 - $length 的整形數,記為 $r,再利用 SASS 的 str-slice 方法,每次從 $str 中選取一個下標為 $r 的隨機字元
  3. randomChars() 就是迴圈呼叫 randomChar() 方法,從 $str 中隨機生成一串字串,長度為傳進去的引數 $number

這樣,每一列的字元,每次都是不一樣的:

當然,上述的方法我認為不是最好的,CSS 的偽元素的 content 是支援字元編碼的,譬如 content: '\3066'; 會被渲染成字元 ,這樣,通過設定字元區間,配合 SASS function 可以更好的生成隨機字元,但是我嘗試了非常久,SASS function 生成的最終產物會在 \3066 這樣的數字間新增上空格,無法最終通過字元編碼轉換成字元,最終放棄...

使用 CSS 實現打字效果

OK,繼續,接下來我們要使用 CSS 實現打字效果,就是讓字元一個一個的出現,像是這樣:

純 CSS 實現文字輸入效果

這裡藉助了 animation 的 steps 的特性實現,也就是逐幀動畫。

從左向右和從上向下原理是一樣的,以從左向右為例,假設我們有 26 個英文字元,我們已知 26 個英文字元組成的字串的長度,那麼我們只需要設定一個動畫,讓它的寬度變化從 0 - 100% 經歷 26 幀即可,配合 overflow: hidden,steps 的每一幀即可展出一個字元。

當然,這裡需要利用一些小技巧,我們如何通過字元的數量知道字串的長度呢?

劃重點:通過等寬字型的特性,配合 CSS 中的 ch 單位

如果不瞭解什麼是等寬字型族,可以看看我的這篇文章 -- 《你該知道的字型 font-family》

CSS 中,ch 單位表示數字 “0” 的寬度。如果字型恰巧又是等寬字型,即每個字元的寬度是一樣的,此時 ch 就能變成每個英文字元的寬度,那麼 26ch 其實也就是整個字串的長度。

利用這個特性,配合 animation 的 steps,我們可以輕鬆的利用 CSS 實現打字動畫效果:

<h1>Pure CSS Typing animation.</h1>
h1 {
    font-family: monospace;
    width: 26ch;
    white-space: nowrap;
    overflow: hidden;
    animation: typing 3s steps(26, end);
}

@keyframes typing {
    0{
        width: 0;
    }
    100% {
        width: 26ch;
     }
}

就可以得到如下結果啦:

純 CSS 實現文字輸入效果

完整的程式碼你可以戳這裡:

CodePen Demo -- 純 CSS 實現文字輸入效果

改造成豎向打字效果

接下來,我們就運用上述技巧,改造一下。將一個橫向的打字效果改造成豎向的打字效果。

核心的虛擬碼如下:

<div>
    <p></p>
    <p></p>
    <p></p>
</div>
$str: 'ぁぃぅぇぉかきくけこんさしすせそた◁▣▤▥▦▧♂♀♥☻►◄▧▨♦ちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎをァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥabcdefghigklmnopqrstuvwxyz123456789%@#$<>^&*_+';
$length: str-length($str);

@function randomChar() {
    $r: random($length);
    @return str-slice($str, $r, $r);
}

@function randomChars($number) {
    $value: '';

    @if $number > 0 {
        @for $i from 1 through $number {
            $value: $value + randomChar();
        }
    }
    @return $value;
}

p {
    width: 12px;
    font-size: 10px;
    word-break: break-all;
}

p::before {
    content: randomChars(20);
    color: #fff;
    animation: typing 4s steps(20, end) infinite;
}

@keyframes typing {
    0% {
        height: 0;
    }
    25% {
        height: 100%;
    }
    100% {
        height: 100%;
    }
}

這樣,我們就實現了豎向的打字效果:

當然,這樣看上去比較整齊劃一,缺少了一定的隨機,也就缺少了一定的美感。

基於此,我們進行 2 點改造:

  1. 基於動畫的時長 animation-time、和動畫的延遲 animation-delay,增加一定幅度內的隨機
  2. 在每次動畫的末尾或者過程中,重新替換偽元素的 content,也就是重新生成一份 content

可以藉助 SASS 非常輕鬆的實現這一點,核心的 SASS 程式碼如下:

$n: 3;
$animationTime: 3;
$perColumnNums: 20;

@for $i from 0 through $n {
    $content: randomChars($perColumnNums);
    $contentNext: randomChars($perColumnNums);
    $delay: random($n);
    $randomAnimationTine: #{$animationTime + random(20) / 10 - 1}s;

    p:nth-child(#{$i})::before {
        content: $content;
        color: #fff;
        animation: typing-#{$i} $randomAnimationTine steps(20, end) #{$delay * 0.1s * -1} infinite;
    }

    @keyframes typing-#{$i} {
        0% {
            height: 0;
        }
        25% {
            height: 100%;
        }
        100% {
            height: 100%;
            content: $contentNext;
        }
    }
}

看看效果,已經有不錯的改觀:

當然,上述由橫向打字轉變為豎向打字效果其實是有一些不一樣的。在現有的豎向排列規則下,無法通過 ch 配合字元數拿到實際的豎向高度。所以這裡有一定的取捨,實際放慢動畫來看,沒個字的現出不一定是完整的。

當然,在快速的動畫效果下幾乎是察覺不到的。

增加光影與透明度變化

最後一步,就是增加光影及透明度的變化。

最佳的效果是要讓每個新出現的字元保持亮度最大,同時已經出現過的字元亮度慢慢減弱。

但是由於這裡我們無法精細操控每一個字元,只能操控每一行字元,所以在實現方式上必須另闢蹊徑。

最終的方式是借用了另外一個偽元素進行同步的遮罩以實現最終的效果。下面我們就來一步一步看看過程。

給文字增添亮色及高光

第一步就是給文字增添亮色及高光,這點非常容易,就是選取一個黑色底色下的亮色,並且藉助 text-shadow 讓文字發光。

p::before {
    color: rgb(179, 255, 199);
    text-shadow: 0 0 1px #fff, 0 0 2px #fff, 0 0 5px currentColor, 0 0 10px currentColor;
}

看看效果,左邊是白色字元,中間是改變字元顏色,右邊是改變了字型顏色並且新增了字型陰影的效果:

給文字新增同步遮罩

接下來,就是在文字動畫的行進過程中,同步新增一個黑色到透明的遮罩,儘量還原讓每個新出現的字元保持亮度最大,同時已經出現過的字元亮度慢慢減弱。

這個效果的示意圖大概是這樣的,這裡我將文字層和遮罩層分開,並且底色從黑色改為白色,方便理解:

蒙層遮罩原理圖

大概的遮罩的層的虛擬碼如下,用到了元素的另外一個偽元素:

p::after {
    content: '';
    background: linear-gradient(rgba(0, 0, 0, .9), transparent 75%, transparent);
    background-size: 100% 220%;
    background-repeat: no-repeat;
    animation: mask 4s infinite linear;
}

@keyframes mask {
    0% {
        background-position: 0 220%;
    } 
    30% {
        background-position: 0 0%;
    }
    100% {
        background-position: 0 0%;
    }
}

好,合在一起的最終效果大概就是這樣:

通過調整 @keyframes mask 的一些引數,可以得到不一樣的字元漸隱效果,需要一定的除錯。

完整程式碼及效果

OK,拆解了一下主要的步驟,最後上一下完整程式碼,應用了 Pug 模板引擎和 SASS 語法。

完整程式碼加起來不過 100 行。

.g-container
    -for(var i=0; i<50; i++)
        p
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200&display=swap');

$str: 'ぁぃぅぇぉかきくけこんさしすせそた◁▣▤▥▦▧♂♀♥☻►◄▧▨♦ちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎをァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥabcdefghigklmnopqrstuvwxyz123456789%@#$<>^&*_+';
$length: str-length($str);
$n: 50;
$animationTime: 4;
$perColumnNums: 25;

@function randomChar() {
    $r: random($length);
    @return str-slice($str, $r, $r);
}

@function randomChars($number) {
    $value: '';

    @if $number > 0 {
        @for $i from 1 through $number {
            $value: $value + randomChar();
        }
    }
    @return $value;
}

body, html {
    width: 100%;
    height: 100%;
    background: #000;
    display: flex;
    overflow: hidden;
}

.g-container {
    width: 100vw;
    display: flex;
    justify-content: space-between;
    flex-wrap: nowrap;
    flex-direction: row;
    font-family: 'Inconsolata', monospace, sans-serif;
}

p {
    position: relative;
    width: 5vh;
    height: 100vh;
    text-align: center;
    font-size: 5vh;
    word-break: break-all;
    white-space: pre-wrap;
    
    &::before,
    &::after {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 100%;
        overflow: hidden;
    }
}

@for $i from 0 through $n {
    $content: randomChars($perColumnNums);
    $contentNext: randomChars($perColumnNums);
    $delay: random($n);
    $randomAnimationTine: #{$animationTime + random(20) / 10 - 1}s;
  
    p:nth-child(#{$i})::before {
        content: $content;
        color: rgb(179, 255, 199);
        text-shadow: 0 0 1px #fff, 0 0 2px #fff, 0 0 5px currentColor, 0 0 10px currentColor;
        animation: typing-#{$i} $randomAnimationTine steps(20, end) #{$delay * 0.1s * -1} infinite;
        z-index: 1;
    }

    p:nth-child(#{$i})::after {
        $alpha: random(40) / 100 + 0.6;
        content: '';
        background: linear-gradient(rgba(0, 0, 0, $alpha), rgba(0, 0, 0, $alpha), rgba(0, 0, 0, $alpha), transparent 75%, transparent);
        background-size: 100% 220%;
        background-repeat: no-repeat;
        animation: mask $randomAnimationTine infinite #{($delay - 2) * 0.1s * -1} linear;
        z-index: 2;
    }

    @keyframes typing-#{$i} {
        0% {
            height: 0;
        }
        25% {
            height: 100%;
        }
        100% {
            height: 100%;
            content: $contentNext;
        }
    }
}

@keyframes mask{
    0% {
        background-position: 0 220%;
    } 
    30% {
        background-position: 0 0%;
    }
    100% {
        background-position: 0 0%;
    }
}

最終效果也就是題圖所示:

Digital Char Rain Animation

完整的程式碼及演示效果你可以戳這裡:

CodePen Demo -- Digital Char Rain Animation

最後

靈感源自 袁川 老師的這個效果 CodePen Demo -- Matrix digital rain,原效果使用了 JavaScript· 實現,本文利用純 CSS 進行了演繹。

更多精彩 CSS 效果可以關注我的 CSS 靈感

好了,本文到此結束,希望對你有幫助 ?

更多精彩 CSS 技術文章彙總在我的 Github -- iCSS ,持續更新,歡迎點個 star 訂閱收藏。

如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

相關文章