拉炮
隨時隨地都可以慶祝!✧。٩(ˊᗜˋ*)و✧*。
使用範例
基本用法
呼叫 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 param = 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(param)
}
</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 param = 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(param)
}
</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">
<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>
原理
利用 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;
}>()