@react-three/postprocessing 3.0.4 → 3.0.5

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.
Files changed (93) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +81 -81
  3. package/dist/EffectComposer.d.ts +7 -6
  4. package/dist/Selection.d.ts +8 -8
  5. package/dist/effects/ASCII.d.ts +5 -4
  6. package/dist/effects/Autofocus.d.ts +8 -7
  7. package/dist/effects/Bloom.d.ts +2 -5
  8. package/dist/effects/BrightnessContrast.d.ts +2 -5
  9. package/dist/effects/ChromaticAberration.d.ts +7 -7
  10. package/dist/effects/ColorAverage.d.ts +7 -8
  11. package/dist/effects/ColorDepth.d.ts +2 -5
  12. package/dist/effects/Depth.d.ts +2 -5
  13. package/dist/effects/DepthOfField.d.ts +7 -18
  14. package/dist/effects/DotScreen.d.ts +2 -5
  15. package/dist/effects/FXAA.d.ts +2 -5
  16. package/dist/effects/Glitch.d.ts +4 -21
  17. package/dist/effects/GodRays.d.ts +7 -19
  18. package/dist/effects/Grid.d.ts +6 -7
  19. package/dist/effects/HueSaturation.d.ts +2 -5
  20. package/dist/effects/LUT.d.ts +4 -3
  21. package/dist/effects/LensFlare.d.ts +8 -8
  22. package/dist/effects/N8AO.d.ts +4 -2
  23. package/dist/effects/Noise.d.ts +2 -5
  24. package/dist/effects/Outline.d.ts +4 -23
  25. package/dist/effects/Pixelation.d.ts +3 -2
  26. package/dist/effects/Ramp.d.ts +1 -5
  27. package/dist/effects/SMAA.d.ts +2 -5
  28. package/dist/effects/SSAO.d.ts +7 -30
  29. package/dist/effects/ScanlineEffect.d.ts +2 -5
  30. package/dist/effects/SelectiveBloom.d.ts +5 -10
  31. package/dist/effects/Sepia.d.ts +2 -5
  32. package/dist/effects/ShockWave.d.ts +2 -5
  33. package/dist/effects/Texture.d.ts +7 -8
  34. package/dist/effects/TiltShift.d.ts +2 -5
  35. package/dist/effects/TiltShift2.d.ts +1 -5
  36. package/dist/effects/ToneMapping.d.ts +2 -6
  37. package/dist/effects/Vignette.d.ts +2 -5
  38. package/dist/effects/Water.d.ts +1 -5
  39. package/dist/index.d.ts +11 -10
  40. package/dist/index.js +855 -551
  41. package/dist/index.js.map +1 -1
  42. package/dist/tests/test-utils.d.ts +12 -0
  43. package/dist/util.d.ts +25 -17
  44. package/dist/wrapEffect.d.ts +12 -0
  45. package/package.json +73 -67
  46. package/src/EffectComposer.tsx +278 -200
  47. package/src/Selection.tsx +86 -46
  48. package/src/effects/ASCII.tsx +136 -135
  49. package/src/effects/Autofocus.tsx +175 -153
  50. package/src/effects/Bloom.tsx +6 -6
  51. package/src/effects/BrightnessContrast.tsx +4 -4
  52. package/src/effects/ChromaticAberration.tsx +30 -5
  53. package/src/effects/ColorAverage.tsx +17 -15
  54. package/src/effects/ColorDepth.tsx +4 -4
  55. package/src/effects/Depth.tsx +4 -4
  56. package/src/effects/DepthOfField.tsx +87 -89
  57. package/src/effects/DotScreen.tsx +4 -4
  58. package/src/effects/FXAA.tsx +4 -4
  59. package/src/effects/Glitch.tsx +37 -40
  60. package/src/effects/GodRays.tsx +20 -16
  61. package/src/effects/Grid.tsx +28 -21
  62. package/src/effects/HueSaturation.tsx +4 -4
  63. package/src/effects/LUT.tsx +27 -26
  64. package/src/effects/LensFlare.tsx +611 -611
  65. package/src/effects/N8AO.tsx +81 -81
  66. package/src/effects/Noise.tsx +4 -4
  67. package/src/effects/Outline.tsx +85 -117
  68. package/src/effects/Pixelation.tsx +17 -15
  69. package/src/effects/Ramp.tsx +150 -150
  70. package/src/effects/SMAA.tsx +4 -4
  71. package/src/effects/SSAO.tsx +44 -41
  72. package/src/effects/ScanlineEffect.tsx +7 -7
  73. package/src/effects/SelectiveBloom.tsx +116 -126
  74. package/src/effects/Sepia.tsx +4 -4
  75. package/src/effects/ShockWave.tsx +4 -4
  76. package/src/effects/Texture.tsx +27 -23
  77. package/src/effects/TiltShift.tsx +4 -4
  78. package/src/effects/TiltShift2.tsx +90 -90
  79. package/src/effects/ToneMapping.tsx +6 -6
  80. package/src/effects/Vignette.tsx +4 -4
  81. package/src/effects/Water.tsx +35 -35
  82. package/src/index.ts +41 -40
  83. package/src/tests/ChromaticAberration.test.tsx +31 -0
  84. package/src/tests/EffectComposer.test.tsx +911 -0
  85. package/src/tests/Outline.test.tsx +89 -0
  86. package/src/tests/Selection.test.tsx +324 -0
  87. package/src/tests/SelectiveBloom.test.tsx +118 -0
  88. package/src/tests/effects.smoke.test.tsx +234 -0
  89. package/src/tests/test-utils.tsx +96 -0
  90. package/src/tests/wrapEffect.test.tsx +209 -0
  91. package/src/util.tsx +96 -55
  92. package/src/wrapEffect.tsx +122 -0
  93. package/src/EffectComposer.test.tsx +0 -126
