CSS 還原拉斯維加斯球數位動畫

2023-10-18 12:01:05

我的小冊 《CSS 技術揭祕與實戰通關》上線了,想了解更多有趣、進階、系統化的 CSS 內容,可以猛擊 - LINK

最近大家刷抖音,是否有刷到拉斯維加斯的新地標 「Sphere」:

場館內部的視覺效果非常驚人,其中一個效果讓我虎軀一震:

我的第一想法就是,這個看起來用 CSS 也可以實現嘛?還有 CSS 不能實現的?

本文,就將嘗試使用 CSS,大致還原這個效果。

拆解動畫效果

其實,上述的動畫效果,本質就是一個 3D 立方體。

同時,3D 立方體上每個面存在顏色不一樣的文字,文字和顏色都在隨機變化。

也就是說,我們需要實現一個 3D 立方體:

同時,我們還需要實現這樣一個動畫效果 -- 文字和顏色都在隨機變化的平面效果:

兩者組合一下,再挪動 3D 元素的景深距離,就能實現我們想要的效果!

好,下面我們一個一個實現。

實現一個 3D 立方體

實現一個 3D 立方體,相對另外一個文字和顏色都在隨機變化的平面效果而言,屬於非常非常簡單的一步了。

我們在非常多篇文章中也講過具體的實現方式:

最常見的 3D 圖形,莫過於一個 3D 立方體。

如果沒有上下兩個面,只是一個 4 個面的圖形,大概是這樣:

這樣一個圖形,利用 CSS 3D,如何快速實現呢?

首先,構造這麼一個結構:

<div class="perspective">
        <div class="container">
                <div class="img">3</div>
                <div class="img">D</div>
                <div class="img">視</div>
                <div class="img">圖</div>
        </div>
</div>

4 個面,就是最內層的 4 個 .img,首先,需要給兩個父容器,設定 3D 的屬性:

.perspective {
  perspective: 3000px;
}
.container {
  width: 400px;
  height: 400px;
  transform-style: preserve-3d;
}

簡單解釋一下:

  1. perspective 可以作用於元素的後代,設定在最上層即可;
  2. transform-style: preserve-3d 設定給最終需要 3D 空間的元素的父容器之上,由於最終是 4 個 .img 需要 3D 空間,因此設定給 .container 即可。

接下來,就是最為核心的,如何設定 4 個 .img 元素的 3D 變換,使之形成 3D 立方體。

技巧就是:先旋轉,再位移

這裡給出一個俯視效果圖:

以上述 Demo 中的正方體為例子,class 為 .img 的 div 塊的高寬為 400px*400px。那麼要利用 4 個 這樣的 div 拼接成一個正方體,需要分別將 4 個 div 繞 Y 軸旋轉 [90°, 180°, 270°, 360°],再 translateY(200px)

值得注意的是,一定是先旋轉角度,再偏移距離,這個順序很重要

程式碼如下:

.img {
        position: absolute;
        top: 0;
        left: 0;
        width: 400px;
        height: 400px;
}
@for $i from 1 through $imgCount {
        .img:nth-child(#{$i}) {
                transform: rotateY(($i * 90deg)) translateZ(200px);
        }
}

效果如下:

此時,可能會覺得圖片太太太大了,此時,我們可以通過給中間層 .container 設定一個恰當的 translateZ 進行視覺大小上的調節。

.container {
    transform: translateZ(-3000px);
}

這樣,就能得到恰當大小的立方體元素效果:

完整的程式碼,你可以戳這裡:CodePen Demo -- 3D Cube

當然,對於我們這個效果,我們 5 要五個面(前後左右與上方即可),因此,我們基於上述的基礎知識鋪墊,重新實現一個我們需要的框架結構:

<div class="perspective">
  <div class="container">
    <div class="g-panel"></div>
    <div class="g-panel"></div>
    <div class="g-panel"></div>
    <div class="g-panel"></div>
    <div class="g-panel"></div>
  </div>
</div>

並且,我們希望我們的圖形是一個立方體,只需要稍微改造長寬和 translateZ() 的即可。這樣,我們就能得到一個前後左右與上方 5 個面的立方體元素。

示意效果如下:

實現文字動畫效果

OK,立方體我們先放在一邊。

接下來,我們嘗試來實現這個效果:

這個效果如果一個文字用一個 DIV 承載實現,那是非常容易的,但是這樣勢必會造成元素過多,再設定動畫效果,則會導致頁面太為卡頓

所以,我們需要另闢蹊徑。這裡,我們可以使用多層漸變配合 background-clip: text

首先,我們利用等寬字型,隨機實現一列文字:

