This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Corrpt Web App is a client-side SPA for applying real-time glitch and distortion effects to photos using WebGL/Three.js. Inspired by the Glitché iOS app, this web application transforms photos into glitch art entirely in the browser with no server processing.
- Framework: React 19.x + TypeScript 5.x
- Build Tool: Vite 5.x
- Graphics: Three.js (r160+) + @react-three/fiber + @react-three/drei
- State Management: Zustand (global state) + React Context (theme)
- UI Components: Radix UI primitives
- Styling: Tailwind CSS
- Icons: Lucide React
- Linting/Formatting: Biome (use default config)
- Shader Loading: vite-plugin-glsl
# Development
npm run dev # Start dev server with HMR
# Build
npm run build # TypeScript check + production build
# Preview
npm run preview # Preview production build locally
# Linting
npm run lint # Run Biome check on all filesThe application follows a three-stage pipeline:
- Input Stage: File upload, drag-and-drop, or WebRTC camera capture
- Processing Stage: Three.js scene with WebGL shaders applying effects
- Output Stage: Canvas export to PNG/JPEG with Web Share API support
Effects are processed using a multi-pass rendering system with Framebuffer Objects (FBOs):
Original Texture → Effect 1 (FBO 1) → Effect 2 (FBO 2) → Effect N (FBO N) → Screen
Each effect:
- Reads from the previous FBO (or original texture)
- Applies its shader transformation
- Writes to the next FBO in the chain
- Final pass renders to screen canvas
Key Modules (Implemented — Phase 3.1):
EffectDefinition+EffectParameterDef: TypeScript interfaces defining effect metadata, shaders, and parameters — supports float, bool, int, enum, vec2, color types (src/effects/types.ts)EffectRegistry: Map-based registry withregisterEffect(),getEffect(),getAllEffects()(src/effects/registry.ts)EffectPipeline: R3F component managing multi-pass FBO rendering, material caching, and per-frame uniform updates (src/components/canvas/EffectPipeline.tsx)- Effect definitions: Self-registering modules in
src/effects/definitions/— imported via barrel at canvas mount
The store is organized into three slices:
imageStore:
texture: THREE.Texture | nulldimensions: { width, height }originalUrl: string | null (Object URL)fileName: string | nullmimeType: string | nullisLoading,error
effectStore:
activeEffects: string[] (effect IDs)parameters: Record<effectId, Record<paramName, value>>previewMode: 'split' | 'full' | 'compare'
uiStore:
sidebarOpen: booleanactiveModal: 'export' | 'camera' | 'source' | nulltheme: 'dark' | 'light'
State changes automatically trigger uniform updates in shaders via useEffect subscriptions.
- Canvas: R3F
<Canvas orthographic linear flat>withRENDERER_SETTINGSfromsrc/lib/constants.ts - Camera: R3F orthographic camera (
zoom: 1, position: [0, 0, 1]). Viewport units adapt to canvas size. - Geometry:
PlaneGeometry(1, 1)scaled by viewport dimensions for aspect-correct contain/letterbox display - Materials: drei
shaderMaterial()createsPassthroughMaterialregistered as<passthroughMaterial>JSX element (used as final display surface) - Renderer: WebGLRenderer with
preserveDrawingBuffer: true,alpha: true,antialias: false,powerPreference: "high-performance" - Color Management:
linear={true}+flat={true}on Canvas +NoColorSpaceon textures — bypasses Three.js color management for raw sRGB passthrough, avoids double-gamma issues with ShaderMaterial - Render Loop:
useFrameinEffectPipelineincrementsu_time, runs multi-pass FBO rendering, and updates the display mesh each frame
All effect shaders share the vertex shader src/effects/shaders/common/passthrough.vert and must accept these uniforms:
u_texture(sampler2D): Input texture from previous passu_resolution(vec2): Image dimensions in pixelsu_time(float): Elapsed time in seconds
imageStore.loadImage() (src/store/imageStore.ts) manages the File → THREE.Texture pipeline in Zustand global state.
- Validates file type (
SUPPORTED_IMAGE_TYPES) and size (MAX_FILE_SIZE= 50MB) - Pipeline:
File → URL.createObjectURL → HTMLImageElement → THREE.Texture - Texture config:
needsUpdate,NoColorSpace,LinearFiltermin/mag - Disposes texture on
clearImage()and on new image load - Legacy
useImageLoaderhook still exists atsrc/hooks/useImageLoader.ts(unused, kept for reference)
src/
├── app/
│ ├── App.tsx # [2.1] App shell: DropZone > EffectCanvas + ImageActions + EffectDevPanel
│ └── main.tsx # [1.1] React entry point
├── components/
│ ├── canvas/
│ │ ├── EffectCanvas.tsx # [3.1] R3F Canvas wrapper, imports effect definitions barrel
│ │ ├── EffectMaterial.tsx # [1.2] drei shaderMaterial + JSX type augmentation
│ │ ├── EffectPipeline.tsx # [3.1] Multi-pass FBO renderer with material cache
│ │ └── ImagePlane.tsx # [1.2] Simple passthrough quad (superseded by EffectPipeline)
│ ├── controls/
│ │ └── EffectDevPanel.tsx # [3.2] Temporary floating dev panel for effect parameter tweaking
│ ├── input/
│ │ ├── DropZone.tsx # [2.1] Drag-and-drop wrapper with landing/overlay states
│ │ ├── DropZoneLanding.tsx # [2.1] Empty-state card (click-to-browse)
│ │ ├── DropZoneOverlay.tsx # [2.1] Drag-over replacement indicator
│ │ ├── ImageActions.tsx # [2.1] Floating replace button
│ │ └── index.ts # [2.1] Barrel export
│ ├── layout/ # (Phase 4) Layout components (Header, Sidebar, Footer)
│ ├── export/ # (Phase 5) Export functionality
│ └── ui/
│ └── index.ts # Shared UI primitives (Radix-based)
├── effects/
│ ├── definitions/
│ │ ├── index.ts # [3.1] Barrel — imports all effect definitions (side-effect registration)
│ │ ├── passthrough.ts # [3.1] Identity effect definition
│ │ └── rgbShift.ts # [3.2] RGB Shift effect definition
│ ├── processors/
│ │ └── index.ts # (reserved for future CPU-side processors)
│ ├── shaders/
│ │ ├── common/
│ │ │ ├── passthrough.vert # [1.2] Standard vertex shader (shared by all effects)
│ │ │ ├── passthrough.frag # [1.2] Identity fragment shader
│ │ │ └── utils.glsl # Shared GLSL utilities (hash, rand) via #include
│ │ └── rgb-shift/
│ │ └── fragment.glsl # [3.2] RGB channel separation + directional offset
│ ├── registry.ts # [3.1] Map-based effect registry (registerEffect/getEffect/getAllEffects)
│ └── types.ts # [3.1] EffectDefinition, EffectParameterDef, EffectParameterValues
├── hooks/
│ └── useImageLoader.ts # [1.2] Legacy File → THREE.Texture hook (unused, superseded by imageStore)
├── store/
│ ├── index.ts # [1.3] Barrel export
│ ├── types.ts # [1.3] Store type interfaces (ImageStore, EffectStore, UIStore)
│ ├── imageStore.ts # [1.3] Image loading, texture management, validation
│ ├── effectStore.ts # [1.3] Active effects, parameters, preview mode
│ └── uiStore.ts # [1.3] Sidebar, modals, theme
├── lib/
│ ├── constants.ts # [1.2] RENDERER_SETTINGS, SUPPORTED_IMAGE_TYPES, MAX_FILE_SIZE
│ └── cn.ts # Utility for className merging
├── styles/
│ └── globals.css # [1.1] Tailwind import + base styles
└── types/
└── glsl.d.ts # [1.1] Shader import type declarations
Separates RGB channels and offsets them directionally.
Shader Algorithm:
- Calculate offset vector from intensity and angle
- Sample R channel at UV - offset
- Sample G channel at original UV
- Sample B channel at UV + offset
- Combine into final color
Parameters:
intensity(0.0-1.0): Strength of channel offsetangle(0.0-6.28): Direction angle in radiansanimated(bool): Enable time-based animation
GPU-friendly approximation of pixel sorting using brightness threshold detection and directional blur.
Shader Algorithm:
- Calculate pixel brightness
- If brightness in threshold range, apply horizontal smear effect
- Sum samples across direction with configurable spread
Parameters:
threshold(0.0-1.0): Lower brightness boundupperThreshold(0.0-1.0): Upper brightness boundspread(0.0-0.1): Blur amountdirection(0.0-1.0): 0=horizontal, 1=vertical
Uniforms flow from UI controls → Zustand store → Three.js materials per frame:
// UI slider onChange triggers store update
setEffectParam('rgbShift', 'intensity', 0.75);
// EffectPipeline.useFrame reads store and updates cached material uniforms
const params = parameters[effectId];
mat.uniforms.u_intensity.value = params.intensity;
// Shader receives updated value next frame
uniform float u_intensity;Convention: parameter name foo maps to uniform u_foo. Bool parameters are sent as 0.0/1.0 floats.
File → URL.createObjectURL → HTMLImageElement → THREE.Texture (in imageStore.loadImage())
- Validates file type (JPEG/PNG/WebP) and size (max 50MB)
- Texture config:
needsUpdate,NoColorSpace,LinearFilter - Aspect-correct display via viewport contain/letterbox in
EffectPipeline - EXIF orientation: handled natively by target browsers (Chrome 81+, Firefox 77+, Safari 14+)
Use off-screen WebGLRenderer at original image dimensions:
- Create temporary renderer with original dimensions
- Render scene with all active effects
- Extract via
toDataURL()with format/quality - Trigger download or Web Share API
- Dispose renderer to prevent memory leaks
The project follows a 5-stage development plan:
- Foundation (Week 1-2): Project setup, Three.js canvas, state management
- Phase 1.1: Project setup — DONE
- Phase 1.2: Core Three.js setup — DONE
- Phase 1.3: State management — DONE
- Input System (Week 2): File upload and camera capture
- Phase 2.1: File upload + drag-and-drop — DONE
- Phase 2.2: Camera capture — pending
- Effect System (Week 3): Effect architecture, RGB Shift, Pixel Sort, CRT, Noise
- Phase 3.1: Effect architecture (pipeline, registry, definitions) — DONE
- Phase 3.2: RGB Shift effect + dev controls — DONE
- Phase 3.3: Pixel Sort effect — DONE
- Phase 3.4: CRT effect — DONE
- Phase 3.5: Noise effect — DONE
- User Interface (Week 4): Layout, effect controls, visual polish
- Export & Finishing (Week 5): Export system, testing, deployment
Refer to __plans/corrpt-web-app-dev-stages-v1.md for detailed phase breakdown.
- First Contentful Paint: < 1.5s
- Canvas render: ≥ 60fps
- Effect parameter change latency: < 16ms
- Export processing: < 2s
- Memory usage (4K image): < 200MB
Configure in tsconfig.json and vite.config.ts:
@/components→src/components@/hooks→src/hooks@/store→src/store@/effects→src/effects@/lib→src/lib@/types→src/types
Use vite-plugin-glsl to import shaders as strings:
import fragmentShader from './shader.glsl';
import vertexShader from './vertex.glsl';
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: { /* ... */ },
});Configure in vite.config.ts:
glsl({
include: ['**/*.glsl', '**/*.vert', '**/*.frag'],
compress: true,
})Target: Chrome 90+, Firefox 90+, Safari 14+, Edge 90+, iOS Safari 14+, Chrome Android 90+
- Client-side only: All processing in browser, no server required
- React Three Fiber: Preferred over vanilla Three.js for React integration
- Zustand over Redux: Lightweight state management
- Radix UI: Accessible, unstyled primitives for custom design
- Biome: Modern, fast linting/formatting (use default config)
- FBO chain: Multi-pass rendering for effect stacking
- GPU shaders: All effects implemented as GLSL fragment shaders for performance
- drei
shaderMaterial()over rawTHREE.ShaderMaterial: Typed uniform props, R3F reconciler key, reusable class pattern PlaneGeometry(1,1)+ scale overPlaneGeometry(2,2): R3F orthographic camera adapts to canvas size; scaling a unit quad by viewport dimensions is the correct R3F approachlinear+flat+NoColorSpace: Bypasses Three.js color management entirely for raw sRGB passthrough — correct for sRGB content on sRGB monitors.glslfiles over inline strings: Validates vite-plugin-glsl pipeline, enables shader reuse, proper syntax highlighting
- Playwright is installed as a dev dependency for headless browser validation
- Regression test script:
test-assets/validate-canvas.mjs— starts dev server, uploads test image, takes screenshots at multiple viewport sizes - Generated screenshots are in
test-assets/screenshots/(gitignored) - Run:
node test-assets/validate-canvas.mjs