@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.
Files changed (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -81
  3. package/dist/EffectComposer.d.ts +14 -6
  4. package/dist/EffectGroup.d.ts +9 -0
  5. package/dist/Selection.d.ts +8 -8
  6. package/dist/createEffectComponent.d.ts +30 -0
  7. package/dist/effects/ASCII.d.ts +29 -7
  8. package/dist/effects/Autofocus.d.ts +8 -7
  9. package/dist/effects/Bloom.d.ts +10 -5
  10. package/dist/effects/BrightnessContrast.d.ts +15 -5
  11. package/dist/effects/ChromaticAberration.d.ts +30 -7
  12. package/dist/effects/ColorAverage.d.ts +7 -8
  13. package/dist/effects/ColorDepth.d.ts +8 -5
  14. package/dist/effects/Depth.d.ts +13 -5
  15. package/dist/effects/DepthOfField.d.ts +7 -18
  16. package/dist/effects/DotScreen.d.ts +15 -5
  17. package/dist/effects/FXAA.d.ts +11 -5
  18. package/dist/effects/Glitch.d.ts +17 -29
  19. package/dist/effects/GodRays.d.ts +7 -19
  20. package/dist/effects/Grid.d.ts +8 -8
  21. package/dist/effects/HueSaturation.d.ts +15 -5
  22. package/dist/effects/LUT.d.ts +4 -3
  23. package/dist/effects/LensFlare.d.ts +52 -11
  24. package/dist/effects/Noise.d.ts +8 -5
  25. package/dist/effects/Outline.d.ts +9 -25
  26. package/dist/effects/Pixelation.d.ts +3 -2
  27. package/dist/effects/Ramp.d.ts +58 -10
  28. package/dist/effects/SMAA.d.ts +10 -5
  29. package/dist/effects/SSAO.d.ts +7 -30
  30. package/dist/effects/ScanlineEffect.d.ts +13 -5
  31. package/dist/effects/SelectiveBloom.d.ts +5 -10
  32. package/dist/effects/Sepia.d.ts +13 -5
  33. package/dist/effects/ShockWave.d.ts +12 -5
  34. package/dist/effects/Texture.d.ts +7 -8
  35. package/dist/effects/TiltShift.d.ts +10 -5
  36. package/dist/effects/TiltShift2.d.ts +44 -8
  37. package/dist/effects/ToneMapping.d.ts +9 -7
  38. package/dist/effects/Vignette.d.ts +19 -5
  39. package/dist/effects/Water.d.ts +17 -5
  40. package/dist/index.d.ts +15 -11
  41. package/dist/index.js +1841 -45
  42. package/dist/index.js.map +1 -1
  43. package/dist/passes/DepthPicking.d.ts +11 -0
  44. package/dist/{effects → passes}/N8AO.d.ts +5 -2
  45. package/dist/util.d.ts +19 -17
  46. package/dist/wrapEffect.d.ts +12 -0
  47. package/package.json +74 -67
  48. package/src/EffectComposer.tsx +379 -200
  49. package/src/EffectGroup.tsx +74 -0
  50. package/src/Selection.tsx +86 -46
  51. package/src/createEffectComponent.tsx +71 -0
  52. package/src/effects/ASCII.tsx +183 -135
  53. package/src/effects/Autofocus.tsx +122 -153
  54. package/src/effects/Bloom.tsx +34 -6
  55. package/src/effects/BrightnessContrast.tsx +7 -4
  56. package/src/effects/ChromaticAberration.tsx +22 -5
  57. package/src/effects/ColorAverage.tsx +4 -15
  58. package/src/effects/ColorDepth.tsx +25 -4
  59. package/src/effects/Depth.tsx +6 -4
  60. package/src/effects/DepthOfField.tsx +109 -89
  61. package/src/effects/DotScreen.tsx +7 -4
  62. package/src/effects/FXAA.tsx +6 -4
  63. package/src/effects/Glitch.tsx +32 -40
  64. package/src/effects/GodRays.tsx +104 -16
  65. package/src/effects/Grid.tsx +27 -21
  66. package/src/effects/HueSaturation.tsx +7 -4
  67. package/src/effects/LUT.tsx +25 -26
  68. package/src/effects/LensFlare.tsx +765 -611
  69. package/src/effects/Noise.tsx +16 -4
  70. package/src/effects/Outline.tsx +84 -117
  71. package/src/effects/Pixelation.tsx +20 -15
  72. package/src/effects/Ramp.tsx +239 -150
  73. package/src/effects/SMAA.tsx +20 -4
  74. package/src/effects/SSAO.tsx +152 -41
  75. package/src/effects/ScanlineEffect.tsx +7 -7
  76. package/src/effects/SelectiveBloom.tsx +98 -126
  77. package/src/effects/Sepia.tsx +6 -4
  78. package/src/effects/ShockWave.tsx +32 -4
  79. package/src/effects/Texture.tsx +28 -23
  80. package/src/effects/TiltShift.tsx +32 -4
  81. package/src/effects/TiltShift2.tsx +156 -90
  82. package/src/effects/ToneMapping.tsx +19 -6
  83. package/src/effects/Vignette.tsx +7 -4
  84. package/src/effects/Water.tsx +50 -35
  85. package/src/index.ts +43 -40
  86. package/src/passes/DepthPicking.tsx +63 -0
  87. package/src/passes/N8AO.tsx +105 -0
  88. package/src/tests/Autofocus.test.tsx +38 -0
  89. package/src/tests/Bloom.test.tsx +76 -0
  90. package/src/tests/ChromaticAberration.test.tsx +81 -0
  91. package/src/tests/ColorDepth.test.tsx +71 -0
  92. package/src/tests/DepthOfField.test.tsx +130 -0
  93. package/src/tests/DepthPicking.test.tsx +250 -0
  94. package/src/tests/EffectComposer.test.tsx +1404 -0
  95. package/src/tests/EffectGroup.test.tsx +337 -0
  96. package/src/tests/Glitch.test.tsx +56 -0
  97. package/src/tests/GodRays.test.tsx +101 -0
  98. package/src/tests/Grid.test.tsx +34 -0
  99. package/src/tests/LUT.test.tsx +64 -0
  100. package/src/tests/N8AO.test.tsx +92 -0
  101. package/src/tests/Outline.test.tsx +211 -0
  102. package/src/tests/SSAO.test.tsx +140 -0
  103. package/src/tests/Selection.test.tsx +324 -0
  104. package/src/tests/SelectiveBloom.test.tsx +166 -0
  105. package/src/tests/ShockWave.test.tsx +95 -0
  106. package/src/tests/TiltShift.test.tsx +76 -0
  107. package/src/tests/createEffectComponent.test.tsx +286 -0
  108. package/src/tests/effects.smoke.test.tsx +238 -0
  109. package/src/tests/test-utils.tsx +96 -0
  110. package/src/tests/wrapEffect.test.tsx +209 -0
  111. package/src/util.tsx +221 -55
  112. package/src/wrapEffect.tsx +123 -0
  113. package/src/EffectComposer.test.tsx +0 -126
  114. package/src/effects/N8AO.tsx +0 -81
package/src/Selection.tsx CHANGED
@@ -1,46 +1,86 @@
1
- import * as THREE from 'three'
2
- import React, { createContext, useState, useContext, useEffect, useRef, useMemo } from 'react'
3
- import { type ThreeElements } from '@react-three/fiber'
4
-
5
- export type Api = {
6
- selected: THREE.Object3D[]
7
- select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>
8
- enabled: boolean
9
- }
10
- export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
11
- enabled?: boolean
12
- }
13
-
14
- export const selectionContext = /* @__PURE__ */ createContext<Api | null>(null)
15
-
16
- export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) {
17
- const [selected, select] = useState<THREE.Object3D[]>([])
18
- const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
19
- return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
20
- }
21
-
22
- export function Select({ enabled = false, children, ...props }: SelectApi) {
23
- const group = useRef<THREE.Group>(null!)
24
- const api = useContext(selectionContext)
25
- useEffect(() => {
26
- if (api && enabled) {
27
- let changed = false
28
- const current: THREE.Object3D[] = []
29
- group.current.traverse((o) => {
30
- o.type === 'Mesh' && current.push(o)
31
- if (api.selected.indexOf(o) === -1) changed = true
32
- })
33
- if (changed) {
34
- api.select((state) => [...state, ...current])
35
- return () => {
36
- api.select((state) => state.filter((selected) => !current.includes(selected)))
37
- }
38
- }
39
- }
40
- }, [enabled, children, api])
41
- return (
42
- <group ref={group} {...props}>
43
- {children}
44
- </group>
45
- )
46
- }
1
+ import { type ThreeElements } from '@react-three/fiber'
2
+ import {
3
+ createContext,
4
+ use,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type Dispatch,
10
+ type ReactNode,
11
+ type SetStateAction,
12
+ } from 'react'
13
+ import { type Group, type Line, type Mesh, type Object3D, type Points } from 'three'
14
+
15
+ export type Api = {
16
+ selected: Object3D[]
17
+ select: Dispatch<SetStateAction<Object3D[]>>
18
+ enabled: boolean
19
+ }
20
+ export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
21
+ enabled?: boolean
22
+ }
23
+
24
+ export const selectionContext = /* @__PURE__ */ createContext<Api | null>(null)
25
+
26
+ export function Selection({ children, enabled = true }: { enabled?: boolean; children: ReactNode }) {
27
+ const [selected, select] = useState<Object3D[]>([])
28
+ const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
29
+ return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
30
+ }
31
+
32
+ // Covers Mesh/Line/Points subclasses too, unlike `.type`.
33
+ function isSelectable(object: Object3D): boolean {
34
+ const o = object as Partial<Mesh & Line & Points>
35
+ return !!(o.isMesh || o.isLine || o.isPoints)
36
+ }
37
+
38
+ export function Select({ enabled = false, children, ...props }: SelectApi) {
39
+ const group = useRef<Group>(null!)
40
+ // Stable, unlike the context value - avoids retriggering off our own write.
41
+ const select = use(selectionContext)?.select
42
+ const claimed = useRef<Object3D[]>([])
43
+
44
+ useEffect(() => {
45
+ if (!select) return
46
+
47
+ const current: Object3D[] = []
48
+ if (enabled) {
49
+ group.current.traverse((o) => {
50
+ if (isSelectable(o)) current.push(o)
51
+ })
52
+ }
53
+
54
+ const previouslyClaimed = claimed.current
55
+ claimed.current = current
56
+
57
+ select((prev) => {
58
+ const prevSet = new Set(prev)
59
+ const currentSet = new Set(current)
60
+ const toAdd = current.filter((o) => !prevSet.has(o))
61
+ const toRemove = previouslyClaimed.filter((o) => !currentSet.has(o) && prevSet.has(o))
62
+ if (!toAdd.length && !toRemove.length) return prev
63
+ const toRemoveSet = toRemove.length ? new Set(toRemove) : null
64
+ const kept = toRemoveSet ? prev.filter((o) => !toRemoveSet.has(o)) : prev
65
+ return toAdd.length ? [...kept, ...toAdd] : kept
66
+ })
67
+ }, [enabled, children, select])
68
+
69
+ // Separate from the effect above so unmount cleanup doesn't fire on every enabled/children change.
70
+ useEffect(() => {
71
+ return () => {
72
+ if (!select || !claimed.current.length) return
73
+ const stillClaimed = new Set(claimed.current)
74
+ select((prev) => {
75
+ const next = prev.filter((o) => !stillClaimed.has(o))
76
+ return next.length !== prev.length ? next : prev
77
+ })
78
+ }
79
+ }, [select])
80
+
81
+ return (
82
+ <group ref={group} {...props}>
83
+ {children}
84
+ </group>
85
+ )
86
+ }
@@ -0,0 +1,71 @@
1
+ import { extend, useThree } from '@react-three/fiber'
2
+ import type { BlendFunction, Effect, Pass } from 'postprocessing'
3
+ import type { ExoticComponent, JSX, Ref } from 'react'
4
+ import { useRef } from 'react'
5
+ import { useLiveDefaults, useMergeRefs } from './util'
6
+
7
+ export type EffectConstructor = new (...args: any[]) => Effect | Pass
8
+
9
+ // The effect's own options type, straight off its constructor - postprocessing
10
+ // already types every effect's sole options object precisely (either inline or,
11
+ // like BloomEffect, as a named exported type); this just strips the `| undefined`
12
+ // that comes from the parameter being optional.
13
+ export type EffectOptions<T extends EffectConstructor> = NonNullable<ConstructorParameters<T>[0]>
14
+
15
+ const components = new WeakMap<EffectConstructor, ExoticComponent<any> | string>()
16
+ let i = 0
17
+
18
+ const BLEND_KEYS = ['blendMode-blendFunction', 'blendMode-opacity-value']
19
+
20
+ /**
21
+ * Registers `effect` as a JSX intrinsic once per class and returns a
22
+ * component that renders it. Everything else - construction from `args`,
23
+ * live prop application (with the same Color/Vector coercion and reset-
24
+ * to-default on removal any r3f element gets), disposal - is r3f's own
25
+ * reconciler, same rules as `<mesh>`/`<meshStandardMaterial>`. Only fits
26
+ * effects whose constructor works with zero arguments (`new Effect()`) -
27
+ * r3f's own reset-on-removal falls back to `0` otherwise, which is wrong
28
+ * for anything non-numeric. Effects that require e.g. scene/camera stay
29
+ * hand-rolled (see Outline.tsx, SelectiveBloom.tsx, ShockWave.tsx).
30
+ *
31
+ * `blendFunction`/`opacity` are pierced through to `blendMode-*` - every
32
+ * `Effect` has them on a nested `blendMode`, not on the effect itself, so a
33
+ * plain top-level prop would silently land on a stray, unread property.
34
+ * Applied via useLiveDefaults, not as plain JSX props: BlendMode's own
35
+ * constructor requires `blendFunction` (no default), so its constructor
36
+ * length isn't 0 either, and r3f's native reset-on-removal falls back to
37
+ * `changedProps[prop] = 0` - which is BlendFunction.SKIP, not a merely
38
+ * "wrong" blend function but one that hides the effect entirely.
39
+ */
40
+ export function createEffectComponent<T extends EffectConstructor, P extends object>(
41
+ effect: T
42
+ ): (
43
+ props: P & {
44
+ blendFunction?: BlendFunction
45
+ opacity?: number
46
+ args?: ConstructorParameters<T>
47
+ ref?: Ref<InstanceType<T>>
48
+ }
49
+ ) => JSX.Element {
50
+ return function EffectComponent({ blendFunction, opacity, ref, ...props }: any) {
51
+ let Component = components.get(effect)
52
+
53
+ if (!Component) {
54
+ const key = `@react-three/postprocessing/${effect.name}-${i++}`
55
+ extend({ [key]: effect })
56
+ components.set(effect, (Component = key))
57
+ }
58
+
59
+ const camera = useThree((state) => state.camera)
60
+ const localRef = useRef<InstanceType<T>>(null)
61
+ const setRef = useMergeRefs(localRef, ref)
62
+
63
+ useLiveDefaults(
64
+ localRef,
65
+ { 'blendMode-blendFunction': blendFunction, 'blendMode-opacity-value': opacity },
66
+ BLEND_KEYS
67
+ )
68
+
69
+ return <Component ref={setRef} camera={camera} {...props} />
70
+ }
71
+ }
@@ -1,135 +1,183 @@
1
- // From: https://github.com/emilwidlund/ASCII
2
- // https://twitter.com/emilwidlund/status/1652386482420609024
3
-
4
- import { forwardRef, useMemo } from 'react'
5
- import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
6
- import { Effect } from 'postprocessing'
7
-
8
- const fragment = `
9
- uniform sampler2D uCharacters;
10
- uniform float uCharactersCount;
11
- uniform float uCellSize;
12
- uniform bool uInvert;
13
- uniform vec3 uColor;
14
-
15
- const vec2 SIZE = vec2(16.);
16
-
17
- vec3 greyscale(vec3 color, float strength) {
18
- float g = dot(color, vec3(0.299, 0.587, 0.114));
19
- return mix(color, vec3(g), strength);
20
- }
21
-
22
- vec3 greyscale(vec3 color) {
23
- return greyscale(color, 1.0);
24
- }
25
-
26
- void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
27
- vec2 cell = resolution / uCellSize;
28
- vec2 grid = 1.0 / cell;
29
- vec2 pixelizedUV = grid * (0.5 + floor(uv / grid));
30
- vec4 pixelized = texture2D(inputBuffer, pixelizedUV);
31
- float greyscaled = greyscale(pixelized.rgb).r;
32
-
33
- if (uInvert) {
34
- greyscaled = 1.0 - greyscaled;
35
- }
36
-
37
- float characterIndex = floor((uCharactersCount - 1.0) * greyscaled);
38
- vec2 characterPosition = vec2(mod(characterIndex, SIZE.x), floor(characterIndex / SIZE.y));
39
- vec2 offset = vec2(characterPosition.x, -characterPosition.y) / SIZE;
40
- vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset;
41
- vec4 asciiCharacter = texture2D(uCharacters, charUV);
42
-
43
- asciiCharacter.rgb = uColor * asciiCharacter.r;
44
- asciiCharacter.a = pixelized.a;
45
- outputColor = asciiCharacter;
46
- }
47
- `
48
-
49
- interface IASCIIEffectProps {
50
- font?: string
51
- characters?: string
52
- fontSize?: number
53
- cellSize?: number
54
- color?: string
55
- invert?: boolean
56
- }
57
-
58
- class ASCIIEffect extends Effect {
59
- constructor({
60
- font = 'arial',
61
- characters = ` .:,'-^=*+?!|0#X%WM@`,
62
- fontSize = 54,
63
- cellSize = 16,
64
- color = '#ffffff',
65
- invert = false,
66
- }: IASCIIEffectProps = {}) {
67
- const uniforms = new Map<string, Uniform>([
68
- ['uCharacters', new Uniform(new Texture())],
69
- ['uCellSize', new Uniform(cellSize)],
70
- ['uCharactersCount', new Uniform(characters.length)],
71
- ['uColor', new Uniform(new Color(color))],
72
- ['uInvert', new Uniform(invert)],
73
- ])
74
-
75
- super('ASCIIEffect', fragment, { uniforms })
76
-
77
- const charactersTextureUniform = this.uniforms.get('uCharacters')
78
-
79
- if (charactersTextureUniform) {
80
- charactersTextureUniform.value = this.createCharactersTexture(characters, font, fontSize)
81
- }
82
- }
83
-
84
- /** Draws the characters on a Canvas and returns a texture */
85
- public createCharactersTexture(characters: string, font: string, fontSize: number): Texture {
86
- const canvas = document.createElement('canvas')
87
- const SIZE = 1024
88
- const MAX_PER_ROW = 16
89
- const CELL = SIZE / MAX_PER_ROW
90
-
91
- canvas.width = canvas.height = SIZE
92
- const texture = new CanvasTexture(canvas, undefined, RepeatWrapping, RepeatWrapping, NearestFilter, NearestFilter)
93
- const context = canvas.getContext('2d')
94
-
95
- if (!context) {
96
- throw new Error('Context not available')
97
- }
98
-
99
- context.clearRect(0, 0, SIZE, SIZE)
100
- context.font = `${fontSize}px ${font}`
101
- context.textAlign = 'center'
102
- context.textBaseline = 'middle'
103
- context.fillStyle = '#fff'
104
-
105
- for (let i = 0; i < characters.length; i++) {
106
- const char = characters[i]
107
- const x = i % MAX_PER_ROW
108
- const y = Math.floor(i / MAX_PER_ROW)
109
- context.fillText(char, x * CELL + CELL / 2, y * CELL + CELL / 2)
110
- }
111
-
112
- texture.needsUpdate = true
113
- return texture
114
- }
115
- }
116
-
117
- export const ASCII = /* @__PURE__ */ forwardRef<ASCIIEffect, IASCIIEffectProps>(
118
- (
119
- {
120
- font = 'arial',
121
- characters = ` .:,'-^=*+?!|0#X%WM@`,
122
- fontSize = 54,
123
- cellSize = 16,
124
- color = '#ffffff',
125
- invert = false,
126
- },
127
- fref
128
- ) => {
129
- const effect = useMemo(
130
- () => new ASCIIEffect({ characters, font, fontSize, cellSize, color, invert }),
131
- [characters, fontSize, cellSize, color, invert, font]
132
- )
133
- return <primitive ref={fref} object={effect} />
134
- }
135
- )
1
+ // From: https://github.com/emilwidlund/ASCII
2
+ // https://twitter.com/emilwidlund/status/1652386482420609024
3
+
4
+ import { Effect } from 'postprocessing'
5
+ import type { Ref } from 'react'
6
+ import { CanvasTexture, Color, type ColorRepresentation, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
7
+ import { createEffectComponent } from '../createEffectComponent'
8
+
9
+ const fragment = /* glsl */ `
10
+ uniform sampler2D uCharacters;
11
+ uniform float uCharactersCount;
12
+ uniform float uCellSize;
13
+ uniform bool uInvert;
14
+ uniform vec3 uColor;
15
+
16
+ const vec2 SIZE = vec2(16.);
17
+
18
+ vec3 greyscale(vec3 color, float strength) {
19
+ float g = dot(color, vec3(0.299, 0.587, 0.114));
20
+ return mix(color, vec3(g), strength);
21
+ }
22
+
23
+ vec3 greyscale(vec3 color) {
24
+ return greyscale(color, 1.0);
25
+ }
26
+
27
+ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
28
+ vec2 cell = resolution / uCellSize;
29
+ vec2 grid = 1.0 / cell;
30
+ vec2 pixelizedUV = grid * (0.5 + floor(uv / grid));
31
+ vec4 pixelized = texture2D(inputBuffer, pixelizedUV);
32
+ float greyscaled = greyscale(pixelized.rgb).r;
33
+
34
+ if (uInvert) {
35
+ greyscaled = 1.0 - greyscaled;
36
+ }
37
+
38
+ float characterIndex = floor((uCharactersCount - 1.0) * greyscaled);
39
+ vec2 characterPosition = vec2(mod(characterIndex, SIZE.x), floor(characterIndex / SIZE.y));
40
+ vec2 offset = vec2(characterPosition.x, -characterPosition.y) / SIZE;
41
+ vec2 charUV = mod(uv * (cell / SIZE), 1.0 / SIZE) - vec2(0., 1.0 / SIZE) + offset;
42
+ vec4 asciiCharacter = texture2D(uCharacters, charUV);
43
+
44
+ asciiCharacter.rgb = uColor * asciiCharacter.r;
45
+ asciiCharacter.a = pixelized.a;
46
+ outputColor = asciiCharacter;
47
+ }
48
+ `
49
+
50
+ export type ASCIIProps = {
51
+ font?: string
52
+ characters?: string
53
+ fontSize?: number
54
+ cellSize?: number
55
+ color?: ColorRepresentation
56
+ invert?: boolean
57
+ ref?: Ref<ASCIIEffect>
58
+ }
59
+
60
+ class ASCIIEffect extends Effect {
61
+ private _font: string
62
+ private _characters: string
63
+ private _fontSize: number
64
+
65
+ constructor({
66
+ font = 'arial',
67
+ characters = ` .:,'-^=*+?!|0#X%WM@`,
68
+ fontSize = 54,
69
+ cellSize = 16,
70
+ color = '#ffffff',
71
+ invert = false,
72
+ }: Omit<ASCIIProps, 'ref'> = {}) {
73
+ const uniforms = new Map<string, Uniform>([
74
+ ['uCharacters', new Uniform(new Texture())],
75
+ ['uCellSize', new Uniform(cellSize)],
76
+ ['uCharactersCount', new Uniform(characters.length)],
77
+ ['uColor', new Uniform(new Color(color))],
78
+ ['uInvert', new Uniform(invert)],
79
+ ])
80
+
81
+ super('ASCIIEffect', fragment, { uniforms })
82
+
83
+ this._font = font
84
+ this._characters = characters
85
+ this._fontSize = fontSize
86
+ this.updateCharactersTexture()
87
+ }
88
+
89
+ get cellSize(): number {
90
+ return this.uniforms.get('uCellSize')!.value
91
+ }
92
+
93
+ set cellSize(value: number) {
94
+ this.uniforms.get('uCellSize')!.value = value
95
+ }
96
+
97
+ get invert(): boolean {
98
+ return this.uniforms.get('uInvert')!.value
99
+ }
100
+
101
+ set invert(value: boolean) {
102
+ this.uniforms.get('uInvert')!.value = value
103
+ }
104
+
105
+ get color(): Color {
106
+ return this.uniforms.get('uColor')!.value
107
+ }
108
+
109
+ set color(value: ColorRepresentation) {
110
+ this.uniforms.get('uColor')!.value.set(value)
111
+ }
112
+
113
+ get font(): string {
114
+ return this._font
115
+ }
116
+
117
+ set font(value: string) {
118
+ this._font = value
119
+ this.updateCharactersTexture()
120
+ }
121
+
122
+ get characters(): string {
123
+ return this._characters
124
+ }
125
+
126
+ set characters(value: string) {
127
+ this._characters = value
128
+ this.uniforms.get('uCharactersCount')!.value = value.length
129
+ this.updateCharactersTexture()
130
+ }
131
+
132
+ get fontSize(): number {
133
+ return this._fontSize
134
+ }
135
+
136
+ set fontSize(value: number) {
137
+ this._fontSize = value
138
+ this.updateCharactersTexture()
139
+ }
140
+
141
+ // Regenerates the character atlas texture - characters/font/fontSize have
142
+ // no cheaper live update path, unlike the plain-uniform props above.
143
+ private updateCharactersTexture(): void {
144
+ const uniform = this.uniforms.get('uCharacters')!
145
+ const previous = uniform.value as Texture
146
+ uniform.value = this.createCharactersTexture(this._characters, this._font, this._fontSize)
147
+ previous.dispose()
148
+ }
149
+
150
+ /** Draws the characters on a Canvas and returns a texture */
151
+ public createCharactersTexture(characters: string, font: string, fontSize: number): Texture {
152
+ const canvas = document.createElement('canvas')
153
+ const SIZE = 1024
154
+ const MAX_PER_ROW = 16
155
+ const CELL = SIZE / MAX_PER_ROW
156
+
157
+ canvas.width = canvas.height = SIZE
158
+ const texture = new CanvasTexture(canvas, undefined, RepeatWrapping, RepeatWrapping, NearestFilter, NearestFilter)
159
+ const context = canvas.getContext('2d')
160
+
161
+ if (!context) {
162
+ throw new Error('Context not available')
163
+ }
164
+
165
+ context.clearRect(0, 0, SIZE, SIZE)
166
+ context.font = `${fontSize}px ${font}`
167
+ context.textAlign = 'center'
168
+ context.textBaseline = 'middle'
169
+ context.fillStyle = '#fff'
170
+
171
+ for (let i = 0; i < characters.length; i++) {
172
+ const char = characters[i]
173
+ const x = i % MAX_PER_ROW
174
+ const y = Math.floor(i / MAX_PER_ROW)
175
+ context.fillText(char, x * CELL + CELL / 2, y * CELL + CELL / 2)
176
+ }
177
+
178
+ texture.needsUpdate = true
179
+ return texture
180
+ }
181
+ }
182
+
183
+ export const ASCII = /* @__PURE__ */ createEffectComponent<typeof ASCIIEffect, ASCIIProps>(ASCIIEffect)