群 bg
一群小魚游啊游 ◝( •ω• )◟
這個元件的靈感來自沖繩美麗海水族館 ヾ(◍'౪`◍)ノ゙
前陣子到沖繩美麗海水族館看大鯨鯊,偏偏撞上暑假,
人潮比水缸裡的魚還多。
動線擠得像打結的漁網,
我像隻被困的小魚,只能無助地扭動,慢慢向前。
頂著嘈雜與體溫交織的悶熱,
穿過人群,終於抵達心裡惦念的那一隅。
還沒回過神,溫柔的蔚藍已輕撫我的臉龐。
頭頂是被水光切割成無數碎片的天幕,群魚在光裡閃爍。
時間被浪花輕輕托起,
午後在海色中悄悄沉沒。
上下的界線消失,萬物無語,
只剩悸動的心跳與悠游的魚群。
然而終究無法久留,
只能在漸暗的光裡回身,
像一尾不得不離開洄游的小魚。
緩緩退向出口,
任嘈雜與擁擠重新聚攏。
技術關鍵字
名稱 | 描述 |
---|---|
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[]>;
}