mihirmithani commited on
Commit
80bcac8
·
1 Parent(s): cb05f36

Add Kernel Convolution Visualizer - Phase 1 complete

Browse files

- Next.js 14+ with TypeScript, Tailwind CSS
- 32 predefined kernels across 6 categories
- Step-by-step convolution visualization with Canvas
- Zustand state management
- Custom kernel editor (3x3 to 9x9)
- 4 padding modes (zero, reflect, replicate, wrap)
- Playback controls (play, pause, step, speed)
- Comparison views (side-by-side, split)
- Dark mode, responsive layout
- Export functionality

This view is limited to 50 files because it contains too many changes.   See raw diff
.gitignore CHANGED
@@ -3,21 +3,39 @@
3
  # dependencies
4
  /node_modules
5
  /.pnp
6
- .pnp.js
 
 
 
 
 
7
 
8
  # testing
9
  /coverage
10
 
 
 
 
 
11
  # production
12
  /build
13
 
14
  # misc
15
  .DS_Store
16
- .env.local
17
- .env.development.local
18
- .env.test.local
19
- .env.production.local
20
 
 
21
  npm-debug.log*
22
  yarn-debug.log*
23
  yarn-error.log*
 
 
 
 
 
 
 
 
 
 
 
 
3
  # dependencies
4
  /node_modules
5
  /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
 
13
  # testing
14
  /coverage
15
 
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
  # production
21
  /build
22
 
23
  # misc
24
  .DS_Store
25
+ *.pem
 
 
 
26
 
27
+ # debug
28
  npm-debug.log*
29
  yarn-debug.log*
30
  yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
