Skip to content

拉炮 util

隨時隨地都可以慶祝!✧。٩(ˊᗜˋ*)و✧*。

使用範例

基本用法

呼叫 emit 即可發射粒子。

🎉
🎉
🎉
🎉
🎉
🎇
0
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300">
    <util-party-popper
      ref="popperRef"
      v-slot="{ fps }"
      class="h-[60vh] flex"
      :max-concurrency="40"
    >
      <div class="h-full w-full flex flex-col items-center justify-center gap-6">
        <div class="flex gap-6">
          <div
            class="cursor-pointer select-none rounded p-4 text-4xl -rotate-90"
            @click="(event) => emit(event, 'lt')"
          >
            🎉
          </div>

          <div
            class="cursor-pointer select-none rounded p-4 text-4xl -rotate-45"
            @click="(event) => emit(event, 't')"
          >
            🎉
          </div>

          <div
            class="cursor-pointer select-none rounded p-4 text-4xl"
            @click="(event) => emit(event, 'rt')"
          >
            🎉
          </div>
        </div>

        <div class="flex gap-10">
          <div
            class="cursor-pointer select-none rounded p-4 text-4xl -rotate-[135deg]"
            @click="(event) => emit(event, 'l')"
          >
            🎉
          </div>

          <div
            class="rotate-45 cursor-pointer select-none rounded p-4 text-4xl"
            @click="(event) => emit(event, 'r')"
          >
            🎉
          </div>
        </div>

        <div
          class="cursor-pointer select-none rounded p-4 text-4xl"
          @click="emit"
        >
          🎇
        </div>
      </div>

      <div class="absolute left-0 top-0 p-4">
        {{ fps }}
      </div>
    </util-party-popper>
  </div>
</template>

<script setup lang="ts">
import { Scalar } from '@babylonjs/core'
import { useElementBounding } from '@vueuse/core'
import { conditional, constant, isDeepEqual } from 'remeda'
import { ref } from 'vue'
import UtilPartyPopper from '../util-party-popper.vue'

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const popperBounding = useElementBounding(popperRef)

type Direction = 'rt' | 'lt' | 't' | 'l' | 'r'

function emit(
  payload: MouseEvent,
  direction?: Direction,
) {
  const target = payload.target
  if (!(target instanceof Element))
    return

  const bounding = target.getBoundingClientRect()

  const position = {
    x: bounding.left + bounding.width / 2 - popperBounding.left.value,
    y: bounding.top + bounding.height / 2 - popperBounding.top.value,
  }

  const velocityRange = { min: 2, max: 8 }

  if (!direction) {
    popperRef.value?.emit(position)
    return
  }

  const params = conditional(
    direction,
    [
      isDeepEqual('rt'),
      constant(() => ({
        ...position,
        velocity: {
          x: -Scalar.RandomRange(velocityRange.min, velocityRange.max),
          y: Scalar.RandomRange(velocityRange.min, velocityRange.max),
        },
      })),
    ],
    [
      isDeepEqual('lt'),
      constant(() => ({
        ...position,
        velocity: {
          x: Scalar.RandomRange(velocityRange.min, velocityRange.max),
          y: Scalar.RandomRange(velocityRange.min, velocityRange.max),
        },
      })),
    ],
    [
      isDeepEqual('t'),
      constant(() => ({
        ...position,
        velocity: {
          x: Scalar.RandomRange(-2, 2),
          y: Scalar.RandomRange(velocityRange.min, velocityRange.max),
        },
      })),
    ],
    [
      isDeepEqual('l'),
      constant(() => ({
        ...position,
        velocity: {
          x: Scalar.RandomRange(velocityRange.min, velocityRange.max),
          y: Scalar.RandomRange(-2, 2),
        },
      })),
    ],
    [
      isDeepEqual('r'),
      constant(() => ({
        ...position,
        velocity: {
          x: -Scalar.RandomRange(velocityRange.min, velocityRange.max),
          y: Scalar.RandomRange(-2, 2),
        },
      })),
    ],
  )

  popperRef.value?.emit(params)
}
</script>

廣域發射

不只可自訂發射位置,粒子發射範圍也可以調整。

