前端網頁動效
-前端特效 - 如何利用 canvas 做出極具科技感的前端卡片粒子特效
前言
前陣子在 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 屬性在頂部添加一個漸變遮罩。
.card
用 aspect-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 大概會長這個樣子👇
前端卡片粒子特效 - 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_SIZE
和 SPACING
將決定粒子網格的大小和間距,而 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>
元素,對每個元素執行以下操作。
- 獲取每個
<canvas>
元素的父元素parent
以及該父元素設置的 CSS 變量--color
。 - 使用
getBoundingClientRect()
獲取父元素的寬高。 - 並將這些值設置為
<canvas>
元素的寬高,確保<canvas>
能夠完全覆蓋父元素。 - 獲取
<canvas>
的 2D 繪製上下文ctx
。 - 根據
<canvas>
的寬高、GRID_SIZE
和SPACING
計算出網格的列數cols
和行數rows
。 - 最後初始化一個空數組
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);
});
});
看起來很複雜,讓我一一解釋這段程式的步驟:
- 清空
<canvas>
上的內容。 - 獲取當前時間,並過濾掉生命週期已結束的粒子。
- 遍歷網格,對於每個空的網格單元,隨機設定它的生命週期,並將其添加到
particles
陣列中 - 遍歷
particles
陣列,繪製每個粒子。粒子的透明度根據其剩餘生命週期計算得出 - 使用
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 上看完整的程式碼喔!那今天就這樣,我們下篇貼文見~!