前端網頁動效

-

如何用 CSS 和 JavaScript 製作音樂波浪 waveform 背景特效?

this.web

waveform animation 封面

前幾天幫公司專案做了一個簡單的 Landing Page,客戶希望後面有 waveform 的樣式,並隨著滑鼠移動變化波浪。像這樣 👇

今天就來看看怎麼做出這樣的特效吧!

Waveform - HTML 部分

首先,我們來看 HTML

<div class="bg">
  <div class="bg__bars bg__bars--back"></div>
  <div class="bg__bars bg__bars--front"></div>
</div>

這個 HTML 結構相對簡單,整個背景動畫的容器是 <div class="bg">,其中有兩個子元素,分別用來表示背景的前後兩層條紋:

  • .bg__bars--back 是背景層(後面顏色為藍色的條紋)。
  • .bg__bars--front 是前景層(前面顏色為綠色的條紋)。

等等會用 JavaScript 塞很多 barbg__bars 裡面。

Waveform - CSS 部分

:root {
  --cr-bg: #2a3330;
  --cr-green: #7cf5bd;
  --cr-blue: #010024;
}

首先 :root 定義了三個顏色變數,方便在程式中管理顏色:

  • --cr-bg 為背景顏色。
  • --cr-green 為前條紋的顏色。
  • --cr-blue 為背景條紋的顏色。
.bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  background-color: var(--cr-bg);
}

.bg 設置為全螢幕顯示(利用 inset: 0;absolute 定位),背景色使用了 --cr-bg 的深綠色,並且禁止點擊事件(pointer-events: none),這樣條紋就不會影響到其他互動操作。

.bg__bars {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
}

.bg__bars 設置了條紋容器的位置與排列,用 flex 就能讓條紋從左到右排好。並使用align-items: flex-end 讓條紋從下方開始。

.bg__bar {
  position: relative;
  flex: 1 1 0;
  height: 50%;
  transform-origin: bottom;
}

.bg__bar 是單個條紋的樣式,每個條紋的高度都為 50%,也就是螢幕的一半高度,等等直接控制 transform: scale 就能調整 bar 的高度。使用 scale 控制高度的原因是 scale 的效能會比直接控制 Height 來的好。

最後記得用 transform-origin: bottom; 設置動畫變形的原點為條紋的底部,這樣縮放效果才會從下往上展開,而不是從中間開始。如果沒有設定就會像這樣。

transform origin center 結果
.bg__bar__inner {
  position: absolute;
  inset: 0;
  width: calc(100% + 1px);
}

而需要 inner__bar 的原因是,我們有兩個部分需要控制高度:

  1. .bg__bar: 外層控制整體的波浪
  2. .bg__bar__inner: 內層控制自身的脈動效果

.bg__bar__inner 寬度多出 1px 來避免邊緣的視覺縫隙。沒有設置的話,有時候就會出現像這樣的縫隙。

縫隙
.bg__bars--front .bg__bar__inner {
  background-color: var(--cr-green);
}

.bg__bars--back .bg__bar__inner {
  background-color: var(--cr-blue);
}

最後為前景和後景設置不同顏色。

Waveform - JavaScript 部分

接著來解釋 JavaScript,JS 主要負責動態生成條紋並根據視窗大小與滑鼠位置製作波浪效果。

我們先簡單看一下 JS 的架構。

JavaScript 架構
  • barCountsmousePosition 是很多 function 會用的全局變數。
  • initBarscreateBarscreateBar 用來製作 DOM。
  • getScaleupdateBarScaleupdateBars 負責製作波浪效果?
  • handleResizehandleMouseMoveonload 則是處理各自的事件。

全域變數

let barCounts = 0;
let mousePosition = { x: 0, y: 0 };

這些變數用來儲存條紋數量、滑鼠位置。

我們等等會根據螢幕寬度調整 barCounts 的數量,animation 也必須做好 RWD 啊!

initBars 函數

function initBars() {
  const frontBars = document.querySelector('.bg__bars--front');
  const backBars = document.querySelector('.bg__bars--back');
  
  frontBars.innerHTML = '';
  backBars.innerHTML = '';
}

這個函數不難在每次調整大小時清除現有條紋,為下一步生成新的條紋做好準備。

createBars 和 createBar 函數

function createBars() {
  const frontBars = document.querySelector('.bg__bars--front');
  const backBars = document.querySelector('.bg__bars--back');

  for (let i = 0; i < barCounts; i++) {
    createBar(frontBars);
    createBar(backBars);
  }
}

function createBar(barList) {
  const bar = document.createElement('div');
  bar.className = 'bg__bar';
  const inner = document.createElement('div');
  inner.className = 'bg__bar__inner';
  bar.appendChild(inner);
  barList.appendChild(bar);
}
  • createBars 負責根據條紋數量(barCounts)生成條紋,分別為前景和背景創建條紋。
  • createBar 則負責生成單個條紋,並將其插入到指定的條紋列表中。

getScale 函數