廣域發射更有派對的感覺!✧⁑。٩(ˊᗜˋ*)و✧⁕。

👈
👆
👇
🎆
👉
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="h-full w-full flex items-center justify-center gap-10 p-10">
      <div
        class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
        @click="emit('left')"
      >
        👈
      </div>

      <div class="flex flex-col gap-10">
        <div
          class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
          @click="emit('top')"
        >
          👆
        </div>

        <div
          class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
          @click="emit('bottom')"
        >
          👇
        </div>

        <div
          class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
          @click="emit('bottom-center')"
        >
          🎆
        </div>
      </div>

      <div
        class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
        @click="emit('right')"
      >
        👉
      </div>
    </div>

    <util-party-popper
      ref="popperRef"
      class="pointer-events-none left-0 top-0 z-50 h-full w-full !fixed"
      :quantity-of-per-emit="100"
      :max-concurrency="50"
    />
  </div>
</template>

<script setup lang="ts">
import { Scalar } from '@babylonjs/core'
import { useElementBounding } from '@vueuse/core'
import { conditional, constant, isDeepEqual } from 'remeda'

import { ref } from 'vue'

import UtilPartyPopper from '../util-party-popper.vue'

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const popperBounding = useElementBounding(popperRef)

function emit(position: 'top' | 'bottom' | 'left' | 'right' | 'bottom-center') {
  const offset = 50

  const params = conditional(
    position,
    [
      isDeepEqual('top'),
      constant(() => ({
        x: Scalar.RandomRange(0, popperBounding.width.value),
        y: -offset,
        velocity: {
          x: Scalar.RandomRange(1, -1),
          y: Scalar.RandomRange(0, -10),
        },
      })),
    ],
    [
      isDeepEqual('bottom'),
      constant(() => ({
        x: Scalar.RandomRange(0, popperBounding.width.value),
        y: popperBounding.height.value + offset,
        velocity: {
          x: Scalar.RandomRange(1, -1),
          y: Scalar.RandomRange(10, 15),
        },
      })),
    ],
    [
      isDeepEqual('bottom-center'),
      () => ({
        x: Scalar.RandomRange(0, popperBounding.width.value),
        y: popperBounding.height.value + offset,
        velocity: {
          x: 0,
          y: Scalar.RandomRange(8, 20),
        },
      }),
    ],
    [
      isDeepEqual('left'),
      constant(() => ({
        x: -offset,
        y: Scalar.RandomRange(0, popperBounding.height.value),
        velocity: {
          x: Scalar.RandomRange(-5, -10),
          y: Scalar.RandomRange(-1, 1),
        },
      })),
    ],
    [
      isDeepEqual('right'),
      constant(() => ({
        x: popperBounding.width.value + offset,
        y: Scalar.RandomRange(0, popperBounding.height.value),
        velocity: {
          x: Scalar.RandomRange(5, 10),
          y: Scalar.RandomRange(-1, 1),
        },
      })),
    ],
  )

  popperRef.value?.emit(params)
}
</script>

各種形狀

不只是方形,還有各種形狀可以選擇。

🎉
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="h-full w-full flex items-center justify-center gap-10 p-10">
      <div
        class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
        @click="emit()"
      >
        🎉
      </div>
    </div>

    <util-party-popper
      ref="popperRef"
      :confetti="confettiList"
      class="pointer-events-none left-0 top-0 z-50 h-full w-full !fixed"
      :quantity-of-per-emit="50"
      :max-concurrency="50"
    />
  </div>
</template>

<script setup lang="ts">
import type { ExtractArrayType } from '../../../types/main.type'
import { Scalar } from '@babylonjs/core'
import { useElementBounding } from '@vueuse/core'
import { ref } from 'vue'
import UtilPartyPopper from '../util-party-popper.vue'

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const popperBounding = useElementBounding(popperRef)

type Confetti = ExtractArrayType<
  InstanceType<typeof UtilPartyPopper>['confetti']
>
const confettiList: Confetti[] = [
  {
    shape: 'plane',
    width: 10,
    height: 10,
  },
  {
    shape: 'cylinder',
    diameter: 10,
    height: 1,
  },
  {
    shape: 'disc',
    radius: 10,
    tessellation: 3,
    arc: 1,
  },
  {
    shape: 'disc',
    radius: 8,
    tessellation: 8,
    arc: 1,
  },
  {
    shape: 'torus',
    diameter: 12,
    thickness: 2,
  },
]

