前端特效

-

網頁的流動背景怎麼做?讓你的網站背景不再死版

this.web

smooth-liquid-backgound-main-image

第一眼看到這個背景,以為是用 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()

相關系列文章