Skip to content

bg

一群小魚游啊游 ◝( •ω• )◟

這個元件的靈感來自沖繩美麗海水族館 ヾ(◍'౪`◍)ノ゙

2025-07-26 沖繩美麗海水族館

前陣子到沖繩美麗海水族館看大鯨鯊,偏偏撞上暑假,
人潮比水缸裡的魚還多。

動線擠得像打結的漁網,
我像隻被困的小魚,只能無助地扭動,慢慢向前。

頂著嘈雜與體溫交織的悶熱,
穿過人群,終於抵達心裡惦念的那一隅。

還沒回過神,溫柔的蔚藍已輕撫我的臉龐。
頭頂是被水光切割成無數碎片的天幕,群魚在光裡閃爍。

時間被浪花輕輕托起,
午後在海色中悄悄沉沒。

上下的界線消失,萬物無語,
只剩悸動的心跳與悠游的魚群。

然而終究無法久留,
只能在漸暗的光裡回身,
像一尾不得不離開洄游的小魚。

緩緩退向出口,
任嘈雜與擁擠重新聚攏。

技術關鍵字

名稱 描述
JS 動畫基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等
物理模擬模擬真實世界物理現象,如重力、碰撞、速度等物理效果
向量計算處理方向、加速度、速度等等數學運算
Boids模擬鳥群、魚群等群體行為的演算法,常用於動畫和遊戲 AI
Quaternion四元數,用於表示和計算三維空間中的旋轉,避免萬向節鎖(Gimbal lock)問題,常用於 3D 引擎
Euler Angle歐拉角,用於表示三維空間中的旋轉(如 yaw, pitch, roll),容易理解但可能產生萬向節鎖(Gimbal lock)問題

使用範例

基本用法

小魚會朝著滑鼠所在位置游動

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl">
    <bg-flock />
  </div>
</template>

<script setup lang="ts">
import BgFlock from '../bg-flock.vue'
</script>

自定義 boid

討厭魚?組一個自己喜歡的生物吧 (・∀・)9

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl">
    <bg-flock
      ref="bgRef"
      :count="50"
      :target-shell="{
        radius: 150,
        band: 20,
        swirlSpeed: -0.5,
      }"
    >
      <div class="scene h-full w-full">
        <div class="camera">
          <div
            v-for="(boid, i) in viewBoids"
            :key="i"
            class="boid preserve-3d"
            :style="boid.style"
          >
            <div
              class="arrow flex-center text-[8px] text-white"
              :style="{ animationDelay: `${i * 50}ms` }"
            >
              {{ i }}
            </div>
          </div>

          <div class="label z-0 flex-center">

          </div>
        </div>
      </div>
    </bg-flock>
  </div>
</template>

<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed, useTemplateRef } from 'vue'
import BgFlock from '../bg-flock.vue'

const bgRef = useTemplateRef('bgRef')

const viewBoids = computed<Array<{ style: CSSProperties }>>(() => {
  const result: Array<{ style: CSSProperties }> = []

  const boidList = bgRef.value?.boidList ?? []
  for (const boid of boidList) {
    const { yaw, pitch, position } = boid

    const { x, y, z } = position

    const transform
      = `translate3d(${x}px, ${y}px, ${z}px)`
      + ` rotateY(${yaw}rad)`
      + ` rotateZ(${pitch}rad)`

    result.push({
      style: {
        transform,
        zIndex: `${Math.round(z)}`,
      },
    })
  }
  return result
})
</script>

<style scoped lang="sass">
@import url('https://fonts.googleapis.com/css2?family=Liu+Jian+Mao+Cao&display=swap')

.font-liu-jian-mao-cao
  font-family: "Liu Jian Mao Cao", cursive
  font-optical-sizing: auto
  font-style: normal

.scene
  position: relative
  overflow: hidden
  /* 透視感 */
  perspective: 700px
  isolation: isolate

.camera
  position: absolute
  inset: 0
  transform-style: preserve-3d
  /* 可調整角度,可以製造俯視、仰視等等視角 */
  // transform: rotateX(30deg)

.boid
  position: absolute
  left: 0
  top: 0
  width: 0
  height: 0
  pointer-events: none
  will-change: transform
  transform-origin: 0 50% 0

/* 基本箭頭(朝 +X 方向),用 clip-path 畫圖形 */
.arrow
  position: absolute
  width: 30px
  height: 20px
  background: linear-gradient(90deg, light-dark(#2ecc71, #3e5c47), light-dark(#a6f7c5, #b6e3c5))
  clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%)

.label
  @extend .font-liu-jian-mao-cao
  width: 100%
  height: 100%
  font-size: 20vh
  color: light-dark(#333, #DDD)
</style>

魚缸

讓整個網頁變成大海吧!ヾ(◍'౪`◍)ノ゙

查看範例原始碼
vue
<template>
  <div class="w-full">
    <base-checkbox
      v-model="enable"
      label="開啟"
      class="border rounded p-4"
    />

    <bg-flock
      v-if="enable"
      class="pointer-events-none left-0 top-0 z-50 h-full w-full !fixed"
      :count="300"
      :size="10"
      :target-shell="{
        radius: 150,
        band: 50,
        swirlSpeed: 0.5,
      }"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import BgFlock from '../bg-flock.vue'

const enable = ref(false)
</script>

原理

基於 Boids 演算法,預設的魚使用 zdog 繪製。

這個影片介紹得很好,也很推薦大家看看 The Nature of Code,這本書算是我的啟蒙讀物。(*´∀`)~♥

原始碼

API

Props

interface Props {
  /** 初始 boid 數量 */
  count?: number;
  /** 小魚尺寸 */
  size?: number;
  /** 速度倍率 */
  playbackRate?: number;

  boidOptions?: Partial<Pick<
    BoidOptions,
    'maxForce' | 'maxSpeed' | 'angSmooth'
  >>;
  /** 行為權重 */
  behaviorWeights?: BehaviorWeights;
  /** 行為半徑,決定特定行為的考量範圍 */
  behaviorRadii?: BehaviorRadii;
  /** Shell 模式,用於模擬魚環繞特定目標成球的樣子
   *
   * 無則維持原本單點 target 的行為
   */
  targetShell?: {
    radius: number;
    band: number;
    swirlSpeed: number;
  };
}

Methods

interface Expose {
  boidList: ShallowRef<Boid[]>;
}

v0.47.1