Skip to content

Cloth Wrapper wrapper

Whatever you wrap turns into a sheet of cloth. (´,,•ω•,,)

Tech Keywords

NameDescription
Babylon.js3D engine
DOM to ImageConverts DOM elements into images, based on SVG foreignObject
Verlet IntegrationDerives velocity from consecutive positions; inherently stable and well suited to heavily constrained cloth and rope simulation
Mass-Spring SystemDiscretizes an object into mass points linked by springs, iteratively relaxing constraints to simulate cloth, rope, and other soft materials
Physics SimulationSimulates real-world physics such as gravity, collisions, and velocity
Vector MathMath operations for direction, acceleration, velocity, and more

Examples

Basic Usage

🐟
鱈魚
一隻熱愛程式的魚,但是沒有手指可以打鍵盤,更買不到能在水裡用的電腦。
( ´•̥̥̥ ω •̥̥̥` )
View example source
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>

How It Works

Turning DOM Into Cloth

snapdom converts the content into an image, then Babylon.js builds a mesh grid the same size as the content and applies that image as its texture, while the original DOM turns transparent. The camera uses perspective projection with its distance derived from the stage height, so cloth at rest lines up with the original DOM.

The texture held me up for ages. A dark outline kept appearing around the cloth. ( ´•̥̥̥ ω •̥̥̥` )

The culprit was premultiplied alpha. A canvas's fully transparent pixels hold nothing but black, and bilinear filtering and mipmaps blend that black into the rounded corners, so bleeding the neighboring colors two rings outward before upload fixes it. The blend mode has to stay consistent throughout too, and since Babylon.js premultiplies inside its shader, the texture itself must not be premultiplied beforehand.

Mass-Spring and Verlet Integration

The cloth is discretized into a grid of mass points connected by three kinds of springs.

  • Structural: links neighbors horizontally and vertically, holding the weave, the stiffest of the three
  • Shear: links diagonals, resisting skew so the cloth doesn't twist into a fishing net
  • Bend: links across two cells, suppressing overly sharp creases

Simulation runs on Verlet integration, deriving velocity straight from consecutive positions, which stays stable even under heavy constraints. Each step integrates then relaxes the constraints, and the relaxation count is the cloth's stiffness, more iterations feel like canvas and fewer like silk.

The Cursor Is a Ball, a Finger Is a Poke

The cursor acts as a ball rolling against the fabric. Its center sits radius - depth in front of the cloth, so points directly beneath it are pushed the furthest and the displacement tapers outward into a rounded dent. The depth stays fixed; swiping faster only sweeps harder and carries more fabric along.

Touch becomes a poke instead. A tap delivers one inward impulse and the spring network spreads it into a ring of ripples on its own. (・∀・)

Watch out for the compatibility mouse events browsers still emit after a touch. Taking those at face value leaves the ball parked wherever you last tapped, pressing into the cloth forever. (๑•̀ㅂ•́)و✧

Source

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