Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 75 additions & 28 deletions src/effects/ASCII.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// https://twitter.com/emilwidlund/status/1652386482420609024

import { Effect } from 'postprocessing'
import { Ref, useMemo } from 'react'
import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
import { useDispose } from '../util'
import type { Ref } from 'react'
import { CanvasTexture, Color, type ColorRepresentation, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
import { createEffectComponent } from '../createEffectComponent'

const fragment = /* glsl */ `
uniform sampler2D uCharacters;
Expand Down Expand Up @@ -47,25 +47,29 @@ const fragment = /* glsl */ `
}
`

interface IASCIIEffectProps {
export type ASCIIProps = {
font?: string
characters?: string
fontSize?: number
cellSize?: number
color?: string
color?: ColorRepresentation
invert?: boolean
ref?: Ref<ASCIIEffect>
}

class ASCIIEffect extends Effect {
private _font: string
private _characters: string
private _fontSize: number

constructor({
font = 'arial',
characters = ` .:,'-^=*+?!|0#X%WM@`,
fontSize = 54,
cellSize = 16,
color = '#ffffff',
invert = false,
}: Omit<IASCIIEffectProps, 'ref'> = {}) {
}: Omit<ASCIIProps, 'ref'> = {}) {
const uniforms = new Map<string, Uniform>([
['uCharacters', new Uniform(new Texture())],
['uCellSize', new Uniform(cellSize)],
Expand All @@ -76,11 +80,71 @@ class ASCIIEffect extends Effect {

super('ASCIIEffect', fragment, { uniforms })

const charactersTextureUniform = this.uniforms.get('uCharacters')
this._font = font
this._characters = characters
this._fontSize = fontSize
this.updateCharactersTexture()
}

if (charactersTextureUniform) {
charactersTextureUniform.value = this.createCharactersTexture(characters, font, fontSize)
}
get cellSize(): number {
return this.uniforms.get('uCellSize')!.value
}

set cellSize(value: number) {
this.uniforms.get('uCellSize')!.value = value
}

get invert(): boolean {
return this.uniforms.get('uInvert')!.value
}

set invert(value: boolean) {
this.uniforms.get('uInvert')!.value = value
}

get color(): Color {
return this.uniforms.get('uColor')!.value
}

set color(value: ColorRepresentation) {
this.uniforms.get('uColor')!.value.set(value)
}

get font(): string {
return this._font
}

set font(value: string) {
this._font = value
this.updateCharactersTexture()
}

get characters(): string {
return this._characters
}

set characters(value: string) {
this._characters = value
this.uniforms.get('uCharactersCount')!.value = value.length
this.updateCharactersTexture()
}

get fontSize(): number {
return this._fontSize
}

set fontSize(value: number) {
this._fontSize = value
this.updateCharactersTexture()
}

// Regenerates the character atlas texture - characters/font/fontSize have
// no cheaper live update path, unlike the plain-uniform props above.
private updateCharactersTexture(): void {
const uniform = this.uniforms.get('uCharacters')!
const previous = uniform.value as Texture
uniform.value = this.createCharactersTexture(this._characters, this._font, this._fontSize)
previous.dispose()
}

/** Draws the characters on a Canvas and returns a texture */
Expand Down Expand Up @@ -116,21 +180,4 @@ class ASCIIEffect extends Effect {
}
}

export function ASCII({
font = 'arial',
characters = ` .:,'-^=*+?!|0#X%WM@`,
fontSize = 54,
cellSize = 16,
color = '#ffffff',
invert = false,
ref,
}: IASCIIEffectProps) {
const effect = useMemo(
() => new ASCIIEffect({ characters, font, fontSize, cellSize, color, invert }),
[characters, fontSize, cellSize, color, invert, font]
)

useDispose(effect)

return <primitive ref={ref} object={effect} />
}
export const ASCII = /* @__PURE__ */ createEffectComponent<typeof ASCIIEffect, ASCIIProps>(ASCIIEffect)
36 changes: 32 additions & 4 deletions src/effects/Bloom.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { BlendFunction, BloomEffect } from 'postprocessing'
import { wrapEffect } from '../wrapEffect'
import type { Ref } from 'react'
import { useMemo } from 'react'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export const Bloom = /* @__PURE__ */ wrapEffect(BloomEffect, {
blendFunction: BlendFunction.ADD,
})
type BloomOptions = EffectOptions<typeof BloomEffect>

const BloomImpl = /* @__PURE__ */ createEffectComponent<typeof BloomEffect, BloomOptions>(BloomEffect)

export type BloomProps = BloomOptions & { opacity?: number; ref?: Ref<BloomEffect> }

// luminanceThreshold/luminanceSmoothing/mipmapBlur/radius/levels/resolution*
// have no live setter in postprocessing - routed through args so they still
// work as plain props, just via reconstruction instead of mutation.
export function Bloom({
blendFunction = BlendFunction.ADD,
luminanceThreshold,
luminanceSmoothing,
mipmapBlur,
radius,
levels,
resolutionScale,
resolutionX,
resolutionY,
...liveProps
}: BloomProps) {
const args = useMemo<[BloomOptions]>(
() => [
{ luminanceThreshold, luminanceSmoothing, mipmapBlur, radius, levels, resolutionScale, resolutionX, resolutionY },
],
[luminanceThreshold, luminanceSmoothing, mipmapBlur, radius, levels, resolutionScale, resolutionX, resolutionY]
)
return <BloomImpl blendFunction={blendFunction} args={args} {...liveProps} />
}
7 changes: 5 additions & 2 deletions src/effects/BrightnessContrast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { BrightnessContrastEffect } from 'postprocessing'
import { wrapEffect } from '../wrapEffect'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export const BrightnessContrast = /* @__PURE__ */ wrapEffect(BrightnessContrastEffect)
export const BrightnessContrast = /* @__PURE__ */ createEffectComponent<
typeof BrightnessContrastEffect,
EffectOptions<typeof BrightnessContrastEffect>
>(BrightnessContrastEffect)
34 changes: 13 additions & 21 deletions src/effects/ChromaticAberration.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
import type { ReactThreeFiber } from '@react-three/fiber'
import { ChromaticAberrationEffect } from 'postprocessing'
import type { Ref } from 'react'
import { useMemo } from 'react'
import { useDispose, useVector2 } from '../util'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

// radialModulation/modulationOffset are typed as required by postprocessing's
// own .d.ts, but its JSDoc confirms both are optional with defaults - an
// upstream declaration bug, not a real constraint.
export type ChromaticAberrationProps = Omit<
Partial<ConstructorParameters<typeof ChromaticAberrationEffect>[0]>,
'offset'
EffectOptions<typeof ChromaticAberrationEffect>,
'offset' | 'radialModulation' | 'modulationOffset'
> & {
ref?: Ref<ChromaticAberrationEffect>
offset?: ReactThreeFiber.Vector2
radialModulation?: boolean
modulationOffset?: number
ref?: Ref<ChromaticAberrationEffect>
}

export function ChromaticAberration({ ref, ...props }: ChromaticAberrationProps) {
const offset = useVector2(props, 'offset')

const effect = useMemo(
() =>
new ChromaticAberrationEffect({
...props,
offset,
} as ConstructorParameters<typeof ChromaticAberrationEffect>[0]),
[offset, props]
)

useDispose(effect)

return <primitive object={effect} ref={ref} />
}
export const ChromaticAberration = /* @__PURE__ */ createEffectComponent<
typeof ChromaticAberrationEffect,
ChromaticAberrationProps
>(ChromaticAberrationEffect)
19 changes: 3 additions & 16 deletions src/effects/ColorAverage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
import { BlendFunction, ColorAverageEffect } from 'postprocessing'
import type { Ref } from 'react'
import { useMemo } from 'react'
import { useDispose } from '../util'
import { ColorAverageEffect } from 'postprocessing'
import { createEffectComponent } from '../createEffectComponent'

export type ColorAverageProps = {
blendFunction?: BlendFunction
ref?: Ref<ColorAverageEffect>
}

export function ColorAverage({ blendFunction = BlendFunction.NORMAL, ref }: ColorAverageProps) {
const effect = useMemo(() => new ColorAverageEffect(blendFunction), [blendFunction])

useDispose(effect)

return <primitive object={effect} ref={ref} />
}
export const ColorAverage = /* @__PURE__ */ createEffectComponent<typeof ColorAverageEffect, object>(ColorAverageEffect)
25 changes: 23 additions & 2 deletions src/effects/ColorDepth.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import { ColorDepthEffect } from 'postprocessing'
import { wrapEffect } from '../wrapEffect'
import type { Ref } from 'react'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export const ColorDepth = /* @__PURE__ */ wrapEffect(ColorDepthEffect)
const ColorDepthImpl = /* @__PURE__ */ createEffectComponent<
typeof ColorDepthEffect,
Omit<EffectOptions<typeof ColorDepthEffect>, 'bits'> & { bitDepth?: number }
>(ColorDepthEffect)

export type ColorDepthProps = EffectOptions<typeof ColorDepthEffect> & {
opacity?: number
ref?: Ref<ColorDepthEffect>
}

// bits (the constructor's option name) has no live setter of its own in
// postprocessing - only the differently-named bitDepth does (bits is a
// plain, dead field on the instance). Renamed here so it still works as a
// plain prop after the initial mount.
export function ColorDepth({ bits, ...props }: ColorDepthProps) {
// Only set bitDepth when bits is actually provided - r3f's reset-on-
// removal only fires when a key is absent from the new props, not when
// it's present but undefined.
if (bits !== undefined) (props as Record<string, unknown>).bitDepth = bits
return <ColorDepthImpl {...props} />
}
6 changes: 4 additions & 2 deletions src/effects/Depth.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { DepthEffect } from 'postprocessing'
import { wrapEffect } from '../wrapEffect'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export const Depth = /* @__PURE__ */ wrapEffect(DepthEffect)
export const Depth = /* @__PURE__ */ createEffectComponent<typeof DepthEffect, EffectOptions<typeof DepthEffect>>(
DepthEffect
)
7 changes: 5 additions & 2 deletions src/effects/DotScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { DotScreenEffect } from 'postprocessing'
import { wrapEffect } from '../wrapEffect'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export const DotScreen = /* @__PURE__ */ wrapEffect(DotScreenEffect)
export const DotScreen = /* @__PURE__ */ createEffectComponent<
typeof DotScreenEffect,
EffectOptions<typeof DotScreenEffect>
>(DotScreenEffect)
6 changes: 4 additions & 2 deletions src/effects/FXAA.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { FXAAEffect } from 'postprocessing'
import { wrapEffect } from '../wrapEffect'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export const FXAA = /* @__PURE__ */ wrapEffect(FXAAEffect)
export const FXAA = /* @__PURE__ */ createEffectComponent<typeof FXAAEffect, EffectOptions<typeof FXAAEffect>>(
FXAAEffect
)
57 changes: 26 additions & 31 deletions src/effects/Glitch.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
import { ReactThreeFiber, useThree } from '@react-three/fiber'
import type { ReactThreeFiber } from '@react-three/fiber'
import { GlitchEffect, GlitchMode } from 'postprocessing'
import { Ref, useLayoutEffect, useMemo } from 'react'
import { useDispose, useVector2 } from '../util'
import type { Ref } from 'react'
import { useMemo } from 'react'
import { createEffectComponent, type EffectOptions } from '../createEffectComponent'

export type GlitchProps = ConstructorParameters<typeof GlitchEffect>[0] &
Partial<{
mode: GlitchMode
active: boolean
delay: ReactThreeFiber.Vector2
duration: ReactThreeFiber.Vector2
chromaticAberrationOffset: ReactThreeFiber.Vector2
strength: ReactThreeFiber.Vector2
ref?: Ref<GlitchEffect>
}>

export function Glitch({ active = true, ref, ...props }: GlitchProps) {
const invalidate = useThree((state) => state.invalidate)
const delay = useVector2(props, 'delay')
const duration = useVector2(props, 'duration')
const strength = useVector2(props, 'strength')
const chromaticAberrationOffset = useVector2(props, 'chromaticAberrationOffset')

const effect = useMemo(
() => new GlitchEffect({ ...props, delay, duration, strength, chromaticAberrationOffset }),
[delay, duration, props, strength, chromaticAberrationOffset]
)
type GlitchOptions = Omit<
EffectOptions<typeof GlitchEffect>,
'delay' | 'duration' | 'strength' | 'chromaticAberrationOffset'
> & {
delay?: ReactThreeFiber.Vector2
duration?: ReactThreeFiber.Vector2
strength?: ReactThreeFiber.Vector2
chromaticAberrationOffset?: ReactThreeFiber.Vector2
mode?: GlitchMode
}

useLayoutEffect(() => {
effect.mode = active ? props.mode || GlitchMode.SPORADIC : GlitchMode.DISABLED
invalidate()
}, [active, effect, invalidate, props.mode])
const GlitchImpl = /* @__PURE__ */ createEffectComponent<typeof GlitchEffect, GlitchOptions>(GlitchEffect)

useDispose(effect)
export type GlitchProps = GlitchOptions & {
active?: boolean
opacity?: number
ref?: Ref<GlitchEffect>
}

return <primitive ref={ref} object={effect} />
// dtSize only seeds the auto-generated perturbation map at construction time
// (skipped entirely once a perturbationMap is provided) - routed through
// args so it still works as a plain prop.
export function Glitch({ active = true, mode = GlitchMode.SPORADIC, dtSize, ...props }: GlitchProps) {
const args = useMemo<[EffectOptions<typeof GlitchEffect>]>(() => [{ dtSize }], [dtSize])
return <GlitchImpl args={args} mode={active ? mode : GlitchMode.DISABLED} {...props} />
}
Loading