Skip to content

物理包裝器 wrapper

產生物理世界,讓內部元素具有物理效果 ヾ(⌐■_■)ノ♪

使用範例

基本用法

開始運行後,被 body 包裹的元素會產生物理效果

I
T
若有雷同,
那就雷同
• 
世界上
有 10 種
一種
懂二進位
一種不懂
• 
要改的太多了
,不如
改天
• 
程式
,一個能
就行
• 
在我的
機器
可以 work
• 
這 TMD 誰
.
.
.
啊是我
• 
這不是
bug
,這是
feature
• 
你有
試過
重啟
嗎?
• 
昨天
好好的
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300">
    <wrapper-physics
      ref="wrapperRef"
      class="h-[30rem] w-full flex flex-col items-start gap-1 p-4 md:w-[40rem]"
    >
      <div class="flex gap-20 pl-10">
        <wrapper-physics-body>
          <base-btn
            label="開始"
            @click="wrapperRef?.start()"
          />
        </wrapper-physics-body>
        <wrapper-physics-body>
          <base-btn
            label="重置"
            @click="wrapperRef?.reset()"
          />
        </wrapper-physics-body>
      </div>

      <div
        v-for="section, i in sections"
        :key="i"
        class="flex"
        :class="section.class"
      >
        <wrapper-physics-body
          v-for="text, j in section.texts"
          :key="j"
          class="whitespace-nowrap"
        >
          {{ text }}
        </wrapper-physics-body>
      </div>

      <div class="flex-1" />

      <wrapper-physics-body is-static>
        <base-btn label="🐟" />
      </wrapper-physics-body>
    </wrapper-physics>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()

const sections = [
  {
    class: 'text-lg font-bold mt-2',
    texts: ['I', 'T', '語', '錄'],
  },
  {
    class: 'text-sm opacity-50 mb-2',
    texts: ['若有雷同,', '那就雷同'],
  },
  {
    texts: ['• ', '世界上', '有 10 種', '人', ',', '一種', '懂二進位', ',', '一種不懂'],
  },
  {
    texts: ['• ', '要改的太多了', ',不如', '改天', '吧'],
  },
  {
    texts: ['• ', '人', '和', '程式', ',一個能', '跑', '就行'],
  },
  {
    texts: ['• ', '在我的', '機器', '上', '可以 work'],
  },
  {
    texts: ['• ', '這 TMD 誰', '寫', '.', '.', '.', '啊是我'],
  },
  {
    texts: ['• ', '這不是', 'bug', ',這是', 'feature'],
  },
  {
    texts: ['• ', '你有', '試過', '重啟', '嗎?'],
  },
  {
    texts: ['• ', '昨天', '還', '好好的'],
  },
]
</script>

物理性質

調整物理性質,讓元素嗨起來!ᕕ( ゚ ∀。)ᕗ

鱈魚
很肥
🐟
🐟