<div>ABCDEFGHIJKLMN</div>
div {
    font-family: monospace;
    text-align: center;
    font-size: 25px;
    width: 25px;
    line-height: 25px;
    color: #fff;
}

效果大致如下:

此時,如果我們再利用線性漸變,給每個字元的對應空間(也就 25px x 25px),設定上不同的顏色,大概是這樣:

@function randomLinear($count) {
    $value: '';
    
    @for $i from 0 through ($count - 1) {
        $value: $value + randomColor() + string.unquote(" 0 #{$i * 25}px,");
    }
    
    @return linear-gradient(string.unquote(#{$value}) randomColor() 0 100%);
}
@function randomColor() {
    @return rgb(randomNum(255), randomNum(255), randomNum(255));
}
div {
    // ...
    background: randomLinear(14);
}

其中,randomLinear(14) 是一個 SASS 函數,引數 14 表示生成 14 層線性漸變,每一個文字區域的顏色都是隨機的,經過編譯後的其中一種結果如下:

div {
    // ...
    background: linear-gradient(#feea96 0 25px, #edde42 0 50px, #e2344a 0 75px, #cdab7e 0 100px, #e16c8b 0 125px, #dcdc7d 0 150px, #dcb42a 0 175px, #d6a587 0 200px, #984f71 0 225px, #221e34 0 250px, #5e9a69 0 275px, #a955e4 0 300px, #4e908f 0 325px, #8d177e 0 350px);
}

上面,我們按照每間隔 25px 的高度,利用線性漸變隨機設定了一種顏色,最終,能夠得到這麼個效果:

此時,我們只需要再設定 background-clip: text,配合透明文字顏色 color: transparent,就可以實現單個 div 內,單列文字,每個字型的顏色都是不一樣的:

div {
    // ...
    background: randomLinear(14);
    background-clip: text;
    color: transparent;
}

此時,效果如下:

當然,文字顏色可以隨機,那麼文字本身也應該隨機。這個不難,我們也可以藉助 SASS 函數,編寫一個隨機字元的函數,通過元素的偽元素 content 進行設定。

那麼此時,完整的程式碼可能是這樣的:

<div></div>
$str: 'QWERTYUIOPASDFGHJKLZXCVBNMabcdefghigklmnopqrstuvwxyz123456789';
$length: str-length($str);

@function randomLinear($count) {
    $value: '';
    
    @for $i from 0 through ($count - 1) {
        $value: $value + randomColor() + string.unquote(" 0 #{$i * 25}px,");
    }
    
    @return linear-gradient(string.unquote(#{$value}) randomColor() 0 100%);
}
@function randomColor() {
    @return rgb(randomNum(255), randomNum(255), randomNum(255));
}
@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;
}

div {
    position: relative;
    width: 25px;
    height: 350px;

    &::before {
        content: randomChars(14);
        position: absolute;
        font-family: monospace;
        background: randomLinear(14);
        background-clip: text;
        color: transparent;
        text-align: center;
        font-size: 25px;
        width: 25px;
        line-height: 25px;
    }
}

這樣,每次 div 內的文字,都是從上面 SASS 函數中 $str 變數中隨機取的:

接下來,我們需要實現文字的隨機跳變,也很好做,我們需要在一開始,隨機生成多個不同的 content,然後,藉助 CSS 動畫,進行切換。

div {
   &::before {
        content: randomChars(14);
        --content1: "#{randomChars(14)}";
        --content2: "#{randomChars(14)}";
        --content3: "#{randomChars(14)}";
        --content4: "#{randomChars(14)}";
        animation: contentChange 1s infinite;
    }
}

@keyframes contentChange {
    20% {
        content: var(--content1);
    }
    40% {
        content: var(--content2);
    }
    60% {
        content: var(--content3);
    }
    80% {
        content: var(--content4);
    }
}

這裡,我們一次生成了 5 個 content,其中 4 個用 CSS 變數儲存了起來,隨後,在 CSS 動畫中,利用提前生成好的 content,進行字元內容的替換,此時,整個效果如下:

隨機內容有了,單個字型顏色不一樣有了,就差顏色的隨機跳變動畫了,這個也非常好做,我們在多篇文章也提及過,利用 filter: hue-rotate() 可以快速實現內容的顏色切換。

div {
    animation: colorChange 1s steps(12) infinite;
}
@keyframes colorChange {
    100% {
        filter: hue-rotate(360deg);
    }
}

我們利用了 filter: hue-rotate() 加上了步驟動畫(steps),成功的實現了顏色的跳變!效果如下:

當然,我們最終要實現的是整個面隨機顏色加上隨機文字的跳變動畫,只需要在上述的基礎上,利用 SASS 函數,迴圈重複多列操作即可。基於上述所有內容的鋪墊,我們最終的單個面下的動畫效果程式碼如下:


<div class="g-container">
  <div></div>
  // ... 一個 32 個子 div
  <div></div>
</div>
@use "sass:string";

$str: 'QWERTYUIOPASDFGHJKLZXCVBNMabcdefghigklmnopqrstuvwxyz123456789';
$length: str-length($str);
$size: 25;
$count: 41;

@function randomNum($max, $min: 0, $u: 1) {
    @return ($min + random($max)) * $u;
}

@function randomLinear($count) {
    $value: '';
    
    @for $i from 0 through ($count - 1) {
        $value: $value + randomColor() + string.unquote(" 0 #{$i * 25}px,");
    }
    
    @return linear-gradient(string.unquote(#{$value}) randomColor() 0 100%);
}

@function randomColor() {
    @return rgb(randomNum(255), randomNum(255), randomNum(255));
}

@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;
    font-family: monospace;
}

.g-container {
    position: relative;
    width: 800px;
    height: 800px;
    display: flex;
    animation: colorChange 1s steps(12) infinite;
    
    div {
        position: relative;
        width: #{$size}px;
        height: 800px;
        flex-shrink: 0;
        
        &::before {
            position: absolute;
            inset: 0;
            text-align: center;
            font-size: #{$size}px;
            width: #{$size}px;
            text-align: center;
            line-height: #{$size}px;
            color: transparent;
        }
    }
    
    @for $i from 1 to $count {
        div:nth-child(#{$i}) {
            &::before {
                content: randomChars(32);
                --content1: "#{randomChars(32)}";
                --content2: "#{randomChars(32)}";
                --content3: "#{randomChars(32)}";
                --content4: "#{randomChars(32)}";
                animation: contentChange 1s infinite;
                background: randomLinear(32);
                background-clip: text;
            }
        }
    }
}
@keyframes colorChange {
    100% {
        filter: hue-rotate(360deg);
    }
}
@keyframes contentChange {
    20% {
        content: var(--content1);
    }
    40% {
        content: var(--content2);
    }
    60% {
        content: var(--content3);
    }
    80% {
        content: var(--content4);
    }
}

這樣,我們就成功的實現了單個平面下的,顏色隨機,文字隨機,且不斷變化的動畫效果:

單個平面下的完整程式碼,你可以戳這裡:CodePen Demo -- Single Panel Random Text

實現立體效果

有了上面的立方體和單個平面的效果,要實現立體效果就不難了。我們嘗試將兩者結合起來。

改造原有的立方體結構,大致改成如下形式:

.perspective
    .container
        .g-panel
            -for(var i=0; i<32; i++)
                div
        .g-panel
            -for(var i=0; i<32; i++)
                div
        .g-panel
            -for(var i=0; i<32; i++)
                div
        .g-panel
            -for(var i=0; i<32; i++)
                div
        .g-panel
            -for(var i=0; i<32; i++)
                div

上面採用了 PUG 模板引擎來簡化程式碼,編譯後的效果如下:

<div class="perspective">
  <div class="container">
    <div class="g-panel">
      <div></div>
      // ... 32 個
      <div></div>
    <div class="g-panel">
      <div></div>
      // ... 32 個
      <div></div>
    <div class="g-panel">
      <div></div>
      // ... 32 個
      <div></div>
    <div class="g-panel">
      <div></div>
      // ... 32 個
      <div></div>
    <div class="g-panel">
      <div></div>
      // ... 32 個
      <div></div>
  </div>
</div>

這裡,我們只需要實現 5 個面的立方體即可(前後左右以及上方)。

每個 .g-panel,實現一個我們上面鋪墊的單面文字跳變效果,這樣,我們就能得到這麼一個立體的 3D 立方體動畫效果:

接下來,我們只需要稍加偵錯,通過控制 perspectivetransform: translateZ() 控制視覺上的縱深,將畫面的視角放置於整個立方體之中,即可得到這麼個效果:

好,最後,我們模擬文章開頭拉斯維加斯球的效果,讓頂部的平面,向下運動,實現一種天花板往下掉的動畫效果,最終,我們即可使用純 CSS,大致模擬出整個效果:

由於 GIF 錄製問題,實際效果會比 GIF 展示效果更為震撼。

使用 CSS 實現的完整的程式碼以及整個效果,你可以點選這裡進行檢視:CodePen Demo -- Las Vegas Sphere Cube Random Text

最後

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