前端網頁動效

-

前端特效 - 如何利用 canvas 做出極具科技感的前端卡片粒子特效

this.web

前言

前陣子在 clerk 的網站上看到這樣的效果,當滑鼠移到卡片上時,粒子會從卡片中心向外擴散開來,形成一個科技感的視覺效果。 👇

稍微研究了一下,發現他是用 canvas 做的,你知道怎麼做嗎,今天就來用 canvas 帶你做出這個酷炫的前端卡片粒子特效!

前端卡片粒子特效 - HTML

讓我們從 HTML 開始,這裡我們有一個 .cards 容器,內部包含了三個 .card 元素,,且每個卡片都有一個 <canvas> 元素和一個文字段落 <p>。 每個卡片也設定一個不同的 --color CSS variable,,用於調整文字和粒子的顏色。

💡用 CSS variable 來針對每個元素去調整是很常見的用法喔!
  <div class="cards">
    <div
      class="card"
      style="--color: #3bf0a5"
    >
      <canvas></canvas>
      <p class="card__text">crafted</p>
    </div>
    <div
      class="card"
      style="--color: #2ecee0"
    >
      <canvas></canvas>
      <p class="card__text">performance</p>
    </div>
    <div
      class="card"
      style="--color: #f6e231"
    >
      <canvas></canvas>
      <p class="card__text">exceptional</p>
    </div>
  </div>

前端卡片粒子特效 - CSS

接著讓我們來設置 CSS,首先將 canvas 元素被設置為絕對定位,並使用 mask 屬性在頂部添加一個漸變遮罩。

.cardaspect-ratio 來設置了寬高比(1:1) 並用 grid 來置中文字。 最後為了讓 .card__text canvas 的上方,記得設定 z-index

