Skip to content

布料包裝器 wrapper

被包裹的內容會變成一塊布。(´,,•ω•,,)

技術關鍵字

名稱描述
Babylon.js3D 引擎
DOM to Image將 DOM 元素轉換為圖片的技術,基於 SVG foreignObject 實現
Verlet 積分以前後兩幀位置推算速度的數值積分法,天生穩定,適合大量約束的布料與繩索模擬
質點彈簧系統把物體離散成質點並以彈簧相連,透過反覆鬆弛約束模擬布料、繩索等柔性材質
物理模擬模擬真實世界物理現象,如重力、碰撞、速度等物理效果
向量計算處理方向、加速度、速度等等數學運算

使用範例

基本用法

🐟
鱈魚
一隻熱愛程式的魚,但是沒有手指可以打鍵盤,更買不到能在水裡用的電腦。
( ´•̥̥̥ ω •̥̥̥` )
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 p-6">
    <wrapper-cloth :ref="clothRefList.set">
      <img
        src="/low/profile.webp"
        alt=""
        class="w-40 border-[0.25rem] rounded-full object-cover"
      >
    </wrapper-cloth>

    <wrapper-cloth :ref="clothRefList.set">
      <div class="card relative overflow-hidden border rounded p-6">
        <div class="icon">
          <div class="fish">
            🐟
          </div>
        </div>

        <div class="text-center text-xl font-bold">
          {{ t('codfish') }}
        </div>

        <div class="mt-2 max-w-[17rem]">
          {{ t('codfishDescription') }}<br>( ´•̥̥̥ ω •̥̥̥` )
        </div>
      </div>
    </wrapper-cloth>
  </div>
</template>

<script setup lang="ts">
import { useTemplateRefsList } from '@vueuse/core'
import { useData } from 'vitepress'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
import WrapperCloth from '../wrapper-cloth.vue'

const { t } = useI18n()
const data = useData()

const clothRefList = useTemplateRefsList<
  InstanceType<typeof WrapperCloth>
>()

/** 深色模式切換後配色會變,需重新擷取貼圖 */
watch(() => data.isDark.value, () => {
  clothRefList.value.forEach((clothRef) => {
    clothRef.refresh()
  })
})
</script>

<style scoped lang="sass">
.card
  background: light-dark(#EEE, #333)

.icon
  position: absolute
  right: 0rem
  bottom: 0rem
  font-size: 14rem
  line-height: 10rem
  filter: brightness(0)
  transform: translate(14%, 60%) rotate(10deg)
  opacity: 0.04
  .fish
    animation: float 3s infinite ease-in-out

@keyframes float
  0%, 100%
    transform: translate(0%)
  50%
    transform: translate(-5%, 2%) rotate(5deg)
</style>

原理

把 DOM 變成布

先用 snapdom 把內容轉成圖片,再於 Babylon.js 建立一片與內容等大的網格面,把圖片當貼圖鋪上去,原本的 DOM 則轉為透明。相機採透視投影,距離由舞台高度反推,靜止的布料就會和原本的 DOM 對齊。

貼圖這關卡了我很久,布料周圍老是浮出一圈黑邊。( ´•̥̥̥ ω •̥̥̥` )

兇手是預乘 alpha。canvas 的全透明像素只剩下黑,雙線性與 mipmap 會把那個黑混進圓角,上傳前先把鄰近顏色往透明區擴張兩圈就解決了。混合模式也得全程一致,Babylon.js 會在 shader 內自行預乘,貼圖本身反而不能先乘。

質點彈簧與 Verlet 積分

布料離散成質點網格,質點之間拉三種彈簧。

  • 結構彈簧:連接上下左右,維持經緯,最硬
  • 剪力彈簧:連接對角,抵抗歪斜,避免布被扭成漁網
  • 彎曲彈簧:跨兩格連接,抑制過度銳利的折角

模擬採 Verlet 積分,速度直接由前後兩幀的位置推得,遇到大量約束也不易發散。每步先積分再反覆鬆弛約束,鬆弛次數就是布的硬挺程度,越多越像帆布,越少越像絲綢。

滑鼠是一顆球,手指是戳一下

滑鼠當成一顆貼在布面上滾動的球,球心擺在布面前方 半徑 - 壓入深度 的位置,正下方的質點被推得最深、向外漸淺,自然形成圓潤的凹陷。壓入深度固定,揮得越快只是撥得越用力、帶走的布越多。

觸控則改成戳一下,點一下給一記往內的脈衝,彈簧網自己就會擴散成一圈波紋。(・∀・)

要留意觸控後瀏覽器仍會補送一輪相容用的滑鼠事件,照單全收的話,那顆球會停在剛才點過的位置永遠壓著布。(๑•̀ㅂ•́)و✧

原始碼

API

Props

interface Props {
  /** 關閉後回到原本 DOM,不做任何模擬。@default true */
  enabled?: boolean;
  /** 每格邊長(px),越小布料越細膩、效能消耗越高。@default 4 */
  segmentSize?: number;
  /** 重力加速度(px/s²)。@default 1400 */
  gravity?: number;
  /**
   * 每個模擬步的速度保留率,越接近 1 越飄逸、越小越沉穩。
   *
   * 模擬每秒跑 120 步,因此 0.94 代表一秒後只剩約 0.06% 的速度;
   * 太接近 1 的話碰一下就會震盪很久停不下來。
   *
   * @default 0.94
   */
  damping?: number;
  /** 約束鬆弛迭代次數,越多布料越硬挺。@default 2 */
  iterationCount?: number;
  /** 滑鼠視為一顆滾過布面的球,此為球半徑(px)。@default 30 */
  pointerRadius?: number;
  /** 該球壓入布面的深度(px),越深布陷得越明顯。@default 20 */
  pointerDepth?: number;
  /** 揮動速度達 900 px/s 時的力道放大倍率,1 表示力道不隨速度變化。@default 3 */
  pointerSpeedBoost?: number;
}

Emits

interface Emits {
  /** 布料完成初始化、開始模擬時觸發 */
  ready: [];
}

Methods

interface Expose {
  /** 讓布料回到初始狀態 */
  reset: () => void;
  /** 重新擷取內容圖片,內容或主題變更後呼叫 */
  refresh: () => Promise<void>;
}

Slots

interface Slots {
  /** 要變成布料的內容 */
  default?: () => unknown;
}

v0.71.2