From ce6599ef302b342d32a3b01e94422a7350c53a9c Mon Sep 17 00:00:00 2001 From: Daffa Sinulingga Date: Wed, 22 Jul 2026 00:27:04 +0700 Subject: [PATCH] fix(ChromaticAberration): coerce offset prop to Vector2 The v3 rewrite reduced ChromaticAberration to a bare wrapEffect call, so an offset passed as a number or [x, y] tuple (which the prop types accept) is handed to ChromaticAberrationEffect's constructor as-is and stored directly into its Uniform. The effect then exposes a plain array, and the documented imperative pattern ref.current.offset.set(x, y) throws "offset.set is not a function". Route the prop through the existing useVector2 helper, mirroring Glitch. When no offset is passed, the effect's own default is left untouched. v2 had this coercion (#224); it was lost in the v3 rewrite. --- src/effects/ChromaticAberration.test.tsx | 87 ++++++++++++++++++++++++ src/effects/ChromaticAberration.tsx | 23 ++++++- 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/effects/ChromaticAberration.test.tsx diff --git a/src/effects/ChromaticAberration.test.tsx b/src/effects/ChromaticAberration.test.tsx new file mode 100644 index 0000000..96628c5 --- /dev/null +++ b/src/effects/ChromaticAberration.test.tsx @@ -0,0 +1,87 @@ +import * as React from 'react' +import * as THREE from 'three' +import { describe, it, expect } from 'vitest' +import { extend, createRoot, act } from '@react-three/fiber' +import { ChromaticAberrationEffect } from 'postprocessing' +import { EffectComposer } from '../EffectComposer' +import { ChromaticAberration } from './ChromaticAberration' + +// Let React know that we'll be testing effectful components +declare global { + var IS_REACT_ACT_ENVIRONMENT: boolean +} +global.IS_REACT_ACT_ENVIRONMENT = true + +// Create virtual R3F root for testing +extend(THREE as any) +const root = createRoot({ + style: {} as CSSStyleDeclaration, + addEventListener: (() => {}) as any, + removeEventListener: (() => {}) as any, + width: 1280, + height: 800, + clientWidth: 1280, + clientHeight: 800, + getContext: (() => + new Proxy( + {}, + { + get(_target, prop) { + switch (prop) { + case 'getParameter': + return () => 'WebGL 2' // GL_VERSION + case 'getExtension': + return () => ({}) // EXT_blend_minmax + case 'getContextAttributes': + return () => ({ alpha: true }) + case 'getShaderPrecisionFormat': + return () => ({ rangeMin: 1, rangeMax: 1, precision: 1 }) + default: + return () => {} + } + }, + } + )) as any, +} satisfies Partial as HTMLCanvasElement) +root.configure({ frameloop: 'never' }) + +describe('ChromaticAberration', () => { + it('coerces a tuple offset to a Vector2 so the imperative API works', async () => { + const ref = React.createRef() + + await act(async () => + root.render( + + + + ) + ) + + const effect = ref.current! + expect(effect).toBeInstanceOf(ChromaticAberrationEffect) + expect(effect.offset).toBeInstanceOf(THREE.Vector2) + expect(effect.offset.x).toBe(0.001) + expect(effect.offset.y).toBe(0.0005) + // The pattern from the docs/examples that used to throw "offset.set is not a function" + expect(() => effect.offset.set(0.002, 0.001)).not.toThrow() + expect(effect.offset.x).toBe(0.002) + }) + + it('keeps the effect default when no offset is passed', async () => { + const ref = React.createRef() + + await act(async () => + root.render( + + + + ) + ) + + const effect = ref.current! + const fallback = new ChromaticAberrationEffect() + expect(effect.offset).toBeInstanceOf(THREE.Vector2) + expect(effect.offset.x).toBe(fallback.offset.x) + expect(effect.offset.y).toBe(fallback.offset.y) + }) +}) diff --git a/src/effects/ChromaticAberration.tsx b/src/effects/ChromaticAberration.tsx index 5944ffa..41089db 100644 --- a/src/effects/ChromaticAberration.tsx +++ b/src/effects/ChromaticAberration.tsx @@ -1,5 +1,22 @@ import { ChromaticAberrationEffect } from 'postprocessing' -import { type EffectProps, wrapEffect } from '../util' +import { Ref, forwardRef } from 'react' +import { type ReactThreeFiber } from '@react-three/fiber' +import { type EffectProps, useVector2, wrapEffect } from '../util' -export type ChromaticAberrationProps = EffectProps -export const ChromaticAberration = /* @__PURE__ */ wrapEffect(ChromaticAberrationEffect) +export type ChromaticAberrationProps = EffectProps & { + offset?: ReactThreeFiber.Vector2 +} + +const ChromaticAberrationImpl = /* @__PURE__ */ wrapEffect(ChromaticAberrationEffect) + +export const ChromaticAberration = /* @__PURE__ */ forwardRef( + function ChromaticAberration(props: ChromaticAberrationProps, ref: Ref) { + // Coerce number/tuple offsets to a Vector2 before they reach the effect + // constructor, which stores whatever it is given into a Uniform (same + // pattern as Glitch.tsx). Without this, `offset={[x, y]}` type-checks + // but leaves `effect.offset` as a plain array, so the imperative API + // (`ref.current.offset.set(...)`) throws. + const offset = useVector2(props, 'offset') + return + } +)