function emit() {
  popperRef.value?.emit(() => ({
    x: 0,
    y: popperBounding.height.value,
    velocity: {
      x: -Scalar.RandomRange(5, 10),
      y: Scalar.RandomRange(10, 20),
    },
  }))

  popperRef.value?.emit(() => ({
    x: popperBounding.width.value,
    y: popperBounding.height.value,
    velocity: {
      x: Scalar.RandomRange(5, 10),
      y: Scalar.RandomRange(10, 20),
    },
  }))
}
</script>

使用文字

不只形狀,還可以使用文字,有更多理由可以慶祝了。

例如鱈魚又胖了 2 公斤!(/≧▽≦)/

🎉

鱈魚:「這種事別拿出來慶祝啊!╭(°A ,°`)╮」

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="h-full w-full flex items-center justify-center gap-10 p-10">
      <div
        class="cursor-pointer select-none rounded bg-white px-4 py-2 text-2xl"
        @click="emit()"
      >
        🎉
      </div>
    </div>

    <util-party-popper
      ref="popperRef"
      :confetti="confettiList"
      class="pointer-events-none left-0 top-0 z-50 h-full w-full !fixed"
      :quantity-of-per-emit="20"
      :max-concurrency="50"
      :max-angular-velocity="Math.PI / 100"
      :color="{ r: 1, g: 1, b: 1 }"
    />
  </div>
</template>

<script setup lang="ts">
import type { ExtractArrayType } from '../../../types/main.type'
import { Scalar } from '@babylonjs/core'
import { useElementBounding } from '@vueuse/core'
import { ref } from 'vue'
import UtilPartyPopper from '../util-party-popper.vue'

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const popperBounding = useElementBounding(popperRef)

type Confetti = ExtractArrayType<
  InstanceType<typeof UtilPartyPopper>['confetti']
>
const confettiList: Confetti[] = [
  {
    shape: 'text',
    width: 40,
    height: 40,
    char: '🐟',
  },
  {
    shape: 'text',
    width: 40,
    height: 20,
    char: '肥魚',
  },
  {
    shape: 'text',
    width: 80,
    height: 40,
    char: '2 KG!',
  },
  {
    shape: 'text',
    width: 30,
    height: 30,
    char: '✨',
  },
]

function emit() {
  popperRef.value?.emit(() => ({
    x: 0,
    y: Scalar.RandomRange(0, popperBounding.height.value),
    velocity: {
      x: -Scalar.RandomRange(5, 10),
      y: Scalar.RandomRange(-5, 5),
    },
  }))

  popperRef.value?.emit(() => ({
    x: popperBounding.width.value,
    y: Scalar.RandomRange(0, popperBounding.height.value),
    velocity: {
      x: Scalar.RandomRange(5, 10),
      y: Scalar.RandomRange(-5, 5),
    },
  }))
}
</script>

自定義文字

想撒甚麼就撒甚麼!੭ ˙ᗜ˙ )੭

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="h-full w-full flex items-center justify-center gap-10 p-6">
      <base-input
        v-model="text"
        label="輸入 emoji"
      />

      <div
        class="cursor-pointer select-none rounded bg-white px-4 py-2 text-4xl"
        @click="emit()"
      >
        🎉
      </div>
    </div>

    <util-party-popper
      ref="popperRef"
      :key
      :confetti="confettiList"
      class="pointer-events-none left-0 top-0 z-50 h-full w-full !fixed"
      :quantity-of-per-emit="50"
      :max-concurrency="50"
      :max-angular-velocity="Math.PI / 100"
      :color="{ r: 1, g: 1, b: 1 }"
    />
  </div>
</template>

<script setup lang="ts">
import type { ExtractArrayType } from '../../../types/main.type'
import { Scalar } from '@babylonjs/core'
import { useElementBounding, watchDebounced } from '@vueuse/core'
import { map, pipe } from 'remeda'
import { computed, ref } from 'vue'
import BaseInput from '../../base-input.vue'
import UtilPartyPopper from '../util-party-popper.vue'

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const popperBounding = useElementBounding(popperRef)

const text = ref('🎈✨🎉🍖🐟🎁💎')
/** 用於強制更新元件 */
const key = ref(crypto.randomUUID())
watchDebounced(text, () => {
  key.value = crypto.randomUUID()
}, { debounce: 1000 })

type Confetti = ExtractArrayType<
  InstanceType<typeof UtilPartyPopper>['confetti']
>
const confettiList = computed(() => pipe(
  text.value.split(/.*?/u),
  map((char) => ({
    shape: 'text',
    width: 30,
    height: 30,
    char,
  } satisfies Confetti)),
))

function emit() {
  popperRef.value?.emit(() => ({
    x: 0,
    y: Scalar.RandomRange(0, popperBounding.height.value),
    velocity: {
      x: -Scalar.RandomRange(5, 10),
      y: Scalar.RandomRange(-5, 5),
    },
  }))

  popperRef.value?.emit(() => ({
    x: popperBounding.width.value,
    y: Scalar.RandomRange(0, popperBounding.height.value),
    velocity: {
      x: Scalar.RandomRange(5, 10),
      y: Scalar.RandomRange(-5, 5),
    },
  }))
}
</script>

勞贖嘉年華

沿著滑鼠軌跡一路發射彩帶吧。

勞贖!滿滿的勞贖!Σ(ˊДˋ;)

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

    <util-party-popper
      ref="popperRef"
      class="pointer-events-none left-0 top-0 z-50 h-full w-full !fixed"
      :confetti="confettiList"
      :quantity-of-per-emit="2"
      :max-concurrency="500"
      :max-angular-velocity="Math.PI / 100"
      :color="{ r: 1, g: 1, b: 1 }"
    />
  </div>
</template>

<script setup lang="ts">
import type { ExtractArrayType } from '../../../types/main.type'
import { Scalar } from '@babylonjs/core'
import { throttleFilter, useMouseInElement, useMousePressed, whenever } from '@vueuse/core'
import { ref, watch } from 'vue'

import BaseCheckbox from '../../base-checkbox.vue'
import UtilPartyPopper from '../util-party-popper.vue'

const enable = ref(false)

type Confetti = ExtractArrayType<
  InstanceType<typeof UtilPartyPopper>['confetti']
>
const confettiList: Confetti[] = [
  {
    shape: 'text',
    width: 30,
    height: 30,
    char: '🐁',
  },
  {
    shape: 'text',
    width: 40,
    height: 40,
    char: '🐀',
  },
]

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const {
  elementX,
  elementY,
} = useMouseInElement(popperRef, {
  eventFilter: throttleFilter(10),
  scroll: false,
})
const { pressed } = useMousePressed()

whenever(pressed, () => {
  if (!enable.value)
    return

  popperRef.value?.emit(() => ({
    x: elementX.value,
    y: elementY.value,
    velocity: {
      x: Scalar.RandomRange(-10, 10),
      y: Scalar.RandomRange(-10, 10),
    },
  }))
})

watch(() => [elementX, elementY], () => {
  if (!enable.value)
    return

  popperRef.value?.emit(() => ({
    x: elementX.value,
    y: elementY.value,
    velocity: {
      x: Scalar.RandomRange(-2, 2),
      y: Scalar.RandomRange(-2, 2),
    },
  }))
}, { deep: true })
</script>

表單範例

提升填寫表單的動力。(´,,•ω•,,)

您覺得酷酷的元件趣味程度可得幾分? *
查看範例原始碼
vue
<template>
  <div class="relative w-full flex items-center justify-center gap-10 border p-10">
    <div class="flex flex-col gap-4">
      <base-input
        v-model="form.howToKnow"
        label="從何得知酷酷的元件 *"
        @blur="handleBlur('howToKnow')"
      />

      <div>
        <div class="text-sm font-bold">
          您覺得酷酷的元件趣味程度可得幾分? *
        </div>

        <div class="flex justify-around gap-2 py-2">
          <label
            v-for="i in 6"
            :key="i"
            @mouseup="emitFromClick"
          >
            <input
              v-model.number="form.score"
              type="radio"
              :value="i - 1"
            >
            {{ i - 1 }}
          </label>
        </div>
      </div>

      <base-input
        v-model="form.text"
        label="留給鱈魚的話或鼓勵 *"
        @blur="handleBlur('text')"
      />

      <base-btn
        class="mt-6"
        label="送出"
        @click="submit"
      />

      <transition name="opacity">
        <div
          v-if="isSubmitted"
          class="absolute inset-0 z-[40] flex flex-col items-center justify-center gap-6 rounded-xl bg-[#c7f6ff] bg-opacity-90"
          @click="reset"
        >
          <span class="text-xl tracking-wide">
            表單已送出<br>感謝您的回饋!(*´∀`)~♥
          </span>

          <span class="cursor-pointer text-xs">
            (點一下再來一次)
          </span>
        </div>
      </transition>

      <util-party-popper
        ref="popperRef"
        class="pointer-events-none left-0 top-0 z-50 h-full w-full !absolute"
        :quantity-of-per-emit
        :max-concurrency="100"
        :confetti="confettiList"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ExtractArrayType } from '../../../types'