package/src/util.tsx CHANGED
@@ -1,55 +1,96 @@
1
- import React, { RefObject } from 'react'
2
- import { Vector2 } from 'three'
3
- import * as THREE from 'three'
4
- import { type ReactThreeFiber, type ThreeElement, extend, useThree } from '@react-three/fiber'
5
- import type { Effect, Pass, BlendFunction } from 'postprocessing'
6
-
7
- export const resolveRef = <T,>(ref: T | React.RefObject<T>) =>
8
- typeof ref === 'object' && ref != null && 'current' in ref ? ref.current : ref
9
-
10
- export type EffectConstructor = new (...args: any[]) => Effect | Pass
11
-
12
- export type EffectProps<T extends EffectConstructor> = ThreeElement<T> &
13
- ConstructorParameters<T>[0] & {
14
- blendFunction?: BlendFunction
15
- opacity?: number
16
- }
17
-
18
- let i = 0
19
- const components = new WeakMap<EffectConstructor, React.ExoticComponent<any> | string>()
20
-
21
- export const wrapEffect = <T extends EffectConstructor>(effect: T, defaults?: EffectProps<T>) =>
22
- /* @__PURE__ */ function Effect({ blendFunction = defaults?.blendFunction, opacity = defaults?.opacity, ...props }) {
23
- let Component = components.get(effect)
24
- if (!Component) {
25
- const key = `@react-three/postprocessing/${effect.name}-${i++}`
26
- extend({ [key]: effect })
27
- components.set(effect, (Component = key))
28
- }
29
-
30
- const camera = useThree((state) => state.camera)
31
- const args = React.useMemo(
32
- () => [...(defaults?.args ?? []), ...(props.args ?? [{ ...defaults, ...props }])],
33
- // eslint-disable-next-line react-hooks/exhaustive-deps
34
- [JSON.stringify(props)]
35
- )
36
-
37
- return (
38
- <Component
39
- camera={camera}
40
- blendMode-blendFunction={blendFunction}
41
- blendMode-opacity-value={opacity}
42
- {...props}
43
- args={args}
44
- />
45
- )
46
- }
47
-
48
- export const useVector2 = (props: Record<string, unknown>, key: string): THREE.Vector2 => {
49
- const value = props[key] as ReactThreeFiber.Vector2 | undefined
50
- return React.useMemo(() => {
51
- if (typeof value === 'number') return new THREE.Vector2(value, value)
52
- else if (value) return new THREE.Vector2(...(value as THREE.Vector2Tuple))
53
- else return new THREE.Vector2()
54
- }, [value])
55
- }
1
+ import { useThree, type ReactThreeFiber } from '@react-three/fiber'
2
+ import type { Selection as PPSelection } from 'postprocessing'
3
+ import { use, useEffect, useMemo, useRef, type RefObject } from 'react'
4
+ import { Object3D, Vector2, type Vector2Tuple } from 'three'
5
+ import { selectionContext } from './Selection'
6
+
7
+ // Stable reference for array-typed props defaulting to "nothing" - `= []`
8
+ // as a default parameter allocates a new array on every call, which is
9
+ // enough to retrigger any effect that depends on it.
10
+ export const EMPTY_ARRAY: never[] = []
11
+
12
+ export const resolveRef = <T,>(ref: T | RefObject<T>) =>
13
+ typeof ref === 'object' && ref != null && 'current' in ref ? ref.current : ref
14
+
15
+ /**
16
+ * Keeps a postprocessing effect's `selection` (and its render layer) in
17
+ * sync with either mode effects support: the manual
18
+ * `selection` prop (used only when there's no enclosing <Selection>), or
19
+ * the declarative Selection/Select API. The two are mutually exclusive -
20
+ * context wins when both are present.
21
+ */
22
+ export function useSelectionSync(
23
+ effect: { selection: PPSelection },
24
+ selection: Object3D | Object3D[] | RefObject<Object3D | null> | RefObject<Object3D | null>[],
25
+ selectionLayer: number
26
+ ): void {
27
+ const invalidate = useThree((state) => state.invalidate)
28
+ const api = use(selectionContext)
29
+
30
+ useEffect(() => {
31
+ effect.selection.layer = selectionLayer
32
+ invalidate()
33
+ }, [effect, invalidate, selectionLayer])
34
+
35
+ useEffect(() => {
36
+ if (api) return
37
+ const resolved = (Array.isArray(selection) ? selection.map((o) => resolveRef(o)) : [resolveRef(selection)]).filter(
38
+ (o): o is Object3D => o != null
39
+ )
40
+ if (!resolved.length) return
41
+
42
+ effect.selection.set(resolved)
43
+ invalidate()
44
+ return () => {
45
+ effect.selection.clear()
46
+ invalidate()
47
+ }
48
+ }, [effect, selection, api, invalidate])
49
+
50
+ useEffect(() => {
51
+ if (api && api.enabled && api.selected?.length) {
52
+ effect.selection.set(api.selected)
53
+ invalidate()
54
+ return () => {
55
+ effect.selection.clear()
56
+ invalidate()
57
+ }
58
+ }
59
+ }, [api, effect.selection, invalidate])
60
+ }
61
+
62
+ /**
63
+ * r3f never disposes <primitive> objects (their state may be owned outside
64
+ * React), so effects rendered that way must dispose themselves. Guards
65
+ * against double-dispose across StrictMode's dev-only mount/cleanup/mount
66
+ * cycle, where the cleanup closure re-runs against the same instance.
67
+ */
68
+ export const useDispose = <T extends { dispose?: () => void }>(instance: T): void => {
69
+ const disposedRef = useRef<WeakSet<object>>(new WeakSet())
70
+
71
+ useEffect(() => {
72
+ const disposed = disposedRef.current
73
+ return () => {
74
+ if (instance && typeof instance === 'object' && !disposed.has(instance)) {
75
+ disposed.add(instance)
76
+ instance.dispose?.()
77
+ }
78
+ }
79
+ }, [instance])
80
+ }
81
+
82
+ export const useVector2 = (props: Record<string, unknown>, key: string): Vector2 => {
83
+ const value = props[key] as ReactThreeFiber.Vector2 | undefined
84
+
85
+ return useMemo(() => {
86
+ if (typeof value === 'number') {
87
+ return new Vector2(value, value)
88
+ }
89
+
90
+ if (value) {
91
+ return new Vector2(...(value as Vector2Tuple))
92
+ }
93
+
94
+ return new Vector2()
95
+ }, [value])
96
+ }
@@ -0,0 +1,122 @@
1
+ import { extend, useThree } from '@react-three/fiber'
2
+ import type { BlendFunction, Effect, Pass } from 'postprocessing'
3
+ import { useMemo, type ExoticComponent, type JSX, type Ref } from 'react'
4
+
5
+ export type EffectConstructor = new (...args: any[]) => Effect | Pass
6
+
7
+ // Handles three ConstructorParameters<T> shapes: required first param
8
+ // (P), optional first param (Partial<P> — some effects in postprocessing
9
+ // type inner fields as required even though the whole object is
10
+ // omittable), and no params at all ({})
11
+ type FirstConstructorParam<T extends EffectConstructor> =
12
+ ConstructorParameters<T> extends [infer P, ...any[]]
13
+ ? P
14
+ : ConstructorParameters<T> extends [(infer P)?, ...any[]]
15
+ ? Partial<P>
16
+ : {}
17
+
18
+ export type EffectProps<T extends EffectConstructor> = FirstConstructorParam<T> & {
19
+ ref?: Ref<InstanceType<T>>
20
+ blendFunction?: BlendFunction
21
+ opacity?: number
22
+ args?: ConstructorParameters<T>
23
+ }
24
+
25
+ let i = 0
26
+
27
+ const components = new WeakMap<EffectConstructor, ExoticComponent<any> | string>()
28
+
29
+ const identityTokens = new WeakMap<object, number>()
30
+ let nextIdentityToken = 0
31
+
32
+ // Same object in, same token out — lets otherwise-opaque values (textures,
33
+ // refs, any class instance) still change the fingerprint when swapped for
34
+ // a different instance, without walking into them.
35
+ function identityOf(value: object): number {
36
+ let token = identityTokens.get(value)
37
+ if (token === undefined) {
38
+ token = nextIdentityToken++
39
+ identityTokens.set(value, token)
40
+ }
41
+ return token
42
+ }
43
+
44
+ // Walks props into a JSON-safe fingerprint for the args memo below. Two
45
+ // things JSON.stringify can't be trusted with here:
46
+ // - it throws on cycles (refs, or any three.js object with a back-
47
+ // reference), so cycles are cut off with a WeakSet guard instead.
48
+ // - it calls a value's own toJSON first if present, which for e.g.
49
+ // THREE.Texture re-encodes the whole image to a base64 data URL on
50
+ // every single render (measured ~39ms for a 512x512 texture) just to
51
+ // throw the result away. Reading properties directly sidesteps that.
52
+ // - reading properties directly instead means typed arrays need their
53
+ // own case: Object.keys() on one returns every numeric index, so
54
+ // walking a texture's backing buffer element-by-element is even
55
+ // slower (~340ms for the same texture) than the toJSON it replaces.
56
+ // Identity is enough — same buffer means unchanged.
57
+ function fingerprint(value: unknown, seen: WeakSet<object>): unknown {
58
+ if (value === null || typeof value !== 'object') return value
59
+ if (seen.has(value)) return '[Circular]'
60
+
61
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
62
+ return identityOf(value)
63
+ }
64
+
65
+ seen.add(value)
66
+
67
+ if (Array.isArray(value)) {
68
+ return value.map((item) => fingerprint(item, seen))
69
+ }
70
+
71
+ const out: Record<string, unknown> = {}
72
+ for (const key of Object.keys(value).sort()) {
73
+ out[key] = fingerprint((value as Record<string, unknown>)[key], seen)
74
+ }
75
+ return out
76
+ }
77
+
78
+ function stableStringify(value: unknown): string {
79
+ return JSON.stringify(fingerprint(value, new WeakSet()))
80
+ }
81
+
82
+ export function wrapEffect<T extends EffectConstructor>(
83
+ effect: T,
84
+ defaults?: EffectProps<T>
85
+ ): (props: EffectProps<T>) => JSX.Element {
86
+ return function WrappedEffect({
87
+ ref,
88
+ blendFunction = defaults?.blendFunction,
89
+ opacity = defaults?.opacity,
90
+ ...props
91
+ }) {
92
+ let Component = components.get(effect)
93
+
94
+ if (!Component) {
95
+ const key = `@react-three/postprocessing/${effect.name}-${i++}`
96
+ extend({ [key]: effect })
97
+ components.set(effect, (Component = key))
98
+ }
99
+
100
+ const camera = useThree((state) => state.camera)
101
+
102
+ const args = useMemo(
103
+ () => [
104
+ ...((defaults?.args as unknown as any[]) ?? []),
105
+ ...((props.args as unknown as any[]) ?? [{ ...defaults, ...props }]),
106
+ ],
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ [stableStringify(props)]
109
+ )
110
+
111
+ return (
112
+ <Component
113
+ camera={camera}
114
+ blendMode-blendFunction={blendFunction}
115
+ blendMode-opacity-value={opacity}
116
+ {...props}
117
+ args={args}
118
+ ref={ref}
119
+ />
120
+ )
121
+ }
122
+ }
@@ -1,126 +0,0 @@
1
- import * as React from 'react'
2
- import * as THREE from 'three'
3
- import { vi, describe, it, expect } from 'vitest'
4
- import { extend, createRoot, act } from '@react-three/fiber'
5
- import { EffectComposer } from './EffectComposer'
6
- import { EffectComposer as EffectComposerImpl, RenderPass, Pass, Effect, EffectPass } from 'postprocessing'
7
-
8
- // Let React know that we'll be testing effectful components
9
- declare global {
10
- var IS_REACT_ACT_ENVIRONMENT: boolean
11
- }
12
- global.IS_REACT_ACT_ENVIRONMENT = true
13
-
14
- // Create virtual R3F root for testing
15
- extend(THREE as any)
16
- const root = createRoot({
17
- style: {} as CSSStyleDeclaration,
18
- addEventListener: (() => {}) as any,
19
- removeEventListener: (() => {}) as any,
20
- width: 1280,
21
- height: 800,
22
- clientWidth: 1280,
23
- clientHeight: 800,
24
- getContext: (() =>
25
- new Proxy(
26
- {},
27
- {
28
- get(_target, prop) {
29
- switch (prop) {
30
- case 'getParameter':
31
- return () => 'WebGL 2' // GL_VERSION
32
- case 'getExtension':
33
- return () => ({}) // EXT_blend_minmax
34
- case 'getContextAttributes':
35
- return () => ({ alpha: true })
36
- case 'getShaderPrecisionFormat':
37
- return () => ({ rangeMin: 1, rangeMax: 1, precision: 1 })
38
- default:
39
- return () => {}
40
- }
41
- },
42
- }
43
- )) as any,
44
- } satisfies Partial<HTMLCanvasElement> as HTMLCanvasElement)
45
- root.configure({ frameloop: 'never' })
46
-
47
- const EFFECT_SHADER = 'mainImage() {}'
48
-
49
- describe('EffectComposer', () => {
50
- it('should merge effects together', async () => {
51
- const composerRef = React.createRef<EffectComposerImpl>()
52
-
53
- const effectA = new Effect('A', EFFECT_SHADER)
54
- const effectB = new Effect('B', EFFECT_SHADER)
55
- const effectC = new Effect('C', EFFECT_SHADER)
56
- const passA = new Pass()
57
- const passB = new Pass()
58
-
59
- // Forward order
60
- await act(async () =>
61
- root.render(
62
- <EffectComposer ref={composerRef}>
63
- {/* EffectPass(effectA, effectB) */}
64
- <primitive object={effectA} />
65
- <primitive object={effectB} />
66
- {/* PassA */}
67
- <primitive object={passA} />
68
- {/* EffectPass(effectC) */}
69
- <primitive object={effectC} />
70
- {/* PassB */}
71
- <primitive object={passB} />
72
- </EffectComposer>
73
- )
74
- )
75
- expect(composerRef.current!.passes.map((p) => p.constructor)).toStrictEqual([
76
- RenderPass,
77
- EffectPass,
78
- Pass,
79
- EffectPass,
80
- Pass,
81
- ])
82
- // @ts-expect-error
83
- expect((composerRef.current!.passes[1] as EffectPass).effects).toStrictEqual([effectA, effectB])
84
- expect(composerRef.current!.passes[2]).toBe(passA)
85
- // @ts-expect-error
86
- expect((composerRef.current!.passes[3] as EffectPass).effects).toStrictEqual([effectC])
87
- expect(composerRef.current!.passes[4]).toBe(passB)
88
-
89
- // NOTE: instance children ordering is unstable until R3F v9, so we remount from scratch
90
- await act(async () => root.render(null))
91
-
92
- // Reverse order
93
- await act(async () =>
94
- root.render(
95
- <EffectComposer ref={composerRef}>
96
- {/* PassB */}
97
- <primitive object={passB} />
98
- {/* EffectPass(effectC) */}
99
- <primitive object={effectC} />
100
- {/* PassA */}
101
- <primitive object={passA} />
102
- {/* EffectPass(effectB, effectA) */}
103
- <primitive object={effectB} />
104
- <primitive object={effectA} />
105
- </EffectComposer>
106
- )
107
- )
108
- expect(composerRef.current!.passes.map((p) => p.constructor)).toStrictEqual([
109
- RenderPass,
110
- Pass,
111
- EffectPass,
112
- Pass,
113
- EffectPass,
114
- ])
115
- expect(composerRef.current!.passes[1]).toBe(passB)
116
- // @ts-expect-error
117
- expect((composerRef.current!.passes[2] as EffectPass).effects).toStrictEqual([effectC])
118
- expect(composerRef.current!.passes[3]).toBe(passA)
119
- // @ts-expect-error
120
- expect((composerRef.current!.passes[4] as EffectPass).effects).toStrictEqual([effectB, effectA])
121
- })
122
-
123
- it.skip('should split convolution effects', async () => {
124
- await act(async () => root.render(null))
125
- })
126
- })