Skip to content

Party Popper util

Celebrate anytime, anywhere! ✧。٩(ˊᗜˋ*)و✧*。

Tech Keywords

NameDescription
Babylon.js3D engine
Particle SystemSpawns large numbers of small objects, commonly used to simulate smoke, fire, rain, and snow
Physics SimulationSimulates real-world physics such as gravity, collisions, and velocity
Dynamic TextureA texture that can be modified at runtime, commonly used to generate patterns or text on the fly

Usage Examples

Basic Usage

Call emit to launch particles.

🎉
🎉
🎉
🎉
🎉
🎇
0
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 example-wrap">
    <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>

Wide Area Emit

Not only can you customize the launch position, but the particle emission range is also adjustable.

Wide area emission feels even more like a party! ✧⁑。٩(ˊᗜˋ*)و✧⁕。

👈
👆
👇
🎆
👉
View example source code
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 px-4 py-2 text-2xl"
        @click="emit('left')"
      >
        👈
      </div>

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

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

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

      <div
        class="cursor-pointer select-none rounded 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>

Various Shapes

Not just squares -- there are various shapes to choose from.

🎉
View example source code
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 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>

Using Text

Not just shapes -- you can also use text. Even more reasons to celebrate!

For example, Codfish gained another 2 kg! (/≧▽≦)/

🎉

Codfish: "Don't celebrate something like that! ╭(°A ,°`)╮"

View example source code
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 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 { useI18n } from 'vue-i18n'
import UtilPartyPopper from '../util-party-popper.vue'

const { t } = useI18n()

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: t('肥魚'),
  },
  {
    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>

Custom Text

Toss whatever you want! ੭ ˙ᗜ˙ )੭

🎉
View example source code
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="t('輸入 emoji')"
      />

      <div
        class="cursor-pointer select-none rounded 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 { nanoid } from 'nanoid'
import { map, pipe } from 'remeda'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseInput from '../../base-input.vue'
import UtilPartyPopper from '../util-party-popper.vue'

const { t } = useI18n()

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

const text = ref('🎈✨🎉🍖🐟🎁💎')
/** 用於強制更新元件 */
const key = ref(nanoid())
watchDebounced(text, () => {
  key.value = nanoid()
}, { 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>

Mouse Carnival

Launch confetti along the mouse trail.

Mice! Mice everywhere! Σ(ˊДˋ;)

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <base-checkbox
      v-model="enable"
      class="example-ctrl"
      :label="t('啟用')"
    />

    <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 { useI18n } from 'vue-i18n'

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

const { t } = useI18n()
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>

Form Example

Boost your motivation for filling out forms. (´,,•ω•,,)

您覺得酷酷的元件趣味程度可得幾分? *
View example source code
vue
<template>
  <div class="relative w-full flex items-center justify-center gap-10 border rounded-xl p-10">
    <div class="flex flex-col gap-4">
      <base-input
        v-model="form.howToKnow"
        :label="t('從何得知酷酷的元件 *')"
        @blur="handleBlur('howToKnow')"
      />

      <div>
        <div class="text-sm font-bold">
          {{ t('您覺得酷酷的元件趣味程度可得幾分? *') }}
        </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="t('留給鱈魚的話或鼓勵 *')"
        @blur="handleBlur('text')"
      />

      <base-btn
        class="mt-6"
        :label="t('送出')"
        @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 text-black"
          @click="reset"
        >
          <span class="text-xl tracking-wide">
            {{ t('表單已送出') }}<br>{{ t('感謝您的回饋!(*´∀`)~♥') }}
          </span>

          <span class="cursor-pointer text-xs">
            {{ t('(點一下再來一次)') }}
          </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 { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import BaseInput from '../../base-input.vue'
import UtilPartyPopper from '../util-party-popper.vue'

const { t } = useI18n()

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(t('請完成表單 (´,,•ω•,,)'))
    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>

How It Works

Uses babylon.js for particle effects.

WebGPU is used by default for great performance! 。✧。٩(ˊᗜˋ*)و✧*。

For detailed explanation, see this article.

📚 babylon.js

Source Code

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.69.0