import { Scalar } from '@babylonjs/core'
import { promiseTimeout, useElementBounding } from '@vueuse/core'
import { conditional, constant, isDeepEqual, pipe } from 'remeda'
import { computed, nextTick, reactive, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import BaseInput from '../../base-input.vue'
import UtilPartyPopper from '../util-party-popper.vue'

type Confetti = ExtractArrayType<
  InstanceType<typeof UtilPartyPopper>['confetti']
>

const form = ref({
  howToKnow: '',
  score: undefined,
  text: '',
})
const isSubmitted = ref(false)

const popperRef = ref<InstanceType<typeof UtilPartyPopper>>()
const popperBounding = reactive(useElementBounding(popperRef))

const confettiList: Confetti[] = [
  {
    shape: 'plane',
    width: 10,
    height: 10,
  },
  {
    shape: 'cylinder',
    diameter: 10,
    height: 1,
  },
  {
    shape: 'disc',
    radius: 10,
    tessellation: 3,
    arc: 1,
  },
  {
    shape: 'disc',
    radius: 8,
    tessellation: 8,
    arc: 1,
  },
  {
    shape: 'torus',
    diameter: 12,
    thickness: 2,
  },
]
const quantityOfPerEmit = ref(50)

async function emitFromEdge(position: 'top' | 'bottom' | 'left' | 'right') {
  quantityOfPerEmit.value = 50

  const offset = 50

  const params = conditional(
    position,
    [
      isDeepEqual('top'),
      constant(() => ({
        x: Scalar.RandomRange(0, popperBounding.width),
        y: -offset,
        velocity: {
          x: Scalar.RandomRange(1, -1),
          y: Scalar.RandomRange(0, -10),
        },
      })),
    ],
    [
      isDeepEqual('bottom'),
      constant(() => ({
        x: Scalar.RandomRange(0, popperBounding.width),
        y: popperBounding.height + offset,
        velocity: {
          x: Scalar.RandomRange(1, -1),
          y: Scalar.RandomRange(10, 15),
        },
      })),
    ],
    [
      isDeepEqual('left'),
      constant(() => ({
        x: -offset,
        y: Scalar.RandomRange(0, popperBounding.height),
        velocity: {
          x: Scalar.RandomRange(-5, -10),
          y: Scalar.RandomRange(-1, 1),
        },
      })),
    ],
    [
      isDeepEqual('right'),
      constant(() => ({
        x: popperBounding.width + offset,
        y: Scalar.RandomRange(0, popperBounding.height),
        velocity: {
          x: Scalar.RandomRange(5, 10),
          y: Scalar.RandomRange(-1, 1),
        },
      })),
    ],
  )

  await nextTick()
  popperRef.value?.emit(params)
}
async function emitLikeFirework() {
  quantityOfPerEmit.value = 50
  const offset = 50

  const params = pipe(undefined, () => ({
    x: Scalar.RandomRange(0, popperBounding.width),
    y: popperBounding.height + offset,
    velocity: {
      x: 0,
      y: Scalar.RandomRange(10, 20),
    },
  }))

  await nextTick()
  popperRef.value?.emit(params)
}
async function emitFromClick(event: MouseEvent) {
  // FIX: 等待 form.score 更新,未來在想更好的方法
  await promiseTimeout(100)

  const score = form.value.score ?? 0
  quantityOfPerEmit.value = score * 20

  const target = event.target
  if (!(target instanceof Element))
    return

  const bounding = target.getBoundingClientRect()

  const position = {
    x: bounding.left + bounding.width / 2 - popperBounding.left,
    y: bounding.top + bounding.height / 2 - popperBounding.top,
  }

  const velocity = score * 2
  const params = pipe(undefined, () => () => ({
    ...position,
    velocity: {
      x: Scalar.RandomRange(-velocity, velocity),
      y: Scalar.RandomRange(-velocity, velocity),
    },
  }))

  await nextTick()

  popperRef.value?.emit(params)
}

const canSubmit = computed(() =>
  form.value.howToKnow && form.value.score !== undefined && form.value.text,
)

const fieldEdgeMap: Record<
  keyof typeof form.value,
  Array<'top' | 'bottom' | 'left' | 'right'>
> = {
  howToKnow: ['top'],
  score: ['left'],
  text: ['left', 'right'],
}
function handleBlur(field: keyof typeof form.value) {
  if (form.value[field]) {
    fieldEdgeMap[field].forEach(emitFromEdge)
  }
}

function submit() {
  if (!canSubmit.value) {
    // eslint-disable-next-line no-alert
    alert('請完成表單 (´,,•ω•,,)')
    return
  }

  for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      emitLikeFirework()
    }, i * 100)
  }

  setTimeout(() => {
    isSubmitted.value = true
  }, 1000)
}
function reset() {
  isSubmitted.value = false

  form.value = {
    howToKnow: '',
    score: undefined,
    text: '',
  }
}
</script>