function getScale(index, barCounts, isBack, mousePosition) {
  // 解構滑鼠座標
  const { x, y } = mousePosition;

  // 定義動畫參數 
  const WAVE_MULTIPLIER = isBack ? 3 : 4;
  const MOUSE_MULTIPLIER = isBack ? 1.5 : -3;
  const SCALE_OFFSET = isBack ? 0.15 : 0.5;

  // 計算基礎縮放比例
  const baseScale = Math.sin(
    (index / barCounts) * (WAVE_MULTIPLIER * Math.PI) -
    (x / window.innerWidth) * (MOUSE_MULTIPLIER * Math.PI)
  ) * 0.2;

  // 根據滑鼠 Y 軸位置來影響條紋變化
  const yScale = (1 - y / window.innerHeight) * 0.3;

  // 如果是背景條紋,增加縮放量
  // 反之是前景條紋,則減少縮放量
  // 這只是讓互動更有據的小變化
  return isBack
    ? 1 + baseScale + SCALE_OFFSET + yScale
    : 1 - baseScale - SCALE_OFFSET - yScale;
}

getScale 相對複雜一點,它是計算每個條紋在動畫中的垂直縮放比例。條紋的縮放大小會根據它們的位置、滑鼠的位置、以及它們位於前景還是背景進行動態調整。

baseScale 計算基礎縮放比例

這是整個動畫的核心,利用三角函數 Math.sin 來製造波浪效果:

  • (index / barCounts) * (WAVE_MULTIPLIER * Math.PI):這段公式表示條紋在波浪中的位置。index / barCounts 確定條紋的相對位置,WAVE_MULTIPLIER * Math.PI 使波浪形狀在畫面中循環展現出來。
  • (x / window.innerWidth) * (MOUSE_MULTIPLIER * Math.PI):這部分則是根據滑鼠水平位置來調整條紋的波形,讓我們移動滑鼠時,會影響每個條紋的縮放程度。MOUSE_MULTIPLIER 則是用來調整影響的大小和方向。

最後乘上 0.2,縮小計算出的 sin 值,使條紋的縮放變化不要太強烈。

updateBarScale 和 updateBars 函數

function updateBarScale(bar, index, isBack) {
  const scale = getScale(index, barCounts, isBack, mousePosition);
  bar.style.transform = `scaleY(${scale})`;
}

function updateBars() {
  const frontBars = document.querySelector('.bg__bars--front');
  const backBars = document.querySelector('.bg__bars--back');

  const frontBarList = frontBars.querySelectorAll('.bg__bar');
  frontBarList.forEach((bar, i) => updateBarScale(bar, i, false));

  const backBarList = backBars.querySelectorAll('.bg__bar');
  backBarList.forEach((bar, i) => updateBarScale(bar, i, true));
}
  • updateBarScale 會根據條紋的索引及滑鼠位置計算縮放比例,並應用到條紋的 scaleY 屬性上。
  • updateBars 則負責更新所有條紋的縮放比例。

handleResize 與 handleMouseMove 函數

function handleResize() {
  const width = window.innerWidth;

  const width = window.innerWidth;
  barCounts = Math.ceil(width / 200);

  initBars();
  createBars();
  updateBars();
}

function handleMouseMove(e) {
  mousePosition = { x: e.clientX, y: e.clientY };
  requestAnimationFrame(updateBars);
}

接著用 handleResize 根據視窗寬度計算應該生成多少條紋,並更新條紋的排列;handleMouseMove 則根據滑鼠的位置更新條紋的縮放效果。

初始設置

window.addEventListener('resize', handleResize);
window.addEventListener('mousemove', handleMouseMove);

window.onload = function () {
  handleResize();
};

接著監聽 resizemousemove 事件就幾乎完成了整個 animation

Waveform - 內部條紋的脈動效果

最後我們要控制 bar__inner 的脈動效果,我原本是打算 JS 去製作,其實也是可以,不過比起用 CSS 會比較吃效能,所以在公司的專案中,我直接使用 SCSS 來製作 css animation。程式碼如下:

@for $i from 1 through 20 {
  .bg__bar:nth-child(#{$i}) {
    .bg__bar__inner {
      animation: pulse-#{$i} 8s linear (random(100) / 100) + 0s infinite;

      @keyframes pulse-#{$i} {
        @for $percent from 0 through 10 {
          // 在 0%, 10%, 20% ... 等時間點,scale 為 1。
          #{$percent * 10%} {
            transform: scaleY(1);
          }
          // 在 2% 3%, 12% 13%, 22% 23% ... 等時間點隨機 scale。、
          @if ($percent * 10 < 100) {
            #{$percent * 10 + 2%},
            #{$percent * 10 + 3%} {
              transform: scaleY((random(20) + 120) / 100);
            }
          }
        }
      }
    }
  }
}

總結

到這邊就是全部程式碼的內容拉!這邊放上 codepen 讓你去玩玩看~

這個特效挺有趣的~其實做起來也不會很難,核心就是利用 sin 函數做出波浪效果,趕快用到你的專案中吧!

相關系列文章