From de8b009354ba9fae5a3e483961242bc10b5805b7 Mon Sep 17 00:00:00 2001 From: kvvasuu Date: Sat, 10 Oct 2026 10:23:14 +0200 Subject: [PATCH] feat: add LensDistortion effect Wraps postprocessing's LensDistortionEffect, with Vector2 tuple props. --- docs/effects/lens-distortion.mdx | 28 +++++++++++ src/effects/LensDistortion.tsx | 20 ++++++++ src/index.ts | 1 + src/tests/LensDistortion.test.tsx | 78 +++++++++++++++++++++++++++++++ src/tests/effects.smoke.test.tsx | 2 + 5 files changed, 129 insertions(+) create mode 100644 docs/effects/lens-distortion.mdx create mode 100644 src/effects/LensDistortion.tsx create mode 100644 src/tests/LensDistortion.test.tsx diff --git a/docs/effects/lens-distortion.mdx b/docs/effects/lens-distortion.mdx new file mode 100644 index 0000000..42ec633 --- /dev/null +++ b/docs/effects/lens-distortion.mdx @@ -0,0 +1,28 @@ +--- +title: LensDistortion +nav: 1 +--- + +A radial lens distortion effect. Positive `distortion` values give a barrel (fisheye-like) look, negative values a pincushion look. + +```jsx +import { LensDistortion } from '@react-three/postprocessing' + +return ( + +) +``` + +## Props + +| Name | Type | Default | Description | +| -------------- | ------- | ------- | --------------------------------------- | +| distortion | Vector2 | [0, 0] | The radial distortion coefficients. | +| principalPoint | Vector2 | [0, 0] | The center point offset. | +| focalLength | Vector2 | [1, 1] | The focal length. Lower values zoom in. | +| skew | Number | 0 | The skew factor in radians. | diff --git a/src/effects/LensDistortion.tsx b/src/effects/LensDistortion.tsx new file mode 100644 index 0000000..e5e0ae3 --- /dev/null +++ b/src/effects/LensDistortion.tsx @@ -0,0 +1,20 @@ +import type { ReactThreeFiber } from '@react-three/fiber' +import { LensDistortionEffect } from 'postprocessing' +import type { Ref } from 'react' +import { createEffectComponent, type EffectOptions } from '../createEffectComponent' + +// postprocessing's .d.ts types distortion/principalPoint/focalLength as +// required, but the constructor defaults all of them. +export type LensDistortionProps = Omit< + EffectOptions, + 'distortion' | 'principalPoint' | 'focalLength' +> & { + distortion?: ReactThreeFiber.Vector2 + principalPoint?: ReactThreeFiber.Vector2 + focalLength?: ReactThreeFiber.Vector2 + ref?: Ref +} + +export const LensDistortion = /* @__PURE__ */ createEffectComponent( + LensDistortionEffect +) diff --git a/src/index.ts b/src/index.ts index 860ac4d..17d072d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ export * from './effects/Glitch' export * from './effects/GodRays' export * from './effects/Grid' export * from './effects/HueSaturation' +export * from './effects/LensDistortion' export * from './effects/LensFlare' export * from './effects/LUT' export * from './effects/Noise' diff --git a/src/tests/LensDistortion.test.tsx b/src/tests/LensDistortion.test.tsx new file mode 100644 index 0000000..4fc4bcc --- /dev/null +++ b/src/tests/LensDistortion.test.tsx @@ -0,0 +1,78 @@ +import { LensDistortionEffect } from 'postprocessing' +import * as React from 'react' +import { Vector2 } from 'three' +import { describe, expect, it } from 'vitest' +import { EffectComposer } from '../EffectComposer' +import { LensDistortion } from '../effects/LensDistortion' +import { flush, root } from './test-utils' + +describe('LensDistortion', () => { + it('coerces tuple props into the effect Vector2 uniforms', async () => { + const ref = React.createRef() + + await React.act(async () => + root.render( + + + + ) + ) + await flush() + + expect(ref.current!.distortion).toBeInstanceOf(Vector2) + expect(ref.current!.distortion.toArray()).toEqual([0.5, 0.25]) + expect(ref.current!.focalLength.toArray()).toEqual([0.7, 0.8]) + expect(ref.current!.skew).toBeCloseTo(0.1) + + await React.act(async () => root.render(null)) + }) + + it('applies distortion live, without reconstructing the effect', async () => { + const ref = React.createRef() + + const render = (x: number) => + root.render( + + + + ) + + await React.act(async () => render(0.1)) + await flush() + const first = ref.current + + await React.act(async () => render(0.6)) + await flush() + + expect(ref.current).toBe(first) + expect(ref.current!.distortion.x).toBeCloseTo(0.6) + + await React.act(async () => root.render(null)) + }) + + it('resets distortion to its default when the prop is removed', async () => { + const ref = React.createRef() + + await React.act(async () => + root.render( + + + + ) + ) + await flush() + + await React.act(async () => + root.render( + + + + ) + ) + await flush() + + expect(ref.current!.distortion.toArray()).toEqual([0, 0]) + + await React.act(async () => root.render(null)) + }) +}) diff --git a/src/tests/effects.smoke.test.tsx b/src/tests/effects.smoke.test.tsx index e006598..9c2173e 100644 --- a/src/tests/effects.smoke.test.tsx +++ b/src/tests/effects.smoke.test.tsx @@ -42,6 +42,7 @@ import { Glitch } from '../effects/Glitch' import { GodRays } from '../effects/GodRays' import { Grid } from '../effects/Grid' import { HueSaturation } from '../effects/HueSaturation' +import { LensDistortion } from '../effects/LensDistortion' import { LensFlare } from '../effects/LensFlare' import { LUT } from '../effects/LUT' import { Noise } from '../effects/Noise' @@ -99,6 +100,7 @@ const SMOKE_CASES: SmokeCase[] = [ }, { file: 'Grid.tsx', label: 'Grid', effect: (ref) => }, { file: 'HueSaturation.tsx', label: 'HueSaturation', effect: (ref) => }, + { file: 'LensDistortion.tsx', label: 'LensDistortion', effect: (ref) => }, { file: 'LensFlare.tsx', label: 'LensFlare', effect: (ref) => }, { file: 'LUT.tsx', label: 'LUT', effect: (ref) => }, { file: 'N8AO.tsx', label: 'N8AO', effect: (ref) => },