AGENTS.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # This is NOT the Next.js you know
4
+
5
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
+
7
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
+
9
+ <!-- END:nextjs-agent-rules -->
CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
README.md CHANGED
@@ -1,81 +1,155 @@
1
- ---
2
- title: Kernel Convolution Visualizer
3
- emoji: 🐠
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: static
7
- pinned: false
8
- app_build_command: npm run build
9
- app_file: build/index.html
10
- ---
11
-
12
- # Getting Started with Create React App
13
-
14
- This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
15
-
16
- ## Available Scripts
17
-
18
- In the project directory, you can run:
19
-
20
- ### `npm start`
21
-
22
- Runs the app in the development mode.\
23
- Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
24
-
25
- The page will reload when you make changes.\
26
- You may also see any lint errors in the console.
27
-
28
- ### `npm test`
29
-
30
- Launches the test runner in the interactive watch mode.\
31
- See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
32
-
33
- ### `npm run build`
34
-
35
- Builds the app for production to the `build` folder.\
36
- It correctly bundles React in production mode and optimizes the build for the best performance.
37
-
38
- The build is minified and the filenames include the hashes.\
39
- Your app is ready to be deployed!
40
-
41
- See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
42
-
43
- ### `npm run eject`
44
-
45
- **Note: this is a one-way operation. Once you `eject`, you can't go back!**
46
-
47
- If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
48
-
49
- Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
50
-
51
- You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
52
-
53
- ## Learn More
54
-
55
- You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
56
-
57
- To learn React, check out the [React documentation](https://reactjs.org/).
58
-
59
- ### Code Splitting
60
-
61
- This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
62
-
63
- ### Analyzing the Bundle Size
64
-
65
- This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
66
-
67
- ### Making a Progressive Web App
68
-
69
- This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
70
-
71
- ### Advanced Configuration
72
-
73
- This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
74
-
75
- ### Deployment
76
-
77
- This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
78
-
79
- ### `npm run build` fails to minify
80
-
81
- This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Interactive Kernel Convolution Visualizer
2
+
3
+ An educational web application that demonstrates how image convolution works in real-time. Upload any image, choose from predefined kernels or create your own, and watch the convolution process step-by-step with detailed mathematical visualization.
4
+
5
+ ## Features
6
+
7
+ ### Core Visualization
8
+ - **Step-by-step convolution animation** - Watch the kernel matrix move across the image pixel by pixel
9
+ - **Real-time mathematical display** - See element-wise multiplication and summation for each output pixel
10
+ - **Current patch highlighting** - Visual indication of the image region under the kernel
11
+ - **Progressive output rendering** - Output image builds up as computation completes
12
+
13
+ ### Kernel Library
14
+ - **Blur Filters**: Box Blur (3×3, 5×5), Gaussian Blur (3×3, 5×5)
15
+ - **Sharpen Filters**: Standard Sharpen, Strong Sharpen, Unsharp Mask
16
+ - **Edge Detection**: Sobel X/Y/Combined, Prewitt, Roberts Cross, Laplacian
17
+ - **Emboss**: Multiple directional emboss effects
18
+ - **Special Effects**: Motion Blur, Outline, Ridge Detection
19
+ - **Custom Kernel Builder**: Create your own kernels (3×3 to 9×9)
20
+
21
+ ### Interactive Controls
22
+ - Play, Pause, Step Forward, Stop, Reset
23
+ - Adjustable animation speed (0.1x - 10x)
24
+ - Zoom and pan for pixel-level inspection
25
+ - Step mode for manual frame-by-frame advancement
26
+
27
+ ### Comparison & Analysis
28
+ - Side-by-side original vs processed view
29
+ - Split-view slider with draggable divider
30
+ - Synchronized zoom/pan between views
31
+ - Multiple kernel comparison tabs
32
+
33
+ ### Educational Features
34
+ - Real-time convolution equation with current values
35
+ - Kernel purpose and effect descriptions
36
+ - Padding strategy visualization (Zero, Reflect, Replicate, Wrap)
37
+ - Channel-wise processing explanation (RGB)
38
+ - Mathematical formula display
39
+
40
+ ### Export & Sharing
41
+ - Save processed images (PNG)
42
+ - Download convolution computation logs
43
+ - Shareable application state
44
+
45
+ ## Tech Stack
46
+
47
+ - **Framework**: Next.js 14+ (App Router) with TypeScript
48
+ - **State Management**: Zustand
49
+ - **Animation**: Framer Motion + Custom Canvas Animation Loop
50
+ - **Styling**: Tailwind CSS
51
+ - **Visualization**: HTML5 Canvas API
52
+ - **Icons**: Lucide React
53
+ - **UI Components**: Radix UI primitives
54
+
55
+ ## Getting Started
56
+
57
+ ### Prerequisites
58
+ - Node.js 18+
59
+ - npm, yarn, pnpm, or bun
60
+
61
+ ### Installation
62
+
63
+ ```bash
64
+ # Clone the repository
65
+ git clone <repository-url>
66
+ cd kernel-convolution-visualizer
67
+
68
+ # Install dependencies
69
+ npm install
70
+
71
+ # Start development server
72
+ npm run dev
73
+ ```
74
+
75
+ Open [http://localhost:3000](http://localhost:3000) in your browser.
76
+
77
+ ### Available Scripts
78
+
79
+ ```bash
80
+ npm run dev # Start development server
81
+ npm run build # Build for production
82
+ npm run start # Start production server
83
+ npm run lint # Run ESLint
84
+ npm run type-check # Run TypeScript type checking
85
+ ```
86
+
87
+ ## Project Structure
88
+
89
+ ```
90
+ src/
91
+ ├── app/ # Next.js App Router pages
92
+ ├── components/
93
+ │ ├── canvas/ # Canvas-based visualization components
94
+ │ ├── controls/ # Playback, zoom, overlay controls
95
+ │ ├── kernel/ # Kernel selector, editor, library
96
+ │ ├── comparison/ # Split view, side-by-side comparison
97
+ │ ├── education/ # Math display, kernel info panels
98
+ │ └── ui/ # Reusable UI components
99
+ ├── hooks/ # Custom React hooks
100
+ ├── lib/
101
+ │ ├── convolution/ # Core convolution algorithms
102
+ │ ├── kernels/ # Predefined kernel definitions
103
+ │ ├── image/ # Image loading, processing utilities
104
+ │ └── export/ # Export functionality
105
+ ├── store/ # Zustand state management
106
+ ├── types/ # TypeScript definitions
107
+ └── utils/ # Helper functions
108
+ ```
109
+
110
+ ## Usage Guide
111
+
112
+ 1. **Upload an Image**: Drag & drop or click the upload area in the left sidebar
113
+ 2. **Select a Kernel**: Choose from categories (Blur, Sharpen, Edge Detection, etc.) or create a custom kernel
114
+ 3. **Watch Convolution**: Press Play to see the kernel move across the image
115
+ 4. **Inspect Details**: Zoom in to see pixel values, multiplications, and summation
116
+ 5. **Compare Results**: Click "Show Comparison" for side-by-side or split view
117
+ 6. **Experiment**: Try different padding strategies, toggle overlays, adjust speed
118
+ 7. **Export**: Download the processed image
119
+
120
+ ## Convolution Visualization Explained
121
+
122
+ The visualization shows the mathematical operation at each step:
123
+
124
+ ```
125
+ For each output pixel (x, y) and channel c:
126
+ 1. Extract input patch: P[i][j] = image[x+i][y+j][c] for i,j in kernel
127
+ 2. Element-wise multiply: M[i][j] = P[i][j] × K[i][j]
128
+ 3. Sum: S = Σ M[i][j]
129
+ 4. Normalize: output = S / Σ K[i][j] (if normalized)
130
+ ```
131
+
132
+ ## Padding Strategies
133
+
134
+ - **Zero Padding**: Fill outside with zeros (default)
135
+ - **Reflect Padding**: Mirror the image at boundaries
136
+ - **Replicate Padding**: Repeat edge pixels outward
137
+ - **Wrap Padding**: Tile the image (torus topology)
138
+
139
+ ## Contributing
140
+
141
+ 1. Fork the repository
142
+ 2. Create a feature branch
143
+ 3. Make your changes
144
+ 4. Run tests and linting
145
+ 5. Submit a pull request
146
+
147
+ ## License
148
+
149
+ MIT License - feel free to use for educational and commercial purposes.
150
+
151
+ ## Acknowledgments
152
+
153
+ - Inspired by convolution visualizations in computer vision education
154
+ - Kernel definitions from standard image processing literature
155
+ - Built with Next.js, React, and modern web technologies
eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
next.config.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ // Disable Turbopack for now due to webpack config
5
+ turbopack: {},
6
+ // Enable experimental features for better performance
7
+ experimental: {
8
+ optimizePackageImports: ['lucide-react'],
9
+ },
10
+ // Allow cross-origin images for development
11
+ images: {
12
+ remotePatterns: [
13
+ {
14
+ protocol: 'https',
15
+ hostname: '**',
16
+ },
17
+ ],
18
+ },
19
+ };
20
+
21
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,39 +1,38 @@
1
  {
2
- "name": "react-template",
3
  "version": "0.1.0",
4
  "private": true,
5
- "dependencies": {
6
- "@testing-library/dom": "^10.4.0",
7
- "@testing-library/jest-dom": "^6.6.3",
8
- "@testing-library/react": "^16.3.0",
9
- "@testing-library/user-event": "^13.5.0",
10
- "react": "^19.1.0",
11
- "react-dom": "^19.1.0",
12
- "react-scripts": "5.0.1",
13
- "web-vitals": "^2.1.4"
14
- },
15
  "scripts": {
16
- "start": "react-scripts start",
17
- "build": "react-scripts build",
18
- "test": "react-scripts test",
19
- "eject": "react-scripts eject"
 
20
  },
21
- "eslintConfig": {
22
- "extends": [
23
- "react-app",
24
- "react-app/jest"
25
- ]
 
 
 
 
 
 
 
 
 
26
  },
27
- "browserslist": {
28
- "production": [
29
- ">0.2%",
30
- "not dead",
31
- "not op_mini all"
32
- ],
33
- "development": [
34
- "last 1 chrome version",
35
- "last 1 firefox version",
36
- "last 1 safari version"
37
- ]
38
  }
39
- }
 
1
  {
2
+ "name": "kernel-convolution-visualizer",
3
  "version": "0.1.0",
4
  "private": true,
 
 
 
 
 
 
 
 
 
 
5
  "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "type-check": "tsc --noEmit"
11
  },
12
+ "dependencies": {
13
+ "@radix-ui/react-dropdown-menu": "^2.1.24",
14
+ "@radix-ui/react-select": "^2.3.7",
15
+ "@radix-ui/react-slider": "^1.4.7",
16
+ "@radix-ui/react-tabs": "^1.1.21",
17
+ "@radix-ui/react-tooltip": "^1.2.16",
18
+ "clsx": "^2.1.1",
19
+ "framer-motion": "^13.0.0",
20
+ "lucide-react": "^1.28.0",
21
+ "next": "16.3.0",
22
+ "react": "19.2.8",
23
+ "react-dom": "19.2.8",
24
+ "tailwind-merge": "^3.6.0",
25
+ "zustand": "^5.0.14"
26
  },
27
+ "devDependencies": {
28
+ "@tailwindcss/postcss": "^4",
29
+ "@types/node": "^20",
30
+ "@types/offscreencanvas": "^2019.7.3",
31
+ "@types/react": "^19",
32
+ "@types/react-dom": "^19",
33
+ "eslint": "^9",
34
+ "eslint-config-next": "16.3.0",
35
+ "tailwindcss": "^4",
36
+ "typescript": "^5"
 
37
  }
38
+ }
plan.md ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Interactive Kernel Convolution Visualizer - Project Plan
2
+
3
+ ## Project Overview
4
+ Build an interactive web application that demonstrates how image convolution works in real time. The application serves both as an image-processing tool and an educational visualization platform for understanding convolution kernels, their mathematical operations, and their effects on images.
5
+
6
+ ## Target Audience
7
+ - Students learning computer vision and digital image processing
8
+ - Developers learning about CNNs and kernel operations
9
+ - Educators teaching image processing concepts
10
+ - Anyone curious about how image filters work
11
+
12
+ ## Core Features
13
+
14
+ ### 1. Image Management
15
+ - Upload any image (drag & drop, file picker)
16
+ - Support common formats: PNG, JPEG, WebP, BMP
17
+ - Display original image with zoom/pan capabilities
18
+ - Image metadata display (dimensions, file size, color channels)
19
+
20
+ ### 2. Kernel Library
21
+ Predefined kernels:
22
+ - **Blur Filters**: Box Blur (3×3, 5×5, 7×7), Gaussian Blur (3×3, 5×5)
23
+ - **Sharpen Filters**: Sharpen, Unsharp Mask
24
+ - **Edge Detection**: Sobel X, Sobel Y, Sobel Combined, Prewitt, Roberts, Laplacian
25
+ - **Emboss**: Emboss (multiple directions)
26
+ - **Special Effects**: Motion Blur, Outline, Ridge Detection
27
+ - **Custom Kernel Builder**: User-defined matrix with size selector (3×3, 5×5, 7×7, 9×9)
28
+
29
+ ### 3. Convolution Visualization (Core Feature)
30
+ - **Step-by-step Animation**: Kernel matrix moves across image pixel by pixel
31
+ - **Current Patch Highlight**: Visual indication of image region under kernel
32
+ - **Element-wise Multiplication**: Show kernel values × pixel values
33
+ - **Summation Display**: Real-time calculation of output pixel value
34
+ - **Continuous Animation**: Smooth traversal across entire image
35
+ - **Progressive Output Rendering**: Output image builds up as computation completes
36
+
37
+ ### 4. Interactive Controls
38
+ - Play / Pause / Restart / Step Forward / Step Backward
39
+ - Animation speed slider (0.5x - 10x)
40
+ - Zoom controls for pixel-level inspection
41
+ - Toggle overlays:
42
+ - Kernel boundary visualization
43
+ - Pixel value display
44
+ - Multiplication calculation overlay
45
+ - Summation formula display
46
+
47
+ ### 5. Comparison & Analysis
48
+ - Side-by-side original vs processed view
49
+ - Split-view slider (before/after)
50
+ - Synchronized zoom/pan between views
51
+ - Multiple kernel comparison mode (tabbed results)
52
+ - Processing statistics: execution time, operations count, memory usage
53
+
54
+ ### 6. Padding Strategies
55
+ - Visual demonstration of padding effects
56
+ - Options: Zero Padding, Reflect Padding, Replicate Padding, Wrap Padding
57
+ - Border handling visualization at image edges
58
+
59
+ ### 7. Educational Insights
60
+ - Real-time mathematical equation display
61
+ - Kernel purpose/effect description
62
+ - Convolution formula with current values
63
+ - Channel-wise processing explanation (RGB)
64
+ - Links to learning resources
65
+
66
+ ### 8. Export & Sharing
67
+ - Save processed image (PNG, JPEG)
68
+ - Export visualization as GIF/MP4
69
+ - Shareable URL with current state
70
+ - Download convolution computation log (CSV/JSON)
71
+
72
+ ## Technical Architecture
73
+
74
+ ### Frontend Stack
75
+ - **Framework**: Next.js 14+ (App Router) with TypeScript
76
+ - **State Management**: Zustand or React Context
77
+ - **Animation**: Framer Motion for UI, custom Canvas/WebGL for convolution viz
78
+ - **Styling**: Tailwind CSS
79
+ - **Canvas**: HTML5 Canvas API (primary), WebGL fallback for performance
80
+ - **Image Processing**: Custom JavaScript implementation (no heavy deps)
81
+
82
+ ### Component Structure
83
+ ```
84
+ src/
85
+ ├── app/ # Next.js App Router pages
86
+ ├── components/
87
+ │ ├── canvas/ # Canvas-based visualization components
88
+ │ ├── controls/ # Playback, speed, zoom controls
89
+ │ ├── kernel/ # Kernel selector, editor, library
90
+ │ ├── comparison/ # Split view, side-by-side comparison
91
+ │ ├── education/ # Math display, kernel info panels
92
+ │ └── ui/ # Reusable UI components (buttons, sliders, etc.)
93
+ ├── hooks/ # Custom React hooks
94
+ ├── lib/
95
+ │ ├── convolution/ # Core convolution algorithms
96
+ │ ├── kernels/ # Predefined kernel definitions
97
+ │ ├── image/ # Image loading, processing utilities
98
+ │ └── export/ # Export functionality
99
+ ├── store/ # State management
100
+ ├── types/ # TypeScript definitions
101
+ └── utils/ # Helper functions
102
+ ```
103
+
104
+ ### Core Algorithms
105
+
106
+ #### Convolution Engine
107
+ ```typescript
108
+ interface ConvolutionOptions {
109
+ kernel: number[][];
110
+ padding: 'zero' | 'reflect' | 'replicate' | 'wrap';
111
+ normalize: boolean;
112
+ stride: number;
113
+ }
114
+
115
+ function convolve(
116
+ imageData: ImageData,
117
+ kernel: number[][],
118
+ options: ConvolutionOptions,
119
+ onProgress?: (progress: ConvolutionProgress) => void
120
+ ): Promise<ImageData>;
121
+ ```
122
+
123
+ #### Step-by-step Generator
124
+ ```typescript
125
+ interface ConvolutionStep {
126
+ x: number;
127
+ y: number;
128
+ inputPatch: number[][][];
129
+ kernel: number[][];
130
+ multiplications: number[][][];
131
+ sum: number;
132
+ outputValue: number;
133
+ channel: number;
134
+ }
135
+
136
+ function* convolutionSteps(
137
+ imageData: ImageData,
138
+ kernel: number[][],
139
+ options: ConvolutionOptions
140
+ ): Generator<ConvolutionStep>;
141
+ ```
142
+
143
+ ### Animation System
144
+ - RequestAnimationFrame-based loop
145
+ - Configurable frame rate (target 60fps)
146
+ - Step batching for performance (process N pixels per frame)
147
+ - Pause/resume without losing state
148
+ - Step-by-step mode with manual advancement
149
+
150
+ ### Performance Considerations
151
+ - Web Workers for heavy computation
152
+ - Canvas pooling to reduce GC pressure
153
+ - TypedArrays for numerical computation
154
+ - Progressive rendering (don't block UI)
155
+ - Lazy evaluation for comparison views
156
+
157
+ ## Development Phases
158
+
159
+ ### Phase 1: Foundation (Week 1-2)
160
+ - [ ] Project setup (Next.js, TypeScript, Tailwind, ESLint, Prettier)
161
+ - [ ] Basic project structure and component architecture
162
+ - [ ] Image upload and display component
163
+ - [ ] Basic Canvas rendering setup
164
+ - [ ] Kernel data structure and predefined kernels library
165
+
166
+ ### Phase 2: Core Convolution (Week 2-3)
167
+ - [ ] Implement convolution algorithm (CPU)
168
+ - [ ] Implement padding strategies
169
+ - [ ] Create step-by-step generator
170
+ - [ ] Basic convolution visualization (static)
171
+ - [ ] Output image generation
172
+
173
+ ### Phase 3: Animation & Visualization (Week 3-4)
174
+ - [ ] Animation loop with play/pause/step controls
175
+ - [ ] Speed control implementation
176
+ - [ ] Current patch highlighting
177
+ - [ ] Element-wise multiplication visualization
178
+ - [ ] Summation process display
179
+ - [ ] Progressive output rendering
180
+
181
+ ### Phase 4: Interactive Controls (Week 4-5)
182
+ - [ ] Zoom/pan functionality
183
+ - [ ] Overlay toggles (kernel boundary, values, math)
184
+ - [ ] Kernel selector UI with categories
185
+ - [ ] Custom kernel editor
186
+ - [ ] Padding strategy selector with visualization
187
+
188
+ ### Phase 5: Comparison & Analysis (Week 5-6)
189
+ - [ ] Side-by-side comparison view
190
+ - [ ] Split slider component
191
+ - [ ] Synchronized zoom/pan
192
+ - [ ] Multi-kernel comparison tabs
193
+ - [ ] Statistics panel (time, ops, memory)
194
+
195
+ ### Phase 6: Educational Features (Week 6-7)
196
+ - [ ] Real-time equation display
197
+ - [ ] Kernel information panels
198
+ - [ ] Channel processing visualization
199
+ - [ ] Tooltip/explanation system
200
+ - [ ] Learning resource links
201
+
202
+ ### Phase 7: Export & Polish (Week 7-8)
203
+ - [ ] Save processed image
204
+ - [ ] Export animation (GIF/Video)
205
+ - [ ] Shareable state URLs
206
+ - [ ] Performance optimization
207
+ - [ ] Mobile responsiveness
208
+ - [ ] Accessibility improvements
209
+ - [ ] Documentation & help system
210
+
211
+ ### Phase 8: Advanced Features (Week 8+)
212
+ - [ ] WebGL acceleration option
213
+ - [ ] Web Worker for computation
214
+ - [ ] Batch processing multiple images
215
+ - [ ] Kernel composition (sequential filters)
216
+ - [ ] Frequency domain visualization (FFT)
217
+ - [ ] 3D kernel visualization
218
+
219
+ ## Data Structures
220
+
221
+ ### Kernel Definition
222
+ ```typescript
223
+ interface Kernel {
224
+ id: string;
225
+ name: string;
226
+ category: 'blur' | 'sharpen' | 'edge' | 'emboss' | 'custom' | 'other';
227
+ matrix: number[][];
228
+ size: number;
229
+ normalize: boolean;
230
+ description: string;
231
+ formula?: string;
232
+ reference?: string;
233
+ }
234
+ ```
235
+
236
+ ### Convolution State
237
+ ```typescript
238
+ interface ConvolutionState {
239
+ status: 'idle' | 'running' | 'paused' | 'completed' | 'error';
240
+ currentStep: ConvolutionStep | null;
241
+ progress: number; // 0-1
242
+ startTime: number;
243
+ elapsedTime: number;
244
+ processedPixels: number;
245
+ totalPixels: number;
246
+ outputImageData: ImageData | null;
247
+ }
248
+ ```
249
+
250
+ ### App Settings
251
+ ```typescript
252
+ interface AppSettings {
253
+ animationSpeed: number; // 0.5 - 10
254
+ showOverlay: {
255
+ kernelBoundary: boolean;
256
+ pixelValues: boolean;
257
+ multiplications: boolean;
258
+ summation: boolean;
259
+ };
260
+ padding: 'zero' | 'reflect' | 'replicate' | 'wrap';
261
+ zoomLevel: number;
262
+ panOffset: { x: number; y: number };
263
+ comparisonMode: 'side-by-side' | 'split' | 'overlay';
264
+ }
265
+ ```
266
+
267
+ ## Testing Strategy
268
+
269
+ ### Unit Tests
270
+ - Convolution algorithm correctness
271
+ - Padding strategy implementations
272
+ - Kernel normalization
273
+ - Step generator accuracy
274
+ - Export functionality
275
+
276
+ ### Integration Tests
277
+ - Image upload → convolution → output pipeline
278
+ - Animation playback controls
279
+ - Comparison view synchronization
280
+ - State persistence/restoration
281
+
282
+ ### Visual Regression Tests
283
+ - Canvas rendering at different zoom levels
284
+ - Overlay accuracy
285
+ - Split slider positions
286
+ - Kernel matrix display
287
+
288
+ ### Performance Benchmarks
289
+ - Convolution speed (pixels/second)
290
+ - Animation frame rate
291
+ - Memory usage during large image processing
292
+ - Web Worker vs main thread performance
293
+
294
+ ## Accessibility
295
+ - Keyboard navigation for all controls
296
+ - Screen reader announcements for animation state
297
+ - High contrast mode support
298
+ - Reduced motion preference respect
299
+ - Focus indicators
300
+ - Alt text for all visual elements
301
+
302
+ ## Browser Support
303
+ - Chrome 90+
304
+ - Firefox 88+
305
+ - Safari 14+
306
+ - Edge 90+
307
+ - Mobile browsers (iOS Safari, Chrome Android)
308
+
309
+ ## Deployment
310
+ - Static export to Vercel/Netlify
311
+ - CDN for kernel definitions and assets
312
+ - PWA support for offline use
313
+ - Analytics (privacy-friendly)
314
+
315
+ ## Success Metrics
316
+ - Frame rate ≥ 30fps during animation
317
+ - Convolution of 1MP image < 2 seconds
318
+ - Time to interactive < 3 seconds
319
+ - Lighthouse score > 90
320
+ - Zero critical accessibility violations
321
+
322
+ ## Risk Mitigation
323
+ | Risk | Mitigation |
324
+ |------|------------|
325
+ | Performance on large images | Web Workers, progressive rendering, WebGL fallback |
326
+ | Animation smoothness | RequestAnimationFrame, frame budgeting, step batching |
327
+ | Memory leaks | Canvas pooling, proper cleanup, WeakRef for caches |
328
+ | Cross-browser canvas differences | Feature detection, polyfills, fallback rendering |
329
+ | Complex state management | Zustand with devtools, immutable updates |
330
+
331
+ ## Future Extensions
332
+ - Video convolution visualization
333
+ - 3D convolution (volumetric data)
334
+ - CNN layer visualization (multiple kernels, feature maps)
335
+ - Interactive kernel design with real-time preview
336
+ - Frequency domain analysis
337
+ - Integration with ML frameworks (TensorFlow.js, ONNX.js)
338
+ - Collaborative annotation/sharing
339
+ - Plugin system for custom kernels/effects
postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
public/favicon.ico DELETED
Binary file (3.87 kB)
 
public/file.svg ADDED
public/globe.svg ADDED
public/index.html DELETED
@@ -1,43 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1" />
7
- <meta name="theme-color" content="#000000" />
8
- <meta
9
- name="description"
10
- content="Web site created using create-react-app"
11
- />
12
- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13
- <!--
14
- manifest.json provides metadata used when your web app is installed on a
15
- user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
16
- -->
17
- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
18
- <!--
19
- Notice the use of %PUBLIC_URL% in the tags above.
20
- It will be replaced with the URL of the `public` folder during the build.
21
- Only files inside the `public` folder can be referenced from the HTML.
22
-
23
- Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
24
- work correctly both with client-side routing and a non-root public URL.
25
- Learn how to configure a non-root public URL by running `npm run build`.
26
- -->
27
- <title>React App</title>
28
- </head>
29
- <body>
30
- <noscript>You need to enable JavaScript to run this app.</noscript>
31
- <div id="root"></div>
32
- <!--
33
- This HTML file is a template.
34
- If you open it directly in the browser, you will see an empty page.
35
-
36
- You can add webfonts, meta tags, or analytics to this file.
37
- The build step will place the bundled scripts into the <body> tag.
38
-
39
- To begin the development, run `npm start` or `yarn start`.
40
- To create a production bundle, use `npm run build` or `yarn build`.
41
- -->
42
- </body>
43
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
public/logo192.png DELETED
Binary file (5.35 kB)
 
public/logo512.png DELETED
Binary file (9.66 kB)
 
public/manifest.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "short_name": "React App",
3
- "name": "Create React App Sample",
4
- "icons": [
5
- {
6
- "src": "favicon.ico",
7
- "sizes": "64x64 32x32 24x24 16x16",
8
- "type": "image/x-icon"
9
- },
10
- {
11
- "src": "logo192.png",
12
- "type": "image/png",
13
- "sizes": "192x192"
14
- },
15
- {
16
- "src": "logo512.png",
17
- "type": "image/png",
18
- "sizes": "512x512"
19
- }
20
- ],
21
- "start_url": ".",
22
- "display": "standalone",
23
- "theme_color": "#000000",
24
- "background_color": "#ffffff"
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
public/next.svg ADDED
public/robots.txt DELETED
@@ -1,3 +0,0 @@
1
- # https://www.robotstxt.org/robotstxt.html
2
- User-agent: *
3
- Disallow:
 
 
 
 
public/vercel.svg ADDED
public/window.svg ADDED
src/App.css DELETED
@@ -1,38 +0,0 @@
1
- .App {
2
- text-align: center;
3
- }
4
-
5
- .App-logo {
6
- height: 40vmin;
7
- pointer-events: none;
8
- }
9
-
10
- @media (prefers-reduced-motion: no-preference) {
11
- .App-logo {
12
- animation: App-logo-spin infinite 20s linear;
13
- }
14
- }
15
-
16
- .App-header {
17
- background-color: #282c34;
18
- min-height: 100vh;
19
- display: flex;
20
- flex-direction: column;
21
- align-items: center;
22
- justify-content: center;
23
- font-size: calc(10px + 2vmin);
24
- color: white;
25
- }
26
-
27
- .App-link {
28
- color: #61dafb;
29
- }
30
-
31
- @keyframes App-logo-spin {
32
- from {
33
- transform: rotate(0deg);
34
- }
35
- to {
36
- transform: rotate(360deg);
37
- }
38
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/App.js DELETED
@@ -1,25 +0,0 @@
1
- import logo from './logo.svg';
2
- import './App.css';
3
-
4
- function App() {
5
- return (
6
- <div className="App">
7
- <header className="App-header">
8
- <img src={logo} className="App-logo" alt="logo" />
9
- <p>
10
- Edit <code>src/App.js</code> and save to reload.
11
- </p>
12
- <a
13
- className="App-link"
14
- href="https://reactjs.org"
15
- target="_blank"
16
- rel="noopener noreferrer"
17
- >
18
- Learn React
19
- </a>
20
- </header>
21
- </div>
22
- );
23
- }
24
-
25
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/App.test.js DELETED
@@ -1,8 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import App from './App';
3
-
4
- test('renders learn react link', () => {
5
- render(<App />);
6
- const linkElement = screen.getByText(/learn react/i);
7
- expect(linkElement).toBeInTheDocument();
8
- });
 
 
 
 
 
 
 
 
 
src/app/favicon.ico ADDED
src/app/globals.css ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #f9fafb;
5
+ --foreground: #111827;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-geist-sans);
12
+ --font-mono: var(--font-geist-mono);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #111827;
18
+ --foreground: #f9fafb;
19
+ }
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ background: var(--background);
28
+ color: var(--foreground);
29
+ font-family: var(--font-sans);
30
+ }
31
+
32
+ /* Custom scrollbar */
33
+ ::-webkit-scrollbar {
34
+ width: 8px;
35
+ height: 8px;
36
+ }
37
+
38
+ ::-webkit-scrollbar-track {
39
+ background: transparent;
40
+ }
41
+
42
+ ::-webkit-scrollbar-thumb {
43
+ background: #9ca3af;
44
+ border-radius: 4px;
45
+ }
46
+
47
+ ::-webkit-scrollbar-thumb:hover {
48
+ background: #6b7280;
49
+ }
50
+
51
+ /* Focus visible styles */
52
+ :focus-visible {
53
+ outline: 2px solid #3b82f6;
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ /* Animations */
58
+ @keyframes slide-in {
59
+ from {
60
+ opacity: 0;
61
+ transform: translateY(10px);
62
+ }
63
+ to {
64
+ opacity: 1;
65
+ transform: translateY(0);
66
+ }
67
+ }
68
+
69
+ .animate-slide-in {
70
+ animation: slide-in 0.3s ease-out;
71
+ }
72
+
73
+ /* Canvas styles */
74
+ canvas {
75
+ display: block;
76
+ }
77
+
78
+ /* Selection styles */
79
+ ::selection {
80
+ background: #3b82f6;
81
+ color: white;
82
+ }
83
+
84
+ /* Input range styles */
85
+ input[type="range"] {
86
+ -webkit-appearance: none;
87
+ appearance: none;
88
+ }
89
+
90
+ input[type="range"]::-webkit-slider-thumb {
91
+ -webkit-appearance: none;
92
+ appearance: none;
93
+ width: 16px;
94
+ height: 16px;
95
+ border-radius: 50%;
96
+ background: #3b82f6;
97
+ cursor: pointer;
98
+ border: 2px solid white;
99
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
100
+ }
101
+
102
+ input[type="range"]::-moz-range-thumb {
103
+ width: 16px;
104
+ height: 16px;
105
+ border-radius: 50%;
106
+ background: #3b82f6;
107
+ cursor: pointer;
108
+ border: 2px solid white;
109
+ }
110
+
111
+ /* Number input arrows */
112
+ input[type="number"]::-webkit-inner-spin-button,
113
+ input[type="number"]::-webkit-outer-spin-button {
114
+ opacity: 1;
115
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Interactive Kernel Convolution Visualizer",
17
+ description: "Learn how image convolution works with interactive step-by-step visualization",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
27
+ <body className="min-h-full flex flex-col">{children}</body>
28
+ </html>
29
+ );
30
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { useAppStore, useImageStore, useUIStore } from '@/store'
5
+ import { KernelSelector } from '@/components/kernel/KernelSelector'
6
+ import { PlaybackControls } from '@/components/controls/PlaybackControls'
7
+ import { OverlayControls, PaddingControls, KernelInfoPanel } from '@/components/controls/OverlayControls'
8
+ import { ConvolutionCanvas } from '@/components/canvas/ConvolutionCanvas'
9
+ import { SplitView } from '@/components/canvas/SplitView'
10
+ import { ImageCanvas } from '@/components/canvas/ImageCanvas'
11
+ import { Button, Card, CardContent, Badge } from '@/components/ui'
12
+ import { useImageLoader, useDragAndDrop } from '@/hooks/useImageLoader'
13
+ import { useConvolution } from '@/hooks/useConvolution'
14
+ import { cn, formatNumber } from '@/utils/helpers'
15
+ import {
16
+ Upload,
17
+ Image as ImageIcon,
18
+ Download,
19
+ RotateCcw,
20
+ HelpCircle,
21
+ Settings,
22
+ ChevronLeft,
23
+ ChevronRight,
24
+ } from 'lucide-react'
25
+
26
+ export default function Home() {
27
+ const {
28
+ originalImageData,
29
+ processedImageData,
30
+ selectedKernel,
31
+ convolution,
32
+ settings,
33
+ isLoading,
34
+ error,
35
+ showHelp,
36
+ showSettings,
37
+ setOriginalImage,
38
+ setError,
39
+ toggleHelp,
40
+ toggleSettings,
41
+ resetConvolution,
42
+ } = useAppStore()
43
+
44
+ const { runConvolution, pause, resume, stop, step, reset, isRunning, isPaused, canRun } = useConvolution()
45
+ const { fileInputRef, openFileDialog, handleFileSelect } = useImageLoader()
46
+ const { handleDragOver, handleDragLeave, handleDrop, isDragging } = useDragAndDrop((image, imageData) => {
47
+ setOriginalImage(image, imageData)
48
+ resetConvolution()
49
+ })
50
+
51
+ const [showComparison, setShowComparison] = useState(false)
52
+ const [sidebarWidth, setSidebarWidth] = useState(320)
53
+
54
+ const handleImageLoad = useCallback((data: { image: HTMLImageElement; imageData: ImageData }) => {
55
+ setOriginalImage(data.image, data.imageData)
56
+ resetConvolution()
57
+ }, [setOriginalImage, resetConvolution])
58
+
59
+ const handleDownload = useCallback(() => {
60
+ if (!processedImageData) return
61
+ const canvas = document.createElement('canvas')
62
+ canvas.width = processedImageData.width
63
+ canvas.height = processedImageData.height
64
+ const ctx = canvas.getContext('2d')
65
+ if (!ctx) return
66
+ ctx.putImageData(processedImageData, 0, 0)
67
+ const link = document.createElement('a')
68
+ link.download = 'convolved-image.png'
69
+ link.href = canvas.toDataURL('image/png')
70
+ link.click()
71
+ }, [processedImageData])
72
+
73
+ const handleReset = useCallback(() => {
74
+ reset()
75
+ resetConvolution()
76
+ }, [reset, resetConvolution])
77
+
78
+ if (isLoading) {
79
+ return (
80
+ <div className="fixed inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-50">
81
+ <div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent" />
82
+ </div>
83
+ )
84
+ }
85
+
86
+ return (
87
+ <div
88
+ className="h-screen w-screen flex flex-col bg-gray-50 dark:bg-gray-900 overflow-hidden"
89
+ onDragOver={handleDragOver}
90
+ onDragLeave={handleDragLeave}
91
+ onDrop={handleDrop}
92
+ >
93
+ {/* Header */}
94
+ <header className="flex items-center justify-between px-4 py-3 bg-white dark:bg-gray-800 border-b dark:border-gray-700 z-10">
95
+ <div className="flex items-center gap-4">
96
+ <h1 className="text-xl font-bold text-gray-900 dark:text-white">
97
+ Kernel Convolution Visualizer
98
+ </h1>
99
+ <Badge variant="outline" className="text-xs">
100
+ Educational Tool
101
+ </Badge>
102
+ </div>
103
+
104
+ <div className="flex items-center gap-2">
105
+ <Button variant="ghost" size="icon" onClick={toggleHelp} className="relative">
106
+ <HelpCircle className="h-5 w-5" />
107
+ </Button>
108
+ <Button variant="ghost" size="icon" onClick={toggleSettings}>
109
+ <Settings className="h-5 w-5" />
110
+ </Button>
111
+ </div>
112
+ </header>
113
+
114
+ {/* Main Content */}
115
+ <main className="flex-1 flex overflow-hidden">
116
+ {/* Left Sidebar - Controls */}
117
+ <aside
118
+ className={cn(
119
+ 'flex flex-col bg-white dark:bg-gray-800 border-r dark:border-gray-700 overflow-y-auto transition-all duration-200',
120
+ 'min-w-[280px] max-w-[400px]'
121
+ )}
122
+ style={{ width: sidebarWidth }}
123
+ >
124
+ {/* Image Upload */}
125
+ <Card className="m-3">
126
+ <CardContent className="p-4">
127
+ <div
128
+ className={cn(
129
+ 'relative border-2 dashed rounded-lg p-6 text-center transition-colors',
130
+ 'cursor-pointer',
131
+ isDragging
132
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
133
+ : 'border-gray-300 dark:border-gray-600 hover:border-blue-400'
134
+ )}
135
+ onClick={openFileDialog}
136
+ >
137
+ <input
138
+ ref={fileInputRef}
139
+ type="file"
140
+ accept="image/*"
141
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
142
+ onChange={(e) => handleFileSelect(e, handleImageLoad)}
143
+ />
144
+ <ImageIcon className="h-12 w-12 mx-auto text-gray-400 mb-3" />
145
+ <p className="text-gray-600 dark:text-gray-400">
146
+ {originalImageData ? 'Click to change image' : 'Drag & drop or click to upload'}
147
+ </p>
148
+ <p className="text-sm text-gray-400 mt-1">PNG, JPG, WebP, BMP</p>
149
+ </div>
150
+
151
+ {originalImageData && (
152
+ <div className="mt-3 flex items-center justify-between text-sm">
153
+ <span className="text-gray-600 dark:text-gray-400">
154
+ {originalImageData.width} × {originalImageData.height}
155
+ </span>
156
+ <div className="flex gap-2">
157
+ <Button variant="outline" size="sm" onClick={handleDownload}>
158
+ <Download className="h-4 w-4" />
159
+ Save
160
+ </Button>
161
+ <Button variant="outline" size="sm" onClick={handleReset}>
162
+ <RotateCcw className="h-4 w-4" />
163
+ Reset
164
+ </Button>
165
+ </div>
166
+ </div>
167
+ )}
168
+ </CardContent>
169
+ </Card>
170
+
171
+ {/* Kernel Selector */}
172
+ <div className="flex-1 m-3 min-h-0">
173
+ <KernelSelector />
174
+ </div>
175
+
176
+ {/* Playback Controls */}
177
+ <div className="m-3">
178
+ <PlaybackControls />
179
+ </div>
180
+ </aside>
181
+
182
+ {/* Resizer */}
183
+ <div
184
+ className="w-1 cursor-col-resize bg-gray-200 dark:bg-gray-700 hover:bg-blue-500 active:bg-blue-600 flex items-center justify-center"
185
+ onMouseDown={(e) => {
186
+ const startX = e.clientX
187
+ const startWidth = sidebarWidth
188
+ const onMouseMove = (e: MouseEvent) => {
189
+ setSidebarWidth(Math.max(280, Math.min(400, startWidth + (e.clientX - startX))))
190
+ }
191
+ const onMouseUp = () => {
192
+ document.removeEventListener('mousemove', onMouseMove)
193
+ document.removeEventListener('mouseup', onMouseUp)
194
+ }
195
+ document.addEventListener('mousemove', onMouseMove)
196
+ document.addEventListener('mouseup', onMouseUp)
197
+ }}
198
+ >
199
+ <div className="w-px h-8 bg-gray-300 dark:bg-gray-600 rounded" />
200
+ </div>
201
+
202
+ {/* Center - Visualization */}
203
+ <section className="flex-1 flex flex-col overflow-hidden p-4 min-w-0">
204
+ {originalImageData ? (
205
+ <>
206
+ {/* Convolution Visualization */}
207
+ <div className="flex-1 min-h-0 relative">
208
+ <ConvolutionCanvas
209
+ width={originalImageData.width}
210
+ height={originalImageData.height}
211
+ className="w-full h-full"
212
+ />
213
+ </div>
214
+
215
+ {/* Comparison View Toggle */}
216
+ <div className="flex items-center justify-center gap-4 mt-4">
217
+ <Button
218
+ variant={showComparison ? 'primary' : 'outline'}
219
+ onClick={() => setShowComparison(!showComparison)}
220
+ >
221
+ {showComparison ? <ChevronLeft className="h-4 w-4 mr-2" /> : <ChevronRight className="h-4 w-4 mr-2" />}
222
+ {showComparison ? 'Hide Comparison' : 'Show Comparison'}
223
+ </Button>
224
+
225
+ {selectedKernel && (
226
+ <Badge variant="secondary">
227
+ {selectedKernel.name} ({selectedKernel.size}×{selectedKernel.size})
228
+ </Badge>
229
+ )}
230
+
231
+ {convolution.status !== 'idle' && (
232
+ <Badge variant={convolution.status === 'completed' ? 'success' : 'default'}>
233
+ {convolution.status}
234
+ </Badge>
235
+ )}
236
+ </div>
237
+ </>
238
+ ) : (
239
+ <div className="flex-1 flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded-lg">
240
+ <div className="text-center text-gray-500">
241
+ <ImageIcon className="h-16 w-16 mx-auto mb-4 opacity-50" />
242
+ <p className="text-lg">Upload an image to begin</p>
243
+ <p className="text-sm mt-2">Drag & drop or click the upload area</p>
244
+ </div>
245
+ </div>
246
+ )}
247
+
248
+ {/* Comparison View */}
249
+ {showComparison && processedImageData && (
250
+ <div className="mt-4 h-96 flex-1">
251
+ <SplitView
252
+ width={originalImageData?.width || 800}
253
+ height={originalImageData?.height || 600}
254
+ className="w-full h-full"
255
+ />
256
+ </div>
257
+ )}
258
+ </section>
259
+
260
+ {/* Right Sidebar - Info Panels */}
261
+ <aside className="w-80 flex flex-col bg-white dark:bg-gray-800 border-l dark:border-gray-700 overflow-y-auto">
262
+ <div className="p-4 space-y-4">
263
+ <KernelInfoPanel />
264
+ <OverlayControls />
265
+ <PaddingControls />
266
+ </div>
267
+ </aside>
268
+ </main>
269
+
270
+ {/* Error Toast */}
271
+ {error && (
272
+ <div className="fixed bottom-4 right-4 z-50 animate-slide-in">
273
+ <div className="bg-red-600 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 max-w-md">
274
+ <span className="flex-1 text-sm">{error}</span>
275
+ <Button variant="ghost" size="sm" onClick={() => setError(null)}>
276
+ Dismiss
277
+ </Button>
278
+ </div>
279
+ </div>
280
+ )}
281
+
282
+ {/* Help Modal */}
283
+ {showHelp && (
284
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
285
+ <div className="bg-white dark:bg-gray-800 rounded-lg max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto">
286
+ <div className="p-6">
287
+ <h2 className="text-xl font-bold mb-4">How to Use</h2>
288
+ <div className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
289
+ <div>
290
+ <h3 className="font-medium mb-2">1. Upload an Image</h3>
291
+ <p>Drag & drop or click the upload area in the left sidebar.</p>
292
+ </div>
293
+ <div>
294
+ <h3 className="font-medium mb-2">2. Select a Kernel</h3>
295
+ <p>Choose from predefined kernels (Blur, Sharpen, Edge Detection, etc.) or create your own custom kernel.</p>
296
+ </div>
297
+ <div>
298
+ <h3 className="font-medium mb-2">3. Watch the Convolution</h3>
299
+ <p>Press Play to see the kernel move across the image pixel by pixel. The visualization shows:</p>
300
+ <ul className="list-disc list-inside mt-1 space-y-1">
301
+ <li>Current image patch under the kernel</li>
302
+ <li>Element-wise multiplication of kernel and pixel values</li>
303
+ <li>Summation that produces the output pixel</li>
304
+ </ul>
305
+ </div>
306
+ <div>
307
+ <h3 className="font-medium mb-2">4. Control Playback</h3>
308
+ <ul className="list-disc list-inside mt-1 space-y-1">
309
+ <li>Play/Pause/Step through the animation</li>
310
+ <li>Adjust animation speed</li>
311
+ <li>Zoom and pan for pixel-level inspection</li>
312
+ </ul>
313
+ </div>
314
+ <div>
315
+ <h3 className="font-medium mb-2">5. Compare Results</h3>
316
+ <p>Click &ldquo;Show Comparison&rdquo; to see side-by-side or split view of original vs processed image.</p>
317
+ </div>
318
+ <div>
319
+ <h3 className="font-medium mb-2">6. Experiment</h3>
320
+ <ul className="list-disc list-inside mt-1 space-y-1">
321
+ <li>Try different padding strategies (Zero, Reflect, Replicate, Wrap)</li>
322
+ <li>Toggle overlays to see different visualization aspects</li>
323
+ <li>Create custom kernels with the editor</li>
324
+ <li>Download the processed image</li>
325
+ </ul>
326
+ </div>
327
+ </div>
328
+ <div className="mt-6 flex justify-end">
329
+ <Button onClick={toggleHelp}>Got it</Button>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ )}
335
+
336
+ {/* Settings Modal */}
337
+ {showSettings && (
338
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
339
+ <div className="bg-white dark:bg-gray-800 rounded-lg max-w-md w-full mx-4">
340
+ <div className="p-6">
341
+ <h2 className="text-xl font-bold mb-4">Settings</h2>
342
+ <div className="space-y-4">
343
+ <label className="flex items-center gap-3 cursor-pointer">
344
+ <input
345
+ type="checkbox"
346
+ checked={settings.showOverlay.kernelBoundary}
347
+ onChange={() => useAppStore.getState().toggleOverlay('kernelBoundary')}
348
+ className="rounded border-gray-300"
349
+ />
350
+ <span>Show Kernel Boundary</span>
351
+ </label>
352
+ <label className="flex items-center gap-3 cursor-pointer">
353
+ <input
354
+ type="checkbox"
355
+ checked={settings.showOverlay.pixelValues}
356
+ onChange={() => useAppStore.getState().toggleOverlay('pixelValues')}
357
+ className="rounded border-gray-300"
358
+ />
359
+ <span>Show Pixel Values</span>
360
+ </label>
361
+ <label className="flex items-center gap-3 cursor-pointer">
362
+ <input
363
+ type="checkbox"
364
+ checked={settings.showOverlay.multiplications}
365
+ onChange={() => useAppStore.getState().toggleOverlay('multiplications')}
366
+ className="rounded border-gray-300"
367
+ />
368
+ <span>Show Multiplications</span>
369
+ </label>
370
+ <label className="flex items-center gap-3 cursor-pointer">
371
+ <input
372
+ type="checkbox"
373
+ checked={settings.showOverlay.summation}
374
+ onChange={() => useAppStore.getState().toggleOverlay('summation')}
375
+ className="rounded border-gray-300"
376
+ />
377
+ <span>Show Summation</span>
378
+ </label>
379
+ </div>
380
+ <div className="mt-6 flex justify-end">
381
+ <Button onClick={toggleSettings}>Close</Button>
382
+ </div>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ )}
387
+ </div>
388
+ )
389
+ }
src/components/canvas/ConvolutionCanvas.tsx ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useRef, useEffect, useCallback, useMemo } from 'react'
4
+ import { useAppStore, useConvolutionStore, useSettingsStore } from '@/store'
5
+ import type { ConvolutionStep } from '@/types'
6
+ import { cn } from '@/utils/helpers'
7
+
8
+ interface ConvolutionCanvasProps {
9
+ width?: number
10
+ height?: number
11
+ className?: string
12
+ }
13
+
14
+ export function ConvolutionCanvas({ width = 800, height = 600, className }: ConvolutionCanvasProps) {
15
+ const canvasRef = useRef<HTMLCanvasElement>(null)
16
+ const animationRef = useRef<number | null>(null)
17
+
18
+ const { originalImageData, processedImageData, convolution } = useAppStore()
19
+ const { showOverlay, zoomLevel, panOffset, padding } = useSettingsStore()
20
+ const currentStep = convolution.currentStep
21
+
22
+ const drawCanvas = useCallback(() => {
23
+ const canvas = canvasRef.current
24
+ const ctx = canvas?.getContext('2d', { willReadFrequently: true })
25
+ if (!canvas || !ctx) return
26
+
27
+ const displayWidth = Math.round(width * zoomLevel)
28
+ const displayHeight = Math.round(height * zoomLevel)
29
+
30
+ canvas.width = displayWidth
31
+ canvas.height = displayHeight
32
+
33
+ ctx.clearRect(0, 0, displayWidth, displayHeight)
34
+ ctx.imageSmoothingEnabled = false
35
+
36
+ // Draw original image as background (dimmed)
37
+ if (originalImageData) {
38
+ const tempCanvas = document.createElement('canvas')
39
+ tempCanvas.width = originalImageData.width
40
+ tempCanvas.height = originalImageData.height
41
+ const tempCtx = tempCanvas.getContext('2d')
42
+ if (tempCtx) {
43
+ tempCtx.putImageData(originalImageData, 0, 0)
44
+ ctx.globalAlpha = 0.3
45
+ ctx.drawImage(
46
+ tempCanvas,
47
+ 0,
48
+ 0,
49
+ originalImageData.width,
50
+ originalImageData.height,
51
+ panOffset.x,
52
+ panOffset.y,
53
+ originalImageData.width * zoomLevel,
54
+ originalImageData.height * zoomLevel
55
+ )
56
+ ctx.globalAlpha = 1
57
+ }
58
+ }
59
+
60
+ // Draw processed image progressively
61
+ if (processedImageData) {
62
+ const tempCanvas = document.createElement('canvas')
63
+ tempCanvas.width = processedImageData.width
64
+ tempCanvas.height = processedImageData.height
65
+ const tempCtx = tempCanvas.getContext('2d')
66
+ if (tempCtx) {
67
+ tempCtx.putImageData(processedImageData, 0, 0)
68
+ ctx.drawImage(
69
+ tempCanvas,
70
+ 0,
71
+ 0,
72
+ processedImageData.width,
73
+ processedImageData.height,
74
+ panOffset.x,
75
+ panOffset.y,
76
+ processedImageData.width * zoomLevel,
77
+ processedImageData.height * zoomLevel
78
+ )
79
+ }
80
+ }
81
+
82
+ // Draw convolution visualization overlays
83
+ if (currentStep && currentStep.x >= 0 && currentStep.y >= 0) {
84
+ drawConvolutionOverlay(ctx, currentStep, zoomLevel, panOffset, showOverlay)
85
+ }
86
+
87
+ // Draw progress indicator
88
+ if (convolution.status === 'running' || convolution.status === 'paused') {
89
+ drawProgressBar(ctx, displayWidth, displayHeight, convolution.progress)
90
+ }
91
+ }, [
92
+ originalImageData,
93
+ processedImageData,
94
+ currentStep,
95
+ convolution.status,
96
+ convolution.progress,
97
+ zoomLevel,
98
+ panOffset,
99
+ showOverlay,
100
+ width,
101
+ height,
102
+ ])
103
+
104
+ useEffect(() => {
105
+ drawCanvas()
106
+ }, [drawCanvas])
107
+
108
+ // Animation loop for smooth rendering
109
+ useEffect(() => {
110
+ const animate = () => {
111
+ drawCanvas()
112
+ animationRef.current = requestAnimationFrame(animate)
113
+ }
114
+ animationRef.current = requestAnimationFrame(animate)
115
+ return () => {
116
+ if (animationRef.current) cancelAnimationFrame(animationRef.current)
117
+ }
118
+ }, [drawCanvas])
119
+
120
+ return (
121
+ <div className={cn('relative overflow-hidden bg-gray-100 dark:bg-gray-900 rounded-lg', className)}>
122
+ <canvas
123
+ ref={canvasRef}
124
+ width={width}
125
+ height={height}
126
+ className="block"
127
+ style={{ touchAction: 'none' }}
128
+ />
129
+ </div>
130
+ )
131
+ }
132
+
133
+ function drawConvolutionOverlay(
134
+ ctx: CanvasRenderingContext2D,
135
+ step: ConvolutionStep,
136
+ zoom: number,
137
+ pan: { x: number; y: number },
138
+ showOverlay: {
139
+ kernelBoundary: boolean
140
+ pixelValues: boolean
141
+ multiplications: boolean
142
+ summation: boolean
143
+ }
144
+ ) {
145
+ const { x, y, inputPatch, kernel, multiplications, sum, normalizedValue, channel } = step
146
+ const kernelSize = kernel.length
147
+ const halfKernel = Math.floor(kernelSize / 2)
148
+ const cellSize = Math.max(20, Math.min(60, 40 * zoom))
149
+
150
+ const centerX = pan.x + x * zoom
151
+ const centerY = pan.y + y * zoom
152
+
153
+ // Draw kernel boundary
154
+ if (showOverlay.kernelBoundary) {
155
+ ctx.strokeStyle = 'rgba(59, 130, 246, 0.8)'
156
+ ctx.lineWidth = 2
157
+ ctx.setLineDash([5, 5])
158
+ ctx.strokeRect(
159
+ centerX - halfKernel * cellSize * zoom,
160
+ centerY - halfKernel * cellSize * zoom,
161
+ kernelSize * cellSize * zoom,
162
+ kernelSize * cellSize * zoom
163
+ )
164
+ ctx.setLineDash([])
165
+ }
166
+
167
+ // Draw kernel matrix with values
168
+ if (showOverlay.pixelValues || showOverlay.multiplications) {
169
+ const fontSize = Math.max(10, Math.min(14, 12 * zoom))
170
+ ctx.font = `${fontSize}px monospace`
171
+ ctx.textAlign = 'center'
172
+ ctx.textBaseline = 'middle'
173
+
174
+ for (let ky = 0; ky < kernelSize; ky++) {
175
+ for (let kx = 0; kx < kernelSize; kx++) {
176
+ const cellX = centerX + (kx - halfKernel) * cellSize * zoom
177
+ const cellY = centerY + (ky - halfKernel) * cellSize * zoom
178
+
179
+ // Cell background
180
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'
181
+ ctx.fillRect(cellX - cellSize / 2, cellY - cellSize / 2, cellSize, cellSize)
182
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'
183
+ ctx.strokeRect(cellX - cellSize / 2, cellY - cellSize / 2, cellSize, cellSize)
184
+
185
+ if (showOverlay.multiplications && multiplications[ky]?.[kx] !== undefined) {
186
+ // Show multiplication: pixel × kernel
187
+ const pixelVal = inputPatch[ky]?.[kx] ?? 0
188
+ const kernelVal = kernel[ky]?.[kx] ?? 0
189
+ const mult = multiplications[ky][kx]
190
+
191
+ ctx.fillStyle = '#1e40af'
192
+ ctx.fillText(`${pixelVal.toFixed(2)}×${kernelVal.toFixed(2)}`, cellX, cellY - fontSize / 2)
193
+ ctx.fillStyle = '#dc2626'
194
+ ctx.fillText(`=${mult.toFixed(3)}`, cellX, cellY + fontSize / 2)
195
+ } else if (showOverlay.pixelValues) {
196
+ // Show kernel value
197
+ const kernelVal = kernel[ky]?.[kx] ?? 0
198
+ ctx.fillStyle = '#1e40af'
199
+ ctx.fillText(kernelVal.toFixed(2), cellX, cellY)
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // Draw summation display
206
+ if (showOverlay.summation) {
207
+ const fontSize = Math.max(12, Math.min(16, 14 * zoom))
208
+ ctx.font = `bold ${fontSize}px monospace`
209
+ ctx.textAlign = 'left'
210
+ ctx.textBaseline = 'top'
211
+
212
+ const sumX = centerX + (halfKernel + 1) * cellSize * zoom
213
+ const sumY = centerY - halfKernel * cellSize * zoom
214
+
215
+ // Background
216
+ const padding = 8
217
+ const textWidth = ctx.measureText(`Σ = ${sum.toFixed(3)}`).width + padding * 2
218
+ const textHeight = fontSize * 3 + padding * 2
219
+
220
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'
221
+ ctx.fillRect(sumX - padding, sumY - padding, textWidth, textHeight)
222
+ ctx.strokeStyle = '#3b82f6'
223
+ ctx.lineWidth = 2
224
+ ctx.strokeRect(sumX - padding, sumY - padding, textWidth, textHeight)
225
+
226
+ ctx.fillStyle = '#1e40af'
227
+ ctx.fillText(`Channel: ${['R', 'G', 'B', 'A'][channel] ?? channel}`, sumX, sumY)
228
+ ctx.fillText(`Sum: ${sum.toFixed(3)}`, sumX, sumY + fontSize + 4)
229
+ ctx.fillStyle = '#16a34a'
230
+ ctx.fillText(`Output: ${normalizedValue.toFixed(3)}`, sumX, sumY + (fontSize + 4) * 2)
231
+ }
232
+ }
233
+
234
+ function drawProgressBar(
235
+ ctx: CanvasRenderingContext2D,
236
+ width: number,
237
+ height: number,
238
+ progress: number
239
+ ) {
240
+ const barHeight = 4
241
+ const barWidth = width * 0.8
242
+ const x = (width - barWidth) / 2
243
+ const y = height - 20
244
+
245
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
246
+ ctx.fillRect(x, y, barWidth, barHeight)
247
+
248
+ ctx.fillStyle = '#3b82f6'
249
+ ctx.fillRect(x, y, barWidth * progress, barHeight)
250
+
251
+ ctx.fillStyle = '#fff'
252
+ ctx.font = '12px monospace'
253
+ ctx.textAlign = 'center'
254
+ ctx.fillText(`${Math.round(progress * 100)}%`, width / 2, y - 4)
255
+ }
src/components/canvas/ImageCanvas.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useRef, useEffect, useCallback } from 'react'
4
+ import { useAppStore } from '@/store'
5
+ import { cn } from '@/utils/helpers'
6
+
7
+ interface ImageCanvasProps {
8
+ imageData: ImageData | null
9
+ title: string
10
+ width?: number
11
+ height?: number
12
+ className?: string
13
+ onWheel?: (e: React.WheelEvent) => void
14
+ onMouseDown?: (e: React.MouseEvent) => void
15
+ onMouseMove?: (e: React.MouseEvent) => void
16
+ onMouseUp?: (e: React.MouseEvent) => void
17
+ }
18
+
19
+ export function ImageCanvas({
20
+ imageData,
21
+ title,
22
+ width = 400,
23
+ height = 300,
24
+ className,
25
+ onWheel,
26
+ onMouseDown,
27
+ onMouseMove,
28
+ onMouseUp,
29
+ }: ImageCanvasProps) {
30
+ const canvasRef = useRef<HTMLCanvasElement>(null)
31
+ const { settings } = useAppStore()
32
+
33
+ const drawCanvas = useCallback(() => {
34
+ const canvas = canvasRef.current
35
+ const ctx = canvas?.getContext('2d', { willReadFrequently: true })
36
+ if (!canvas || !ctx || !imageData) return
37
+
38
+ const { zoomLevel, panOffset } = settings
39
+ const displayWidth = Math.round(imageData.width * zoomLevel)
40
+ const displayHeight = Math.round(imageData.height * zoomLevel)
41
+
42
+ canvas.width = displayWidth
43
+ canvas.height = displayHeight
44
+
45
+ ctx.clearRect(0, 0, displayWidth, displayHeight)
46
+ ctx.imageSmoothingEnabled = zoomLevel < 2
47
+
48
+ const tempCanvas = document.createElement('canvas')
49
+ tempCanvas.width = imageData.width
50
+ tempCanvas.height = imageData.height
51
+ const tempCtx = tempCanvas.getContext('2d')
52
+ if (tempCtx) {
53
+ tempCtx.putImageData(imageData, 0, 0)
54
+ ctx.drawImage(
55
+ tempCanvas,
56
+ 0,
57
+ 0,
58
+ imageData.width,
59
+ imageData.height,
60
+ panOffset.x,
61
+ panOffset.y,
62
+ displayWidth,
63
+ displayHeight
64
+ )
65
+ }
66
+
67
+ // Draw grid at high zoom
68
+ if (zoomLevel >= 4) {
69
+ drawGrid(ctx, imageData.width, imageData.height, zoomLevel, panOffset)
70
+ }
71
+ }, [imageData, settings.zoomLevel, settings.panOffset])
72
+
73
+ useEffect(() => {
74
+ drawCanvas()
75
+ }, [drawCanvas])
76
+
77
+ return (
78
+ <div className={cn('relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden', className)}>
79
+ <div className="absolute top-2 left-2 z-10 px-2 py-1 bg-black/50 text-white text-xs rounded">
80
+ {title}
81
+ </div>
82
+ <canvas
83
+ ref={canvasRef}
84
+ width={width}
85
+ height={height}
86
+ className="block cursor-grab active:cursor-grabbing"
87
+ style={{ touchAction: 'none' }}
88
+ onWheel={onWheel}
89
+ onMouseDown={onMouseDown}
90
+ onMouseMove={onMouseMove}
91
+ onMouseUp={onMouseUp}
92
+ />
93
+ </div>
94
+ )
95
+ }
96
+
97
+ function drawGrid(
98
+ ctx: CanvasRenderingContext2D,
99
+ imgWidth: number,
100
+ imgHeight: number,
101
+ zoom: number,
102
+ pan: { x: number; y: number }
103
+ ) {
104
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'
105
+ ctx.lineWidth = 0.5
106
+
107
+ for (let x = 0; x <= imgWidth; x++) {
108
+ const px = pan.x + x * zoom
109
+ ctx.beginPath()
110
+ ctx.moveTo(px, 0)
111
+ ctx.lineTo(px, imgHeight * zoom)
112
+ ctx.stroke()
113
+ }
114
+
115
+ for (let y = 0; y <= imgHeight; y++) {
116
+ const py = pan.y + y * zoom
117
+ ctx.beginPath()
118
+ ctx.moveTo(0, py)
119
+ ctx.lineTo(imgWidth * zoom, py)
120
+ ctx.stroke()
121
+ }
122
+ }
src/components/canvas/SplitView.tsx ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useRef, useEffect, useCallback, useState } from 'react'
4
+ import { useAppStore } from '@/store'
5
+ import { cn } from '@/utils/helpers'
6
+
7
+ interface SplitViewProps {
8
+ width?: number
9
+ height?: number
10
+ className?: string
11
+ }
12
+
13
+ export function SplitView({ width = 800, height = 600, className }: SplitViewProps) {
14
+ const canvasRef = useRef<HTMLCanvasElement>(null)
15
+ const { originalImageData, processedImageData } = useAppStore()
16
+ const { splitPosition, mode, setComparisonViewMode, setSplitPosition } = useAppStore((state) => ({
17
+ splitPosition: state.comparison.splitPosition,
18
+ mode: state.comparison.mode,
19
+ setComparisonViewMode: state.setComparisonViewMode,
20
+ setSplitPosition: state.setSplitPosition,
21
+ }))
22
+ const [isDragging, setIsDragging] = useState(false)
23
+
24
+ const drawCanvas = useCallback(() => {
25
+ const canvas = canvasRef.current
26
+ const ctx = canvas?.getContext('2d', { willReadFrequently: true })
27
+ if (!canvas || !ctx || !originalImageData) return
28
+
29
+ const displayWidth = width
30
+ const displayHeight = height
31
+
32
+ canvas.width = displayWidth
33
+ canvas.height = displayHeight
34
+ ctx.clearRect(0, 0, displayWidth, displayHeight)
35
+
36
+ const splitX = displayWidth * splitPosition
37
+
38
+ if (mode === 'split' || mode === 'overlay') {
39
+ // Draw original on left
40
+ if (originalImageData) {
41
+ const tempCanvas = document.createElement('canvas')
42
+ tempCanvas.width = originalImageData.width
43
+ tempCanvas.height = originalImageData.height
44
+ const tempCtx = tempCanvas.getContext('2d')
45
+ if (tempCtx) {
46
+ tempCtx.putImageData(originalImageData, 0, 0)
47
+ ctx.drawImage(
48
+ tempCanvas,
49
+ 0,
50
+ 0,
51
+ originalImageData.width,
52
+ originalImageData.height,
53
+ 0,
54
+ 0,
55
+ splitX,
56
+ displayHeight
57
+ )
58
+ }
59
+ }
60
+
61
+ // Draw processed on right
62
+ if (processedImageData) {
63
+ const tempCanvas = document.createElement('canvas')
64
+ tempCanvas.width = processedImageData.width
65
+ tempCanvas.height = processedImageData.height
66
+ const tempCtx = tempCanvas.getContext('2d')
67
+ if (tempCtx) {
68
+ tempCtx.putImageData(processedImageData, 0, 0)
69
+ ctx.drawImage(
70
+ tempCanvas,
71
+ splitX * (originalImageData.width / displayWidth),
72
+ 0,
73
+ processedImageData.width - splitX * (originalImageData.width / displayWidth),
74
+ processedImageData.height,
75
+ splitX,
76
+ 0,
77
+ displayWidth - splitX,
78
+ displayHeight
79
+ )
80
+ }
81
+ }
82
+
83
+ // Draw divider
84
+ ctx.strokeStyle = '#fff'
85
+ ctx.lineWidth = 3
86
+ ctx.setLineDash([10, 5])
87
+ ctx.beginPath()
88
+ ctx.moveTo(splitX, 0)
89
+ ctx.lineTo(splitX, displayHeight)
90
+ ctx.stroke()
91
+ ctx.setLineDash([])
92
+
93
+ // Draw handle
94
+ ctx.fillStyle = '#3b82f6'
95
+ ctx.beginPath()
96
+ ctx.arc(splitX, displayHeight / 2, 12, 0, Math.PI * 2)
97
+ ctx.fill()
98
+ ctx.strokeStyle = '#fff'
99
+ ctx.lineWidth = 2
100
+ ctx.stroke()
101
+ } else if (mode === 'side-by-side') {
102
+ // Side by side view
103
+ const halfWidth = displayWidth / 2
104
+ if (originalImageData) {
105
+ const tempCanvas = document.createElement('canvas')
106
+ tempCanvas.width = originalImageData.width
107
+ tempCanvas.height = originalImageData.height
108
+ const tempCtx = tempCanvas.getContext('2d')
109
+ if (tempCtx) {
110
+ tempCtx.putImageData(originalImageData, 0, 0)
111
+ ctx.drawImage(tempCanvas, 0, 0, halfWidth, displayHeight)
112
+ }
113
+ }
114
+ if (processedImageData) {
115
+ const tempCanvas = document.createElement('canvas')
116
+ tempCanvas.width = processedImageData.width
117
+ tempCanvas.height = processedImageData.height
118
+ const tempCtx = tempCanvas.getContext('2d')
119
+ if (tempCtx) {
120
+ tempCtx.putImageData(processedImageData, 0, 0)
121
+ ctx.drawImage(tempCanvas, halfWidth, 0, halfWidth, displayHeight)
122
+ }
123
+ }
124
+ // Draw center line
125
+ ctx.strokeStyle = 'rgba(255,255,255,0.5)'
126
+ ctx.lineWidth = 1
127
+ ctx.beginPath()
128
+ ctx.moveTo(halfWidth, 0)
129
+ ctx.lineTo(halfWidth, displayHeight)
130
+ ctx.stroke()
131
+ }
132
+ }, [originalImageData, processedImageData, splitPosition, mode, width, height])
133
+
134
+ useEffect(() => {
135
+ drawCanvas()
136
+ }, [drawCanvas])
137
+
138
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
139
+ if (mode !== 'split') return
140
+ const rect = canvasRef.current?.getBoundingClientRect()
141
+ if (!rect) return
142
+ const x = e.clientX - rect.left
143
+ if (Math.abs(x - width * splitPosition) < 20) {
144
+ setIsDragging(true)
145
+ }
146
+ }, [mode, splitPosition, width])
147
+
148
+ const handleMouseMove = useCallback(
149
+ (e: React.MouseEvent) => {
150
+ if (!isDragging || mode !== 'split') return
151
+ const rect = canvasRef.current?.getBoundingClientRect()
152
+ if (!rect) return
153
+ const x = e.clientX - rect.left
154
+ const newPosition = Math.max(0.1, Math.min(0.9, x / width))
155
+ setSplitPosition(newPosition)
156
+ },
157
+ [isDragging, mode, width, setSplitPosition]
158
+ )
159
+
160
+ const handleMouseUp = useCallback(() => {
161
+ setIsDragging(false)
162
+ }, [])
163
+
164
+ return (
165
+ <div className={cn('relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden', className)}>
166
+ <canvas
167
+ ref={canvasRef}
168
+ width={width}
169
+ height={height}
170
+ className="block"
171
+ style={{ cursor: isDragging ? 'ew-resize' : mode === 'split' ? 'ew-resize' : 'default' }}
172
+ onMouseDown={handleMouseDown}
173
+ onMouseMove={handleMouseMove}
174
+ onMouseUp={handleMouseUp}
175
+ onMouseLeave={handleMouseUp}
176
+ />
177
+ <div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex gap-2">
178
+ <button
179
+ className={cn('px-3 py-1 text-xs rounded', mode === 'split' && 'bg-blue-600 text-white')}
180
+ onClick={() => setComparisonViewMode('split')}
181
+ >
182
+ Split
183
+ </button>
184
+ <button
185
+ className={cn('px-3 py-1 text-xs rounded', mode === 'side-by-side' && 'bg-blue-600 text-white')}
186
+ onClick={() => setComparisonViewMode('side-by-side')}
187
+ >
188
+ Side by Side
189
+ </button>
190
+ <button
191
+ className={cn('px-3 py-1 text-xs rounded', mode === 'overlay' && 'bg-blue-600 text-white')}
192
+ onClick={() => setComparisonViewMode('overlay')}
193
+ >
194
+ Overlay
195
+ </button>
196
+ </div>
197
+ </div>
198
+ )
199
+ }
src/components/canvas/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export { ConvolutionCanvas } from './ConvolutionCanvas'
2
+ export { ImageCanvas } from './ImageCanvas'
3
+ export { SplitView } from './SplitView'
src/components/controls/OverlayControls.tsx ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useAppStore } from '@/store'
4
+ import { PADDING_MODES } from '@/lib/kernels'
5
+ import { Select, Card, CardContent, Slider, Button } from '@/components/ui'
6
+ import { cn } from '@/utils/helpers'
7
+ import { Settings, Layers, Grid } from 'lucide-react'
8
+
9
+ export function OverlayControls() {
10
+ const { settings, updateSettings, toggleOverlay, setPadding } = useAppStore()
11
+ const { showOverlay, padding } = settings
12
+
13
+ return (
14
+ <Card className="w-full">
15
+ <CardContent className="p-4 space-y-4">
16
+ <h3 className="font-medium flex items-center gap-2">
17
+ <Settings className="h-4 w-4" />
18
+ Visualization Overlays
19
+ </h3>
20
+
21
+ <div className="grid grid-cols-2 gap-2">
22
+ {Object.entries(showOverlay).map(([key, value]) => (
23
+ <label key={key} className="flex items-center gap-2 cursor-pointer">
24
+ <input
25
+ type="checkbox"
26
+ checked={value}
27
+ onChange={() => toggleOverlay(key as keyof typeof showOverlay)}
28
+ className="rounded border-gray-300"
29
+ />
30
+ <span className="text-sm capitalize">{key.replace(/([A-Z])/g, ' $1')}</span>
31
+ </label>
32
+ ))}
33
+ </div>
34
+ </CardContent>
35
+ </Card>
36
+ )
37
+ }
38
+
39
+ export function PaddingControls() {
40
+ const { settings, setPadding } = useAppStore()
41
+ const { padding } = settings
42
+
43
+ return (
44
+ <Card className="w-full">
45
+ <CardContent className="p-4 space-y-4">
46
+ <h3 className="font-medium flex items-center gap-2">
47
+ <Grid className="h-4 w-4" />
48
+ Padding Strategy
49
+ </h3>
50
+
51
+ <Select
52
+ value={padding}
53
+ onChange={(e) => setPadding(e.target.value as typeof padding)}
54
+ options={PADDING_MODES.map((p) => ({ value: p.value, label: p.label }))}
55
+ placeholder="Select padding mode"
56
+ />
57
+
58
+ <div className="text-sm text-gray-500 dark:text-gray-400">
59
+ {PADDING_MODES.find((p) => p.value === padding)?.description}
60
+ </div>
61
+
62
+ {/* Padding visualization would go here */}
63
+ </CardContent>
64
+ </Card>
65
+ )
66
+ }
67
+
68
+ export function KernelInfoPanel() {
69
+ const { selectedKernel } = useAppStore()
70
+
71
+ if (!selectedKernel) {
72
+ return (
73
+ <Card className="w-full">
74
+ <CardContent className="p-4 text-center text-gray-500">
75
+ No kernel selected
76
+ </CardContent>
77
+ </Card>
78
+ )
79
+ }
80
+
81
+ const { name, category, matrix, size, normalize, description, formula, reference } = selectedKernel
82
+
83
+ return (
84
+ <Card className="w-full">
85
+ <CardContent className="p-4 space-y-4">
86
+ <div className="flex items-start justify-between gap-4">
87
+ <div>
88
+ <h3 className="font-medium">{name}</h3>
89
+ <div className="flex items-center gap-2 mt-1">
90
+ <span className="px-2 py-0.5 text-xs bg-blue-100 text-blue-800 rounded dark:bg-blue-900/30 dark:text-blue-300">
91
+ {category}
92
+ </span>
93
+ <span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-800 rounded dark:bg-gray-800 dark:text-gray-300">
94
+ {size}×{size}
95
+ </span>
96
+ {normalize && (
97
+ <span className="px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded dark:bg-green-900/30 dark:text-green-300">
98
+ Normalized
99
+ </span>
100
+ )}
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <p className="text-sm text-gray-600 dark:text-gray-400">{description}</p>
106
+
107
+ {formula && (
108
+ <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded font-mono text-sm">
109
+ <span className="text-gray-500">Formula: </span>
110
+ <span>{formula}</span>
111
+ </div>
112
+ )}
113
+
114
+ <div className="space-y-2">
115
+ <h4 className="text-sm font-medium">Kernel Matrix</h4>
116
+ <div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
117
+ {matrix.map((row, i) =>
118
+ row.map((val, j) => (
119
+ <div
120
+ key={`${i}-${j}`}
121
+ className={cn(
122
+ 'p-2 text-center text-sm font-mono rounded',
123
+ val > 0 ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300' : 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300'
124
+ )}
125
+ >
126
+ {val.toFixed(val === Math.round(val) ? 0 : 2)}
127
+ </div>
128
+ ))
129
+ )}
130
+ </div>
131
+ </div>
132
+
133
+ {reference && (
134
+ <a
135
+ href={reference}
136
+ target="_blank"
137
+ rel="noopener noreferrer"
138
+ className="text-sm text-blue-600 hover:underline flex items-center gap-1"
139
+ >
140
+ <Layers className="h-3 w-3" />
141
+ Reference
142
+ </a>
143
+ )}
144
+ </CardContent>
145
+ </Card>
146
+ )
147
+ }
src/components/controls/PlaybackControls.tsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useAppStore } from '@/store'
4
+ import { useConvolution } from '@/hooks/useConvolution'
5
+ import { Button, Slider, Card, CardContent } from '@/components/ui'
6
+ import { cn } from '@/utils/helpers'
7
+ import { Play, Pause, RotateCcw, StepForward, ZoomIn, ZoomOut, Maximize } from 'lucide-react'
8
+
9
+ export function PlaybackControls() {
10
+ const {
11
+ convolution,
12
+ settings,
13
+ startConvolution,
14
+ pauseConvolution,
15
+ resumeConvolution,
16
+ stopConvolution,
17
+ updateConvolutionStep,
18
+ updateConvolutionProgress,
19
+ completeConvolution,
20
+ setConvolutionError,
21
+ resetConvolution,
22
+ setAnimationSpeed,
23
+ setZoomLevel,
24
+ setPanOffset,
25
+ toggleOverlay,
26
+ setPadding,
27
+ setComparisonMode,
28
+ setStepMode,
29
+ } = useAppStore()
30
+
31
+ const { runConvolution, pause, resume, stop, step, reset, isRunning, isPaused, canRun, toggleStepMode } = useConvolution()
32
+
33
+ const { status, progress, elapsedTime } = convolution
34
+ const { animationSpeed, zoomLevel, stepMode } = settings
35
+
36
+ const isCompleted = status === 'completed'
37
+ const isActive = isRunning || isPaused || isCompleted
38
+
39
+ const formatTime = (ms: number) => {
40
+ if (ms < 1000) return `${ms}ms`
41
+ return `${(ms / 1000).toFixed(1)}s`
42
+ }
43
+
44
+ return (
45
+ <Card className="w-full">
46
+ <CardContent className="p-4 space-y-4">
47
+ {/* Main Controls */}
48
+ <div className="flex items-center gap-2 flex-wrap">
49
+ <Button
50
+ variant={isRunning ? 'secondary' : 'primary'}
51
+ size="lg"
52
+ onClick={isRunning ? pause : runConvolution}
53
+ disabled={!canRun}
54
+ className="min-w-[100px]"
55
+ >
56
+ {isRunning ? (
57
+ <>
58
+ <Pause className="h-4 w-4" />
59
+ Pause
60
+ </>
61
+ ) : (
62
+ <>
63
+ <Play className="h-4 w-4" />
64
+ {isPaused ? 'Resume' : 'Play'}
65
+ </>
66
+ )}
67
+ </Button>
68
+
69
+ <Button variant="outline" size="lg" onClick={step} disabled={isRunning || isCompleted}>
70
+ <StepForward className="h-4 w-4" />
71
+ Step
72
+ </Button>
73
+
74
+ <Button variant="outline" size="lg" onClick={stop} disabled={status === 'idle'}>
75
+ <RotateCcw className="h-4 w-4" />
76
+ Stop
77
+ </Button>
78
+
79
+ <Button variant="outline" size="lg" onClick={reset} disabled={status === 'idle'}>
80
+ <RotateCcw className="h-4 w-4" />
81
+ Reset
82
+ </Button>
83
+ </div>
84
+
85
+ {/* Speed Control */}
86
+ <div>
87
+ <label className="flex items-center justify-between text-sm mb-1">
88
+ <span>Animation Speed</span>
89
+ <span className="font-mono text-blue-600">{animationSpeed.toFixed(1)}x</span>
90
+ </label>
91
+ <Slider
92
+ min={0.1}
93
+ max={10}
94
+ step={0.1}
95
+ value={animationSpeed}
96
+ onValueChange={setAnimationSpeed}
97
+ showValue
98
+ valueFormat={(v: number) => `${v.toFixed(1)}x`}
99
+ />
100
+ </div>
101
+
102
+ {/* Zoom Controls */}
103
+ <div>
104
+ <label className="flex items-center justify-between text-sm mb-1">
105
+ <span>Zoom</span>
106
+ <span className="font-mono text-blue-600">{Math.round(zoomLevel * 100)}%</span>
107
+ </label>
108
+ <Slider
109
+ min={0.1}
110
+ max={20}
111
+ step={0.1}
112
+ value={zoomLevel}
113
+ onValueChange={setZoomLevel}
114
+ showValue
115
+ valueFormat={(v: number) => `${Math.round(v * 100)}%`}
116
+ />
117
+ <div className="flex gap-2 mt-2">
118
+ <Button variant="outline" size="sm" onClick={() => setZoomLevel(Math.min(20, zoomLevel * 1.2))}>
119
+ <ZoomIn className="h-4 w-4" />
120
+ </Button>
121
+ <Button variant="outline" size="sm" onClick={() => setZoomLevel(Math.max(0.1, zoomLevel / 1.2))}>
122
+ <ZoomOut className="h-4 w-4" />
123
+ </Button>
124
+ <Button variant="outline" size="sm" onClick={() => { setZoomLevel(1); setPanOffset({ x: 0, y: 0 }) }}>
125
+ <Maximize className="h-4 w-4" />
126
+ Fit
127
+ </Button>
128
+ </div>
129
+ </div>
130
+
131
+ {/* Progress */}
132
+ {isActive && (
133
+ <div className="space-y-2 pt-2 border-t dark:border-gray-700">
134
+ <div className="flex items-center justify-between text-sm">
135
+ <span>Progress</span>
136
+ <span className="font-mono">{Math.round(progress * 100)}%</span>
137
+ </div>
138
+ <div className="h-2 bg-gray-200 rounded-full overflow-hidden dark:bg-gray-700">
139
+ <div
140
+ className="h-full bg-blue-600 transition-all duration-300"
141
+ style={{ width: `${progress * 100}%` }}
142
+ />
143
+ </div>
144
+ <div className="flex justify-between text-xs text-gray-500">
145
+ <span>Elapsed: {formatTime(elapsedTime)}</span>
146
+ <span>{status === 'running' ? 'Running' : status === 'paused' ? 'Paused' : 'Completed'}</span>
147
+ </div>
148
+ </div>
149
+ )}
150
+
151
+ {/* Step Mode Toggle */}
152
+ <div className="flex items-center justify-between pt-2 border-t dark:border-gray-700">
153
+ <label className="flex items-center gap-2 cursor-pointer">
154
+ <input
155
+ type="checkbox"
156
+ checked={stepMode}
157
+ onChange={(e) => toggleStepMode(e.target.checked)}
158
+ className="rounded border-gray-300"
159
+ />
160
+ <span className="text-sm">Step Mode (manual advance)</span>
161
+ </label>
162
+ </div>
163
+ </CardContent>
164
+ </Card>
165
+ )
166
+ }
src/components/controls/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { PlaybackControls } from './PlaybackControls'
2
+ export { OverlayControls, PaddingControls, KernelInfoPanel } from './OverlayControls'
src/components/kernel/KernelSelector.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAppStore } from '@/store'
5
+ import { PREDEFINED_KERNELS, KERNEL_CATEGORIES, getKernelsByCategory } from '@/lib/kernels'
6
+ import type { KernelCategory } from '@/lib/kernels'
7
+ import { Button, Card, CardContent, Badge, Input } from '@/components/ui'
8
+ import { cn } from '@/utils/helpers'
9
+ import { Search, SlidersHorizontal, Plus } from 'lucide-react'
10
+
11
+ export function KernelSelector() {
12
+ const { selectedKernel, setSelectedKernel, customKernelMatrix, customKernelSize, customKernelNormalize, setCustomKernel, setCustomKernelSize, setCustomKernelNormalize, updateCustomKernelCell } = useAppStore()
13
+ const [activeCategory, setActiveCategory] = useState<KernelCategory>('blur')
14
+ const [searchQuery, setSearchQuery] = useState('')
15
+ const [showCustom, setShowCustom] = useState(false)
16
+
17
+ const filteredKernels = PREDEFINED_KERNELS.filter(
18
+ (k) =>
19
+ k.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
20
+ k.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
21
+ k.description.toLowerCase().includes(searchQuery.toLowerCase())
22
+ )
23
+
24
+ const kernelsByCategory = KERNEL_CATEGORIES.map((cat) => ({
25
+ ...cat,
26
+ kernels: getKernelsByCategory(cat.value).filter((k) =>
27
+ k.name.toLowerCase().includes(searchQuery.toLowerCase())
28
+ ),
29
+ }))
30
+
31
+ const handleKernelSelect = (kernel: typeof PREDEFINED_KERNELS[0]) => {
32
+ setSelectedKernel(kernel)
33
+ setShowCustom(false)
34
+ }
35
+
36
+ const handleCustomKernelSubmit = () => {
37
+ const kernel = {
38
+ id: `custom-${Date.now()}`,
39
+ name: `Custom ${customKernelSize}×${customKernelSize}`,
40
+ category: 'custom' as KernelCategory,
41
+ matrix: customKernelMatrix,
42
+ size: customKernelSize,
43
+ normalize: customKernelNormalize,
44
+ description: 'User-defined custom kernel',
45
+ }
46
+ setSelectedKernel(kernel)
47
+ }
48
+
49
+ return (
50
+ <Card className="w-full max-w-2xl">
51
+ <div className="flex items-center justify-between p-4 border-b dark:border-gray-700">
52
+ <h2 className="text-lg font-semibold">Kernel Selector</h2>
53
+ <Button variant="ghost" size="icon" onClick={() => setShowCustom(!showCustom)}>
54
+ <Plus className="h-4 w-4" />
55
+ </Button>
56
+ </div>
57
+
58
+ <CardContent className="p-4">
59
+ {/* Search */}
60
+ <div className="relative mb-4">
61
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
62
+ <input
63
+ type="text"
64
+ placeholder="Search kernels..."
65
+ value={searchQuery}
66
+ onChange={(e) => setSearchQuery(e.target.value)}
67
+ className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-800 dark:text-white"
68
+ />
69
+ </div>
70
+
71
+ {/* Category Tabs */}
72
+ <div className="flex gap-1 mb-4 overflow-x-auto pb-2">
73
+ {KERNEL_CATEGORIES.map((cat) => (
74
+ <button
75
+ key={cat.value}
76
+ onClick={() => setActiveCategory(cat.value)}
77
+ className={cn(
78
+ 'px-3 py-1.5 text-sm rounded-full whitespace-nowrap transition-colors',
79
+ activeCategory === cat.value
80
+ ? 'bg-blue-600 text-white'
81
+ : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'
82
+ )}
83
+ >
84
+ {cat.label}
85
+ </button>
86
+ ))}
87
+ </div>
88
+
89
+ {/* Custom Kernel Editor */}
90
+ {showCustom && (
91
+ <div className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border dark:border-gray-700">
92
+ <h3 className="font-medium mb-3">Custom Kernel Editor</h3>
93
+ <div className="flex items-center gap-4 mb-3">
94
+ <Input
95
+ type="number"
96
+ label="Size"
97
+ value={customKernelSize}
98
+ onChange={(e) => setCustomKernelSize(Math.max(3, Math.min(9, parseInt(e.target.value) || 3)))}
99
+ min={3}
100
+ max={9}
101
+ step={2}
102
+ className="w-24"
103
+ />
104
+ <label className="flex items-center gap-2 text-sm">
105
+ <input
106
+ type="checkbox"
107
+ checked={customKernelNormalize}
108
+ onChange={(e) => setCustomKernelNormalize(e.target.checked)}
109
+ className="rounded border-gray-300"
110
+ />
111
+ Normalize
112
+ </label>
113
+ </div>
114
+ <div className="grid gap-1 mb-3" style={{ gridTemplateColumns: `repeat(${customKernelSize}, 1fr)` }}>
115
+ {Array.from({ length: customKernelSize }, (_, row) =>
116
+ Array.from({ length: customKernelSize }, (_, col) => (
117
+ <input
118
+ key={`${row}-${col}`}
119
+ type="number"
120
+ step="0.1"
121
+ value={customKernelMatrix[row]?.[col] ?? 0}
122
+ onChange={(e) => updateCustomKernelCell(row, col, parseFloat(e.target.value) || 0)}
123
+ className="w-full h-8 text-center text-sm border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700"
124
+ />
125
+ ))
126
+ )}
127
+ </div>
128
+ <Button onClick={handleCustomKernelSubmit} className="w-full">
129
+ Apply Custom Kernel
130
+ </Button>
131
+ </div>
132
+ )}
133
+
134
+ {/* Kernel List */}
135
+ <div className="space-y-3 max-h-96 overflow-y-auto">
136
+ {kernelsByCategory
137
+ .filter((cat) => cat.kernels.length > 0)
138
+ .map((cat) => (
139
+ <div key={cat.value} className="space-y-2">
140
+ <h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
141
+ {cat.label}
142
+ </h4>
143
+ <div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))' }}>
144
+ {cat.kernels.map((kernel) => (
145
+ <button
146
+ key={kernel.id}
147
+ onClick={() => handleKernelSelect(kernel)}
148
+ className={cn(
149
+ 'p-3 text-left rounded-lg border transition-all',
150
+ 'hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20',
151
+ selectedKernel?.id === kernel.id
152
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
153
+ : 'border-gray-200 dark:border-gray-700'
154
+ )}
155
+ >
156
+ <div className="font-medium text-sm">{kernel.name}</div>
157
+ <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">
158
+ {kernel.description}
159
+ </div>
160
+ <div className="flex items-center gap-1 mt-2">
161
+ <Badge variant="outline" className="text-xs">
162
+ {kernel.size}×{kernel.size}
163
+ </Badge>
164
+ {kernel.normalize && (
165
+ <Badge variant="secondary" className="text-xs">
166
+ Normalized
167
+ </Badge>
168
+ )}
169
+ </div>
170
+ </button>
171
+ ))}
172
+ </div>
173
+ </div>
174
+ ))}
175
+ {filteredKernels.length === 0 && (
176
+ <div className="text-center py-8 text-gray-500">
177
+ No kernels found matching &ldquo;{searchQuery}&rdquo;
178
+ </div>
179
+ )}
180
+ </div>
181
+ </CardContent>
182
+ </Card>
183
+ )
184
+ }
src/components/ui/Badge.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
7
+ variant?: 'default' | 'secondary' | 'success' | 'warning' | 'danger' | 'outline'
8
+ }
9
+
10
+ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
11
+ ({ className, variant = 'default', children, ...props }, ref) => {
12
+ const variants = {
13
+ default: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
14
+ secondary: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300',
15
+ success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
16
+ warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
17
+ danger: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
18
+ outline: 'border border-gray-300 text-gray-700 dark:border-gray-600 dark:text-gray-300',
19
+ }
20
+
21
+ return (
22
+ <span
23
+ ref={ref}
24
+ className={cn(
25
+ 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
26
+ variants[variant],
27
+ className
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </span>
33
+ )
34
+ }
35
+ )
36
+
37
+ Badge.displayName = 'Badge'
src/components/ui/Button.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, ButtonHTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
8
+ size?: 'sm' | 'md' | 'lg' | 'icon'
9
+ loading?: boolean
10
+ }
11
+
12
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
13
+ ({ className, variant = 'primary', size = 'md', loading, disabled, children, ...props }, ref) => {
14
+ const baseStyles =
15
+ 'inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50'
16
+
17
+ const variants = {
18
+ primary: 'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500',
19
+ secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700',
20
+ outline: 'border border-gray-300 bg-transparent hover:bg-gray-100 focus-visible:ring-gray-500 dark:border-gray-600 dark:hover:bg-gray-800',
21
+ ghost: 'bg-transparent hover:bg-gray-100 focus-visible:ring-gray-500 dark:hover:bg-gray-800',
22
+ danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500',
23
+ }
24
+
25
+ const sizes = {
26
+ sm: 'h-8 px-3 text-sm gap-1.5',
27
+ md: 'h-10 px-4 text-sm gap-2',
28
+ lg: 'h-12 px-6 text-base gap-2',
29
+ icon: 'h-10 w-10',
30
+ }
31
+
32
+ return (
33
+ <button
34
+ ref={ref}
35
+ className={cn(baseStyles, variants[variant], sizes[size], className)}
36
+ disabled={disabled || loading}
37
+ {...props}
38
+ >
39
+ {loading && (
40
+ <svg
41
+ className="animate-spin h-4 w-4"
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ fill="none"
44
+ viewBox="0 0 24 24"
45
+ >
46
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
47
+ <path
48
+ className="opacity-75"
49
+ fill="currentColor"
50
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
51
+ />
52
+ </svg>
53
+ )}
54
+ {children}
55
+ </button>
56
+ )
57
+ }
58
+ )
59
+
60
+ Button.displayName = 'Button'
src/components/ui/Card.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
7
+ variant?: 'default' | 'outlined' | 'elevated'
8
+ }
9
+
10
+ export const Card = forwardRef<HTMLDivElement, CardProps>(
11
+ ({ className, variant = 'default', children, ...props }, ref) => {
12
+ const variants = {
13
+ default: 'bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700',
14
+ outlined: 'bg-transparent border-2 border-gray-300 dark:border-gray-600',
15
+ elevated: 'bg-white dark:bg-gray-800 shadow-lg border-none',
16
+ }
17
+
18
+ return (
19
+ <div
20
+ ref={ref}
21
+ className={cn('rounded-lg', variants[variant], className)}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </div>
26
+ )
27
+ }
28
+ )
29
+
30
+ Card.displayName = 'Card'
31
+
32
+ export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
33
+ ({ className, children, ...props }, ref) => (
34
+ <div ref={ref} className={cn('px-6 py-4 border-b border-gray-200 dark:border-gray-700', className)} {...props}>
35
+ {children}
36
+ </div>
37
+ )
38
+ )
39
+
40
+ CardHeader.displayName = 'CardHeader'
41
+
42
+ export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
43
+ ({ className, children, ...props }, ref) => (
44
+ <h3 ref={ref} className={cn('text-lg font-semibold text-gray-900 dark:text-gray-100', className)} {...props}>
45
+ {children}
46
+ </h3>
47
+ )
48
+ )
49
+
50
+ CardTitle.displayName = 'CardTitle'
51
+
52
+ export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
53
+ ({ className, children, ...props }, ref) => (
54
+ <p ref={ref} className={cn('text-sm text-gray-500 dark:text-gray-400 mt-1', className)} {...props}>
55
+ {children}
56
+ </p>
57
+ )
58
+ )
59
+
60
+ CardDescription.displayName = 'CardDescription'
61
+
62
+ export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
63
+ ({ className, children, ...props }, ref) => (
64
+ <div ref={ref} className={cn('px-6 py-4', className)} {...props}>
65
+ {children}
66
+ </div>
67
+ )
68
+ )
69
+
70
+ CardContent.displayName = 'CardContent'
71
+
72
+ export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
73
+ ({ className, children, ...props }, ref) => (
74
+ <div ref={ref} className={cn('px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center gap-2', className)} {...props}>
75
+ {children}
76
+ </div>
77
+ )
78
+ )
79
+
80
+ CardFooter.displayName = 'CardFooter'
src/components/ui/Input.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, InputHTMLAttributes, LabelHTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
7
+ label?: string
8
+ error?: string
9
+ helperText?: string
10
+ }
11
+
12
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
13
+ ({ className, label, error, helperText, id, ...props }, ref) => {
14
+ const inputId = id || label?.toLowerCase().replace(/\s+/g, '-')
15
+
16
+ return (
17
+ <div className="w-full">
18
+ {label && (
19
+ <label
20
+ htmlFor={inputId}
21
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
22
+ >
23
+ {label}
24
+ </label>
25
+ )}
26
+ <input
27
+ ref={ref}
28
+ id={inputId}
29
+ className={cn(
30
+ 'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm',
31
+ 'placeholder:text-gray-400',
32
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
33
+ 'disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-100',
34
+ 'dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500',
35
+ error && 'border-red-500 focus:ring-red-500',
36
+ className
37
+ )}
38
+ aria-invalid={error ? 'true' : 'false'}
39
+ aria-describedby={error ? `${inputId}-error` : helperText ? `${inputId}-helper` : undefined}
40
+ {...props}
41
+ />
42
+ {error && (
43
+ <p id={`${inputId}-error`} className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">
44
+ {error}
45
+ </p>
46
+ )}
47
+ {helperText && !error && (
48
+ <p id={`${inputId}-helper`} className="mt-1 text-sm text-gray-500 dark:text-gray-400">
49
+ {helperText}
50
+ </p>
51
+ )}
52
+ </div>
53
+ )
54
+ }
55
+ )
56
+
57
+ Input.displayName = 'Input'
58
+
59
+ // Removed empty LabelProps interface - using LabelHTMLAttributes directly
src/components/ui/Select.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, SelectHTMLAttributes, OptionHTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
7
+ label?: string
8
+ error?: string
9
+ options: { value: string; label: string }[]
10
+ placeholder?: string
11
+ }
12
+
13
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
14
+ ({ className, label, error, options, placeholder, id, ...props }, ref) => {
15
+ const selectId = id || label?.toLowerCase().replace(/\s+/g, '-')
16
+
17
+ return (
18
+ <div className="w-full">
19
+ {label && (
20
+ <label
21
+ htmlFor={selectId}
22
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
23
+ >
24
+ {label}
25
+ </label>
26
+ )}
27
+ <select
28
+ ref={ref}
29
+ id={selectId}
30
+ className={cn(
31
+ 'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm',
32
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
33
+ 'disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-100',
34
+ 'dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100',
35
+ error && 'border-red-500 focus:ring-red-500',
36
+ className
37
+ )}
38
+ aria-invalid={error ? 'true' : 'false'}
39
+ {...props}
40
+ >
41
+ {placeholder && (
42
+ <option value="" disabled>
43
+ {placeholder}
44
+ </option>
45
+ )}
46
+ {options.map((option) => (
47
+ <option key={option.value} value={option.value}>
48
+ {option.label}
49
+ </option>
50
+ ))}
51
+ </select>
52
+ {error && (
53
+ <p id={`${selectId}-error`} className="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">
54
+ {error}
55
+ </p>
56
+ )}
57
+ </div>
58
+ )
59
+ }
60
+ )
61
+
62
+ Select.displayName = 'Select'
src/components/ui/Slider.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, InputHTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
7
+ label?: string
8
+ min: number
9
+ max: number
10
+ step?: number
11
+ value: number
12
+ onValueChange: (value: number) => void
13
+ showValue?: boolean
14
+ valueFormat?: (value: number) => string
15
+ }
16
+
17
+ export const Slider = forwardRef<HTMLInputElement, SliderProps>(
18
+ ({ className, label, min, max, step = 1, value, onValueChange, showValue = false, valueFormat, ...props }, ref) => {
19
+ const sliderId = label?.toLowerCase().replace(/\s+/g, '-')
20
+
21
+ return (
22
+ <div className={cn('w-full', className)}>
23
+ {label && (
24
+ <div className="flex items-center justify-between mb-1">
25
+ <label htmlFor={sliderId} className="text-sm font-medium text-gray-700 dark:text-gray-300">
26
+ {label}
27
+ </label>
28
+ {showValue && (
29
+ <span className="text-sm text-gray-500 dark:text-gray-400 font-mono">
30
+ {valueFormat ? valueFormat(value) : value}
31
+ </span>
32
+ )}
33
+ </div>
34
+ )}
35
+ <input
36
+ ref={ref}
37
+ id={sliderId}
38
+ type="range"
39
+ min={min}
40
+ max={max}
41
+ step={step}
42
+ value={value}
43
+ onChange={(e) => onValueChange(Number(e.target.value))}
44
+ className={cn(
45
+ 'w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer',
46
+ 'accent-blue-600 dark:bg-gray-700',
47
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
48
+ 'disabled:cursor-not-allowed disabled:opacity-50'
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ )
54
+ }
55
+ )
56
+
57
+ Slider.displayName = 'Slider'
src/components/ui/Tooltip.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { forwardRef, HTMLAttributes } from 'react'
4
+ import { cn } from '@/utils/helpers'
5
+
6
+ export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
7
+ content: string
8
+ position?: 'top' | 'bottom' | 'left' | 'right'
9
+ children: React.ReactElement
10
+ }
11
+
12
+ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
13
+ ({ className, content, position = 'top', children, ...props }, ref) => {
14
+ if (!React.isValidElement(children)) {
15
+ return <>{children}</>
16
+ }
17
+
18
+ const positions = {
19
+ top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
20
+ bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
21
+ left: 'right-full top-1/2 -translate-y-1/2 mr-2',
22
+ right: 'left-full top-1/2 -translate-y-1/2 ml-2',
23
+ }
24
+
25
+ return (
26
+ <div ref={ref} className={cn('relative inline-block', className)} {...props}>
27
+ {children}
28
+ <div
29
+ className={cn(
30
+ 'absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-900 rounded shadow-lg',
31
+ 'opacity-0 invisible transition-all duration-200',
32
+ 'group-hover:opacity-100 group-hover:visible',
33
+ 'whitespace-nowrap',
34
+ positions[position]
35
+ )}
36
+ role="tooltip"
37
+ >
38
+ {content}
39
+ <div
40
+ className={cn(
41
+ 'absolute w-0 h-0 border-4 border-transparent',
42
+ position === 'top' && 'bottom-[-8px] left-1/2 -translate-x-1/2 border-t-gray-900',
43
+ position === 'bottom' && 'top-[-8px] left-1/2 -translate-x-1/2 border-b-gray-900',
44
+ position === 'left' && 'right-[-8px] top-1/2 -translate-y-1/2 border-l-gray-900',
45
+ position === 'right' && 'left-[-8px] top-1/2 -translate-y-1/2 border-r-gray-900'
46
+ )}
47
+ />
48
+ </div>
49
+ </div>
50
+ )
51
+ }
52
+ )
53
+
54
+ Tooltip.displayName = 'Tooltip'
55
+
56
+ // Need to import React
57
+ import React from 'react'
src/components/ui/index.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export { Button } from './Button'
2
+ export { Input } from './Input'
3
+ export { Select } from './Select'
4
+ export { Slider } from './Slider'
5
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'
6
+ export { Badge } from './Badge'
7
+ export { Tooltip } from './Tooltip'
src/hooks/useCanvas.ts ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useRef, useEffect } from 'react'
2
+ import { useAppStore } from '@/store'
3
+
4
+ export function useCanvas() {
5
+ const canvasRef = useRef<HTMLCanvasElement>(null)
6
+ const contextRef = useRef<CanvasRenderingContext2D | null>(null)
7
+
8
+ useEffect(() => {
9
+ if (canvasRef.current) {
10
+ contextRef.current = canvasRef.current.getContext('2d', { willReadFrequently: true })
11
+ }
12
+ }, [])
13
+
14
+ const drawImage = useCallback((imageData: ImageData) => {
15
+ const ctx = contextRef.current
16
+ const canvas = canvasRef.current
17
+ if (!ctx || !canvas) return
18
+
19
+ canvas.width = imageData.width
20
+ canvas.height = imageData.height
21
+ ctx.putImageData(imageData, 0, 0)
22
+ }, [])
23
+
24
+ const drawImageScaled = useCallback(
25
+ (imageData: ImageData, scale: number, offsetX: number, offsetY: number) => {
26
+ const ctx = contextRef.current
27
+ const canvas = canvasRef.current
28
+ if (!ctx || !canvas) return
29
+
30
+ const displayWidth = Math.round(imageData.width * scale)
31
+ const displayHeight = Math.round(imageData.height * scale)
32
+
33
+ canvas.width = displayWidth
34
+ canvas.height = displayHeight
35
+
36
+ ctx.imageSmoothingEnabled = false
37
+ ctx.drawImage(
38
+ createImageBitmapFromImageData(imageData),
39
+ 0,
40
+ 0,
41
+ imageData.width,
42
+ imageData.height,
43
+ offsetX,
44
+ offsetY,
45
+ displayWidth,
46
+ displayHeight
47
+ )
48
+ },
49
+ []
50
+ )
51
+
52
+ const clear = useCallback(() => {
53
+ const ctx = contextRef.current
54
+ const canvas = canvasRef.current
55
+ if (!ctx || !canvas) return
56
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
57
+ }, [])
58
+
59
+ const getCanvas = useCallback(() => canvasRef.current, [])
60
+ const getContext = useCallback(() => contextRef.current, [])
61
+
62
+ return {
63
+ canvasRef,
64
+ drawImage,
65
+ drawImageScaled,
66
+ clear,
67
+ getCanvas,
68
+ getContext,
69
+ }
70
+ }
71
+
72
+ function createImageBitmapFromImageData(imageData: ImageData): ImageBitmap {
73
+ const canvas = document.createElement('canvas')
74
+ canvas.width = imageData.width
75
+ canvas.height = imageData.height
76
+ const ctx = canvas.getContext('2d')
77
+ if (!ctx) throw new Error('Failed to create canvas context')
78
+ ctx.putImageData(imageData, 0, 0)
79
+ // Note: createImageBitmap is async, this is a synchronous fallback
80
+ return canvas as unknown as ImageBitmap
81
+ }
82
+
83
+ export function useZoomPan() {
84
+ const { settings, setZoomLevel, setPanOffset } = useAppStore()
85
+ const zoomRef = useRef(settings.zoomLevel)
86
+ const panRef = useRef(settings.panOffset)
87
+
88
+ useEffect(() => {
89
+ zoomRef.current = settings.zoomLevel
90
+ panRef.current = settings.panOffset
91
+ }, [settings.zoomLevel, settings.panOffset])
92
+
93
+ const zoomIn = useCallback(() => {
94
+ const newZoom = Math.min(zoomRef.current * 1.2, 20)
95
+ setZoomLevel(newZoom)
96
+ }, [setZoomLevel])
97
+
98
+ const zoomOut = useCallback(() => {
99
+ const newZoom = Math.max(zoomRef.current / 1.2, 0.1)
100
+ setZoomLevel(newZoom)
101
+ }, [setZoomLevel])
102
+
103
+ const resetZoom = useCallback(() => {
104
+ setZoomLevel(1)
105
+ setPanOffset({ x: 0, y: 0 })
106
+ }, [setZoomLevel, setPanOffset])
107
+
108
+ const setZoom = useCallback(
109
+ (zoom: number) => {
110
+ setZoomLevel(Math.max(0.1, Math.min(20, zoom)))
111
+ },
112
+ [setZoomLevel]
113
+ )
114
+
115
+ const pan = useCallback(
116
+ (deltaX: number, deltaY: number) => {
117
+ const newPan = {
118
+ x: panRef.current.x + deltaX,
119
+ y: panRef.current.y + deltaY,
120
+ }
121
+ setPanOffset(newPan)
122
+ },
123
+ [setPanOffset]
124
+ )
125
+
126
+ const panTo = useCallback(
127
+ (x: number, y: number) => {
128
+ setPanOffset({ x, y })
129
+ },
130
+ [setPanOffset]
131
+ )
132
+
133
+ return {
134
+ zoom: settings.zoomLevel,
135
+ panOffset: settings.panOffset,
136
+ zoomIn,
137
+ zoomOut,
138
+ resetZoom,
139
+ setZoom,
140
+ pan,
141
+ panTo,
142
+ }
143
+ }
src/hooks/useConvolution.ts ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useRef, useEffect } from 'react'
2
+ import { useAppStore } from '@/store'
3
+ import { convolutionSteps, convolveImage } from '@/lib/convolution/engine'
4
+ import type { PaddingMode, ConvolutionStep } from '@/types'
5
+
6
+ export function useConvolution() {
7
+ const {
8
+ originalImageData,
9
+ selectedKernel,
10
+ customKernelMatrix,
11
+ customKernelNormalize,
12
+ settings,
13
+ convolution,
14
+ startConvolution,
15
+ pauseConvolution,
16
+ resumeConvolution,
17
+ stopConvolution,
18
+ updateConvolutionStep,
19
+ updateConvolutionProgress,
20
+ completeConvolution,
21
+ setConvolutionError,
22
+ resetConvolution,
23
+ } = useAppStore()
24
+
25
+ const animationRef = useRef<number | null>(null)
26
+ const generatorRef = useRef<Generator<ConvolutionStep> | null>(null)
27
+ const isPausedRef = useRef(false)
28
+ const stepModeRef = useRef(false)
29
+
30
+ const kernel = selectedKernel?.matrix ?? customKernelMatrix
31
+ const normalize = selectedKernel?.normalize ?? customKernelNormalize
32
+ const padding = settings.padding
33
+
34
+ const runConvolution = useCallback(async () => {
35
+ if (!originalImageData || !kernel) return
36
+
37
+ try {
38
+ startConvolution()
39
+
40
+ const extendedImage = {
41
+ data: originalImageData.data,
42
+ width: originalImageData.width,
43
+ height: originalImageData.height,
44
+ channels: 4,
45
+ }
46
+
47
+ const gen = convolutionSteps(extendedImage, kernel, padding, normalize, (progress) => {
48
+ const totalPixels = originalImageData.width * originalImageData.height * 4
49
+ const processedPixels = Math.round(progress * totalPixels)
50
+ const elapsedTime = Date.now() - convolution.startTime
51
+ updateConvolutionProgress(progress, processedPixels, elapsedTime)
52
+ })
53
+
54
+ generatorRef.current = gen
55
+
56
+ const processBatch = () => {
57
+ if (isPausedRef.current || stepModeRef.current) {
58
+ animationRef.current = requestAnimationFrame(processBatch)
59
+ return
60
+ }
61
+
62
+ const batchSize = Math.max(1, Math.floor(1000 * settings.animationSpeed))
63
+ let lastStep: ConvolutionStep | null = null
64
+
65
+ for (let i = 0; i < batchSize; i++) {
66
+ const result = gen.next()
67
+ if (result.done) {
68
+ if (lastStep && lastStep.x === -1) {
69
+ // Final step with output image
70
+ const outputImageData = new ImageData(
71
+ new Uint8ClampedArray(originalImageData.data.length),
72
+ originalImageData.width,
73
+ originalImageData.height
74
+ )
75
+ // We need to reconstruct from the generator's final output
76
+ // For now, use the direct convolution
77
+ const output = convolveImage(originalImageData, kernel, padding, normalize)
78
+ completeConvolution(output)
79
+ }
80
+ return
81
+ }
82
+ lastStep = result.value
83
+ updateConvolutionStep(result.value)
84
+ }
85
+
86
+ animationRef.current = requestAnimationFrame(processBatch)
87
+ }
88
+
89
+ animationRef.current = requestAnimationFrame(processBatch)
90
+ } catch (error) {
91
+ setConvolutionError(error instanceof Error ? error.message : 'Convolution failed')
92
+ }
93
+ }, [
94
+ originalImageData,
95
+ kernel,
96
+ normalize,
97
+ padding,
98
+ settings.animationSpeed,
99
+ convolution.startTime,
100
+ startConvolution,
101
+ updateConvolutionStep,
102
+ updateConvolutionProgress,
103
+ completeConvolution,
104
+ setConvolutionError,
105
+ ])
106
+
107
+ const pause = useCallback(() => {
108
+ isPausedRef.current = true
109
+ pauseConvolution()
110
+ }, [pauseConvolution])
111
+
112
+ const resume = useCallback(() => {
113
+ isPausedRef.current = false
114
+ resumeConvolution()
115
+ }, [resumeConvolution])
116
+
117
+ const stop = useCallback(() => {
118
+ if (animationRef.current) {
119
+ cancelAnimationFrame(animationRef.current)
120
+ animationRef.current = null
121
+ }
122
+ isPausedRef.current = false
123
+ stepModeRef.current = false
124
+ generatorRef.current = null
125
+ stopConvolution()
126
+ }, [stopConvolution])
127
+
128
+ const step = useCallback(() => {
129
+ if (!generatorRef.current) return
130
+ stepModeRef.current = true
131
+ const result = generatorRef.current.next()
132
+ if (!result.done) {
133
+ updateConvolutionStep(result.value)
134
+ } else if (result.value && result.value.x === -1) {
135
+ const output = convolveImage(originalImageData!, kernel, padding, normalize)
136
+ completeConvolution(output)
137
+ }
138
+ }, [originalImageData, kernel, padding, normalize, updateConvolutionStep, completeConvolution])
139
+
140
+ const reset = useCallback(() => {
141
+ stop()
142
+ resetConvolution()
143
+ }, [stop, resetConvolution])
144
+
145
+ const toggleStepMode = useCallback((enabled: boolean) => {
146
+ stepModeRef.current = enabled
147
+ }, [])
148
+
149
+ useEffect(() => {
150
+ return () => {
151
+ if (animationRef.current) {
152
+ cancelAnimationFrame(animationRef.current)
153
+ }
154
+ }
155
+ }, [])
156
+
157
+ return {
158
+ convolution,
159
+ runConvolution,
160
+ pause,
161
+ resume,
162
+ stop,
163
+ step,
164
+ reset,
165
+ toggleStepMode,
166
+ isRunning: convolution.status === 'running',
167
+ isPaused: convolution.status === 'paused',
168
+ isCompleted: convolution.status === 'completed',
169
+ canRun: !!originalImageData && !!kernel && convolution.status !== 'running',
170
+ }
171
+ }
src/hooks/useImageLoader.ts ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useRef, useEffect, useState, useReducer } from 'react'
2
+
3
+ export function useImageLoader() {
4
+ const fileInputRef = useRef<HTMLInputElement>(null)
5
+
6
+ const loadImage = useCallback(
7
+ (file: File): Promise<{ image: HTMLImageElement; imageData: ImageData }> => {
8
+ return new Promise((resolve, reject) => {
9
+ const url = URL.createObjectURL(file)
10
+ const img = new Image()
11
+ img.onload = () => {
12
+ const canvas = document.createElement('canvas')
13
+ canvas.width = img.width
14
+ canvas.height = img.height
15
+ const ctx = canvas.getContext('2d')
16
+ if (!ctx) {
17
+ URL.revokeObjectURL(url)
18
+ reject(new Error('Failed to get canvas context'))
19
+ return
20
+ }
21
+ ctx.drawImage(img, 0, 0)
22
+ const imageData = ctx.getImageData(0, 0, img.width, img.height)
23
+ URL.revokeObjectURL(url)
24
+ resolve({ image: img, imageData })
25
+ }
26
+ img.onerror = () => {
27
+ URL.revokeObjectURL(url)
28
+ reject(new Error('Failed to load image'))
29
+ }
30
+ img.src = url
31
+ })
32
+ },
33
+ []
34
+ )
35
+
36
+ const openFileDialog = useCallback(() => {
37
+ fileInputRef.current?.click()
38
+ }, [])
39
+
40
+ const handleFileSelect = useCallback(
41
+ (event: React.ChangeEvent<HTMLInputElement>, onLoad: (data: { image: HTMLImageElement; imageData: ImageData }) => void) => {
42
+ const file = event.target.files?.[0]
43
+ if (!file) return
44
+
45
+ if (!file.type.startsWith('image/')) {
46
+ alert('Please select an image file')
47
+ return
48
+ }
49
+
50
+ loadImage(file).then(onLoad).catch((error) => {
51
+ alert(`Failed to load image: ${error.message}`)
52
+ })
53
+ },
54
+ [loadImage]
55
+ )
56
+
57
+ return {
58
+ fileInputRef,
59
+ loadImage,
60
+ openFileDialog,
61
+ handleFileSelect,
62
+ }
63
+ }
64
+
65
+ export function useDragAndDrop(onLoad: (image: HTMLImageElement, imageData: ImageData) => void) {
66
+ const [isDragging, setIsDragging] = useState(false)
67
+
68
+ const loadImageFromFile = useCallback(
69
+ (file: File): Promise<{ image: HTMLImageElement; imageData: ImageData }> => {
70
+ return new Promise((resolve, reject) => {
71
+ const url = URL.createObjectURL(file)
72
+ const img = new Image()
73
+ img.onload = () => {
74
+ const canvas = document.createElement('canvas')
75
+ canvas.width = img.width
76
+ canvas.height = img.height
77
+ const ctx = canvas.getContext('2d')
78
+ if (!ctx) {
79
+ URL.revokeObjectURL(url)
80
+ reject(new Error('Failed to get canvas context'))
81
+ return
82
+ }
83
+ ctx.drawImage(img, 0, 0)
84
+ const imageData = ctx.getImageData(0, 0, img.width, img.height)
85
+ URL.revokeObjectURL(url)
86
+ resolve({ image: img, imageData })
87
+ }
88
+ img.onerror = () => {
89
+ URL.revokeObjectURL(url)
90
+ reject(new Error('Failed to load image'))
91
+ }
92
+ img.src = url
93
+ })
94
+ },
95
+ []
96
+ )
97
+
98
+ const handleDragOver = useCallback((e: React.DragEvent) => {
99
+ e.preventDefault()
100
+ e.stopPropagation()
101
+ setIsDragging(true)
102
+ }, [])
103
+
104
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
105
+ e.preventDefault()
106
+ e.stopPropagation()
107
+ setIsDragging(false)
108
+ }, [])
109
+
110
+ const handleDrop = useCallback(
111
+ async (e: React.DragEvent) => {
112
+ e.preventDefault()
113
+ e.stopPropagation()
114
+ setIsDragging(false)
115
+
116
+ const file = e.dataTransfer.files[0]
117
+ if (!file || !file.type.startsWith('image/')) return
118
+
119
+ try {
120
+ const { image, imageData } = await loadImageFromFile(file)
121
+ onLoad(image, imageData)
122
+ } catch (error) {
123
+ alert(`Failed to load image: ${error instanceof Error ? error.message : 'Unknown error'}`)
124
+ }
125
+ },
126
+ [onLoad, loadImageFromFile]
127
+ )
128
+
129
+ return {
130
+ handleDragOver,
131
+ handleDragLeave,
132
+ handleDrop,
133
+ isDragging,
134
+ }
135
+ }
src/index.css DELETED
@@ -1,13 +0,0 @@
1
- body {
2
- margin: 0;
3
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
- sans-serif;
6
- -webkit-font-smoothing: antialiased;
7
- -moz-osx-font-smoothing: grayscale;
8
- }
9
-
10
- code {
11
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
- monospace;
13
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/index.js DELETED
@@ -1,17 +0,0 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom/client';
3
- import './index.css';
4
- import App from './App';
5
- import reportWebVitals from './reportWebVitals';
6
-
7
- const root = ReactDOM.createRoot(document.getElementById('root'));
8
- root.render(
9
- <React.StrictMode>
10
- <App />
11
- </React.StrictMode>
12
- );
13
-
14
- // If you want to start measuring performance in your app, pass a function
15
- // to log results (for example: reportWebVitals(console.log))
16
- // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17
- reportWebVitals();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/convolution/engine.ts ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PaddingMode, ConvolutionStep, ImageDataExtended } from '@/types'
2
+
3
+ export function getPixel(
4
+ image: ImageDataExtended,
5
+ x: number,
6
+ y: number,
7
+ channel: number,
8
+ padding: PaddingMode
9
+ ): number {
10
+ const { width, height, channels, data } = image
11
+
12
+ let px = x
13
+ let py = y
14
+
15
+ switch (padding) {
16
+ case 'zero':
17
+ if (px < 0 || px >= width || py < 0 || py >= height) return 0
18
+ break
19
+ case 'replicate':
20
+ px = Math.max(0, Math.min(px, width - 1))
21
+ py = Math.max(0, Math.min(py, height - 1))
22
+ break
23
+ case 'reflect':
24
+ if (px < 0) px = -px
25
+ if (px >= width) px = 2 * width - px - 2
26
+ if (py < 0) py = -py
27
+ if (py >= height) py = 2 * height - py - 2
28
+ px = Math.max(0, Math.min(px, width - 1))
29
+ py = Math.max(0, Math.min(py, height - 1))
30
+ break
31
+ case 'wrap':
32
+ px = ((px % width) + width) % width
33
+ py = ((py % height) + height) % height
34
+ break
35
+ }
36
+
37
+ const index = (py * width + px) * channels + channel
38
+ return data[index] / 255
39
+ }
40
+
41
+ export function setPixel(
42
+ image: ImageDataExtended,
43
+ x: number,
44
+ y: number,
45
+ channel: number,
46
+ value: number
47
+ ): void {
48
+ const { width, channels, data } = image
49
+ if (x < 0 || x >= width || y < 0 || y >= image.height) return
50
+ const index = (y * width + x) * channels + channel
51
+ data[index] = Math.max(0, Math.min(255, Math.round(value * 255)))
52
+ }
53
+
54
+ export function* convolutionSteps(
55
+ image: ImageDataExtended,
56
+ kernel: number[][],
57
+ padding: PaddingMode,
58
+ normalize: boolean,
59
+ onProgress?: (progress: number) => void
60
+ ): Generator<ConvolutionStep> {
61
+ const { width, height, channels } = image
62
+ const kernelSize = kernel.length
63
+ const halfKernel = Math.floor(kernelSize / 2)
64
+
65
+ const totalPixels = width * height * channels
66
+ let processedPixels = 0
67
+
68
+ const kernelSum = kernel.flat().reduce((a, b) => a + b, 0)
69
+ const normalizeFactor = normalize && kernelSum !== 0 ? 1 / kernelSum : 1
70
+
71
+ const outputData = new Float32Array(width * height * channels)
72
+
73
+ for (let y = 0; y < height; y++) {
74
+ for (let x = 0; x < width; x++) {
75
+ for (let c = 0; c < channels; c++) {
76
+ const inputPatch: number[][] = []
77
+ const multiplications: number[][] = []
78
+ let sum = 0
79
+
80
+ for (let ky = 0; ky < kernelSize; ky++) {
81
+ const patchRow: number[] = []
82
+ const multRow: number[] = []
83
+ for (let kx = 0; kx < kernelSize; kx++) {
84
+ const px = x + kx - halfKernel
85
+ const py = y + ky - halfKernel
86
+ const pixelValue = getPixel(image, px, py, c, padding)
87
+ const kernelValue = kernel[ky][kx]
88
+ patchRow.push(pixelValue)
89
+ const mult = pixelValue * kernelValue
90
+ multRow.push(mult)
91
+ sum += mult
92
+ }
93
+ inputPatch.push(patchRow)
94
+ multiplications.push(multRow)
95
+ }
96
+
97
+ const outputValue = sum
98
+ const normalizedValue = sum * normalizeFactor
99
+
100
+ const outputIndex = (y * width + x) * channels + c
101
+ outputData[outputIndex] = normalizedValue
102
+
103
+ processedPixels++
104
+ const progress = processedPixels / totalPixels
105
+
106
+ yield {
107
+ x,
108
+ y,
109
+ channel: c,
110
+ inputPatch,
111
+ kernel,
112
+ multiplications,
113
+ sum: outputValue,
114
+ outputValue: normalizedValue,
115
+ normalizedValue,
116
+ progress,
117
+ }
118
+
119
+ if (onProgress) onProgress(progress)
120
+ }
121
+ }
122
+ }
123
+
124
+ // Convert output to ImageData
125
+ const outputImageData = new ImageData(width, height)
126
+ for (let i = 0; i < outputData.length; i++) {
127
+ outputImageData.data[i] = Math.max(0, Math.min(255, Math.round(outputData[i] * 255)))
128
+ }
129
+
130
+ // Final yield with complete output
131
+ yield {
132
+ x: -1,
133
+ y: -1,
134
+ channel: -1,
135
+ inputPatch: [],
136
+ kernel,
137
+ multiplications: [],
138
+ sum: 0,
139
+ outputValue: 0,
140
+ normalizedValue: 0,
141
+ progress: 1,
142
+ }
143
+ }
144
+
145
+ export function convolveImage(
146
+ imageData: ImageData,
147
+ kernel: number[][],
148
+ padding: PaddingMode = 'zero',
149
+ normalize: boolean = true
150
+ ): ImageData {
151
+ const extended: ImageDataExtended = {
152
+ data: imageData.data,
153
+ width: imageData.width,
154
+ height: imageData.height,
155
+ channels: 4,
156
+ }
157
+
158
+ const { width, height } = imageData
159
+ const kernelSize = kernel.length
160
+ const halfKernel = Math.floor(kernelSize / 2)
161
+ const kernelSum = kernel.flat().reduce((a, b) => a + b, 0)
162
+ const normalizeFactor = normalize && kernelSum !== 0 ? 1 / kernelSum : 1
163
+
164
+ const outputData = new Uint8ClampedArray(imageData.data.length)
165
+
166
+ for (let y = 0; y < height; y++) {
167
+ for (let x = 0; x < width; x++) {
168
+ for (let c = 0; c < 3; c++) {
169
+ let sum = 0
170
+ for (let ky = 0; ky < kernelSize; ky++) {
171
+ for (let kx = 0; kx < kernelSize; kx++) {
172
+ const px = x + kx - halfKernel
173
+ const py = y + ky - halfKernel
174
+ const pixelValue = getPixel(extended, px, py, c, padding)
175
+ sum += pixelValue * kernel[ky][kx]
176
+ }
177
+ }
178
+ const outputIndex = (y * width + x) * 4 + c
179
+ outputData[outputIndex] = Math.max(0, Math.min(255, Math.round(sum * normalizeFactor * 255)))
180
+ }
181
+ outputData[(y * width + x) * 4 + 3] = 255
182
+ }
183
+ }
184
+
185
+ return new ImageData(outputData, imageData.width, imageData.height)
186
+ }