<style lang="sass" scoped>
.opacity-enter-active, .opacity-leave-active
  transition-duration: 0.4s
.opacity-enter-from, .opacity-leave-to
  opacity: 0 !important
</style>

原理

利用 babylon.js 製作粒子效果。

📚 babylon.js

預設使用 WebGPU,性能好棒棒!。✧。٩(ˊᗜˋ*)و✧*。

原始碼

API

Props

type Confetti = {
  shape: 'plane';
  width: number;
  height: number;
} | {
  shape: 'cylinder';
  height: number;
  diameter: number;
} | {
  shape: 'disc';
  radius: number;
  /** the number of disc/polygon sides */
  tessellation: number;
  /** ratio of the circumference between 0 and 1 */
  arc: number;
} | {
  shape: 'torus';
  diameter: number;
  /** number of segments along the circle */
  thickness: number;
} | {
  shape: 'polyhedron';
  /** polyhedron type in the range。0-14
   *
   * https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/polyhedra/polyhedra_by_numbers
   */
  type: number;
  size: number;
  sizeX?: number;
  sizeY?: number;
  sizeZ?: number;
} | {
  shape: 'text';
  width: number;
  height: number;
  char: string;
}

interface Props {
  /** 紙屑參數。初始化後即固定,不支援動態變更 */
  confetti?: Confetti | Confetti[];

  /** 每次發射數量
   *
   * @default 20
   */
  quantityOfPerEmit?: number;

  /** 最大同時觸發次數。
   *
   * @default 10
   */
  maxConcurrency?: number;

  /** 最大角速度
   *
   * @default 1.5
   */
  maxAngularVelocity?: number;

  /** 重力
   *
   * @default -0.01
   */
  gravity?: number;

  /** 空氣阻力。速度衰減比率
   *
   * @default 0.985
   */
  airResistance?: number;

  /** 預設發射速度 */
  velocity?: Vector | ((index: number) => Vector);

  /** 粒子顏色 */
  color?: Color | ((index: number) => Color);
}

Methods

defineExpose({
  /** 發射粒子,如果提供 function,則可以分別設定粒子參數 */
  emit,
  /** 目前畫面 FPS */
  fps,
})

Slots

defineSlots<{
  default?: (data: { fps: number }) => unknown;
}>()

v0.28.0