前端特效
-網頁的流動背景怎麼做?讓你的網站背景不再死版
第一眼看到這個背景,以為是用 webGl 做的,研究之後發現其實很簡單。
原理是幾個 div 元素在後面跑,並對整個 container 做 blur。
讓我們先來看 HTML 架構 👇
流動背景 - HTML
HTML 很簡單,基本上就是一個 container 包住多個 div。
<div class="blobs">
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
</div>
<h2>Smooth liquid background effect</h2>
流動背景 - CSS
CSS 的部分也不會很難,首先我們要連決定流動背景的元素。這裡我採用 color-mix 的搭配 css variable 快速做出多個顏色。
color-mix
可以依照比例來調配顏色,所以我先隨便選了兩個主色,接著用 color-mix
調整 percent 來做出多個合適顏色。這也是用 color-mix
的好處,你可以隨意變換 main 的顏色來快速調配。
:root {
--cr-main-1: #2ac9de;
--cr-main-2: #f087f4;
--cr-1: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 20%);
--cr-2: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 35%);
--cr-3: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 50%);
--cr-4: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 65%);
--cr-5: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 80%);
}
接著讓 container fixed 在畫面中。並對整個 container 做一個 blur 的效果。
.blobs {
position: fixed;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: blur(140px);
}
最後製作每個 blob 的樣式,我利用css max() 函數來限制住 blob 的最小值,搭配 vw 來讓 blob 根據螢幕寬度自動調整大小。並將 blob 做成圓形。最後分別設置每個 blob 的顏色
.blob {
width: max(240px, 20vw);
aspect-ratio: 1;
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
}
.blob:nth-of-type(1) {
background: var(--cr-main-1);
}
.blob:nth-of-type(2) {
background: var(--cr-main-2);
}
.blob:nth-of-type(3) {
background: var(--cr-1);
}
.blob:nth-of-type(4) {
background: whitesmoke;
}
.blob:nth-of-type(5) {
background: var(--cr-3);
}
.blob:nth-of-type(6) {
background: var(--cr-4);
}
.blob:nth-of-type(7) {
background: var(--cr-2);
}
到這邊你會發現全部的 blob 在左上角不會動,所以我們現在要來加動畫了。
流動背景 - JS 動畫
我們可以用 css animation 來做設計動畫,但我想讓他流動的更隨機,所以用 JS 做。
首先先設置流動的最大和最小速度,以及簡單寫一個返回隨機數字的函數。
const MIN_SPEED = 0.5
const MAX_SPEED = 2
function randomNumber(min, max) {
return Math.random() * (max - min) + min
}
接著為了方便統一管理所有的 blob 我用 class 來製作 blob 物件。基本上就是設置初始位置、隨機速度、獲取大小等等基本的操作
class Blob {
constructor(el) {
this.el = el
// 獲取 blob 寬度和高度 ()
const boundingRect = this.el.getBoundingClientRect()
this.size = boundingRect.width
// 隨機 blob 初始位置
this.initialX = randomNumber(0, window.innerWidth - this.size)
this.initialY = randomNumber(0, window.innerHeight - this.size)
this.el.style.top = `${this.initialY}px`
this.el.style.left = `${this.initialX}px`
// 隨機速度
this.vx =
randomNumber(MIN_SPEED, MAX_SPEED) * (Math.random() > 0.5 ? 1 : -1)
this.vy =
randomNumber(MIN_SPEED, MAX_SPEED) * (Math.random() > 0.5 ? 1 : -1)
// 設定初始位置
this.x = this.initialX
this.y = this.initialY
}
update() {}
}
接著我們希望在 blob 碰到邊緣時,會往反方向前進,所以簡單寫個函數判斷是否超過螢幕邊緣。
由於 CSS 中元素位置的基準點預設會在元素的左上角,所以我們要減掉 size。
class Blob {
constructor(el) {// ...}
update() {
this.x += this.vx // 當下位置會隨著速度變化
this.y += this.vy
// 判斷 blob 是否超過螢幕的四個邊緣
if (this.x >= window.innerWidth - this.size) {
this.x = window.innerWidth - this.size
this.vx *= -1
}
if (this.y >= window.innerHeight - this.size) {
this.y = window.innerHeight - this.size
this.vy *= -1
}
if (this.x <= 0) {
this.x = 0
this.vx *= -1
}
if (this.y <= 0) {
this.y = 0
this.vy *= -1
}
// 最後更新 transltate
// 因為 translate 是根據相對位置增加會減少
// 所以要 this.x - this.initial 來找到相對位置
this.el.style.transform =
`translate(${this.x - this.initialX}px, ${this.y - this.initialY
}px)`
}
}
因為 translate 是根據相對位置增加會減少,所以要 this.x - this.initial 來找到相對位置。
最後,利用 requestAnimationFrame 更新 JS 動畫。
function initBlobs() {
const blobEls = document.querySelectorAll('.blob')
const blobs = Array.from(blobEls).map((blobEl) => new Blob(blobEl))
function update() {
requestAnimationFrame(update)
blobs.forEach((blob) => blob.update()
)
}
requestAnimationFrame(update)
}
initBlobs()
到這邊就大功告成了~最後附上完整程式碼和 codepen 參觀。
完整程式碼和 Codepen
<div class="blobs">
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
<div class="blob"></div>
</div>
<h2>Smooth liquid background effect</h2>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
:root {
--cr-main-1: #2ac9de;
--cr-main-2: #f087f4;
--cr-1: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 20%);
--cr-2: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 35%);
--cr-3: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 50%);
--cr-4: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 65%);
--cr-5: color-mix(in srgb, var(--cr-main-1), var(--cr-main-2) 80%);
}
html,
body {
font-family: 'Poppins', sans-serif;
height: 100%;
}
body {
display: grid;
place-content: center;
background: whitesmoke;
}
.blobs {
position: fixed;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: blur(140px);
}
.blob {
width: max(240px, 28vw);
aspect-ratio: 1;
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
}
.blob:nth-of-type(1) {
background: var(--cr-main-1);
}
.blob:nth-of-type(2) {
background: var(--cr-main-2);
}
.blob:nth-of-type(3) {
background: var(--cr-1);
}
.blob:nth-of-type(4) {
background: whitesmoke;
}
.blob:nth-of-type(5) {
background: var(--cr-3);
}
.blob:nth-of-type(6) {
background: var(--cr-4);
}
.blob:nth-of-type(7) {
background: var(--cr-2);
}
h2 {
font-size: clamp(24px, 6vw, 64px);
max-width: 400px;
text-align: center;
font-weight: bold;
text-transform: uppercase;
opacity: 0.8;
mix-blend-mode: overlay;
}
const MIN_SPEED = 0.5
const MAX_SPEED = 2
function randomNumber(min, max) {
return Math.random() * (max - min) + min
}
class Blob {
constructor(el) {
this.el = el
const boundingRect = this.el.getBoundingClientRect()
this.size = boundingRect.width
// 隨機初始位置
this.initialX = randomNumber(0, window.innerWidth - this.size)
this.initialY = randomNumber(0, window.innerHeight - this.size)
this.el.style.top = `${this.initialY}px`
this.el.style.left = `${this.initialX}px`
// 速度
this.vx =
randomNumber(MIN_SPEED, MAX_SPEED) * (Math.random() > 0.5 ? 1 : -1)
this.vy =
randomNumber(MIN_SPEED, MAX_SPEED) * (Math.random() > 0.5 ? 1 : -1)
this.x = this.initialX
this.y = this.initialY
}
update() {
this.x += this.vx
this.y += this.vy
if (this.x >= window.innerWidth - this.size) {
this.x = window.innerWidth - this.size
this.vx *= -1
}
if (this.y >= window.innerHeight - this.size) {
this.y = window.innerHeight - this.size
this.vy *= -1
}
if (this.x <= 0) {
this.x = 0
this.vx *= -1
}
if (this.y <= 0) {
this.y = 0
this.vy *= -1
}
this.el.style.transform =
`translate(${this.x - this.initialX}px, ${this.y - this.initialY
}px)`
}
}
function initBlobs() {
const blobEls = document.querySelectorAll('.blob')
const blobs = Array.from(blobEls).map((blobEl) => new Blob(blobEl))
function update() {
requestAnimationFrame(update)
blobs.forEach((blob) => blob.update()
)
}
requestAnimationFrame(update)
}
initBlobs()