canvas {
  position: absolute;
  mask: linear-gradient(to top, transparent 10%, #000 100%);
}

.cards {
  display: flex;
  gap: 16px;
}

.card {
  display: grid;
  place-content: center;
  position: relative;
  width: 160px;
  aspect-ratio: 1 / 1;
  color: var(--color);
  border: 0.5px solid rgba(255, 255, 255, 0.2);
  border-radius: 16px;
  transition: 0.4s ease-in-out;
}

.card__text {
  position: relative;
  z-index: 1;
  color: #fff;
  opacity: 0.5;
  transition: 0.4s ease-in-out;
}

做完 CSS 大概會長這個樣子👇

CSS 結果

前端卡片粒子特效 - JavaScript

接著來到最核心的部分,我會逐一拆解每段 code,讓你能清楚了解我的製作絲路。

document.addEventListener('DOMContentLoaded', function () {
  // ...
});

首先,使用 addEventListener 方法,在文檔完全加載後才傳入的要執行函數。這樣做是為了確保在執行JavaScript時,HTML元素已經完全加載並準備就緒。

document.addEventListener('DOMContentLoaded', function () {
  const allCanvas = document.querySelectorAll('.card > canvas');
});

Canvas 基本設定

接著使用 querySelectorAll 獲取所有匹配 .card > canvas 選擇器的元素,也就是所有卡片內部的 <canvas> 元素,並將它們存儲在 allCanvas 常量中。

document.addEventListener('DOMContentLoaded', function () {
  const allCanvas = document.querySelectorAll('.card > canvas');

  const GRID_SIZE = 1;
  const SPACING = 3;
  const LIFETIME = 20000;
});

仔細觀察效果後,發現每個粒子間要有一些間距,並且每個粒子有生命週期,會慢慢消失又突然出現。

所以我們這裡先定義了一些常量,用於控制粒子的大小、間距和生命週期。

GRID_SIZESPACING 將決定粒子網格的大小和間距,而 LIFETIME 則決定了每個粒子的最大存活時間(20秒)。

💡 習慣上,全部大寫的變數代表是不會更改的變數。
document.addEventListener('DOMContentLoaded', function () {
  const allCanvas = document.querySelectorAll('.card > canvas');

  const GRID_SIZE = 1;
  const SPACING = 3;
  const LIFETIME = 20000;

  // 👇
  allCanvas.forEach((canvas) => {
    const parent = canvas.parentNode;
    const color = parent.style.getPropertyValue('--color');
    const { width, height } = parent.getBoundingClientRect();
    canvas.width = width;
    canvas.height = height;

    const ctx = canvas.getContext('2d');
    const cols = Math.floor((width + SPACING) / (GRID_SIZE + SPACING));
    const rows = Math.floor((height + SPACING) / (GRID_SIZE + SPACING));
    let particles = [];
  });
});

接著使用 forEach 方法遍歷所有獲取到的 <canvas> 元素,對每個元素執行以下操作。

  1. 獲取每個 <canvas> 元素的父元素 parent 以及該父元素設置的 CSS 變量 --color
  2. 使用 getBoundingClientRect() 獲取父元素的寬高。
  3. 並將這些值設置為 <canvas> 元素的寬高,確保 <canvas> 能夠完全覆蓋父元素。
  4. 獲取 <canvas> 的 2D 繪製上下文 ctx
  5. 根據 <canvas> 的寬高、GRID_SIZESPACING 計算出網格的列數 cols 和行數 rows
  6. 最後初始化一個空數組 particles 用於存儲粒子數據。

Draw() 函數

接著是整個動畫的核心部分 - drawGrid 函數。先來看 code。

document.addEventListener('DOMContentLoaded', function () {
  // ...

  allCanvas.forEach((canvas) => {
    // ...

    // 👇
    function drawGrid() {
      // 清空 <canvas> 上的內容
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      // 獲取當前時間,並過濾掉生命週期已結束的粒子。
      let currentTime = Date.now();
      particles = particles.filter(particle => currentTime - particle.startTime < particle.lifetime);

      for (let i = 1; i < rows - 1; i++) {
        for (let j = 1; j < cols - 1; j++) {
          // 遍歷網格
          let x = j * (GRID_SIZE + SPACING);
          let y = i * (GRID_SIZE + SPACING);
          let existingParticle = particles.find(p => p.x === x && p.y === y);
          // 對於每個空的網格單元,隨機設定它的生命週期,並將其添加到 particles 陣列中
          if (!existingParticle) {
            let lifetime = Math.random() * LIFETIME;
            particles.push({
              x,
              y,
              startTime: Date.now(),
              lifetime
            });
          }
        }
      }

      // 遍歷 particles 陣列,繪製每個粒子。粒子的透明度根據其剩餘生命週期計算得出
      particles.forEach(particle => {
        ctx.fillStyle = color;
        ctx.globalAlpha = (particle.lifetime - (Date.now() - particle.startTime)) / LIFETIME;
        ctx.fillRect(particle.x, particle.y, GRID_SIZE, GRID_SIZE);
      });

    requestAnimationFrame(drawGrid);
  });
});

看起來很複雜,讓我一一解釋這段程式的步驟:

  1. 清空 <canvas> 上的內容。
  2. 獲取當前時間,並過濾掉生命週期已結束的粒子。
  3. 遍歷網格,對於每個空的網格單元,隨機設定它的生命週期,並將其添加到 particles 陣列中
  4. 遍歷 particles 陣列,繪製每個粒子。粒子的透明度根據其剩餘生命週期計算得出
  5. 使用 requestAnimationFrame 請求瀏覽器在下一帧繼續執行 drawGrid 函數。

通過不斷重複這個過程,就能實現持續的粒子動畫效果。

最後,調用一次 drawGrid 函數啟動動畫。

document.addEventListener('DOMContentLoaded', function () {
  // ...

  allCanvas.forEach((canvas) => {
    // ...

    function drawGrid() {
      // ...
    });
  
  // 👇
  drawGrid()
});

到這邊就幾乎完成了!最後,我們來添加 hover 效果。

前端卡片粒子特效 - Hover

當鼠標懸停在 .card 上時,內部的 canvas 將通過改變 clip-path 來變成一個大圓形,覆蓋整個卡片區域。

並且提高 .card__text 的位置和改變一些樣式的顏色,使其更具互動感!

canvas {
  opacity: 0;
  clip-path: circle(0 at 50% 70%);
  transition: opacity 0.8s ease-in-out, clip-path 0.1s ease-in-out;
  transition-delay: 0s, 0.4s;
}

.card:hover canvas {
  opacity: 1;
  clip-path: circle(100% at 50% 50%);
  transition-duration: 0.1s, 0.4s;
  transition-delay: 0s, 0s;
}

.card:hover {
  border-color: rgba(255, 255, 255, 0.25);
}

.card:hover .card__text {
  color: inherit;
  opacity: 1;
  transform: translateY(-8px);
}

到這邊就大功告成啦!希望這篇文章可以讓你學到 canvas 如何應用在一些常見的組件上,製作炫砲的動效!

最後附上 codepen 的連結~可以去 codepen 上看完整的程式碼喔!那今天就這樣,我們下篇貼文見~!

相關系列文章