@react-three/postprocessing 3.0.4 → 3.1.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/LICENSE +21 -21
- package/README.md +80 -81
- package/dist/EffectComposer.d.ts +14 -6
- package/dist/EffectGroup.d.ts +9 -0
- package/dist/Selection.d.ts +8 -8
- package/dist/createEffectComponent.d.ts +30 -0
- package/dist/effects/ASCII.d.ts +29 -7
- package/dist/effects/Autofocus.d.ts +8 -7
- package/dist/effects/Bloom.d.ts +10 -5
- package/dist/effects/BrightnessContrast.d.ts +15 -5
- package/dist/effects/ChromaticAberration.d.ts +30 -7
- package/dist/effects/ColorAverage.d.ts +7 -8
- package/dist/effects/ColorDepth.d.ts +8 -5
- package/dist/effects/Depth.d.ts +13 -5
- package/dist/effects/DepthOfField.d.ts +7 -18
- package/dist/effects/DotScreen.d.ts +15 -5
- package/dist/effects/FXAA.d.ts +11 -5
- package/dist/effects/Glitch.d.ts +17 -29
- package/dist/effects/GodRays.d.ts +7 -19
- package/dist/effects/Grid.d.ts +8 -8
- package/dist/effects/HueSaturation.d.ts +15 -5
- package/dist/effects/LUT.d.ts +4 -3
- package/dist/effects/LensFlare.d.ts +52 -11
- package/dist/effects/Noise.d.ts +8 -5
- package/dist/effects/Outline.d.ts +9 -25
- package/dist/effects/Pixelation.d.ts +3 -2
- package/dist/effects/Ramp.d.ts +58 -10
- package/dist/effects/SMAA.d.ts +10 -5
- package/dist/effects/SSAO.d.ts +7 -30
- package/dist/effects/ScanlineEffect.d.ts +13 -5
- package/dist/effects/SelectiveBloom.d.ts +5 -10
- package/dist/effects/Sepia.d.ts +13 -5
- package/dist/effects/ShockWave.d.ts +12 -5
- package/dist/effects/Texture.d.ts +7 -8
- package/dist/effects/TiltShift.d.ts +10 -5
- package/dist/effects/TiltShift2.d.ts +44 -8
- package/dist/effects/ToneMapping.d.ts +9 -7
- package/dist/effects/Vignette.d.ts +19 -5
- package/dist/effects/Water.d.ts +17 -5
- package/dist/index.d.ts +15 -11
- package/dist/index.js +1841 -45
- package/dist/index.js.map +1 -1
- package/dist/passes/DepthPicking.d.ts +11 -0
- package/dist/{effects → passes}/N8AO.d.ts +5 -2
- package/dist/util.d.ts +19 -17
- package/dist/wrapEffect.d.ts +12 -0
- package/package.json +74 -67
- package/src/EffectComposer.tsx +379 -200
- package/src/EffectGroup.tsx +74 -0
- package/src/Selection.tsx +86 -46
- package/src/createEffectComponent.tsx +71 -0
- package/src/effects/ASCII.tsx +183 -135
- package/src/effects/Autofocus.tsx +122 -153
- package/src/effects/Bloom.tsx +34 -6
- package/src/effects/BrightnessContrast.tsx +7 -4
- package/src/effects/ChromaticAberration.tsx +22 -5
- package/src/effects/ColorAverage.tsx +4 -15
- package/src/effects/ColorDepth.tsx +25 -4
- package/src/effects/Depth.tsx +6 -4
- package/src/effects/DepthOfField.tsx +109 -89
- package/src/effects/DotScreen.tsx +7 -4
- package/src/effects/FXAA.tsx +6 -4
- package/src/effects/Glitch.tsx +32 -40
- package/src/effects/GodRays.tsx +104 -16
- package/src/effects/Grid.tsx +27 -21
- package/src/effects/HueSaturation.tsx +7 -4
- package/src/effects/LUT.tsx +25 -26
- package/src/effects/LensFlare.tsx +765 -611
- package/src/effects/Noise.tsx +16 -4
- package/src/effects/Outline.tsx +84 -117
- package/src/effects/Pixelation.tsx +20 -15
- package/src/effects/Ramp.tsx +239 -150
- package/src/effects/SMAA.tsx +20 -4
- package/src/effects/SSAO.tsx +152 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +98 -126
- package/src/effects/Sepia.tsx +6 -4
- package/src/effects/ShockWave.tsx +32 -4
- package/src/effects/Texture.tsx +28 -23
- package/src/effects/TiltShift.tsx +32 -4
- package/src/effects/TiltShift2.tsx +156 -90
- package/src/effects/ToneMapping.tsx +19 -6
- package/src/effects/Vignette.tsx +7 -4
- package/src/effects/Water.tsx +50 -35
- package/src/index.ts +43 -40
- package/src/passes/DepthPicking.tsx +63 -0
- package/src/passes/N8AO.tsx +105 -0
- package/src/tests/Autofocus.test.tsx +38 -0
- package/src/tests/Bloom.test.tsx +76 -0
- package/src/tests/ChromaticAberration.test.tsx +81 -0
- package/src/tests/ColorDepth.test.tsx +71 -0
- package/src/tests/DepthOfField.test.tsx +130 -0
- package/src/tests/DepthPicking.test.tsx +250 -0
- package/src/tests/EffectComposer.test.tsx +1404 -0
- package/src/tests/EffectGroup.test.tsx +337 -0
- package/src/tests/Glitch.test.tsx +56 -0
- package/src/tests/GodRays.test.tsx +101 -0
- package/src/tests/Grid.test.tsx +34 -0
- package/src/tests/LUT.test.tsx +64 -0
- package/src/tests/N8AO.test.tsx +92 -0
- package/src/tests/Outline.test.tsx +211 -0
- package/src/tests/SSAO.test.tsx +140 -0
- package/src/tests/Selection.test.tsx +324 -0
- package/src/tests/SelectiveBloom.test.tsx +166 -0
- package/src/tests/ShockWave.test.tsx +95 -0
- package/src/tests/TiltShift.test.tsx +76 -0
- package/src/tests/createEffectComponent.test.tsx +286 -0
- package/src/tests/effects.smoke.test.tsx +238 -0
- package/src/tests/test-utils.tsx +96 -0
- package/src/tests/wrapEffect.test.tsx +209 -0
- package/src/util.tsx +221 -55
- package/src/wrapEffect.tsx +123 -0
- package/src/EffectComposer.test.tsx +0 -126
- package/src/effects/N8AO.tsx +0 -81
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { EffectComposer as EffectComposerImpl, SelectiveBloomEffect } from 'postprocessing'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Mesh, Object3D, PointLight } from 'three'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { EffectComposer } from '../EffectComposer'
|
|
6
|
+
import { SelectiveBloom } from '../effects/SelectiveBloom'
|
|
7
|
+
import { Select, Selection } from '../Selection'
|
|
8
|
+
import { flush, root, waitForComposer } from './test-utils'
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await React.act(async () => {
|
|
12
|
+
root.render(null)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('SelectiveBloom', () => {
|
|
17
|
+
it('does not reconstruct the effect (with its GPU resources) on unrelated re-renders', async () => {
|
|
18
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
19
|
+
const effectRef = React.createRef<SelectiveBloomEffect>()
|
|
20
|
+
const light = new PointLight()
|
|
21
|
+
|
|
22
|
+
const render = (tick: number) =>
|
|
23
|
+
root.render(
|
|
24
|
+
<EffectComposer ref={composerRef}>
|
|
25
|
+
<SelectiveBloom ref={effectRef} lights={[light]} intensity={2} />
|
|
26
|
+
<group name={`tick-${tick}`} />
|
|
27
|
+
</EffectComposer>
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
await React.act(async () => render(0))
|
|
31
|
+
await waitForComposer(composerRef)
|
|
32
|
+
await flush()
|
|
33
|
+
const first = effectRef.current
|
|
34
|
+
expect(first).toBeTruthy()
|
|
35
|
+
|
|
36
|
+
for (let t = 1; t <= 5; t++) {
|
|
37
|
+
await React.act(async () => render(t))
|
|
38
|
+
await flush()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
expect(effectRef.current).toBe(first)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('sets its selection from the Selection/Select API and clears it when the object deselects', async () => {
|
|
45
|
+
const effectRef = React.createRef<SelectiveBloomEffect>()
|
|
46
|
+
const meshRef = React.createRef<Mesh>()
|
|
47
|
+
const light = new PointLight()
|
|
48
|
+
|
|
49
|
+
const render = (enabled: boolean) =>
|
|
50
|
+
root.render(
|
|
51
|
+
<EffectComposer>
|
|
52
|
+
<Selection>
|
|
53
|
+
<Select enabled={enabled}>
|
|
54
|
+
<mesh ref={meshRef}>
|
|
55
|
+
<boxGeometry />
|
|
56
|
+
<meshBasicMaterial />
|
|
57
|
+
</mesh>
|
|
58
|
+
</Select>
|
|
59
|
+
<SelectiveBloom ref={effectRef} lights={[light]} />
|
|
60
|
+
</Selection>
|
|
61
|
+
</EffectComposer>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await React.act(async () => render(true))
|
|
65
|
+
await flush()
|
|
66
|
+
await flush()
|
|
67
|
+
|
|
68
|
+
expect(Array.from(effectRef.current!.selection)).toContain(meshRef.current)
|
|
69
|
+
|
|
70
|
+
await React.act(async () => render(false))
|
|
71
|
+
await flush()
|
|
72
|
+
await flush()
|
|
73
|
+
|
|
74
|
+
expect(Array.from(effectRef.current!.selection)).not.toContain(meshRef.current)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('moves lights to the new render layer when selectionLayer changes', async () => {
|
|
78
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
79
|
+
const effectRef = React.createRef<SelectiveBloomEffect>()
|
|
80
|
+
const light = new PointLight()
|
|
81
|
+
const onLayer = (n: number) => light.layers.test({ mask: 1 << n } as never)
|
|
82
|
+
|
|
83
|
+
const render = (layer: number) =>
|
|
84
|
+
root.render(
|
|
85
|
+
<EffectComposer ref={composerRef}>
|
|
86
|
+
<SelectiveBloom ref={effectRef} lights={[light]} selectionLayer={layer} />
|
|
87
|
+
</EffectComposer>
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
await React.act(async () => render(10))
|
|
91
|
+
await waitForComposer(composerRef)
|
|
92
|
+
await flush()
|
|
93
|
+
await flush()
|
|
94
|
+
expect(onLayer(10)).toBe(true)
|
|
95
|
+
|
|
96
|
+
await React.act(async () => render(15))
|
|
97
|
+
await flush()
|
|
98
|
+
await flush()
|
|
99
|
+
|
|
100
|
+
expect(onLayer(15)).toBe(true)
|
|
101
|
+
expect(onLayer(10)).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('does not throw when a lights ref has not attached yet', async () => {
|
|
105
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
106
|
+
const unattachedRef = React.createRef<Object3D>()
|
|
107
|
+
|
|
108
|
+
await React.act(async () =>
|
|
109
|
+
root.render(
|
|
110
|
+
<EffectComposer ref={composerRef}>
|
|
111
|
+
<SelectiveBloom lights={[unattachedRef]} />
|
|
112
|
+
</EffectComposer>
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
await waitForComposer(composerRef)
|
|
116
|
+
await expect(flush()).resolves.not.toThrow()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('applies intensity live, without reconstructing the effect', async () => {
|
|
120
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
121
|
+
const effectRef = React.createRef<SelectiveBloomEffect>()
|
|
122
|
+
const light = new PointLight()
|
|
123
|
+
|
|
124
|
+
const render = (intensity: number) =>
|
|
125
|
+
root.render(
|
|
126
|
+
<EffectComposer ref={composerRef}>
|
|
127
|
+
<SelectiveBloom ref={effectRef} lights={[light]} intensity={intensity} />
|
|
128
|
+
</EffectComposer>
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
await React.act(async () => render(1))
|
|
132
|
+
await waitForComposer(composerRef)
|
|
133
|
+
await flush()
|
|
134
|
+
const first = effectRef.current
|
|
135
|
+
expect(first!.intensity).toBe(1)
|
|
136
|
+
|
|
137
|
+
await React.act(async () => render(3))
|
|
138
|
+
await flush()
|
|
139
|
+
|
|
140
|
+
expect(effectRef.current).toBe(first)
|
|
141
|
+
expect(effectRef.current!.intensity).toBe(3)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('still reconstructs when luminanceThreshold changes (no live setter in postprocessing)', async () => {
|
|
145
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
146
|
+
const effectRef = React.createRef<SelectiveBloomEffect>()
|
|
147
|
+
const light = new PointLight()
|
|
148
|
+
|
|
149
|
+
const render = (luminanceThreshold: number) =>
|
|
150
|
+
root.render(
|
|
151
|
+
<EffectComposer ref={composerRef}>
|
|
152
|
+
<SelectiveBloom ref={effectRef} lights={[light]} luminanceThreshold={luminanceThreshold} />
|
|
153
|
+
</EffectComposer>
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
await React.act(async () => render(0.5))
|
|
157
|
+
await waitForComposer(composerRef)
|
|
158
|
+
await flush()
|
|
159
|
+
const first = effectRef.current
|
|
160
|
+
|
|
161
|
+
await React.act(async () => render(0.8))
|
|
162
|
+
await flush()
|
|
163
|
+
|
|
164
|
+
expect(effectRef.current).not.toBe(first)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EffectComposer as EffectComposerImpl, ShockWaveEffect } from 'postprocessing'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Vector3 } from 'three'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { EffectComposer } from '../EffectComposer'
|
|
6
|
+
import { ShockWave } from '../effects/ShockWave'
|
|
7
|
+
import { flush, root } from './test-utils'
|
|
8
|
+
|
|
9
|
+
describe('ShockWave', () => {
|
|
10
|
+
it('applies speed and position, which createEffectComponent cannot (ShockWaveEffect takes them as a 3rd ctor arg)', async () => {
|
|
11
|
+
const ref = React.createRef<ShockWaveEffect>()
|
|
12
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
13
|
+
const position = new Vector3(1, 2, 3)
|
|
14
|
+
|
|
15
|
+
await React.act(async () =>
|
|
16
|
+
root.render(
|
|
17
|
+
<EffectComposer ref={composerRef}>
|
|
18
|
+
<ShockWave ref={ref} speed={5} position={position} />
|
|
19
|
+
</EffectComposer>
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
await flush()
|
|
24
|
+
|
|
25
|
+
expect(ref.current!.speed).toBe(5)
|
|
26
|
+
expect(ref.current!.position).toBe(position)
|
|
27
|
+
|
|
28
|
+
await React.act(async () => root.render(null))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('updates speed/position live, without reconstructing the instance', async () => {
|
|
32
|
+
const ref = React.createRef<ShockWaveEffect>()
|
|
33
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
34
|
+
|
|
35
|
+
await React.act(async () =>
|
|
36
|
+
root.render(
|
|
37
|
+
<EffectComposer ref={composerRef}>
|
|
38
|
+
<ShockWave ref={ref} speed={1} />
|
|
39
|
+
</EffectComposer>
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
await flush()
|
|
44
|
+
|
|
45
|
+
const firstInstance = ref.current
|
|
46
|
+
|
|
47
|
+
await React.act(async () =>
|
|
48
|
+
root.render(
|
|
49
|
+
<EffectComposer ref={composerRef}>
|
|
50
|
+
<ShockWave ref={ref} speed={2} waveSize={0.5} />
|
|
51
|
+
</EffectComposer>
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
await flush()
|
|
56
|
+
|
|
57
|
+
expect(ref.current).toBe(firstInstance)
|
|
58
|
+
expect(ref.current!.speed).toBe(2)
|
|
59
|
+
expect(ref.current!.waveSize).toBe(0.5)
|
|
60
|
+
|
|
61
|
+
await React.act(async () => root.render(null))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('resets speed to its constructor default when the prop is removed', async () => {
|
|
65
|
+
const ref = React.createRef<ShockWaveEffect>()
|
|
66
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
67
|
+
|
|
68
|
+
await React.act(async () =>
|
|
69
|
+
root.render(
|
|
70
|
+
<EffectComposer ref={composerRef}>
|
|
71
|
+
<ShockWave ref={ref} speed={5} />
|
|
72
|
+
</EffectComposer>
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
await flush()
|
|
77
|
+
|
|
78
|
+
expect(ref.current!.speed).toBe(5)
|
|
79
|
+
const defaultSpeed = 2
|
|
80
|
+
|
|
81
|
+
await React.act(async () =>
|
|
82
|
+
root.render(
|
|
83
|
+
<EffectComposer ref={composerRef}>
|
|
84
|
+
<ShockWave ref={ref} />
|
|
85
|
+
</EffectComposer>
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
await flush()
|
|
90
|
+
|
|
91
|
+
expect(ref.current!.speed).toBe(defaultSpeed)
|
|
92
|
+
|
|
93
|
+
await React.act(async () => root.render(null))
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { EffectComposer as EffectComposerImpl, TiltShiftEffect } from 'postprocessing'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { EffectComposer } from '../EffectComposer'
|
|
5
|
+
import { TiltShift } from '../effects/TiltShift'
|
|
6
|
+
import { flush, root } from './test-utils'
|
|
7
|
+
|
|
8
|
+
describe('TiltShift', () => {
|
|
9
|
+
it('applies resolutionScale at construction (previously never reached the effect at all)', async () => {
|
|
10
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
11
|
+
const ref = React.createRef<TiltShiftEffect>()
|
|
12
|
+
|
|
13
|
+
await React.act(async () =>
|
|
14
|
+
root.render(
|
|
15
|
+
<EffectComposer ref={composerRef}>
|
|
16
|
+
<TiltShift ref={ref} resolutionScale={0.25} />
|
|
17
|
+
</EffectComposer>
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
await flush()
|
|
22
|
+
|
|
23
|
+
expect(ref.current!.blurPass.resolution.scale).toBeCloseTo(0.25)
|
|
24
|
+
|
|
25
|
+
await React.act(async () => root.render(null))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('reconstructs when resolutionScale (construction-only) changes', async () => {
|
|
29
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
30
|
+
const ref = React.createRef<TiltShiftEffect>()
|
|
31
|
+
|
|
32
|
+
const render = (resolutionScale: number) =>
|
|
33
|
+
root.render(
|
|
34
|
+
<EffectComposer ref={composerRef}>
|
|
35
|
+
<TiltShift ref={ref} resolutionScale={resolutionScale} />
|
|
36
|
+
</EffectComposer>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
await React.act(async () => render(0.25))
|
|
40
|
+
await flush()
|
|
41
|
+
const first = ref.current
|
|
42
|
+
|
|
43
|
+
await React.act(async () => render(0.5))
|
|
44
|
+
await flush()
|
|
45
|
+
|
|
46
|
+
expect(ref.current).not.toBe(first)
|
|
47
|
+
expect(ref.current!.blurPass.resolution.scale).toBeCloseTo(0.5)
|
|
48
|
+
|
|
49
|
+
await React.act(async () => root.render(null))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('applies offset live, without reconstructing the effect', async () => {
|
|
53
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
54
|
+
const ref = React.createRef<TiltShiftEffect>()
|
|
55
|
+
|
|
56
|
+
const render = (offset: number) =>
|
|
57
|
+
root.render(
|
|
58
|
+
<EffectComposer ref={composerRef}>
|
|
59
|
+
<TiltShift ref={ref} offset={offset} />
|
|
60
|
+
</EffectComposer>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await React.act(async () => render(0.1))
|
|
64
|
+
await flush()
|
|
65
|
+
const first = ref.current
|
|
66
|
+
expect(first!.offset).toBeCloseTo(0.1)
|
|
67
|
+
|
|
68
|
+
await React.act(async () => render(0.2))
|
|
69
|
+
await flush()
|
|
70
|
+
|
|
71
|
+
expect(ref.current).toBe(first)
|
|
72
|
+
expect(ref.current!.offset).toBeCloseTo(0.2)
|
|
73
|
+
|
|
74
|
+
await React.act(async () => root.render(null))
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Effect, EffectComposer as EffectComposerImpl } from 'postprocessing'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Uniform } from 'three'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { createEffectComponent } from '../createEffectComponent'
|
|
6
|
+
import { EffectComposer } from '../EffectComposer'
|
|
7
|
+
import { flush, root } from './test-utils'
|
|
8
|
+
|
|
9
|
+
// Zero-arity, real accessor - the class of effect createEffectComponent
|
|
10
|
+
// targets. Matches BloomEffect's shape: `constructor({...} = {})`.
|
|
11
|
+
class FakeEffect extends Effect {
|
|
12
|
+
private _value: number
|
|
13
|
+
constructor({ value = 0 }: { value?: number } = {}) {
|
|
14
|
+
super('FakeEffect', 'mainImage() {}')
|
|
15
|
+
this._value = value
|
|
16
|
+
}
|
|
17
|
+
get value() {
|
|
18
|
+
return this._value
|
|
19
|
+
}
|
|
20
|
+
set value(v: number) {
|
|
21
|
+
this._value = v
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FakeEffectComponent = /* @__PURE__ */ createEffectComponent<typeof FakeEffect, { value?: number }>(FakeEffect)
|
|
26
|
+
|
|
27
|
+
// Options stored only in `uniforms` (mirrors WaterEffectImpl/RampEffect
|
|
28
|
+
// before this branch gave them real accessors) - `factor` here HAS a real
|
|
29
|
+
// accessor, proving that's what makes a plain JSX prop actually reach it.
|
|
30
|
+
class UniformFixtureEffect extends Effect {
|
|
31
|
+
constructor({ factor = 0 }: { factor?: number } = {}) {
|
|
32
|
+
super('UniformFixtureEffect', 'mainImage() {}', { uniforms: new Map([['factor', new Uniform(factor)]]) })
|
|
33
|
+
}
|
|
34
|
+
get factor(): number {
|
|
35
|
+
return this.uniforms.get('factor')!.value
|
|
36
|
+
}
|
|
37
|
+
set factor(v: number) {
|
|
38
|
+
this.uniforms.get('factor')!.value = v
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const UniformFixtureComponent = /* @__PURE__ */ createEffectComponent<typeof UniformFixtureEffect, { factor?: number }>(
|
|
43
|
+
UniformFixtureEffect
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
describe('createEffectComponent', () => {
|
|
47
|
+
it('constructs the effect and passes props through to the instance', async () => {
|
|
48
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
49
|
+
|
|
50
|
+
await React.act(async () =>
|
|
51
|
+
root.render(
|
|
52
|
+
<EffectComposer ref={composerRef}>
|
|
53
|
+
<FakeEffectComponent value={42} />
|
|
54
|
+
</EffectComposer>
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
await flush()
|
|
59
|
+
|
|
60
|
+
// @ts-expect-error - `effects` isn't part of the public Pass typing
|
|
61
|
+
const effect = composerRef.current!.passes[1].effects[0]
|
|
62
|
+
|
|
63
|
+
expect(effect).toBeInstanceOf(FakeEffect)
|
|
64
|
+
expect(effect.value).toBe(42)
|
|
65
|
+
|
|
66
|
+
await React.act(async () => root.render(null))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('applies a live prop without reconstructing the instance (r3f-native, no args change)', async () => {
|
|
70
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
71
|
+
const ref = React.createRef<FakeEffect>()
|
|
72
|
+
|
|
73
|
+
const render = (value: number) =>
|
|
74
|
+
root.render(
|
|
75
|
+
<EffectComposer ref={composerRef}>
|
|
76
|
+
<FakeEffectComponent ref={ref} value={value} />
|
|
77
|
+
</EffectComposer>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
await React.act(async () => render(1))
|
|
81
|
+
await flush()
|
|
82
|
+
const first = ref.current
|
|
83
|
+
expect(first!.value).toBe(1)
|
|
84
|
+
|
|
85
|
+
await React.act(async () => render(2))
|
|
86
|
+
await flush()
|
|
87
|
+
|
|
88
|
+
expect(ref.current).toBe(first)
|
|
89
|
+
expect(ref.current!.value).toBe(2)
|
|
90
|
+
|
|
91
|
+
await React.act(async () => root.render(null))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('resets a live prop to its constructor default when removed (r3f-native diffProps)', async () => {
|
|
95
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
96
|
+
const ref = React.createRef<FakeEffect>()
|
|
97
|
+
|
|
98
|
+
await React.act(async () =>
|
|
99
|
+
root.render(
|
|
100
|
+
<EffectComposer ref={composerRef}>
|
|
101
|
+
<FakeEffectComponent ref={ref} value={5} />
|
|
102
|
+
</EffectComposer>
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
await flush()
|
|
106
|
+
expect(ref.current!.value).toBe(5)
|
|
107
|
+
|
|
108
|
+
await React.act(async () =>
|
|
109
|
+
root.render(
|
|
110
|
+
<EffectComposer ref={composerRef}>
|
|
111
|
+
<FakeEffectComponent ref={ref} />
|
|
112
|
+
</EffectComposer>
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
await flush()
|
|
116
|
+
|
|
117
|
+
expect(ref.current!.value).toBe(0)
|
|
118
|
+
|
|
119
|
+
await React.act(async () => root.render(null))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('reconstructs when an explicit args prop changes, same as any other r3f element', async () => {
|
|
123
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
124
|
+
const ref = React.createRef<FakeEffect>()
|
|
125
|
+
|
|
126
|
+
const render = (value: number) =>
|
|
127
|
+
root.render(
|
|
128
|
+
<EffectComposer ref={composerRef}>
|
|
129
|
+
<FakeEffectComponent ref={ref} args={[{ value }]} />
|
|
130
|
+
</EffectComposer>
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await React.act(async () => render(1))
|
|
134
|
+
await flush()
|
|
135
|
+
const first = ref.current
|
|
136
|
+
expect(first!.value).toBe(1)
|
|
137
|
+
|
|
138
|
+
await React.act(async () => render(2))
|
|
139
|
+
await flush()
|
|
140
|
+
|
|
141
|
+
expect(ref.current).not.toBe(first)
|
|
142
|
+
expect(ref.current!.value).toBe(2)
|
|
143
|
+
|
|
144
|
+
await React.act(async () => root.render(null))
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('applies blendFunction/opacity through blendMode, not as a stray top-level property', async () => {
|
|
148
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
149
|
+
const ref = React.createRef<FakeEffect>()
|
|
150
|
+
|
|
151
|
+
await React.act(async () =>
|
|
152
|
+
root.render(
|
|
153
|
+
<EffectComposer ref={composerRef}>
|
|
154
|
+
<FakeEffectComponent ref={ref} blendFunction={7 as never} opacity={0.5} />
|
|
155
|
+
</EffectComposer>
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
await flush()
|
|
159
|
+
|
|
160
|
+
expect(ref.current!.blendMode.blendFunction).toBe(7)
|
|
161
|
+
expect(ref.current!.blendMode.opacity.value).toBe(0.5)
|
|
162
|
+
expect((ref.current as unknown as { blendFunction?: unknown }).blendFunction).toBeUndefined()
|
|
163
|
+
|
|
164
|
+
await React.act(async () => root.render(null))
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('resets blendFunction/opacity to blendMode\'s own defaults when the props are removed', async () => {
|
|
168
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
169
|
+
const ref = React.createRef<FakeEffect>()
|
|
170
|
+
|
|
171
|
+
await React.act(async () =>
|
|
172
|
+
root.render(
|
|
173
|
+
<EffectComposer ref={composerRef}>
|
|
174
|
+
<FakeEffectComponent ref={ref} />
|
|
175
|
+
</EffectComposer>
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
await flush()
|
|
179
|
+
const defaultBlendFunction = ref.current!.blendMode.blendFunction
|
|
180
|
+
const defaultOpacity = ref.current!.blendMode.opacity.value
|
|
181
|
+
|
|
182
|
+
await React.act(async () =>
|
|
183
|
+
root.render(
|
|
184
|
+
<EffectComposer ref={composerRef}>
|
|
185
|
+
<FakeEffectComponent ref={ref} blendFunction={7 as never} opacity={0.5} />
|
|
186
|
+
</EffectComposer>
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
await flush()
|
|
190
|
+
expect(ref.current!.blendMode.blendFunction).toBe(7)
|
|
191
|
+
expect(ref.current!.blendMode.opacity.value).toBe(0.5)
|
|
192
|
+
|
|
193
|
+
await React.act(async () =>
|
|
194
|
+
root.render(
|
|
195
|
+
<EffectComposer ref={composerRef}>
|
|
196
|
+
<FakeEffectComponent ref={ref} />
|
|
197
|
+
</EffectComposer>
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
await flush()
|
|
201
|
+
|
|
202
|
+
expect(ref.current!.blendMode.blendFunction).toBe(defaultBlendFunction)
|
|
203
|
+
expect(ref.current!.blendMode.opacity.value).toBe(defaultOpacity)
|
|
204
|
+
|
|
205
|
+
await React.act(async () => root.render(null))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('updates a uniforms-Map-backed prop live via its accessor, without reconstructing', async () => {
|
|
209
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
210
|
+
const ref = React.createRef<UniformFixtureEffect>()
|
|
211
|
+
|
|
212
|
+
const render = (factor: number) =>
|
|
213
|
+
root.render(
|
|
214
|
+
<EffectComposer ref={composerRef}>
|
|
215
|
+
<UniformFixtureComponent ref={ref} factor={factor} />
|
|
216
|
+
</EffectComposer>
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
await React.act(async () => render(1))
|
|
220
|
+
await flush()
|
|
221
|
+
const first = ref.current
|
|
222
|
+
expect(first!.uniforms.get('factor')!.value).toBe(1)
|
|
223
|
+
|
|
224
|
+
await React.act(async () => render(2))
|
|
225
|
+
await flush()
|
|
226
|
+
|
|
227
|
+
expect(ref.current).toBe(first)
|
|
228
|
+
expect(ref.current!.uniforms.get('factor')!.value).toBe(2)
|
|
229
|
+
|
|
230
|
+
await React.act(async () => root.render(null))
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('disposes the instance on unmount', async () => {
|
|
234
|
+
const disposeSpy = vi.spyOn(FakeEffect.prototype, 'dispose')
|
|
235
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await React.act(async () =>
|
|
239
|
+
root.render(
|
|
240
|
+
<EffectComposer ref={composerRef}>
|
|
241
|
+
<FakeEffectComponent value={1} />
|
|
242
|
+
</EffectComposer>
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
await flush()
|
|
246
|
+
|
|
247
|
+
await React.act(async () => root.render(null))
|
|
248
|
+
|
|
249
|
+
expect(disposeSpy).toHaveBeenCalledTimes(1)
|
|
250
|
+
} finally {
|
|
251
|
+
disposeSpy.mockRestore()
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('forwards a callback ref\'s own returned cleanup (React 19 ref cleanup), instead of dropping it', async () => {
|
|
256
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
257
|
+
const events: string[] = []
|
|
258
|
+
const cleanup = vi.fn(() => {
|
|
259
|
+
events.push('cleanup')
|
|
260
|
+
})
|
|
261
|
+
const callbackRef = vi.fn((instance: FakeEffect | null) => {
|
|
262
|
+
events.push(instance ? 'attach' : 'attach-null')
|
|
263
|
+
if (instance) return cleanup
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await React.act(async () =>
|
|
267
|
+
root.render(
|
|
268
|
+
<EffectComposer ref={composerRef}>
|
|
269
|
+
<FakeEffectComponent ref={callbackRef} value={1} />
|
|
270
|
+
</EffectComposer>
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
await flush()
|
|
274
|
+
|
|
275
|
+
expect(callbackRef).toHaveBeenCalledTimes(1)
|
|
276
|
+
expect(cleanup).not.toHaveBeenCalled()
|
|
277
|
+
|
|
278
|
+
await React.act(async () => root.render(null))
|
|
279
|
+
|
|
280
|
+
// React 19 ref-cleanup semantics: once a cleanup is returned, it's
|
|
281
|
+
// called directly - the callback itself is never re-invoked with null.
|
|
282
|
+
expect(cleanup).toHaveBeenCalledTimes(1)
|
|
283
|
+
expect(callbackRef).toHaveBeenCalledTimes(1)
|
|
284
|
+
expect(events).toEqual(['attach', 'cleanup'])
|
|
285
|
+
})
|
|
286
|
+
})
|