鱈魚:「我怎麼覺得這個範例偷嘴我?╭(°A ,°`)╮」

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <div class="flex gap-2">
      <base-btn
        label="開始"
        @click="wrapperRef?.start()"
      />
      <base-btn
        label="重置"
        @click="wrapperRef?.reset()"
      />
    </div>

    <wrapper-physics
      ref="wrapperRef"
      class="h-[30rem] w-full flex flex-col items-center justify-center border border-dashed md:w-[40rem]"
    >
      <div class="flex">
        <wrapper-physics-body
          v-for="item, i in list"
          :key="i"
          v-bind="item"
        >
          {{ item.text }}
        </wrapper-physics-body>
      </div>

      <wrapper-physics-body>
        🐟
      </wrapper-physics-body>
    </wrapper-physics>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

type BodyProp = InstanceType<typeof WrapperPhysicsBody>['$props']

interface Item extends BodyProp {
  text: string;
}

const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()

/** 回彈力設為 1.5,表示回彈速度會是進入速度的 1.5 倍
 *
 * 畫面會超嗨喔!ᕕ( ゚ ∀。)ᕗ
 */
const restitution = 1.5

const list: Item[] = [
  { text: '鱈魚', isStatic: true },
  { text: '真', restitution },
  { text: '的', restitution },
  { text: '不', restitution },
  { text: '是', frictionAir: 1 },
  { text: '一', restitution },
  { text: '種', restitution },
  { text: '很肥', isStatic: true },
  { text: '的', restitution },
  { text: '🐟', restitution },
]
</script>

調整重力

調整重力方向,讓元素飄起來!(ノ>ω<)ノ

🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
重力方向:
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <wrapper-physics
      v-bind="env"
      immediate
      class="h-[30rem] w-full flex flex-col items-center justify-center border border-dashed md:w-[40rem]"
    >
      <div class="flex">
        <wrapper-physics-body
          v-for="item in list"
          :key="item"
          v-bind="bodyProp"
          class="select-none"
          :class="item"
        >
          🐟
        </wrapper-physics-body>
      </div>
    </wrapper-physics>
  </div>

  <div class="flex flex-col gap-6 p-4 md:flex-row">
    <div class="">
      重力方向:
      <analog-stick
        size="14rem"
        @trigger="handleTrigger"
      />
    </div>

    <div class="flex-col">
      <base-input
        v-model.number="bodyProp.friction"
        type="range"
        :label="`摩擦力: ${bodyProp.friction}`"
        class="w-full"
        :min="0"
        :step="0.01"
        :max="1"
      />
      <base-input
        v-model.number="bodyProp.frictionAir"
        type="range"
        :label="`空氣阻力: ${bodyProp.frictionAir}`"
        class="w-full"
        :min="0"
        :step="0.01"
        :max="1"
      />
      <base-input
        v-model.number="bodyProp.restitution"
        type="range"
        :label="`彈力: ${bodyProp.restitution}`"
        class="w-full"
        :min="0"
        :step="0.01"
        :max="1.1"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import AnalogStick from '../../analog-stick.vue'
import BaseInput from '../../base-input.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

type BodyProp = InstanceType<typeof WrapperPhysicsBody>['$props']

const env = ref({
  gravity: {
    scale: 0.001,
    x: 0,
    y: 1,
  },
})

const bodyProp = ref<BodyProp>({
  frictionAir: 0.01,
  friction: 0.01,
  restitution: 1,
})

const list = [
  'text-base',
  'text-lgF',
  'text-xl',
  'text-2xl',
  'text-3xl',
  'text-4xl',
  'text-5xl',
  'text-6xl',
  'text-7xl',
]

function handleTrigger(e: { x: number; y: number }) {
  env.value.gravity.x = e.x
  env.value.gravity.y = e.y
}
</script>

視窗滾動

結合滾動效果,將捲動之速度映射至重力加速度,製造上下搖晃效果

🐟
🐟
🐟
🐟
🐟
🐟

鱈魚:「夭壽虐魚喔!Σ(っ °Д °;)っ」

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300">
    <wrapper-physics
      v-bind="env"
      immediate
      class="h-[100vh] w-full flex items-start justify-center"
    >
      <div class="flex">
        <wrapper-physics-body
          v-for="item in list"
          :key="item"
          v-bind="bodyProp"
          class="select-none"
          :class="item"
        >
          🐟
        </wrapper-physics-body>
      </div>
    </wrapper-physics>
  </div>
</template>

<script setup lang="ts">
import {
  refAutoReset,
  useWindowScroll,
} from '@vueuse/core'
import { clamp } from 'remeda'
import { computed, watch } from 'vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

const { y } = useWindowScroll()

const gravityScale = refAutoReset(0.001, 200)
watch(y, (value, prevValue) => {
  const delta = clamp(
    value - prevValue,
    { max: 100, min: -100 },
  )

  gravityScale.value = 0.001 + delta * 0.00015
})

const env = computed(() => ({
  gravity: {
    scale: gravityScale.value,
    x: 0,
    y: 1,
  },
}))

const bodyProp = {
  frictionAir: 0,
  friction: 0,
  restitution: 0.5,
}

const list = [
  'text-base',
  'text-xl',
  'text-2xl',
  'text-4xl',
  'text-5xl',
  'text-7xl',
]
</script>

<style scoped lang="sass">
</style>

物體形狀

調整物體碰撞箱(hit box)外型,例如圓形才符合鱈魚的真實體型。╮(╯▽╰)╭

鱈魚:「現在是當我聽不到是不是?ಠ_ಠ」

甚麼是碰撞箱(hit box)

碰撞箱是指實際上在物理世界中參與碰撞的實際形狀。

你一定在想為甚麼不用物體本身的形狀?

因為物體通常很複雜,如果使用過度複雜的形狀進行運算會導致性能問題,所以通常都會適當簡化,使用形狀更單純的碰撞箱呦。(. ❛ ᴗ ❛.)

🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
重力方向:
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300">
    <wrapper-physics
      v-bind="env"
      immediate
      class="h-[30rem] w-full flex items-center justify-center md:w-[40rem]"
    >
      <template v-for="item in list">
        <wrapper-physics-body
          v-for="index in item.quantity"
          :key="index"
          v-bind="bodyProp"
          class="aspect-square flex select-none items-center justify-center rounded-full bg-slate-300"
          :class="item.class"
        >
          🐟
        </wrapper-physics-body>
      </template>
    </wrapper-physics>
  </div>

  <div class="flex gap-6 p-4">
    <div class="">
      重力方向:
      <analog-stick
        size="14rem"
        @trigger="handleTrigger"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import AnalogStick from '../../analog-stick.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

const env = ref({
  gravity: {
    scale: 0.001,
    x: 0,
    y: 1,
  },
})

const bodyProp: InstanceType<typeof WrapperPhysicsBody>['$props'] = {
  polygon: 'circle',
  frictionAir: 0,
  friction: 0,
  restitution: 0.7,
}

function handleTrigger(e: { x: number; y: number }) {
  env.value.gravity.x = e.x
  env.value.gravity.y = e.y
}

const list = [
  {
    class: 'w-14 h-14 text-3xl',
    quantity: 2,
  },
  {
    class: 'w-10 h-10',
    quantity: 3,
  },
  {
    class: 'w-6 h-6 text-xs',
    quantity: 10,
  },
]
</script>

Scope Prop

使用 Scope Prop 玩出更多花樣

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <div class="flex gap-2">
      <base-btn
        label="開始"
        @click="wrapperRef?.start()"
      />
      <base-btn
        label="重置"
        @click="wrapperRef?.reset()"
      />
    </div>

    <wrapper-physics
      v-bind="env"
      ref="wrapperRef"
      class="h-[30rem] w-full flex flex-row-reverse items-center justify-center gap-4 border border-dashed md:w-[40rem]"
    >
      <div
        v-for="text, i in list"
        :key="i"
        class="flex flex-col"
      >
        <wrapper-physics-body
          v-for="word, j in text.split('')"
          v-slot="scope"
          :key="j"
          polygon="circle"
          :friction-air="(j + i) * 0.005"
        >
          <span :style="getStyle(scope)">
            {{ word }}
          </span>
        </wrapper-physics-body>
      </div>
    </wrapper-physics>
  </div>
</template>

<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { ref } from 'vue'
import { getVectorLength, mapNumber } from '../../../common/utils'
import BaseBtn from '../../base-btn.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

type ScopeProp = Parameters<
  NonNullable<InstanceType<typeof WrapperPhysicsBody>['$slots']['default']>
>[0]

const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()

const env = ref({
  gravity: {
    scale: 0.0005,
    x: -1,
    y: -0.3,
  },
})

const list = [
  '改不盡的需求與鍵盤的呢喃交織',
  '隨時間凋零,不是任務',
  '而是我不再新鮮的',
  '肝',
]

function getStyle(scope: ScopeProp): CSSProperties {
  /** 計算偏移距離 */
  const distance = getVectorLength({
    x: scope.offsetX,
    y: scope.offsetY,
  })

  /** 偏越多越淡 */
  const opacity = mapNumber(
    distance,
    0,
    scope.height * 5,
    1,
    0,
  )

  return { opacity }
}
</script>

手機感測器

配合手機的加速度計,做出更有趣的互動!(/≧▽≦)/

TIP

感謝前端社團和 Line 社群的朋友們提供靈感!

(o゜▽ ゜)o☆

🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
無法取得此設備的感測器。(>人<;)
查看範例原始碼
vue
<template>
  <div class="relative w-full flex flex-col gap-4 border border-gray-300">
    <wrapper-physics
      v-bind="env"
      immediate
      class="h-[30rem] w-full flex items-center justify-center md:w-[40rem]"
    >
      <template v-for="item in list">
        <wrapper-physics-body
          v-for="index in item.quantity"
          :key="index"
          v-bind="bodyProp"
          class="aspect-square flex select-none items-center justify-center rounded-full bg-slate-300"
          :class="item.class"
        >
          🐟
        </wrapper-physics-body>
      </template>
    </wrapper-physics>

    <div
      v-if="!isSupport"
      class="not-support-box"
    >
      <div class="rounded bg-red-500 p-5 text-white">
        無法取得此設備的感測器。(>人<;)
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { throttleFilter, useDeviceMotion } from '@vueuse/core'
import { isNullish, pipe } from 'remeda'
import { computed, ref, watch } from 'vue'
import { getVectorLength, mapNumber } from '../../../common/utils'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'

const {
  accelerationIncludingGravity: acceleration,
} = useDeviceMotion({
  eventFilter: throttleFilter(35),
})

const isSupport = computed(() => {
  if (acceleration.value === null) {
    return false
  }

  return acceleration.value.x !== null
})

watch(acceleration, (value) => {
  if (!isSupport.value || !value)
    return

  updateEnv(value)
}, { deep: true })

const env = ref({
  gravity: {
    scale: 0.001,
    x: 0,
    y: 1,
  },
})

function updateEnv(acc: DeviceMotionEventAcceleration) {
  const { x, y } = acc
  if (isNullish(x) || isNullish(y))
    return

  const scale = pipe(
    getVectorLength({ x, y }),
    /** 重力最大值為 9.8 */
    (value) => mapNumber(
      value,
      0,
      9.8,
      0,
      0.0005,
    ),
  )

  /** x 方向與環境 x 軸相反 */
  env.value.gravity = {
    scale,
    x: -x,
    y,
  }
}

const bodyProp = {
  frictionAir: 0.01,
  friction: 0.01,
  restitution: 0.1,
}

const list = [
  {
    class: 'w-14 h-14 text-3xl',
    quantity: 2,
  },
  {
    class: 'w-10 h-10',
    quantity: 3,
  },
  {
    class: 'w-6 h-6 text-xs',
    quantity: 10,
  },
]
</script>

<style lang="sass">
.not-support-box
  position: absolute
  width: 100%
  height: 100%
  display: flex
  justify-content: center
  align-items: center
</style>

原理

概念為利用 Matter.js 模擬物理效果,並將對應元素之狀態同步至 DOM 元素上。

甚麼是 Matter.js

Matter.js 是一個很成熟的 JavaScript 2D 物理引擎套件,官網上有很多有趣的範例

📚 Matter.js

具體流程如下:

  1. wrapper-physics 收集內部註冊的 wrapper-physics-body。
  2. 根據 body 的位置、尺寸與 prop,建立對應的 Matter.js 物體。
  3. 透過 Matter.js 模擬物理效果並將物理效果儲存。
  4. body 取得儲存的物理效果,並同步至 DOM 元素上。

如此這般,我們成功實現物理效果模擬了!就像是替身一樣!(ノ>ω<)ノ

JOJO!我不想當 DOM 了!(⊙益⊙)

原始碼

API

需要兩個元件,分別為:

  • wrapper-physics:產生物理世界。
  • wrapper-physics-body:定義物體,放在物理世界中。

🧩 WrapperPhysics

定義物理世界與特性,對其中物體產生交互作用。

Props

<template>
  <div
    ref="wrapperRef"
    class="wrapper-physics relative overflow-hidden"
  >
    <slot />

    <canvas
      v-if="debug"
      ref="canvasRef"
      class="pointer-events-none absolute inset-0 bg-transparent"
    />
  </div>
</template>

<script setup lang="ts">
import type { ElBody, UpdateParam } from '.'
import { useElementBounding, useIntervalFn } from '@vueuse/core'
import Matter from 'matter-js'
import { map, pick, pipe } from 'remeda'
import {
  onBeforeUnmount,
  onMounted,
  provide,
  reactive,
  ref,
  shallowRef,
  watch,
} from 'vue'
import { PROVIDE_KEY } from '.'

// #endregion Props
const props = withDefaults(defineProps<Props>(), {
  immediate: false,
  gravity: () => ({
    scale: 0.001,
    x: 0,
    y: 1,
  }),
})

const {
  Engine,
  Render,
  Runner,
  Bodies,
  Composite,
  Body,
} = Matter

// #region Props
interface Props {
  /** 立即開始,物體會在元件建立完成後馬上會開始掉落 */
  immediate?: boolean;

  /** 重力加速度
   *
   * x, y 為加速度的方向,scale 為加速度的大小
   */
  gravity?: Matter.Gravity;
}
const debug = false

const wrapperRef = ref<HTMLDivElement>()
const canvasRef = ref<HTMLCanvasElement>()
const wrapperBounding = reactive(
  useElementBounding(wrapperRef, {
    windowResize: false,
    windowScroll: false,
  }),
)

/** 物理世界座標初始值
 *
 * 以免畫面滾動後,重新建立物理世界時,物體位置不正確
 */
let wrapperInitPosition = {
  x: 0,
  y: 0,
}
onMounted(() => {
  wrapperInitPosition = {
    x: wrapperBounding.x,
    y: wrapperBounding.y,
  }
})

const engine = shallowRef(Engine.create({
  gravity: props.gravity,
}))
watch(() => props.gravity, (value) => {
  if (value) {
    engine.value.gravity = value
  }
}, {
  immediate: true,
  deep: true,
})

const runner = shallowRef(Runner.create())

function init() {
  const result = pipe(Array.from(bodyMap.values()),
    /** 初始化所有 body */
    map((elBody) => {
      const {
        polygon = 'rectangle',
        width,
        height,
      } = elBody

      /**
       * el body 的 xy 是相對於網頁左上角為 0 點,
       * 所以要先減去 wrapper 的 x, y 來取得相對於
       * wrapper 的 x, y,再加上 width, height 的
       * 一半,偏移自身中心
       */
      const { x, y } = {
        x: elBody.x - wrapperInitPosition.x + width / 2,
        y: elBody.y - wrapperInitPosition.y + height / 2,
      }

      const body = pipe(0, () => {
        if (polygon === 'circle') {
          const r = Math.max(width, height) / 2
          return Bodies.circle(x, y, r, {
            ...pick(elBody, ['frictionAir', 'friction', 'restitution', 'mass', 'isStatic']),
            label: elBody.id,
          })
        }

        return Bodies.rectangle(x, y, width, height, {
          ...pick(elBody, ['frictionAir', 'friction', 'restitution', 'mass', 'isStatic']),
          label: elBody.id,
        })
      })

      // 更新初始值
      const data = bodyMap.get(elBody.id)
      if (data) {
        bodyMap.set(elBody.id, {
          ...data,
          initial: {
            offsetX: body.position.x,
            offsetY: body.position.y,
            rotate: body.angle,
          },
        })
      }

      return body
    }),
    /** 初始化牆壁 */
    (bodies) => {
      const thickness = 100
      const offset = 0

      const { width, height } = wrapperBounding

      const list = [
        Bodies.rectangle(
          width / 2,
          -thickness / 2 - offset,
          width * 2,
          thickness,
          { isStatic: true, label: 'top' },
        ),
        Bodies.rectangle(
          width + thickness / 2 + offset,
          height / 2,
          thickness,
          height * 2,
          { isStatic: true, label: 'right' },
        ),
        Bodies.rectangle(
          width / 2,
          height + thickness / 2 + offset,
          width * 2,
          thickness,
          { isStatic: true, label: 'bottom' },
        ),
        Bodies.rectangle(
          -thickness / 2 - offset,
          height / 2,
          thickness,
          height * 2,
          { isStatic: true, label: 'left' },
        ),
      ]

      bodies.push(...list)

      return bodies
    })
  Composite.add(engine.value.world, result)

  if (debug) {
    const { width, height } = wrapperBounding

    const render = Render.create({
      canvas: canvasRef.value,
      engine: engine.value,
      bounds: {
        min: { x: 0, y: 0 },
        max: { x: width, y: height },
      },
      options: {
        width,
        height,
        background: 'transparent',
        wireframeBackground: 'transparent',
        // showPerformance: true,
      },
    })

    Render.run(render)
  }
}

function start() {
  Runner.run(runner.value, engine.value)
  resumeUpdate()
}
function clear() {
  Composite.clear(engine.value.world, true)
  Engine.clear(engine.value)
  Runner.stop(runner.value)

  bodyInfoMap.clear()
}
function reset() {
  clear()

  engine.value = Engine.create({
    gravity: props.gravity,
  })
  runner.value = Runner.create()
  init()
  pauseUpdate()
}

// 持續更新狀態
const {
  pause: pauseUpdate,
  resume: resumeUpdate,
} = useIntervalFn(() => {
  const list = Composite.allBodies(engine.value.world)

  list.forEach((body) => {
    /** id 存在 label 中 */
    const id = body.label
    const info = bodyMap.get(id)

    if (!bodyMap.has(id) || !info) {
      return
    }

    const { initial } = info
    const value = {
      ...{
        offsetX: body.position.x - initial.offsetX,
        offsetY: body.position.y - initial.offsetY,
      },
      rotate: body.angle * 180 / Math.PI,
    }

    bodyInfoMap.set(id, value)
  })
}, 10)

onMounted(() => {
  init()

  if (props.immediate) {
    start()
  }
})

onBeforeUnmount(() => {
  clear()
})

/** 儲存已建立的 body */
const bodyMap = new Map<string, ElBody>()
/** body 物理模擬資料 */
const bodyInfoMap = new Map<string, {
  offsetX: number;
  offsetY: number;
  rotate: number;
}>()

function bindBody(item: ElBody) {
  bodyMap.set(item.id, item)
}
function unbindBody(id: string) {
  bodyMap.delete(id)
  bodyInfoMap.delete(id)
}
function updateBody(id: string, param: UpdateParam) {
  const bodyData = bodyMap.get(id)
  if (bodyData) {
    bodyMap.set(id, {
      ...bodyData,
      frictionAir: param.frictionAir,
      friction: param.friction,
      restitution: param.restitution,
      mass: param.mass,
      isStatic: param.isStatic,
    })
  }

  const target = Composite.allBodies(engine.value.world).find(
    (body) => body.label === id,
  )
  if (!target)
    return

  param.frictionAir && (target.frictionAir = param.frictionAir)
  param.friction && (target.friction = param.friction)
  param.restitution && (target.restitution = param.restitution)
  param.mass && Body.setMass(target, param.mass)
  param.isStatic && Body.setStatic(target, param.isStatic)
  param.velocity && Body.setVelocity(target, param.velocity)
  param.angularVelocity && Body.setAngularVelocity(target, param.angularVelocity)
}

provide(PROVIDE_KEY, {
  bindBody,
  unbindBody,
  updateBody,
  getInfo(id) {
    return bodyInfoMap.get(id)
  },
})

// #region Methods
defineExpose({
  /** 開始 */
  start,
  /** 重置所有元素,元素會回到初始位置 */
  reset,
})
// #endregion Methods
</script>

<style scoped lang="sass">
</style>

Methods

defineExpose({
  /** 開始 */
  start,
  /** 重置所有元素,元素會回到初始位置 */
  reset,
})

🧩 WrapperPhysicsBody

定義物體與性質,放在物理世界中,會受其物理規則影響。

Props

interface Props {
  /** 物體形狀,預設為 rectangle
   *
   * - rectangle:尺寸同 DOM 之長寬
   * - circle:取 DOM 長寬最大值為直徑
   */
  polygon?: 'rectangle' | 'circle';

  /** 空氣阻力。物體在空氣中受到的阻力 */
  frictionAir?: number;
  /** 摩擦力。物體本身的摩擦力,必須為 0 ~ 1,0 表示持續滑動,1 表示受力後會立即停止 */
  friction?: number;
  /** 回彈力。碰撞的回彈係數,0 表示不反彈,1 表示完全反彈 */
  restitution?: number;
  /** 物體質量 */
  mass?: number;
  /** 靜止。會變成像地面那樣完全靜止的存在 */
  isStatic?: boolean;
}

Slots

defineSlots<{
  default?: (props: {
    width: number;
    height: number;
    /** X 偏移量 */
    offsetX: number;
    /** Y 偏移量 */
    offsetY: number;
    /** 旋轉量 */
    rotate: number;
  }) => unknown;
}>()

v0.21.2