threlte-vfx 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/VFXEmitter.svelte +171 -0
- package/dist/VFXParticles.svelte +773 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.js +113 -1
- package/package.json +21 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { T, useThrelte, useTask } from '@threlte/core'
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import { Vector3, Quaternion, Group } from 'three/webgpu'
|
|
5
|
+
import {
|
|
6
|
+
EmitterController,
|
|
7
|
+
isWebGPUBackend,
|
|
8
|
+
coreStore,
|
|
9
|
+
type EmitterControllerOptions,
|
|
10
|
+
} from 'core-vfx'
|
|
11
|
+
|
|
12
|
+
// Reusable temp objects for transforms (avoid allocations in render loop)
|
|
13
|
+
const worldPos = new Vector3()
|
|
14
|
+
const worldQuat = new Quaternion()
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
name = undefined,
|
|
18
|
+
particlesRef = undefined,
|
|
19
|
+
position = [0, 0, 0],
|
|
20
|
+
emitCount = 10,
|
|
21
|
+
delay = 0,
|
|
22
|
+
autoStart = true,
|
|
23
|
+
loop = true,
|
|
24
|
+
localDirection = false,
|
|
25
|
+
direction = undefined,
|
|
26
|
+
overrides = null,
|
|
27
|
+
onEmit = undefined,
|
|
28
|
+
children,
|
|
29
|
+
}: {
|
|
30
|
+
name?: string
|
|
31
|
+
particlesRef?: any
|
|
32
|
+
position?: [number, number, number]
|
|
33
|
+
emitCount?: number
|
|
34
|
+
delay?: number
|
|
35
|
+
autoStart?: boolean
|
|
36
|
+
loop?: boolean
|
|
37
|
+
localDirection?: boolean
|
|
38
|
+
direction?: EmitterControllerOptions['direction']
|
|
39
|
+
overrides?: Record<string, unknown> | null
|
|
40
|
+
onEmit?: EmitterControllerOptions['onEmit']
|
|
41
|
+
children?: import('svelte').Snippet
|
|
42
|
+
} = $props()
|
|
43
|
+
|
|
44
|
+
const { renderer } = useThrelte()
|
|
45
|
+
|
|
46
|
+
let groupRef: Group | null = $state(null)
|
|
47
|
+
let isWebGPU = $state(false)
|
|
48
|
+
|
|
49
|
+
const controller = new EmitterController({
|
|
50
|
+
emitCount,
|
|
51
|
+
delay,
|
|
52
|
+
autoStart,
|
|
53
|
+
loop,
|
|
54
|
+
localDirection,
|
|
55
|
+
direction,
|
|
56
|
+
overrides,
|
|
57
|
+
onEmit,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
function getParticleSystem() {
|
|
61
|
+
if (particlesRef) {
|
|
62
|
+
return particlesRef.value || particlesRef
|
|
63
|
+
}
|
|
64
|
+
return name ? coreStore.getState().getParticles(name) : undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Watch option changes
|
|
68
|
+
$effect(() => {
|
|
69
|
+
const _deps = [
|
|
70
|
+
emitCount,
|
|
71
|
+
delay,
|
|
72
|
+
autoStart,
|
|
73
|
+
loop,
|
|
74
|
+
localDirection,
|
|
75
|
+
direction,
|
|
76
|
+
overrides,
|
|
77
|
+
onEmit,
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
controller.updateOptions({
|
|
81
|
+
emitCount,
|
|
82
|
+
delay,
|
|
83
|
+
autoStart,
|
|
84
|
+
loop,
|
|
85
|
+
localDirection,
|
|
86
|
+
direction,
|
|
87
|
+
overrides,
|
|
88
|
+
onEmit,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
function checkWebGPU() {
|
|
93
|
+
if (renderer && isWebGPUBackend(renderer)) {
|
|
94
|
+
isWebGPU = true
|
|
95
|
+
const system = getParticleSystem()
|
|
96
|
+
if (system) controller.setSystem(system)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onMount(() => {
|
|
101
|
+
checkWebGPU()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Frame loop
|
|
105
|
+
useTask((delta) => {
|
|
106
|
+
if (!isWebGPU) return
|
|
107
|
+
|
|
108
|
+
if (!controller.getSystem()) {
|
|
109
|
+
const system = getParticleSystem()
|
|
110
|
+
if (system) controller.setSystem(system)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!groupRef) return
|
|
114
|
+
|
|
115
|
+
groupRef.getWorldPosition(worldPos)
|
|
116
|
+
groupRef.getWorldQuaternion(worldQuat)
|
|
117
|
+
controller.update(delta, worldPos, worldQuat)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Exposed API
|
|
121
|
+
export function emit(emitOverrides: Record<string, unknown> | null = null) {
|
|
122
|
+
if (!groupRef) return false
|
|
123
|
+
|
|
124
|
+
if (!controller.getSystem()) {
|
|
125
|
+
const system = getParticleSystem()
|
|
126
|
+
if (system) controller.setSystem(system)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!controller.getSystem()) {
|
|
130
|
+
if (name) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`VFXEmitter: No particle system found for name "${name}"`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
groupRef.getWorldPosition(worldPos)
|
|
139
|
+
groupRef.getWorldQuaternion(worldQuat)
|
|
140
|
+
return controller.emitAtPosition(worldPos, worldQuat, emitOverrides)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function burst(count: number) {
|
|
144
|
+
if (!groupRef) return false
|
|
145
|
+
|
|
146
|
+
if (!controller.getSystem()) {
|
|
147
|
+
const system = getParticleSystem()
|
|
148
|
+
if (system) controller.setSystem(system)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!controller.getSystem()) return false
|
|
152
|
+
|
|
153
|
+
groupRef.getWorldPosition(worldPos)
|
|
154
|
+
groupRef.getWorldQuaternion(worldQuat)
|
|
155
|
+
return controller.burst(count, worldPos, worldQuat)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function start() {
|
|
159
|
+
controller.start()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function stop() {
|
|
163
|
+
controller.stop()
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<T.Group bind:ref={groupRef} position={position}>
|
|
168
|
+
{#if children}
|
|
169
|
+
{@render children()}
|
|
170
|
+
{/if}
|
|
171
|
+
</T.Group>
|
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { T, useThrelte, useTask } from '@threlte/core'
|
|
3
|
+
import { onMount, onDestroy, untrack } from 'svelte'
|
|
4
|
+
import * as THREE from 'three/webgpu'
|
|
5
|
+
import { coreStore } from 'core-vfx'
|
|
6
|
+
import {
|
|
7
|
+
Appearance,
|
|
8
|
+
Blending,
|
|
9
|
+
EmitterShape,
|
|
10
|
+
Lighting,
|
|
11
|
+
VFXParticleSystem,
|
|
12
|
+
isWebGPUBackend,
|
|
13
|
+
isNonDefaultRotation,
|
|
14
|
+
normalizeProps,
|
|
15
|
+
updateUniforms,
|
|
16
|
+
updateUniformsPartial,
|
|
17
|
+
resolveFeatures,
|
|
18
|
+
type VFXParticleSystemOptions,
|
|
19
|
+
type TurbulenceConfig,
|
|
20
|
+
type AttractorConfig,
|
|
21
|
+
type CollisionConfig,
|
|
22
|
+
type FrictionConfig,
|
|
23
|
+
type FlipbookConfig,
|
|
24
|
+
type StretchConfig,
|
|
25
|
+
type Rotation3DInput,
|
|
26
|
+
} from 'core-vfx'
|
|
27
|
+
|
|
28
|
+
// Props
|
|
29
|
+
let {
|
|
30
|
+
name = undefined,
|
|
31
|
+
debug = false,
|
|
32
|
+
maxParticles = 10000,
|
|
33
|
+
size = [0.1, 0.3],
|
|
34
|
+
colorStart = ['#ffffff'],
|
|
35
|
+
colorEnd = null,
|
|
36
|
+
fadeSize = [1, 0],
|
|
37
|
+
fadeSizeCurve = null,
|
|
38
|
+
fadeOpacity = [1, 0],
|
|
39
|
+
fadeOpacityCurve = null,
|
|
40
|
+
velocityCurve = null,
|
|
41
|
+
gravity = [0, 0, 0],
|
|
42
|
+
lifetime = [1, 2],
|
|
43
|
+
direction = [[-1, 1], [0, 1], [-1, 1]],
|
|
44
|
+
startPosition = [[0, 0], [0, 0], [0, 0]],
|
|
45
|
+
speed = [0.1, 0.1],
|
|
46
|
+
friction = { intensity: 0, easing: 'linear' },
|
|
47
|
+
appearance = Appearance.GRADIENT,
|
|
48
|
+
alphaMap = null,
|
|
49
|
+
flipbook = null,
|
|
50
|
+
rotation = [0, 0],
|
|
51
|
+
rotationSpeed = [0, 0],
|
|
52
|
+
rotationSpeedCurve = null,
|
|
53
|
+
geometry = null,
|
|
54
|
+
orientToDirection = false,
|
|
55
|
+
orientAxis = 'z',
|
|
56
|
+
stretchBySpeed = null,
|
|
57
|
+
lighting = Lighting.STANDARD,
|
|
58
|
+
shadow = false,
|
|
59
|
+
blending = Blending.NORMAL,
|
|
60
|
+
intensity = 1,
|
|
61
|
+
position = [0, 0, 0],
|
|
62
|
+
autoStart = true,
|
|
63
|
+
delay = 0,
|
|
64
|
+
backdropNode = null,
|
|
65
|
+
opacityNode = null,
|
|
66
|
+
colorNode = null,
|
|
67
|
+
alphaTestNode = null,
|
|
68
|
+
castShadowNode = null,
|
|
69
|
+
emitCount = 1,
|
|
70
|
+
emitterShape = EmitterShape.BOX,
|
|
71
|
+
emitterRadius = [0, 1],
|
|
72
|
+
emitterAngle = Math.PI / 4,
|
|
73
|
+
emitterHeight = [0, 1],
|
|
74
|
+
emitterSurfaceOnly = false,
|
|
75
|
+
emitterDirection = [0, 1, 0],
|
|
76
|
+
turbulence = null,
|
|
77
|
+
attractors = null,
|
|
78
|
+
attractToCenter = false,
|
|
79
|
+
startPositionAsDirection = false,
|
|
80
|
+
softParticles = false,
|
|
81
|
+
softDistance = 0.5,
|
|
82
|
+
collision = null,
|
|
83
|
+
curveTexturePath = null,
|
|
84
|
+
depthTest = true,
|
|
85
|
+
renderOrder = 0,
|
|
86
|
+
}: {
|
|
87
|
+
name?: string
|
|
88
|
+
debug?: boolean
|
|
89
|
+
maxParticles?: number
|
|
90
|
+
size?: [number, number] | number
|
|
91
|
+
colorStart?: string[]
|
|
92
|
+
colorEnd?: string[] | null
|
|
93
|
+
fadeSize?: [number, number]
|
|
94
|
+
fadeSizeCurve?: unknown[] | null
|
|
95
|
+
fadeOpacity?: [number, number]
|
|
96
|
+
fadeOpacityCurve?: unknown[] | null
|
|
97
|
+
velocityCurve?: unknown[] | null
|
|
98
|
+
gravity?: [number, number, number]
|
|
99
|
+
lifetime?: [number, number]
|
|
100
|
+
direction?: VFXParticleSystemOptions['direction']
|
|
101
|
+
startPosition?: VFXParticleSystemOptions['startPosition']
|
|
102
|
+
speed?: [number, number] | number
|
|
103
|
+
friction?: FrictionConfig
|
|
104
|
+
appearance?: string | number
|
|
105
|
+
alphaMap?: THREE.Texture | null
|
|
106
|
+
flipbook?: FlipbookConfig | null
|
|
107
|
+
rotation?: Rotation3DInput
|
|
108
|
+
rotationSpeed?: Rotation3DInput
|
|
109
|
+
rotationSpeedCurve?: unknown[] | null
|
|
110
|
+
geometry?: THREE.BufferGeometry | null
|
|
111
|
+
orientToDirection?: boolean
|
|
112
|
+
orientAxis?: string
|
|
113
|
+
stretchBySpeed?: StretchConfig | null
|
|
114
|
+
lighting?: string | number
|
|
115
|
+
shadow?: boolean
|
|
116
|
+
blending?: string | number
|
|
117
|
+
intensity?: number
|
|
118
|
+
position?: [number, number, number]
|
|
119
|
+
autoStart?: boolean
|
|
120
|
+
delay?: number
|
|
121
|
+
backdropNode?: unknown
|
|
122
|
+
opacityNode?: unknown
|
|
123
|
+
colorNode?: unknown
|
|
124
|
+
alphaTestNode?: unknown
|
|
125
|
+
castShadowNode?: unknown
|
|
126
|
+
emitCount?: number
|
|
127
|
+
emitterShape?: string | number
|
|
128
|
+
emitterRadius?: [number, number]
|
|
129
|
+
emitterAngle?: number
|
|
130
|
+
emitterHeight?: [number, number]
|
|
131
|
+
emitterSurfaceOnly?: boolean
|
|
132
|
+
emitterDirection?: [number, number, number]
|
|
133
|
+
turbulence?: TurbulenceConfig | null
|
|
134
|
+
attractors?: AttractorConfig[] | null
|
|
135
|
+
attractToCenter?: boolean
|
|
136
|
+
startPositionAsDirection?: boolean
|
|
137
|
+
softParticles?: boolean
|
|
138
|
+
softDistance?: number
|
|
139
|
+
collision?: CollisionConfig | null
|
|
140
|
+
curveTexturePath?: string | null
|
|
141
|
+
depthTest?: boolean
|
|
142
|
+
renderOrder?: number
|
|
143
|
+
} = $props()
|
|
144
|
+
|
|
145
|
+
const { renderer } = useThrelte()
|
|
146
|
+
|
|
147
|
+
let warnedWebGL = false
|
|
148
|
+
let mounted = false
|
|
149
|
+
|
|
150
|
+
// Internal state — NOT tracked by effects (plain variables)
|
|
151
|
+
let _system: VFXParticleSystem | null = null
|
|
152
|
+
let _renderObject: THREE.Object3D | null = null
|
|
153
|
+
let _isWebGPU = false
|
|
154
|
+
let _emitting = autoStart
|
|
155
|
+
|
|
156
|
+
// Reactive state for the template only
|
|
157
|
+
let renderObjectForTemplate: THREE.Object3D | null = $state(null)
|
|
158
|
+
let isWebGPUForTemplate = $state(false)
|
|
159
|
+
|
|
160
|
+
let debugValues: Record<string, unknown> | null = null
|
|
161
|
+
|
|
162
|
+
// Track structural props for recreation
|
|
163
|
+
let activeMaxParticles = $state(maxParticles)
|
|
164
|
+
let activeLighting: string | number = $state(lighting)
|
|
165
|
+
let activeAppearance: string | number = $state(appearance)
|
|
166
|
+
let activeOrientToDirection = $state(orientToDirection)
|
|
167
|
+
let activeGeometry: THREE.BufferGeometry | null = $state(geometry)
|
|
168
|
+
let activeShadow = $state(shadow)
|
|
169
|
+
let activeFadeSizeCurve: unknown[] | null = $state(fadeSizeCurve)
|
|
170
|
+
let activeFadeOpacityCurve: unknown[] | null = $state(fadeOpacityCurve)
|
|
171
|
+
let activeVelocityCurve: unknown[] | null = $state(velocityCurve)
|
|
172
|
+
let activeRotationSpeedCurve: unknown[] | null = $state(rotationSpeedCurve)
|
|
173
|
+
let activeTurbulence = $state(
|
|
174
|
+
turbulence !== null && (turbulence?.intensity ?? 0) > 0
|
|
175
|
+
)
|
|
176
|
+
let activeAttractors = $state(
|
|
177
|
+
attractors !== null && (attractors?.length ?? 0) > 0
|
|
178
|
+
)
|
|
179
|
+
let activeCollision = $state(collision !== null)
|
|
180
|
+
let activeNeedsPerParticleColor = $state(
|
|
181
|
+
colorStart.length > 1 || colorEnd !== null
|
|
182
|
+
)
|
|
183
|
+
let activeNeedsRotation = $state(
|
|
184
|
+
isNonDefaultRotation(rotation) || isNonDefaultRotation(rotationSpeed)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// Debug panel refs
|
|
188
|
+
let prevGeometryType: unknown = null
|
|
189
|
+
let prevGeometryArgs: unknown = null
|
|
190
|
+
|
|
191
|
+
function buildOptions(): VFXParticleSystemOptions {
|
|
192
|
+
const dbg = debug ? debugValues : null
|
|
193
|
+
return {
|
|
194
|
+
maxParticles: untrack(() => activeMaxParticles) as number,
|
|
195
|
+
size: (dbg?.size ?? size) as VFXParticleSystemOptions['size'],
|
|
196
|
+
colorStart: (dbg?.colorStart ?? colorStart) as string[],
|
|
197
|
+
colorEnd:
|
|
198
|
+
dbg?.colorEnd !== undefined
|
|
199
|
+
? (dbg.colorEnd as string[] | null)
|
|
200
|
+
: colorEnd,
|
|
201
|
+
fadeSize: (dbg?.fadeSize ?? fadeSize) as [number, number],
|
|
202
|
+
fadeSizeCurve: untrack(() => activeFadeSizeCurve) as VFXParticleSystemOptions['fadeSizeCurve'],
|
|
203
|
+
fadeOpacity: (dbg?.fadeOpacity ?? fadeOpacity) as [number, number],
|
|
204
|
+
fadeOpacityCurve: untrack(() => activeFadeOpacityCurve) as VFXParticleSystemOptions['fadeOpacityCurve'],
|
|
205
|
+
velocityCurve: untrack(() => activeVelocityCurve) as VFXParticleSystemOptions['velocityCurve'],
|
|
206
|
+
gravity: (dbg?.gravity ?? gravity) as [number, number, number],
|
|
207
|
+
lifetime: (dbg?.lifetime ?? lifetime) as [number, number],
|
|
208
|
+
direction: (dbg?.direction ?? direction) as VFXParticleSystemOptions['direction'],
|
|
209
|
+
startPosition: (dbg?.startPosition ?? startPosition) as VFXParticleSystemOptions['startPosition'],
|
|
210
|
+
speed: (dbg?.speed ?? speed) as VFXParticleSystemOptions['speed'],
|
|
211
|
+
friction: (dbg?.friction ?? friction) as FrictionConfig,
|
|
212
|
+
appearance: untrack(() => activeAppearance) as VFXParticleSystemOptions['appearance'],
|
|
213
|
+
alphaMap,
|
|
214
|
+
flipbook,
|
|
215
|
+
rotation: (dbg?.rotation ?? rotation) as Rotation3DInput,
|
|
216
|
+
rotationSpeed: (dbg?.rotationSpeed ?? rotationSpeed) as Rotation3DInput,
|
|
217
|
+
rotationSpeedCurve: untrack(() => activeRotationSpeedCurve) as VFXParticleSystemOptions['rotationSpeedCurve'],
|
|
218
|
+
geometry: untrack(() => activeGeometry),
|
|
219
|
+
orientToDirection: untrack(() => activeOrientToDirection) as boolean,
|
|
220
|
+
orientAxis: (dbg?.orientAxis ?? orientAxis) as string,
|
|
221
|
+
stretchBySpeed: (dbg?.stretchBySpeed ?? stretchBySpeed) as StretchConfig | null,
|
|
222
|
+
lighting: untrack(() => activeLighting) as VFXParticleSystemOptions['lighting'],
|
|
223
|
+
shadow: untrack(() => activeShadow) as boolean,
|
|
224
|
+
blending: (dbg?.blending ?? blending) as VFXParticleSystemOptions['blending'],
|
|
225
|
+
intensity: (dbg?.intensity ?? intensity) as number,
|
|
226
|
+
position: (dbg?.position ?? position) as [number, number, number],
|
|
227
|
+
autoStart: (dbg?.autoStart ?? autoStart) as boolean,
|
|
228
|
+
delay: (dbg?.delay ?? delay) as number,
|
|
229
|
+
emitCount: (dbg?.emitCount ?? emitCount) as number,
|
|
230
|
+
emitterShape: (dbg?.emitterShape ?? emitterShape) as VFXParticleSystemOptions['emitterShape'],
|
|
231
|
+
emitterRadius: (dbg?.emitterRadius ?? emitterRadius) as [number, number],
|
|
232
|
+
emitterAngle: (dbg?.emitterAngle ?? emitterAngle) as number,
|
|
233
|
+
emitterHeight: (dbg?.emitterHeight ?? emitterHeight) as [number, number],
|
|
234
|
+
emitterSurfaceOnly: (dbg?.emitterSurfaceOnly ?? emitterSurfaceOnly) as boolean,
|
|
235
|
+
emitterDirection: (dbg?.emitterDirection ?? emitterDirection) as [number, number, number],
|
|
236
|
+
turbulence: (dbg?.turbulence ?? turbulence) as TurbulenceConfig | null,
|
|
237
|
+
attractors: (dbg?.attractors ?? attractors) as AttractorConfig[] | null,
|
|
238
|
+
attractToCenter: (dbg?.attractToCenter ?? attractToCenter) as boolean,
|
|
239
|
+
startPositionAsDirection: (dbg?.startPositionAsDirection ?? startPositionAsDirection) as boolean,
|
|
240
|
+
softParticles: (dbg?.softParticles ?? softParticles) as boolean,
|
|
241
|
+
softDistance: (dbg?.softDistance ?? softDistance) as number,
|
|
242
|
+
collision: (dbg?.collision ?? collision) as CollisionConfig | null,
|
|
243
|
+
backdropNode: backdropNode as VFXParticleSystemOptions['backdropNode'],
|
|
244
|
+
opacityNode: opacityNode as VFXParticleSystemOptions['opacityNode'],
|
|
245
|
+
colorNode: colorNode as VFXParticleSystemOptions['colorNode'],
|
|
246
|
+
alphaTestNode: alphaTestNode as VFXParticleSystemOptions['alphaTestNode'],
|
|
247
|
+
castShadowNode: castShadowNode as VFXParticleSystemOptions['castShadowNode'],
|
|
248
|
+
depthTest: (dbg?.depthTest ?? depthTest) as boolean,
|
|
249
|
+
renderOrder: (dbg?.renderOrder ?? renderOrder) as number,
|
|
250
|
+
curveTexturePath,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createSystem() {
|
|
255
|
+
const r = renderer as unknown as THREE.WebGPURenderer
|
|
256
|
+
if (!r) return null
|
|
257
|
+
return new VFXParticleSystem(r, buildOptions())
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function destroySystem() {
|
|
261
|
+
if (!_system) return
|
|
262
|
+
if (name) {
|
|
263
|
+
coreStore.getState().unregisterParticles(name)
|
|
264
|
+
}
|
|
265
|
+
_system.dispose()
|
|
266
|
+
_system = null
|
|
267
|
+
_renderObject = null
|
|
268
|
+
renderObjectForTemplate = null
|
|
269
|
+
isWebGPUForTemplate = false
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function initSystem() {
|
|
273
|
+
const oldSystem = _system
|
|
274
|
+
if (oldSystem) {
|
|
275
|
+
oldSystem.initialized = false
|
|
276
|
+
if (name) {
|
|
277
|
+
coreStore.getState().unregisterParticles(name)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
_system = null
|
|
281
|
+
_renderObject = null
|
|
282
|
+
|
|
283
|
+
if (!renderer) {
|
|
284
|
+
console.warn('threlte-vfx: No renderer instance available')
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!isWebGPUBackend(renderer)) {
|
|
289
|
+
if (!warnedWebGL) {
|
|
290
|
+
warnedWebGL = true
|
|
291
|
+
console.warn(
|
|
292
|
+
'threlte-vfx: WebGPU backend not detected. Particle system disabled.'
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
_isWebGPU = false
|
|
296
|
+
isWebGPUForTemplate = false
|
|
297
|
+
renderObjectForTemplate = null
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_isWebGPU = true
|
|
302
|
+
const newSystem = createSystem()
|
|
303
|
+
if (!newSystem) return
|
|
304
|
+
|
|
305
|
+
_system = newSystem
|
|
306
|
+
_renderObject = newSystem.renderObject
|
|
307
|
+
|
|
308
|
+
newSystem.init()
|
|
309
|
+
|
|
310
|
+
if (name) {
|
|
311
|
+
coreStore.getState().registerParticles(name, {
|
|
312
|
+
spawn: (x = 0, y = 0, z = 0, count = 20, overrides = null) => {
|
|
313
|
+
const [px, py, pz] = newSystem.position
|
|
314
|
+
newSystem.spawn(px + x, py + y, pz + z, count, overrides)
|
|
315
|
+
},
|
|
316
|
+
start: () => {
|
|
317
|
+
newSystem.start()
|
|
318
|
+
_emitting = true
|
|
319
|
+
},
|
|
320
|
+
stop: () => {
|
|
321
|
+
newSystem.stop()
|
|
322
|
+
_emitting = false
|
|
323
|
+
},
|
|
324
|
+
get isEmitting() {
|
|
325
|
+
return _emitting
|
|
326
|
+
},
|
|
327
|
+
clear: () => newSystem.clear(),
|
|
328
|
+
uniforms: newSystem.uniforms,
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (debug) {
|
|
333
|
+
initDebugPanel()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Update template-facing reactive state last
|
|
337
|
+
isWebGPUForTemplate = true
|
|
338
|
+
renderObjectForTemplate = newSystem.renderObject
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Debug panel support
|
|
342
|
+
function handleDebugUpdate(newValues: Record<string, unknown>) {
|
|
343
|
+
debugValues = { ...debugValues, ...newValues }
|
|
344
|
+
if (!_system) return
|
|
345
|
+
|
|
346
|
+
if ('colorStart' in newValues && newValues.colorStart) {
|
|
347
|
+
const currentColorEnd = debugValues?.colorEnd
|
|
348
|
+
if (!currentColorEnd) {
|
|
349
|
+
newValues = { ...newValues, colorEnd: null }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if ('colorEnd' in newValues && !newValues.colorEnd) {
|
|
353
|
+
newValues = {
|
|
354
|
+
...newValues,
|
|
355
|
+
colorEnd: null,
|
|
356
|
+
colorStart:
|
|
357
|
+
newValues.colorStart ??
|
|
358
|
+
debugValues?.colorStart ?? ['#ffffff'],
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
updateUniformsPartial(_system.uniforms, newValues)
|
|
363
|
+
|
|
364
|
+
if ('fadeSizeCurve' in newValues) {
|
|
365
|
+
activeFadeSizeCurve = newValues.fadeSizeCurve as unknown[] | null
|
|
366
|
+
}
|
|
367
|
+
if ('fadeOpacityCurve' in newValues) {
|
|
368
|
+
activeFadeOpacityCurve = newValues.fadeOpacityCurve as unknown[] | null
|
|
369
|
+
}
|
|
370
|
+
if ('velocityCurve' in newValues) {
|
|
371
|
+
activeVelocityCurve = newValues.velocityCurve as unknown[] | null
|
|
372
|
+
}
|
|
373
|
+
if ('rotationSpeedCurve' in newValues) {
|
|
374
|
+
activeRotationSpeedCurve = newValues.rotationSpeedCurve as unknown[] | null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if ('turbulence' in newValues) {
|
|
378
|
+
_system.setTurbulenceSpeed(
|
|
379
|
+
(newValues.turbulence as TurbulenceConfig | null)?.speed ?? 1
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const newFeatures = resolveFeatures(
|
|
384
|
+
debugValues as Record<string, unknown>
|
|
385
|
+
)
|
|
386
|
+
if (newFeatures.needsRotation !== activeNeedsRotation) {
|
|
387
|
+
activeNeedsRotation = newFeatures.needsRotation
|
|
388
|
+
}
|
|
389
|
+
if (newFeatures.needsPerParticleColor !== activeNeedsPerParticleColor) {
|
|
390
|
+
activeNeedsPerParticleColor = newFeatures.needsPerParticleColor
|
|
391
|
+
}
|
|
392
|
+
if (newFeatures.turbulence !== activeTurbulence) {
|
|
393
|
+
activeTurbulence = newFeatures.turbulence
|
|
394
|
+
}
|
|
395
|
+
if (newFeatures.attractors !== activeAttractors) {
|
|
396
|
+
activeAttractors = newFeatures.attractors
|
|
397
|
+
}
|
|
398
|
+
if (newFeatures.collision !== activeCollision) {
|
|
399
|
+
activeCollision = newFeatures.collision
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (newValues.position) {
|
|
403
|
+
_system.setPosition(newValues.position as [number, number, number])
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if ('delay' in newValues) _system.setDelay((newValues.delay as number) ?? 0)
|
|
407
|
+
if ('emitCount' in newValues)
|
|
408
|
+
_system.setEmitCount((newValues.emitCount as number) ?? 1)
|
|
409
|
+
|
|
410
|
+
if (newValues.autoStart !== undefined) {
|
|
411
|
+
_emitting = newValues.autoStart as boolean
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (_system.material && newValues.blending !== undefined) {
|
|
415
|
+
;(_system.material as any).blending = newValues.blending
|
|
416
|
+
;(_system.material as any).needsUpdate = true
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (
|
|
420
|
+
newValues.maxParticles !== undefined &&
|
|
421
|
+
newValues.maxParticles !== activeMaxParticles
|
|
422
|
+
) {
|
|
423
|
+
activeMaxParticles = newValues.maxParticles as number
|
|
424
|
+
_system.initialized = false
|
|
425
|
+
_system.nextIndex = 0
|
|
426
|
+
}
|
|
427
|
+
if (
|
|
428
|
+
newValues.lighting !== undefined &&
|
|
429
|
+
newValues.lighting !== activeLighting
|
|
430
|
+
) {
|
|
431
|
+
activeLighting = newValues.lighting as string | number
|
|
432
|
+
}
|
|
433
|
+
if (
|
|
434
|
+
newValues.appearance !== undefined &&
|
|
435
|
+
newValues.appearance !== activeAppearance
|
|
436
|
+
) {
|
|
437
|
+
activeAppearance = newValues.appearance as string | number
|
|
438
|
+
}
|
|
439
|
+
if (
|
|
440
|
+
newValues.orientToDirection !== undefined &&
|
|
441
|
+
newValues.orientToDirection !== activeOrientToDirection
|
|
442
|
+
) {
|
|
443
|
+
activeOrientToDirection = newValues.orientToDirection as boolean
|
|
444
|
+
}
|
|
445
|
+
if (
|
|
446
|
+
newValues.shadow !== undefined &&
|
|
447
|
+
newValues.shadow !== activeShadow
|
|
448
|
+
) {
|
|
449
|
+
activeShadow = newValues.shadow as boolean
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if ('geometryType' in newValues || 'geometryArgs' in newValues) {
|
|
453
|
+
const geoType =
|
|
454
|
+
newValues.geometryType ?? prevGeometryType
|
|
455
|
+
const geoArgs =
|
|
456
|
+
newValues.geometryArgs ?? prevGeometryArgs
|
|
457
|
+
const geoTypeChanged =
|
|
458
|
+
'geometryType' in newValues &&
|
|
459
|
+
geoType !== prevGeometryType
|
|
460
|
+
const geoArgsChanged =
|
|
461
|
+
'geometryArgs' in newValues &&
|
|
462
|
+
JSON.stringify(geoArgs) !==
|
|
463
|
+
JSON.stringify(prevGeometryArgs)
|
|
464
|
+
|
|
465
|
+
if (geoTypeChanged || geoArgsChanged) {
|
|
466
|
+
prevGeometryType = geoType
|
|
467
|
+
prevGeometryArgs = geoArgs
|
|
468
|
+
|
|
469
|
+
import('debug-vfx').then((mod) => {
|
|
470
|
+
const { createGeometry, GeometryType } = mod
|
|
471
|
+
if (geoType === GeometryType.NONE || !geoType) {
|
|
472
|
+
if (activeGeometry !== null && !geometry) {
|
|
473
|
+
activeGeometry.dispose()
|
|
474
|
+
}
|
|
475
|
+
activeGeometry = null
|
|
476
|
+
} else {
|
|
477
|
+
const newGeometry = createGeometry(geoType as string, geoArgs as Record<string, number> | undefined)
|
|
478
|
+
if (newGeometry) {
|
|
479
|
+
if (
|
|
480
|
+
activeGeometry !== null &&
|
|
481
|
+
activeGeometry !== geometry
|
|
482
|
+
) {
|
|
483
|
+
activeGeometry.dispose()
|
|
484
|
+
}
|
|
485
|
+
activeGeometry = newGeometry
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function initDebugPanel() {
|
|
494
|
+
import('debug-vfx').then((mod) => {
|
|
495
|
+
const { renderDebugPanel, detectGeometryTypeAndArgs } = mod
|
|
496
|
+
|
|
497
|
+
if (!debugValues) {
|
|
498
|
+
const initialValues: Record<string, unknown> = {
|
|
499
|
+
name,
|
|
500
|
+
maxParticles,
|
|
501
|
+
size,
|
|
502
|
+
colorStart,
|
|
503
|
+
colorEnd,
|
|
504
|
+
fadeSize,
|
|
505
|
+
fadeSizeCurve: fadeSizeCurve || null,
|
|
506
|
+
fadeOpacity,
|
|
507
|
+
fadeOpacityCurve: fadeOpacityCurve || null,
|
|
508
|
+
velocityCurve: velocityCurve || null,
|
|
509
|
+
gravity,
|
|
510
|
+
lifetime,
|
|
511
|
+
direction,
|
|
512
|
+
startPosition,
|
|
513
|
+
startPositionAsDirection,
|
|
514
|
+
speed,
|
|
515
|
+
friction,
|
|
516
|
+
appearance,
|
|
517
|
+
rotation,
|
|
518
|
+
rotationSpeed,
|
|
519
|
+
rotationSpeedCurve: rotationSpeedCurve || null,
|
|
520
|
+
orientToDirection,
|
|
521
|
+
orientAxis,
|
|
522
|
+
stretchBySpeed: stretchBySpeed || null,
|
|
523
|
+
lighting,
|
|
524
|
+
shadow,
|
|
525
|
+
blending,
|
|
526
|
+
intensity,
|
|
527
|
+
position,
|
|
528
|
+
autoStart,
|
|
529
|
+
delay,
|
|
530
|
+
emitCount,
|
|
531
|
+
emitterShape,
|
|
532
|
+
emitterRadius,
|
|
533
|
+
emitterAngle,
|
|
534
|
+
emitterHeight,
|
|
535
|
+
emitterSurfaceOnly,
|
|
536
|
+
emitterDirection,
|
|
537
|
+
turbulence,
|
|
538
|
+
attractToCenter,
|
|
539
|
+
softParticles,
|
|
540
|
+
softDistance,
|
|
541
|
+
collision,
|
|
542
|
+
...detectGeometryTypeAndArgs(geometry),
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
debugValues = initialValues
|
|
546
|
+
prevGeometryType = initialValues.geometryType
|
|
547
|
+
prevGeometryArgs = initialValues.geometryArgs
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
renderDebugPanel(debugValues, handleDebugUpdate, 'threlte')
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Watch structural props for recreation (skip in debug mode)
|
|
555
|
+
$effect(() => {
|
|
556
|
+
// Read all structural props to track them
|
|
557
|
+
const _deps = [
|
|
558
|
+
maxParticles,
|
|
559
|
+
lighting,
|
|
560
|
+
appearance,
|
|
561
|
+
orientToDirection,
|
|
562
|
+
geometry,
|
|
563
|
+
shadow,
|
|
564
|
+
fadeSizeCurve,
|
|
565
|
+
fadeOpacityCurve,
|
|
566
|
+
velocityCurve,
|
|
567
|
+
rotationSpeedCurve,
|
|
568
|
+
colorStart.length,
|
|
569
|
+
colorEnd,
|
|
570
|
+
rotation,
|
|
571
|
+
rotationSpeed,
|
|
572
|
+
turbulence,
|
|
573
|
+
attractors,
|
|
574
|
+
collision,
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
if (debug) return
|
|
578
|
+
|
|
579
|
+
// Use untrack for writes to avoid circular dependencies
|
|
580
|
+
untrack(() => {
|
|
581
|
+
activeMaxParticles = maxParticles
|
|
582
|
+
activeLighting = lighting
|
|
583
|
+
activeAppearance = appearance
|
|
584
|
+
activeOrientToDirection = orientToDirection
|
|
585
|
+
activeGeometry = geometry
|
|
586
|
+
activeShadow = shadow
|
|
587
|
+
activeFadeSizeCurve = fadeSizeCurve
|
|
588
|
+
activeFadeOpacityCurve = fadeOpacityCurve
|
|
589
|
+
activeVelocityCurve = velocityCurve
|
|
590
|
+
activeRotationSpeedCurve = rotationSpeedCurve
|
|
591
|
+
activeNeedsPerParticleColor =
|
|
592
|
+
colorStart.length > 1 || colorEnd !== null
|
|
593
|
+
activeNeedsRotation =
|
|
594
|
+
isNonDefaultRotation(rotation) ||
|
|
595
|
+
isNonDefaultRotation(rotationSpeed)
|
|
596
|
+
activeTurbulence =
|
|
597
|
+
turbulence !== null && (turbulence?.intensity ?? 0) > 0
|
|
598
|
+
activeAttractors =
|
|
599
|
+
attractors !== null && (attractors?.length ?? 0) > 0
|
|
600
|
+
activeCollision = collision !== null
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// Watch structural active values for system recreation
|
|
605
|
+
$effect(() => {
|
|
606
|
+
// Read all active values to track them
|
|
607
|
+
const _deps = [
|
|
608
|
+
activeMaxParticles,
|
|
609
|
+
activeLighting,
|
|
610
|
+
activeAppearance,
|
|
611
|
+
activeOrientToDirection,
|
|
612
|
+
activeGeometry,
|
|
613
|
+
activeShadow,
|
|
614
|
+
activeNeedsPerParticleColor,
|
|
615
|
+
activeNeedsRotation,
|
|
616
|
+
activeTurbulence,
|
|
617
|
+
activeAttractors,
|
|
618
|
+
activeCollision,
|
|
619
|
+
activeFadeSizeCurve,
|
|
620
|
+
activeFadeOpacityCurve,
|
|
621
|
+
activeVelocityCurve,
|
|
622
|
+
activeRotationSpeedCurve,
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
if (!mounted) return
|
|
626
|
+
|
|
627
|
+
// Use untrack for the actual init to avoid reading/writing
|
|
628
|
+
// reactive state that would re-trigger this effect
|
|
629
|
+
untrack(() => {
|
|
630
|
+
if (!_isWebGPU) return
|
|
631
|
+
initSystem()
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
// Watch non-structural props for uniform updates (skip in debug mode)
|
|
636
|
+
$effect(() => {
|
|
637
|
+
const _deps = [
|
|
638
|
+
position,
|
|
639
|
+
size,
|
|
640
|
+
fadeSize,
|
|
641
|
+
fadeOpacity,
|
|
642
|
+
gravity,
|
|
643
|
+
friction,
|
|
644
|
+
speed,
|
|
645
|
+
lifetime,
|
|
646
|
+
direction,
|
|
647
|
+
rotation,
|
|
648
|
+
rotationSpeed,
|
|
649
|
+
intensity,
|
|
650
|
+
colorStart,
|
|
651
|
+
colorEnd,
|
|
652
|
+
collision,
|
|
653
|
+
emitterShape,
|
|
654
|
+
emitterRadius,
|
|
655
|
+
emitterAngle,
|
|
656
|
+
emitterHeight,
|
|
657
|
+
emitterSurfaceOnly,
|
|
658
|
+
emitterDirection,
|
|
659
|
+
turbulence,
|
|
660
|
+
startPosition,
|
|
661
|
+
attractors,
|
|
662
|
+
attractToCenter,
|
|
663
|
+
startPositionAsDirection,
|
|
664
|
+
softParticles,
|
|
665
|
+
softDistance,
|
|
666
|
+
orientAxis,
|
|
667
|
+
stretchBySpeed,
|
|
668
|
+
delay,
|
|
669
|
+
emitCount,
|
|
670
|
+
]
|
|
671
|
+
|
|
672
|
+
if (debug) return
|
|
673
|
+
|
|
674
|
+
untrack(() => {
|
|
675
|
+
if (!_system) return
|
|
676
|
+
|
|
677
|
+
_system.setPosition(position as [number, number, number])
|
|
678
|
+
_system.setDelay(delay)
|
|
679
|
+
_system.setEmitCount(emitCount)
|
|
680
|
+
_system.setTurbulenceSpeed(turbulence?.speed ?? 1)
|
|
681
|
+
|
|
682
|
+
const normalized = normalizeProps({
|
|
683
|
+
size,
|
|
684
|
+
speed,
|
|
685
|
+
fadeSize,
|
|
686
|
+
fadeOpacity,
|
|
687
|
+
lifetime,
|
|
688
|
+
gravity,
|
|
689
|
+
direction,
|
|
690
|
+
startPosition,
|
|
691
|
+
rotation,
|
|
692
|
+
rotationSpeed,
|
|
693
|
+
friction,
|
|
694
|
+
intensity,
|
|
695
|
+
colorStart,
|
|
696
|
+
colorEnd,
|
|
697
|
+
emitterShape,
|
|
698
|
+
emitterRadius,
|
|
699
|
+
emitterAngle,
|
|
700
|
+
emitterHeight,
|
|
701
|
+
emitterSurfaceOnly,
|
|
702
|
+
emitterDirection,
|
|
703
|
+
turbulence,
|
|
704
|
+
attractors,
|
|
705
|
+
attractToCenter,
|
|
706
|
+
startPositionAsDirection,
|
|
707
|
+
softParticles,
|
|
708
|
+
softDistance,
|
|
709
|
+
collision,
|
|
710
|
+
orientAxis,
|
|
711
|
+
stretchBySpeed,
|
|
712
|
+
} as any)
|
|
713
|
+
updateUniforms(_system.uniforms, normalized)
|
|
714
|
+
})
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
// Frame loop
|
|
718
|
+
useTask((delta) => {
|
|
719
|
+
if (!_system || !_system.initialized) return
|
|
720
|
+
_system.update(delta)
|
|
721
|
+
if (_emitting) {
|
|
722
|
+
_system.autoEmit(delta)
|
|
723
|
+
}
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
onMount(() => {
|
|
727
|
+
mounted = true
|
|
728
|
+
initSystem()
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
onDestroy(() => {
|
|
732
|
+
mounted = false
|
|
733
|
+
if (debug) {
|
|
734
|
+
import('debug-vfx').then((mod) => {
|
|
735
|
+
mod.destroyDebugPanel()
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
destroySystem()
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// Exposed API
|
|
742
|
+
export function spawn(
|
|
743
|
+
x = 0,
|
|
744
|
+
y = 0,
|
|
745
|
+
z = 0,
|
|
746
|
+
count = 20,
|
|
747
|
+
overrides: Record<string, unknown> | null = null
|
|
748
|
+
) {
|
|
749
|
+
if (!_system) return
|
|
750
|
+
const [px, py, pz] = _system.position
|
|
751
|
+
_system.spawn(px + x, py + y, pz + z, count, overrides)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export function start() {
|
|
755
|
+
if (!_system) return
|
|
756
|
+
_system.start()
|
|
757
|
+
_emitting = true
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function stop() {
|
|
761
|
+
if (!_system) return
|
|
762
|
+
_system.stop()
|
|
763
|
+
_emitting = false
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function clear() {
|
|
767
|
+
_system?.clear()
|
|
768
|
+
}
|
|
769
|
+
</script>
|
|
770
|
+
|
|
771
|
+
{#if isWebGPUForTemplate && renderObjectForTemplate}
|
|
772
|
+
<T is={renderObjectForTemplate} />
|
|
773
|
+
{/if}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,28 @@
|
|
|
1
|
+
import * as core_vfx from 'core-vfx';
|
|
2
|
+
import { CoreState } from 'core-vfx';
|
|
3
|
+
export { Appearance, AttractorConfig, AttractorType, BaseParticleProps, Blending, CollisionConfig, CurveChannel, CurveData, CurvePoint, CurveTextureResolved, CurveTextureResult, Easing, EmitterController, EmitterControllerOptions, EmitterShape, FlipbookConfig, FrictionConfig, Lighting, NormalizedParticleProps, ParticleData, Rotation3DInput, StretchConfig, TurbulenceConfig, VFXParticleSystem, VFXParticleSystemOptions, bakeCurveToArray, buildCurveTextureBin, createCombinedCurveTexture, isNonDefaultRotation, isWebGPUBackend, normalizeProps, resolveCurveTexture } from 'core-vfx';
|
|
4
|
+
import { Readable } from 'svelte/store';
|
|
1
5
|
|
|
2
|
-
|
|
6
|
+
declare function useVFXEmitter(name: string): {
|
|
7
|
+
emit: (position?: [number, number, number], count?: number, overrides?: Record<string, unknown> | null) => boolean;
|
|
8
|
+
burst: (position?: [number, number, number], count?: number, overrides?: Record<string, unknown> | null) => boolean;
|
|
9
|
+
start: () => boolean;
|
|
10
|
+
stop: () => boolean;
|
|
11
|
+
clear: () => boolean;
|
|
12
|
+
isEmitting: () => boolean;
|
|
13
|
+
getUniforms: () => Record<string, unknown> | null;
|
|
14
|
+
getParticles: () => core_vfx.ParticleSystemRef | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
declare function useVFXStore(): Readable<CoreState>;
|
|
18
|
+
declare function useVFXStore<T>(selector: (state: CoreState) => T): Readable<T>;
|
|
19
|
+
declare namespace useVFXStore {
|
|
20
|
+
var getState: () => CoreState;
|
|
21
|
+
var setState: {
|
|
22
|
+
(partial: CoreState | Partial<CoreState> | ((state: CoreState) => CoreState | Partial<CoreState>), replace?: false): void;
|
|
23
|
+
(state: CoreState | ((state: CoreState) => CoreState), replace: true): void;
|
|
24
|
+
};
|
|
25
|
+
var subscribe: (listener: (state: CoreState, prevState: CoreState) => void) => () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { useVFXEmitter, useVFXStore };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,114 @@
|
|
|
1
|
+
// src/useVFXEmitter.ts
|
|
2
|
+
import { useThrelte } from "@threlte/core";
|
|
3
|
+
import { coreStore, isWebGPUBackend } from "core-vfx";
|
|
4
|
+
function useVFXEmitter(name) {
|
|
5
|
+
const { renderer } = useThrelte();
|
|
6
|
+
const checkWebGPU = () => {
|
|
7
|
+
return renderer && isWebGPUBackend(renderer);
|
|
8
|
+
};
|
|
9
|
+
const getParticles = () => coreStore.getState().getParticles(name);
|
|
10
|
+
const emit = (position = [0, 0, 0], count = 20, overrides = null) => {
|
|
11
|
+
if (!checkWebGPU()) return false;
|
|
12
|
+
const [x, y, z] = position;
|
|
13
|
+
return coreStore.getState().emit(name, { x, y, z, count, overrides });
|
|
14
|
+
};
|
|
15
|
+
const burst = (position = [0, 0, 0], count = 50, overrides = null) => {
|
|
16
|
+
if (!checkWebGPU()) return false;
|
|
17
|
+
const [x, y, z] = position;
|
|
18
|
+
return coreStore.getState().emit(name, { x, y, z, count, overrides });
|
|
19
|
+
};
|
|
20
|
+
const start = () => {
|
|
21
|
+
if (!checkWebGPU()) return false;
|
|
22
|
+
return coreStore.getState().start(name);
|
|
23
|
+
};
|
|
24
|
+
const stop = () => {
|
|
25
|
+
if (!checkWebGPU()) return false;
|
|
26
|
+
return coreStore.getState().stop(name);
|
|
27
|
+
};
|
|
28
|
+
const clear = () => {
|
|
29
|
+
if (!checkWebGPU()) return false;
|
|
30
|
+
return coreStore.getState().clear(name);
|
|
31
|
+
};
|
|
32
|
+
const isEmitting = () => {
|
|
33
|
+
var _a;
|
|
34
|
+
if (!checkWebGPU()) return false;
|
|
35
|
+
const particles = getParticles();
|
|
36
|
+
return (_a = particles == null ? void 0 : particles.isEmitting) != null ? _a : false;
|
|
37
|
+
};
|
|
38
|
+
const getUniforms = () => {
|
|
39
|
+
var _a;
|
|
40
|
+
if (!checkWebGPU()) return null;
|
|
41
|
+
const particles = getParticles();
|
|
42
|
+
return (_a = particles == null ? void 0 : particles.uniforms) != null ? _a : null;
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
emit,
|
|
46
|
+
burst,
|
|
47
|
+
start,
|
|
48
|
+
stop,
|
|
49
|
+
clear,
|
|
50
|
+
isEmitting,
|
|
51
|
+
getUniforms,
|
|
52
|
+
getParticles: () => checkWebGPU() ? getParticles() : null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/svelte-store.ts
|
|
57
|
+
import { coreStore as coreStore2 } from "core-vfx";
|
|
58
|
+
function useVFXStore(selector) {
|
|
59
|
+
const pick = selector != null ? selector : ((s) => s);
|
|
60
|
+
return {
|
|
61
|
+
subscribe(run) {
|
|
62
|
+
run(pick(coreStore2.getState()));
|
|
63
|
+
const unsubscribe = coreStore2.subscribe((state) => {
|
|
64
|
+
run(pick(state));
|
|
65
|
+
});
|
|
66
|
+
return unsubscribe;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
useVFXStore.getState = coreStore2.getState;
|
|
71
|
+
useVFXStore.setState = coreStore2.setState;
|
|
72
|
+
useVFXStore.subscribe = coreStore2.subscribe;
|
|
73
|
+
|
|
1
74
|
// src/index.ts
|
|
2
|
-
|
|
75
|
+
import {
|
|
76
|
+
Appearance,
|
|
77
|
+
Blending,
|
|
78
|
+
EmitterShape,
|
|
79
|
+
AttractorType,
|
|
80
|
+
Easing,
|
|
81
|
+
Lighting,
|
|
82
|
+
bakeCurveToArray,
|
|
83
|
+
createCombinedCurveTexture,
|
|
84
|
+
buildCurveTextureBin,
|
|
85
|
+
CurveChannel
|
|
86
|
+
} from "core-vfx";
|
|
87
|
+
import {
|
|
88
|
+
VFXParticleSystem,
|
|
89
|
+
EmitterController,
|
|
90
|
+
isWebGPUBackend as isWebGPUBackend2,
|
|
91
|
+
isNonDefaultRotation,
|
|
92
|
+
normalizeProps,
|
|
93
|
+
resolveCurveTexture
|
|
94
|
+
} from "core-vfx";
|
|
95
|
+
export {
|
|
96
|
+
Appearance,
|
|
97
|
+
AttractorType,
|
|
98
|
+
Blending,
|
|
99
|
+
CurveChannel,
|
|
100
|
+
Easing,
|
|
101
|
+
EmitterController,
|
|
102
|
+
EmitterShape,
|
|
103
|
+
Lighting,
|
|
104
|
+
VFXParticleSystem,
|
|
105
|
+
bakeCurveToArray,
|
|
106
|
+
buildCurveTextureBin,
|
|
107
|
+
createCombinedCurveTexture,
|
|
108
|
+
isNonDefaultRotation,
|
|
109
|
+
isWebGPUBackend2 as isWebGPUBackend,
|
|
110
|
+
normalizeProps,
|
|
111
|
+
resolveCurveTexture,
|
|
112
|
+
useVFXEmitter,
|
|
113
|
+
useVFXStore
|
|
114
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threlte-vfx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"svelte": "./dist/index.js",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/mustache-dev/r3f-vfx.git"
|
|
@@ -10,7 +11,16 @@
|
|
|
10
11
|
"exports": {
|
|
11
12
|
".": {
|
|
12
13
|
"types": "./dist/index.d.ts",
|
|
14
|
+
"svelte": "./dist/index.js",
|
|
13
15
|
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./VFXParticles.svelte": {
|
|
18
|
+
"svelte": "./dist/VFXParticles.svelte",
|
|
19
|
+
"import": "./dist/VFXParticles.svelte"
|
|
20
|
+
},
|
|
21
|
+
"./VFXEmitter.svelte": {
|
|
22
|
+
"svelte": "./dist/VFXEmitter.svelte",
|
|
23
|
+
"import": "./dist/VFXEmitter.svelte"
|
|
14
24
|
}
|
|
15
25
|
},
|
|
16
26
|
"files": [
|
|
@@ -18,15 +28,23 @@
|
|
|
18
28
|
],
|
|
19
29
|
"scripts": {
|
|
20
30
|
"dev": "tsup --watch",
|
|
21
|
-
"build": "tsup",
|
|
31
|
+
"build": "tsup && cp src/*.svelte dist/",
|
|
22
32
|
"typecheck": "tsc",
|
|
23
33
|
"copy-readme": "cp ../../README.md README.md",
|
|
24
34
|
"prepublishOnly": "bun run typecheck && bun run build && bun run copy-readme"
|
|
25
35
|
},
|
|
26
36
|
"dependencies": {
|
|
27
|
-
"core-vfx": "0.1.0"
|
|
37
|
+
"core-vfx": "0.1.0",
|
|
38
|
+
"debug-vfx": "0.1.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@threlte/core": ">=8.0.0",
|
|
42
|
+
"svelte": ">=5.0.0",
|
|
43
|
+
"three": ">=0.182.0"
|
|
28
44
|
},
|
|
29
45
|
"devDependencies": {
|
|
46
|
+
"@threlte/core": "8.3.1",
|
|
47
|
+
"svelte": "5.28.2",
|
|
30
48
|
"tsup": "8.5.1",
|
|
31
49
|
"typescript": "5.9.3"
|
|
32
50
|
}
|