mihirmithani commited on
Commit ·
25e7739
1
Parent(s): a320533
Phase 2
Browse files- package-lock.json +461 -0
- package.json +2 -1
- src/app/page.tsx +19 -3
- src/components/canvas/ConvolutionCanvas.tsx +22 -2
- src/components/canvas/StaticConvolutionView.tsx +367 -0
- src/components/ui/Tabs.tsx +54 -0
- src/components/ui/index.ts +2 -1
- src/hooks/useStaticConvolution.ts +97 -0
- src/lib/convolution/engine.ts +63 -0
- updates.md +33 -1
package-lock.json
CHANGED
|
@@ -28,6 +28,7 @@
|
|
| 28 |
"@types/offscreencanvas": "^2019.7.3",
|
| 29 |
"@types/react": "^19",
|
| 30 |
"@types/react-dom": "^19",
|
|
|
|
| 31 |
"eslint": "^9",
|
| 32 |
"eslint-config-next": "16.3.0",
|
| 33 |
"tailwindcss": "^4",
|
|
@@ -3364,6 +3365,27 @@
|
|
| 3364 |
"dev": true,
|
| 3365 |
"license": "MIT"
|
| 3366 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3367 |
"node_modules/baseline-browser-mapping": {
|
| 3368 |
"version": "2.11.12",
|
| 3369 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz",
|
|
@@ -3376,6 +3398,18 @@
|
|
| 3376 |
"node": ">=6.0.0"
|
| 3377 |
}
|
| 3378 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3379 |
"node_modules/brace-expansion": {
|
| 3380 |
"version": "1.1.18",
|
| 3381 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
|
@@ -3434,6 +3468,31 @@
|
|
| 3434 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 3435 |
}
|
| 3436 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3437 |
"node_modules/call-bind": {
|
| 3438 |
"version": "1.0.9",
|
| 3439 |
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
|
@@ -3514,6 +3573,21 @@
|
|
| 3514 |
],
|
| 3515 |
"license": "CC-BY-4.0"
|
| 3516 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3517 |
"node_modules/chalk": {
|
| 3518 |
"version": "4.1.2",
|
| 3519 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
@@ -3531,6 +3605,13 @@
|
|
| 3531 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 3532 |
}
|
| 3533 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3534 |
"node_modules/client-only": {
|
| 3535 |
"version": "0.0.1",
|
| 3536 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
@@ -3681,6 +3762,32 @@
|
|
| 3681 |
}
|
| 3682 |
}
|
| 3683 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3684 |
"node_modules/deep-is": {
|
| 3685 |
"version": "0.1.4",
|
| 3686 |
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
|
@@ -3782,6 +3889,16 @@
|
|
| 3782 |
"dev": true,
|
| 3783 |
"license": "MIT"
|
| 3784 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3785 |
"node_modules/enhanced-resolve": {
|
| 3786 |
"version": "5.24.5",
|
| 3787 |
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
|
|
@@ -4424,6 +4541,16 @@
|
|
| 4424 |
"node": ">=0.10.0"
|
| 4425 |
}
|
| 4426 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4427 |
"node_modules/fast-deep-equal": {
|
| 4428 |
"version": "3.1.3",
|
| 4429 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
@@ -4588,6 +4715,13 @@
|
|
| 4588 |
}
|
| 4589 |
}
|
| 4590 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4591 |
"node_modules/function-bind": {
|
| 4592 |
"version": "1.1.2",
|
| 4593 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
@@ -4731,6 +4865,13 @@
|
|
| 4731 |
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
| 4732 |
}
|
| 4733 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4734 |
"node_modules/glob-parent": {
|
| 4735 |
"version": "6.0.2",
|
| 4736 |
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
|
@@ -4905,6 +5046,27 @@
|
|
| 4905 |
"hermes-estree": "0.25.1"
|
| 4906 |
}
|
| 4907 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4908 |
"node_modules/ignore": {
|
| 4909 |
"version": "5.3.2",
|
| 4910 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
@@ -4942,6 +5104,20 @@
|
|
| 4942 |
"node": ">=0.8.19"
|
| 4943 |
}
|
| 4944 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4945 |
"node_modules/internal-slot": {
|
| 4946 |
"version": "1.1.0",
|
| 4947 |
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
|
@@ -5927,6 +6103,19 @@
|
|
| 5927 |
"node": ">=8.6"
|
| 5928 |
}
|
| 5929 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5930 |
"node_modules/minimatch": {
|
| 5931 |
"version": "3.1.5",
|
| 5932 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
|
@@ -5950,6 +6139,13 @@
|
|
| 5950 |
"url": "https://github.com/sponsors/ljharb"
|
| 5951 |
}
|
| 5952 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5953 |
"node_modules/motion-dom": {
|
| 5954 |
"version": "13.0.0",
|
| 5955 |
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.0.0.tgz",
|
|
@@ -5990,6 +6186,13 @@
|
|
| 5990 |
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 5991 |
}
|
| 5992 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5993 |
"node_modules/napi-postinstall": {
|
| 5994 |
"version": "0.3.4",
|
| 5995 |
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
|
@@ -6094,6 +6297,39 @@
|
|
| 6094 |
"node": "^10 || ^12 || >=14"
|
| 6095 |
}
|
| 6096 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6097 |
"node_modules/node-exports-info": {
|
| 6098 |
"version": "1.6.2",
|
| 6099 |
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz",
|
|
@@ -6246,6 +6482,16 @@
|
|
| 6246 |
"url": "https://github.com/sponsors/ljharb"
|
| 6247 |
}
|
| 6248 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6249 |
"node_modules/optionator": {
|
| 6250 |
"version": "0.9.4",
|
| 6251 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
|
@@ -6413,6 +6659,34 @@
|
|
| 6413 |
"node": "^10 || ^12 || >=14"
|
| 6414 |
}
|
| 6415 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6416 |
"node_modules/prelude-ls": {
|
| 6417 |
"version": "1.2.1",
|
| 6418 |
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
|
@@ -6435,6 +6709,17 @@
|
|
| 6435 |
"react-is": "^16.13.1"
|
| 6436 |
}
|
| 6437 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6438 |
"node_modules/punycode": {
|
| 6439 |
"version": "2.3.1",
|
| 6440 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
@@ -6466,6 +6751,32 @@
|
|
| 6466 |
],
|
| 6467 |
"license": "MIT"
|
| 6468 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6469 |
"node_modules/react": {
|
| 6470 |
"version": "19.2.8",
|
| 6471 |
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
|
@@ -6563,6 +6874,21 @@
|
|
| 6563 |
}
|
| 6564 |
}
|
| 6565 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6566 |
"node_modules/reflect.getprototypeof": {
|
| 6567 |
"version": "1.0.10",
|
| 6568 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
@@ -6706,6 +7032,27 @@
|
|
| 6706 |
"url": "https://github.com/sponsors/ljharb"
|
| 6707 |
}
|
| 6708 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6709 |
"node_modules/safe-push-apply": {
|
| 6710 |
"version": "1.0.0",
|
| 6711 |
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
|
@@ -6968,6 +7315,53 @@
|
|
| 6968 |
"url": "https://github.com/sponsors/ljharb"
|
| 6969 |
}
|
| 6970 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6971 |
"node_modules/source-map-js": {
|
| 6972 |
"version": "1.2.1",
|
| 6973 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
@@ -6998,6 +7392,16 @@
|
|
| 6998 |
"node": ">= 0.4"
|
| 6999 |
}
|
| 7000 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7001 |
"node_modules/string.prototype.includes": {
|
| 7002 |
"version": "2.0.1",
|
| 7003 |
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
|
@@ -7215,6 +7619,36 @@
|
|
| 7215 |
"url": "https://opencollective.com/webpack"
|
| 7216 |
}
|
| 7217 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7218 |
"node_modules/tinyglobby": {
|
| 7219 |
"version": "0.2.17",
|
| 7220 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
|
@@ -7321,6 +7755,19 @@
|
|
| 7321 |
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 7322 |
"license": "0BSD"
|
| 7323 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7324 |
"node_modules/type-check": {
|
| 7325 |
"version": "0.4.0",
|
| 7326 |
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
|
@@ -7598,6 +8045,13 @@
|
|
| 7598 |
}
|
| 7599 |
}
|
| 7600 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7601 |
"node_modules/which": {
|
| 7602 |
"version": "2.0.2",
|
| 7603 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
@@ -7713,6 +8167,13 @@
|
|
| 7713 |
"node": ">=0.10.0"
|
| 7714 |
}
|
| 7715 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7716 |
"node_modules/yallist": {
|
| 7717 |
"version": "3.1.1",
|
| 7718 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
|
|
|
| 28 |
"@types/offscreencanvas": "^2019.7.3",
|
| 29 |
"@types/react": "^19",
|
| 30 |
"@types/react-dom": "^19",
|
| 31 |
+
"canvas": "^3.2.3",
|
| 32 |
"eslint": "^9",
|
| 33 |
"eslint-config-next": "16.3.0",
|
| 34 |
"tailwindcss": "^4",
|
|
|
|
| 3365 |
"dev": true,
|
| 3366 |
"license": "MIT"
|
| 3367 |
},
|
| 3368 |
+
"node_modules/base64-js": {
|
| 3369 |
+
"version": "1.5.1",
|
| 3370 |
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 3371 |
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
| 3372 |
+
"dev": true,
|
| 3373 |
+
"funding": [
|
| 3374 |
+
{
|
| 3375 |
+
"type": "github",
|
| 3376 |
+
"url": "https://github.com/sponsors/feross"
|
| 3377 |
+
},
|
| 3378 |
+
{
|
| 3379 |
+
"type": "patreon",
|
| 3380 |
+
"url": "https://www.patreon.com/feross"
|
| 3381 |
+
},
|
| 3382 |
+
{
|
| 3383 |
+
"type": "consulting",
|
| 3384 |
+
"url": "https://feross.org/support"
|
| 3385 |
+
}
|
| 3386 |
+
],
|
| 3387 |
+
"license": "MIT"
|
| 3388 |
+
},
|
| 3389 |
"node_modules/baseline-browser-mapping": {
|
| 3390 |
"version": "2.11.12",
|
| 3391 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz",
|
|
|
|
| 3398 |
"node": ">=6.0.0"
|
| 3399 |
}
|
| 3400 |
},
|
| 3401 |
+
"node_modules/bl": {
|
| 3402 |
+
"version": "4.1.0",
|
| 3403 |
+
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
| 3404 |
+
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
| 3405 |
+
"dev": true,
|
| 3406 |
+
"license": "MIT",
|
| 3407 |
+
"dependencies": {
|
| 3408 |
+
"buffer": "^5.5.0",
|
| 3409 |
+
"inherits": "^2.0.4",
|
| 3410 |
+
"readable-stream": "^3.4.0"
|
| 3411 |
+
}
|
| 3412 |
+
},
|
| 3413 |
"node_modules/brace-expansion": {
|
| 3414 |
"version": "1.1.18",
|
| 3415 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
|
|
|
| 3468 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 3469 |
}
|
| 3470 |
},
|
| 3471 |
+
"node_modules/buffer": {
|
| 3472 |
+
"version": "5.7.1",
|
| 3473 |
+
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
| 3474 |
+
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
| 3475 |
+
"dev": true,
|
| 3476 |
+
"funding": [
|
| 3477 |
+
{
|
| 3478 |
+
"type": "github",
|
| 3479 |
+
"url": "https://github.com/sponsors/feross"
|
| 3480 |
+
},
|
| 3481 |
+
{
|
| 3482 |
+
"type": "patreon",
|
| 3483 |
+
"url": "https://www.patreon.com/feross"
|
| 3484 |
+
},
|
| 3485 |
+
{
|
| 3486 |
+
"type": "consulting",
|
| 3487 |
+
"url": "https://feross.org/support"
|
| 3488 |
+
}
|
| 3489 |
+
],
|
| 3490 |
+
"license": "MIT",
|
| 3491 |
+
"dependencies": {
|
| 3492 |
+
"base64-js": "^1.3.1",
|
| 3493 |
+
"ieee754": "^1.1.13"
|
| 3494 |
+
}
|
| 3495 |
+
},
|
| 3496 |
"node_modules/call-bind": {
|
| 3497 |
"version": "1.0.9",
|
| 3498 |
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
|
|
|
| 3573 |
],
|
| 3574 |
"license": "CC-BY-4.0"
|
| 3575 |
},
|
| 3576 |
+
"node_modules/canvas": {
|
| 3577 |
+
"version": "3.2.3",
|
| 3578 |
+
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz",
|
| 3579 |
+
"integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==",
|
| 3580 |
+
"dev": true,
|
| 3581 |
+
"hasInstallScript": true,
|
| 3582 |
+
"license": "MIT",
|
| 3583 |
+
"dependencies": {
|
| 3584 |
+
"node-addon-api": "^7.0.0",
|
| 3585 |
+
"prebuild-install": "^7.1.3"
|
| 3586 |
+
},
|
| 3587 |
+
"engines": {
|
| 3588 |
+
"node": "^18.12.0 || >= 20.9.0"
|
| 3589 |
+
}
|
| 3590 |
+
},
|
| 3591 |
"node_modules/chalk": {
|
| 3592 |
"version": "4.1.2",
|
| 3593 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
|
|
| 3605 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 3606 |
}
|
| 3607 |
},
|
| 3608 |
+
"node_modules/chownr": {
|
| 3609 |
+
"version": "1.1.4",
|
| 3610 |
+
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
| 3611 |
+
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
| 3612 |
+
"dev": true,
|
| 3613 |
+
"license": "ISC"
|
| 3614 |
+
},
|
| 3615 |
"node_modules/client-only": {
|
| 3616 |
"version": "0.0.1",
|
| 3617 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
|
|
| 3762 |
}
|
| 3763 |
}
|
| 3764 |
},
|
| 3765 |
+
"node_modules/decompress-response": {
|
| 3766 |
+
"version": "6.0.0",
|
| 3767 |
+
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
| 3768 |
+
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
| 3769 |
+
"dev": true,
|
| 3770 |
+
"license": "MIT",
|
| 3771 |
+
"dependencies": {
|
| 3772 |
+
"mimic-response": "^3.1.0"
|
| 3773 |
+
},
|
| 3774 |
+
"engines": {
|
| 3775 |
+
"node": ">=10"
|
| 3776 |
+
},
|
| 3777 |
+
"funding": {
|
| 3778 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 3779 |
+
}
|
| 3780 |
+
},
|
| 3781 |
+
"node_modules/deep-extend": {
|
| 3782 |
+
"version": "0.6.0",
|
| 3783 |
+
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
| 3784 |
+
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
| 3785 |
+
"dev": true,
|
| 3786 |
+
"license": "MIT",
|
| 3787 |
+
"engines": {
|
| 3788 |
+
"node": ">=4.0.0"
|
| 3789 |
+
}
|
| 3790 |
+
},
|
| 3791 |
"node_modules/deep-is": {
|
| 3792 |
"version": "0.1.4",
|
| 3793 |
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
|
|
|
| 3889 |
"dev": true,
|
| 3890 |
"license": "MIT"
|
| 3891 |
},
|
| 3892 |
+
"node_modules/end-of-stream": {
|
| 3893 |
+
"version": "1.4.5",
|
| 3894 |
+
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
| 3895 |
+
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
| 3896 |
+
"dev": true,
|
| 3897 |
+
"license": "MIT",
|
| 3898 |
+
"dependencies": {
|
| 3899 |
+
"once": "^1.4.0"
|
| 3900 |
+
}
|
| 3901 |
+
},
|
| 3902 |
"node_modules/enhanced-resolve": {
|
| 3903 |
"version": "5.24.5",
|
| 3904 |
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
|
|
|
|
| 4541 |
"node": ">=0.10.0"
|
| 4542 |
}
|
| 4543 |
},
|
| 4544 |
+
"node_modules/expand-template": {
|
| 4545 |
+
"version": "2.0.3",
|
| 4546 |
+
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
| 4547 |
+
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
| 4548 |
+
"dev": true,
|
| 4549 |
+
"license": "(MIT OR WTFPL)",
|
| 4550 |
+
"engines": {
|
| 4551 |
+
"node": ">=6"
|
| 4552 |
+
}
|
| 4553 |
+
},
|
| 4554 |
"node_modules/fast-deep-equal": {
|
| 4555 |
"version": "3.1.3",
|
| 4556 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
|
|
| 4715 |
}
|
| 4716 |
}
|
| 4717 |
},
|
| 4718 |
+
"node_modules/fs-constants": {
|
| 4719 |
+
"version": "1.0.0",
|
| 4720 |
+
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
| 4721 |
+
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
| 4722 |
+
"dev": true,
|
| 4723 |
+
"license": "MIT"
|
| 4724 |
+
},
|
| 4725 |
"node_modules/function-bind": {
|
| 4726 |
"version": "1.1.2",
|
| 4727 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
|
|
| 4865 |
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
| 4866 |
}
|
| 4867 |
},
|
| 4868 |
+
"node_modules/github-from-package": {
|
| 4869 |
+
"version": "0.0.0",
|
| 4870 |
+
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
| 4871 |
+
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
| 4872 |
+
"dev": true,
|
| 4873 |
+
"license": "MIT"
|
| 4874 |
+
},
|
| 4875 |
"node_modules/glob-parent": {
|
| 4876 |
"version": "6.0.2",
|
| 4877 |
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
|
|
|
| 5046 |
"hermes-estree": "0.25.1"
|
| 5047 |
}
|
| 5048 |
},
|
| 5049 |
+
"node_modules/ieee754": {
|
| 5050 |
+
"version": "1.2.1",
|
| 5051 |
+
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
| 5052 |
+
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
| 5053 |
+
"dev": true,
|
| 5054 |
+
"funding": [
|
| 5055 |
+
{
|
| 5056 |
+
"type": "github",
|
| 5057 |
+
"url": "https://github.com/sponsors/feross"
|
| 5058 |
+
},
|
| 5059 |
+
{
|
| 5060 |
+
"type": "patreon",
|
| 5061 |
+
"url": "https://www.patreon.com/feross"
|
| 5062 |
+
},
|
| 5063 |
+
{
|
| 5064 |
+
"type": "consulting",
|
| 5065 |
+
"url": "https://feross.org/support"
|
| 5066 |
+
}
|
| 5067 |
+
],
|
| 5068 |
+
"license": "BSD-3-Clause"
|
| 5069 |
+
},
|
| 5070 |
"node_modules/ignore": {
|
| 5071 |
"version": "5.3.2",
|
| 5072 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
|
|
| 5104 |
"node": ">=0.8.19"
|
| 5105 |
}
|
| 5106 |
},
|
| 5107 |
+
"node_modules/inherits": {
|
| 5108 |
+
"version": "2.0.4",
|
| 5109 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 5110 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 5111 |
+
"dev": true,
|
| 5112 |
+
"license": "ISC"
|
| 5113 |
+
},
|
| 5114 |
+
"node_modules/ini": {
|
| 5115 |
+
"version": "1.3.8",
|
| 5116 |
+
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
| 5117 |
+
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
| 5118 |
+
"dev": true,
|
| 5119 |
+
"license": "ISC"
|
| 5120 |
+
},
|
| 5121 |
"node_modules/internal-slot": {
|
| 5122 |
"version": "1.1.0",
|
| 5123 |
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
|
|
|
| 6103 |
"node": ">=8.6"
|
| 6104 |
}
|
| 6105 |
},
|
| 6106 |
+
"node_modules/mimic-response": {
|
| 6107 |
+
"version": "3.1.0",
|
| 6108 |
+
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
| 6109 |
+
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
| 6110 |
+
"dev": true,
|
| 6111 |
+
"license": "MIT",
|
| 6112 |
+
"engines": {
|
| 6113 |
+
"node": ">=10"
|
| 6114 |
+
},
|
| 6115 |
+
"funding": {
|
| 6116 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 6117 |
+
}
|
| 6118 |
+
},
|
| 6119 |
"node_modules/minimatch": {
|
| 6120 |
"version": "3.1.5",
|
| 6121 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
|
|
|
| 6139 |
"url": "https://github.com/sponsors/ljharb"
|
| 6140 |
}
|
| 6141 |
},
|
| 6142 |
+
"node_modules/mkdirp-classic": {
|
| 6143 |
+
"version": "0.5.3",
|
| 6144 |
+
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
| 6145 |
+
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
| 6146 |
+
"dev": true,
|
| 6147 |
+
"license": "MIT"
|
| 6148 |
+
},
|
| 6149 |
"node_modules/motion-dom": {
|
| 6150 |
"version": "13.0.0",
|
| 6151 |
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.0.0.tgz",
|
|
|
|
| 6186 |
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 6187 |
}
|
| 6188 |
},
|
| 6189 |
+
"node_modules/napi-build-utils": {
|
| 6190 |
+
"version": "2.0.0",
|
| 6191 |
+
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
| 6192 |
+
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
| 6193 |
+
"dev": true,
|
| 6194 |
+
"license": "MIT"
|
| 6195 |
+
},
|
| 6196 |
"node_modules/napi-postinstall": {
|
| 6197 |
"version": "0.3.4",
|
| 6198 |
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
|
|
|
| 6297 |
"node": "^10 || ^12 || >=14"
|
| 6298 |
}
|
| 6299 |
},
|
| 6300 |
+
"node_modules/node-abi": {
|
| 6301 |
+
"version": "3.94.0",
|
| 6302 |
+
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
|
| 6303 |
+
"integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
|
| 6304 |
+
"dev": true,
|
| 6305 |
+
"license": "MIT",
|
| 6306 |
+
"dependencies": {
|
| 6307 |
+
"semver": "^7.3.5"
|
| 6308 |
+
},
|
| 6309 |
+
"engines": {
|
| 6310 |
+
"node": ">=10"
|
| 6311 |
+
}
|
| 6312 |
+
},
|
| 6313 |
+
"node_modules/node-abi/node_modules/semver": {
|
| 6314 |
+
"version": "7.8.5",
|
| 6315 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
| 6316 |
+
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
| 6317 |
+
"dev": true,
|
| 6318 |
+
"license": "ISC",
|
| 6319 |
+
"bin": {
|
| 6320 |
+
"semver": "bin/semver.js"
|
| 6321 |
+
},
|
| 6322 |
+
"engines": {
|
| 6323 |
+
"node": ">=10"
|
| 6324 |
+
}
|
| 6325 |
+
},
|
| 6326 |
+
"node_modules/node-addon-api": {
|
| 6327 |
+
"version": "7.1.1",
|
| 6328 |
+
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
| 6329 |
+
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
| 6330 |
+
"dev": true,
|
| 6331 |
+
"license": "MIT"
|
| 6332 |
+
},
|
| 6333 |
"node_modules/node-exports-info": {
|
| 6334 |
"version": "1.6.2",
|
| 6335 |
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz",
|
|
|
|
| 6482 |
"url": "https://github.com/sponsors/ljharb"
|
| 6483 |
}
|
| 6484 |
},
|
| 6485 |
+
"node_modules/once": {
|
| 6486 |
+
"version": "1.4.0",
|
| 6487 |
+
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
| 6488 |
+
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
| 6489 |
+
"dev": true,
|
| 6490 |
+
"license": "ISC",
|
| 6491 |
+
"dependencies": {
|
| 6492 |
+
"wrappy": "1"
|
| 6493 |
+
}
|
| 6494 |
+
},
|
| 6495 |
"node_modules/optionator": {
|
| 6496 |
"version": "0.9.4",
|
| 6497 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
|
|
|
| 6659 |
"node": "^10 || ^12 || >=14"
|
| 6660 |
}
|
| 6661 |
},
|
| 6662 |
+
"node_modules/prebuild-install": {
|
| 6663 |
+
"version": "7.1.3",
|
| 6664 |
+
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
| 6665 |
+
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
| 6666 |
+
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
| 6667 |
+
"dev": true,
|
| 6668 |
+
"license": "MIT",
|
| 6669 |
+
"dependencies": {
|
| 6670 |
+
"detect-libc": "^2.0.0",
|
| 6671 |
+
"expand-template": "^2.0.3",
|
| 6672 |
+
"github-from-package": "0.0.0",
|
| 6673 |
+
"minimist": "^1.2.3",
|
| 6674 |
+
"mkdirp-classic": "^0.5.3",
|
| 6675 |
+
"napi-build-utils": "^2.0.0",
|
| 6676 |
+
"node-abi": "^3.3.0",
|
| 6677 |
+
"pump": "^3.0.0",
|
| 6678 |
+
"rc": "^1.2.7",
|
| 6679 |
+
"simple-get": "^4.0.0",
|
| 6680 |
+
"tar-fs": "^2.0.0",
|
| 6681 |
+
"tunnel-agent": "^0.6.0"
|
| 6682 |
+
},
|
| 6683 |
+
"bin": {
|
| 6684 |
+
"prebuild-install": "bin.js"
|
| 6685 |
+
},
|
| 6686 |
+
"engines": {
|
| 6687 |
+
"node": ">=10"
|
| 6688 |
+
}
|
| 6689 |
+
},
|
| 6690 |
"node_modules/prelude-ls": {
|
| 6691 |
"version": "1.2.1",
|
| 6692 |
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
|
|
|
| 6709 |
"react-is": "^16.13.1"
|
| 6710 |
}
|
| 6711 |
},
|
| 6712 |
+
"node_modules/pump": {
|
| 6713 |
+
"version": "3.0.4",
|
| 6714 |
+
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
| 6715 |
+
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
| 6716 |
+
"dev": true,
|
| 6717 |
+
"license": "MIT",
|
| 6718 |
+
"dependencies": {
|
| 6719 |
+
"end-of-stream": "^1.1.0",
|
| 6720 |
+
"once": "^1.3.1"
|
| 6721 |
+
}
|
| 6722 |
+
},
|
| 6723 |
"node_modules/punycode": {
|
| 6724 |
"version": "2.3.1",
|
| 6725 |
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
|
|
| 6751 |
],
|
| 6752 |
"license": "MIT"
|
| 6753 |
},
|
| 6754 |
+
"node_modules/rc": {
|
| 6755 |
+
"version": "1.2.8",
|
| 6756 |
+
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
| 6757 |
+
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
| 6758 |
+
"dev": true,
|
| 6759 |
+
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
| 6760 |
+
"dependencies": {
|
| 6761 |
+
"deep-extend": "^0.6.0",
|
| 6762 |
+
"ini": "~1.3.0",
|
| 6763 |
+
"minimist": "^1.2.0",
|
| 6764 |
+
"strip-json-comments": "~2.0.1"
|
| 6765 |
+
},
|
| 6766 |
+
"bin": {
|
| 6767 |
+
"rc": "cli.js"
|
| 6768 |
+
}
|
| 6769 |
+
},
|
| 6770 |
+
"node_modules/rc/node_modules/strip-json-comments": {
|
| 6771 |
+
"version": "2.0.1",
|
| 6772 |
+
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
| 6773 |
+
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
| 6774 |
+
"dev": true,
|
| 6775 |
+
"license": "MIT",
|
| 6776 |
+
"engines": {
|
| 6777 |
+
"node": ">=0.10.0"
|
| 6778 |
+
}
|
| 6779 |
+
},
|
| 6780 |
"node_modules/react": {
|
| 6781 |
"version": "19.2.8",
|
| 6782 |
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
|
|
|
| 6874 |
}
|
| 6875 |
}
|
| 6876 |
},
|
| 6877 |
+
"node_modules/readable-stream": {
|
| 6878 |
+
"version": "3.6.2",
|
| 6879 |
+
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
| 6880 |
+
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
| 6881 |
+
"dev": true,
|
| 6882 |
+
"license": "MIT",
|
| 6883 |
+
"dependencies": {
|
| 6884 |
+
"inherits": "^2.0.3",
|
| 6885 |
+
"string_decoder": "^1.1.1",
|
| 6886 |
+
"util-deprecate": "^1.0.1"
|
| 6887 |
+
},
|
| 6888 |
+
"engines": {
|
| 6889 |
+
"node": ">= 6"
|
| 6890 |
+
}
|
| 6891 |
+
},
|
| 6892 |
"node_modules/reflect.getprototypeof": {
|
| 6893 |
"version": "1.0.10",
|
| 6894 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
|
|
| 7032 |
"url": "https://github.com/sponsors/ljharb"
|
| 7033 |
}
|
| 7034 |
},
|
| 7035 |
+
"node_modules/safe-buffer": {
|
| 7036 |
+
"version": "5.2.1",
|
| 7037 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 7038 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 7039 |
+
"dev": true,
|
| 7040 |
+
"funding": [
|
| 7041 |
+
{
|
| 7042 |
+
"type": "github",
|
| 7043 |
+
"url": "https://github.com/sponsors/feross"
|
| 7044 |
+
},
|
| 7045 |
+
{
|
| 7046 |
+
"type": "patreon",
|
| 7047 |
+
"url": "https://www.patreon.com/feross"
|
| 7048 |
+
},
|
| 7049 |
+
{
|
| 7050 |
+
"type": "consulting",
|
| 7051 |
+
"url": "https://feross.org/support"
|
| 7052 |
+
}
|
| 7053 |
+
],
|
| 7054 |
+
"license": "MIT"
|
| 7055 |
+
},
|
| 7056 |
"node_modules/safe-push-apply": {
|
| 7057 |
"version": "1.0.0",
|
| 7058 |
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
|
|
|
| 7315 |
"url": "https://github.com/sponsors/ljharb"
|
| 7316 |
}
|
| 7317 |
},
|
| 7318 |
+
"node_modules/simple-concat": {
|
| 7319 |
+
"version": "1.0.1",
|
| 7320 |
+
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
| 7321 |
+
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
| 7322 |
+
"dev": true,
|
| 7323 |
+
"funding": [
|
| 7324 |
+
{
|
| 7325 |
+
"type": "github",
|
| 7326 |
+
"url": "https://github.com/sponsors/feross"
|
| 7327 |
+
},
|
| 7328 |
+
{
|
| 7329 |
+
"type": "patreon",
|
| 7330 |
+
"url": "https://www.patreon.com/feross"
|
| 7331 |
+
},
|
| 7332 |
+
{
|
| 7333 |
+
"type": "consulting",
|
| 7334 |
+
"url": "https://feross.org/support"
|
| 7335 |
+
}
|
| 7336 |
+
],
|
| 7337 |
+
"license": "MIT"
|
| 7338 |
+
},
|
| 7339 |
+
"node_modules/simple-get": {
|
| 7340 |
+
"version": "4.0.1",
|
| 7341 |
+
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
| 7342 |
+
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
| 7343 |
+
"dev": true,
|
| 7344 |
+
"funding": [
|
| 7345 |
+
{
|
| 7346 |
+
"type": "github",
|
| 7347 |
+
"url": "https://github.com/sponsors/feross"
|
| 7348 |
+
},
|
| 7349 |
+
{
|
| 7350 |
+
"type": "patreon",
|
| 7351 |
+
"url": "https://www.patreon.com/feross"
|
| 7352 |
+
},
|
| 7353 |
+
{
|
| 7354 |
+
"type": "consulting",
|
| 7355 |
+
"url": "https://feross.org/support"
|
| 7356 |
+
}
|
| 7357 |
+
],
|
| 7358 |
+
"license": "MIT",
|
| 7359 |
+
"dependencies": {
|
| 7360 |
+
"decompress-response": "^6.0.0",
|
| 7361 |
+
"once": "^1.3.1",
|
| 7362 |
+
"simple-concat": "^1.0.0"
|
| 7363 |
+
}
|
| 7364 |
+
},
|
| 7365 |
"node_modules/source-map-js": {
|
| 7366 |
"version": "1.2.1",
|
| 7367 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
|
| 7392 |
"node": ">= 0.4"
|
| 7393 |
}
|
| 7394 |
},
|
| 7395 |
+
"node_modules/string_decoder": {
|
| 7396 |
+
"version": "1.3.0",
|
| 7397 |
+
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
| 7398 |
+
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
| 7399 |
+
"dev": true,
|
| 7400 |
+
"license": "MIT",
|
| 7401 |
+
"dependencies": {
|
| 7402 |
+
"safe-buffer": "~5.2.0"
|
| 7403 |
+
}
|
| 7404 |
+
},
|
| 7405 |
"node_modules/string.prototype.includes": {
|
| 7406 |
"version": "2.0.1",
|
| 7407 |
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
|
|
|
|
| 7619 |
"url": "https://opencollective.com/webpack"
|
| 7620 |
}
|
| 7621 |
},
|
| 7622 |
+
"node_modules/tar-fs": {
|
| 7623 |
+
"version": "2.1.5",
|
| 7624 |
+
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
|
| 7625 |
+
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
|
| 7626 |
+
"dev": true,
|
| 7627 |
+
"license": "MIT",
|
| 7628 |
+
"dependencies": {
|
| 7629 |
+
"chownr": "^1.1.1",
|
| 7630 |
+
"mkdirp-classic": "^0.5.2",
|
| 7631 |
+
"pump": "^3.0.0",
|
| 7632 |
+
"tar-stream": "^2.1.4"
|
| 7633 |
+
}
|
| 7634 |
+
},
|
| 7635 |
+
"node_modules/tar-stream": {
|
| 7636 |
+
"version": "2.2.0",
|
| 7637 |
+
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
| 7638 |
+
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
| 7639 |
+
"dev": true,
|
| 7640 |
+
"license": "MIT",
|
| 7641 |
+
"dependencies": {
|
| 7642 |
+
"bl": "^4.0.3",
|
| 7643 |
+
"end-of-stream": "^1.4.1",
|
| 7644 |
+
"fs-constants": "^1.0.0",
|
| 7645 |
+
"inherits": "^2.0.3",
|
| 7646 |
+
"readable-stream": "^3.1.1"
|
| 7647 |
+
},
|
| 7648 |
+
"engines": {
|
| 7649 |
+
"node": ">=6"
|
| 7650 |
+
}
|
| 7651 |
+
},
|
| 7652 |
"node_modules/tinyglobby": {
|
| 7653 |
"version": "0.2.17",
|
| 7654 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
|
|
|
| 7755 |
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 7756 |
"license": "0BSD"
|
| 7757 |
},
|
| 7758 |
+
"node_modules/tunnel-agent": {
|
| 7759 |
+
"version": "0.6.0",
|
| 7760 |
+
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
| 7761 |
+
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
| 7762 |
+
"dev": true,
|
| 7763 |
+
"license": "Apache-2.0",
|
| 7764 |
+
"dependencies": {
|
| 7765 |
+
"safe-buffer": "^5.0.1"
|
| 7766 |
+
},
|
| 7767 |
+
"engines": {
|
| 7768 |
+
"node": "*"
|
| 7769 |
+
}
|
| 7770 |
+
},
|
| 7771 |
"node_modules/type-check": {
|
| 7772 |
"version": "0.4.0",
|
| 7773 |
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
|
|
|
| 8045 |
}
|
| 8046 |
}
|
| 8047 |
},
|
| 8048 |
+
"node_modules/util-deprecate": {
|
| 8049 |
+
"version": "1.0.2",
|
| 8050 |
+
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
| 8051 |
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
| 8052 |
+
"dev": true,
|
| 8053 |
+
"license": "MIT"
|
| 8054 |
+
},
|
| 8055 |
"node_modules/which": {
|
| 8056 |
"version": "2.0.2",
|
| 8057 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
|
|
| 8167 |
"node": ">=0.10.0"
|
| 8168 |
}
|
| 8169 |
},
|
| 8170 |
+
"node_modules/wrappy": {
|
| 8171 |
+
"version": "1.0.2",
|
| 8172 |
+
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
| 8173 |
+
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
| 8174 |
+
"dev": true,
|
| 8175 |
+
"license": "ISC"
|
| 8176 |
+
},
|
| 8177 |
"node_modules/yallist": {
|
| 8178 |
"version": "3.1.1",
|
| 8179 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
package.json
CHANGED
|
@@ -30,9 +30,10 @@
|
|
| 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 |
-
}
|
|
|
|
| 30 |
"@types/offscreencanvas": "^2019.7.3",
|
| 31 |
"@types/react": "^19",
|
| 32 |
"@types/react-dom": "^19",
|
| 33 |
+
"canvas": "^3.2.3",
|
| 34 |
"eslint": "^9",
|
| 35 |
"eslint-config-next": "16.3.0",
|
| 36 |
"tailwindcss": "^4",
|
| 37 |
"typescript": "^5"
|
| 38 |
}
|
| 39 |
+
}
|
src/app/page.tsx
CHANGED
|
@@ -6,6 +6,7 @@ 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'
|
|
@@ -50,6 +51,11 @@ export default function Home() {
|
|
| 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)
|
|
@@ -209,6 +215,7 @@ export default function Home() {
|
|
| 209 |
width={originalImageData.width}
|
| 210 |
height={originalImageData.height}
|
| 211 |
className="w-full h-full"
|
|
|
|
| 212 |
/>
|
| 213 |
</div>
|
| 214 |
|
|
@@ -260,9 +267,18 @@ export default function Home() {
|
|
| 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 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
</div>
|
| 267 |
</aside>
|
| 268 |
</main>
|
|
|
|
| 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 { StaticConvolutionView } from '@/components/canvas/StaticConvolutionView'
|
| 10 |
import { SplitView } from '@/components/canvas/SplitView'
|
| 11 |
import { ImageCanvas } from '@/components/canvas/ImageCanvas'
|
| 12 |
import { Button, Card, CardContent, Badge } from '@/components/ui'
|
|
|
|
| 51 |
|
| 52 |
const [showComparison, setShowComparison] = useState(false)
|
| 53 |
const [sidebarWidth, setSidebarWidth] = useState(320)
|
| 54 |
+
const [inspectedPosition, setInspectedPosition] = useState<{ x: number; y: number } | null>(null)
|
| 55 |
+
|
| 56 |
+
const handleInspectClick = useCallback((x: number, y: number) => {
|
| 57 |
+
setInspectedPosition({ x, y })
|
| 58 |
+
}, [])
|
| 59 |
|
| 60 |
const handleImageLoad = useCallback((data: { image: HTMLImageElement; imageData: ImageData }) => {
|
| 61 |
setOriginalImage(data.image, data.imageData)
|
|
|
|
| 215 |
width={originalImageData.width}
|
| 216 |
height={originalImageData.height}
|
| 217 |
className="w-full h-full"
|
| 218 |
+
onInspectClick={handleInspectClick}
|
| 219 |
/>
|
| 220 |
</div>
|
| 221 |
|
|
|
|
| 267 |
{/* Right Sidebar - Info Panels */}
|
| 268 |
<aside className="w-80 flex flex-col bg-white dark:bg-gray-800 border-l dark:border-gray-700 overflow-y-auto">
|
| 269 |
<div className="p-4 space-y-4">
|
| 270 |
+
{inspectedPosition ? (
|
| 271 |
+
<StaticConvolutionView
|
| 272 |
+
inspectedPosition={inspectedPosition}
|
| 273 |
+
onClearInspection={() => setInspectedPosition(null)}
|
| 274 |
+
/>
|
| 275 |
+
) : (
|
| 276 |
+
<>
|
| 277 |
+
<KernelInfoPanel />
|
| 278 |
+
<OverlayControls />
|
| 279 |
+
<PaddingControls />
|
| 280 |
+
</>
|
| 281 |
+
)}
|
| 282 |
</div>
|
| 283 |
</aside>
|
| 284 |
</main>
|
src/components/canvas/ConvolutionCanvas.tsx
CHANGED
|
@@ -9,9 +9,10 @@ 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 |
|
|
@@ -101,6 +102,24 @@ export function ConvolutionCanvas({ width = 800, height = 600, className }: Conv
|
|
| 101 |
height,
|
| 102 |
])
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
useEffect(() => {
|
| 105 |
drawCanvas()
|
| 106 |
}, [drawCanvas])
|
|
@@ -123,8 +142,9 @@ export function ConvolutionCanvas({ width = 800, height = 600, className }: Conv
|
|
| 123 |
ref={canvasRef}
|
| 124 |
width={width}
|
| 125 |
height={height}
|
| 126 |
-
className="block"
|
| 127 |
style={{ touchAction: 'none' }}
|
|
|
|
| 128 |
/>
|
| 129 |
</div>
|
| 130 |
)
|
|
|
|
| 9 |
width?: number
|
| 10 |
height?: number
|
| 11 |
className?: string
|
| 12 |
+
onInspectClick?: (x: number, y: number) => void
|
| 13 |
}
|
| 14 |
|
| 15 |
+
export function ConvolutionCanvas({ width = 800, height = 600, className, onInspectClick }: ConvolutionCanvasProps) {
|
| 16 |
const canvasRef = useRef<HTMLCanvasElement>(null)
|
| 17 |
const animationRef = useRef<number | null>(null)
|
| 18 |
|
|
|
|
| 102 |
height,
|
| 103 |
])
|
| 104 |
|
| 105 |
+
const handleCanvasClick = useCallback(
|
| 106 |
+
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
| 107 |
+
if (!onInspectClick) return
|
| 108 |
+
const rect = e.currentTarget.getBoundingClientRect()
|
| 109 |
+
const canvasX = e.clientX - rect.left
|
| 110 |
+
const canvasY = e.clientY - rect.top
|
| 111 |
+
|
| 112 |
+
// Convert canvas coordinates to image coordinates
|
| 113 |
+
const imageX = Math.floor((canvasX - panOffset.x) / zoomLevel)
|
| 114 |
+
const imageY = Math.floor((canvasY - panOffset.y) / zoomLevel)
|
| 115 |
+
|
| 116 |
+
if (imageX >= 0 && imageX < width && imageY >= 0 && imageY < height) {
|
| 117 |
+
onInspectClick(imageX, imageY)
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
[onInspectClick, panOffset, zoomLevel, width, height]
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
useEffect(() => {
|
| 124 |
drawCanvas()
|
| 125 |
}, [drawCanvas])
|
|
|
|
| 142 |
ref={canvasRef}
|
| 143 |
width={width}
|
| 144 |
height={height}
|
| 145 |
+
className="block cursor-crosshair"
|
| 146 |
style={{ touchAction: 'none' }}
|
| 147 |
+
onClick={onInspectClick ? handleCanvasClick : undefined}
|
| 148 |
/>
|
| 149 |
</div>
|
| 150 |
)
|
src/components/canvas/StaticConvolutionView.tsx
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useCallback, useMemo, useState } from 'react'
|
| 4 |
+
import { useStaticConvolution } from '@/hooks/useStaticConvolution'
|
| 5 |
+
import { useAppStore } from '@/store'
|
| 6 |
+
import { Card, CardContent, Badge, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
|
| 7 |
+
import { cn } from '@/utils/helpers'
|
| 8 |
+
import { Calculator, Grid, Palette } from 'lucide-react'
|
| 9 |
+
|
| 10 |
+
interface StaticConvolutionViewProps {
|
| 11 |
+
className?: string
|
| 12 |
+
inspectedPosition: { x: number; y: number } | null
|
| 13 |
+
onClearInspection: () => void
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function StaticConvolutionView({ className, inspectedPosition, onClearInspection }: StaticConvolutionViewProps) {
|
| 17 |
+
const {
|
| 18 |
+
computeStep,
|
| 19 |
+
getPaddedImagePreview,
|
| 20 |
+
kernel,
|
| 21 |
+
normalize,
|
| 22 |
+
padding,
|
| 23 |
+
imageWidth,
|
| 24 |
+
imageHeight,
|
| 25 |
+
hasImage,
|
| 26 |
+
hasKernel,
|
| 27 |
+
} = useStaticConvolution()
|
| 28 |
+
|
| 29 |
+
const { settings } = useAppStore()
|
| 30 |
+
const [activeChannel, setActiveChannel] = useState(0)
|
| 31 |
+
const [activeTab, setActiveTab] = useState<string>('overview')
|
| 32 |
+
|
| 33 |
+
if (!hasImage || !hasKernel) {
|
| 34 |
+
return (
|
| 35 |
+
<Card className={cn('w-full', className)}>
|
| 36 |
+
<CardContent className="p-6 text-center text-gray-500 dark:text-gray-400">
|
| 37 |
+
<Grid className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
| 38 |
+
<p>Upload an image and select a kernel to inspect convolution steps</p>
|
| 39 |
+
</CardContent>
|
| 40 |
+
</Card>
|
| 41 |
+
)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const step = inspectedPosition ? computeStep(inspectedPosition.x, inspectedPosition.y, activeChannel) : null
|
| 45 |
+
|
| 46 |
+
const channelLabels = ['Red', 'Green', 'Blue', 'Alpha']
|
| 47 |
+
const channelColors = ['text-red-600', 'text-green-600', 'text-blue-600', 'text-gray-600']
|
| 48 |
+
|
| 49 |
+
// Get padded image preview for visualization
|
| 50 |
+
const paddedPreview = inspectedPosition ? getPaddedImagePreview(kernel.length) : null
|
| 51 |
+
const halfKernel = Math.floor(kernel.length / 2)
|
| 52 |
+
const centerPx = halfKernel
|
| 53 |
+
const centerPy = halfKernel
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<Card className={cn('w-full flex flex-col', className)}>
|
| 57 |
+
<CardContent className="p-4 flex-1 flex flex-col min-h-0">
|
| 58 |
+
{/* Header */}
|
| 59 |
+
<div className="flex items-center justify-between mb-4">
|
| 60 |
+
<h3 className="font-medium flex items-center gap-2">
|
| 61 |
+
<Calculator className="h-5 w-5" />
|
| 62 |
+
Static Convolution Inspector
|
| 63 |
+
</h3>
|
| 64 |
+
<div className="flex items-center gap-2">
|
| 65 |
+
<Badge variant="outline" className="text-xs">
|
| 66 |
+
{padding} padding
|
| 67 |
+
</Badge>
|
| 68 |
+
{normalize && <Badge variant="secondary" className="text-xs">Normalized</Badge>}
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
{/* Inspected Position Info */}
|
| 73 |
+
{inspectedPosition && (
|
| 74 |
+
<div className="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
| 75 |
+
<div className="flex items-center justify-between">
|
| 76 |
+
<div className="flex items-center gap-4">
|
| 77 |
+
<span className="font-medium">Position: ({inspectedPosition.x}, {inspectedPosition.y})</span>
|
| 78 |
+
<div className="flex items-center gap-2">
|
| 79 |
+
<Palette className="h-4 w-4 text-gray-500" />
|
| 80 |
+
<select
|
| 81 |
+
value={activeChannel}
|
| 82 |
+
onChange={(e) => setActiveChannel(parseInt(e.target.value))}
|
| 83 |
+
className="text-sm border border-gray-300 rounded px-2 py-1 dark:border-gray-600 dark:bg-gray-700"
|
| 84 |
+
>
|
| 85 |
+
{channelLabels.map((label, i) => (
|
| 86 |
+
<option key={i} value={i} className={channelColors[i]}>
|
| 87 |
+
{label} Channel
|
| 88 |
+
</option>
|
| 89 |
+
))}
|
| 90 |
+
</select>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
<button
|
| 94 |
+
onClick={onClearInspection}
|
| 95 |
+
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
| 96 |
+
>
|
| 97 |
+
Clear Selection
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
)}
|
| 102 |
+
|
| 103 |
+
{/* Kernel Matrix Display */}
|
| 104 |
+
<div className="mb-4">
|
| 105 |
+
<h4 className="text-sm font-medium mb-2">Kernel Matrix ({kernel.length}×{kernel.length})</h4>
|
| 106 |
+
<div className="grid gap-1 mb-4" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}>
|
| 107 |
+
{kernel.map((row, i) =>
|
| 108 |
+
row.map((val, j) => (
|
| 109 |
+
<div
|
| 110 |
+
key={`${i}-${j}`}
|
| 111 |
+
className={cn(
|
| 112 |
+
'p-2 text-center text-sm font-mono rounded border',
|
| 113 |
+
val > 0
|
| 114 |
+
? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300 border-green-200'
|
| 115 |
+
: val < 0
|
| 116 |
+
? 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200'
|
| 117 |
+
: 'bg-gray-50 text-gray-800 dark:bg-gray-800 dark:text-gray-300 border-gray-200'
|
| 118 |
+
)}
|
| 119 |
+
>
|
| 120 |
+
{val.toFixed(val === Math.round(val) ? 0 : 2)}
|
| 121 |
+
</div>
|
| 122 |
+
))
|
| 123 |
+
)}
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
{/* Step Details Tabs */}
|
| 128 |
+
{step && (
|
| 129 |
+
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0">
|
| 130 |
+
<TabsList className="grid w-full grid-cols-5 mb-4">
|
| 131 |
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
| 132 |
+
<TabsTrigger value="patch">Image Patch</TabsTrigger>
|
| 133 |
+
<TabsTrigger value="multiplications">Multiplications</TabsTrigger>
|
| 134 |
+
<TabsTrigger value="summation">Summation</TabsTrigger>
|
| 135 |
+
<TabsTrigger value="padding">Padding</TabsTrigger>
|
| 136 |
+
</TabsList>
|
| 137 |
+
|
| 138 |
+
<TabsContent value="overview" className="flex-1 overflow-auto space-y-4">
|
| 139 |
+
<div className="grid gap-4 sm:grid-cols-2">
|
| 140 |
+
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
| 141 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Input Patch (Normalized 0-1)</h5>
|
| 142 |
+
<div className="grid gap-0.5 text-xs font-mono" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}>
|
| 143 |
+
{step.inputPatch.map((row, i) =>
|
| 144 |
+
row.map((val, j) => (
|
| 145 |
+
<div
|
| 146 |
+
key={`${i}-${j}`}
|
| 147 |
+
className={cn(
|
| 148 |
+
'p-1.5 text-center rounded border',
|
| 149 |
+
val > 0.5 ? 'bg-blue-50 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700'
|
| 150 |
+
)}
|
| 151 |
+
>
|
| 152 |
+
{val.toFixed(3)}
|
| 153 |
+
</div>
|
| 154 |
+
))
|
| 155 |
+
)}
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
| 160 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Kernel Values</h5>
|
| 161 |
+
<div className="grid gap-0.5 text-xs font-mono" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}>
|
| 162 |
+
{step.kernel.map((row, i) =>
|
| 163 |
+
row.map((val, j) => (
|
| 164 |
+
<div
|
| 165 |
+
key={`${i}-${j}`}
|
| 166 |
+
className={cn(
|
| 167 |
+
'p-1.5 text-center rounded border',
|
| 168 |
+
val > 0 ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300' :
|
| 169 |
+
val < 0 ? 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300' :
|
| 170 |
+
'bg-gray-50 text-gray-800 dark:bg-gray-800 dark:text-gray-300'
|
| 171 |
+
)}
|
| 172 |
+
>
|
| 173 |
+
{val.toFixed(val === Math.round(val) ? 0 : 2)}
|
| 174 |
+
</div>
|
| 175 |
+
))
|
| 176 |
+
)}
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg sm:col-span-2">
|
| 181 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Computation Summary</h5>
|
| 182 |
+
<div className="grid gap-2 sm:grid-cols-3 text-sm">
|
| 183 |
+
<div className="font-mono">
|
| 184 |
+
<span className="text-gray-500">Raw Sum: </span>
|
| 185 |
+
<span className="text-blue-600 dark:text-blue-400">{step.sum.toFixed(6)}</span>
|
| 186 |
+
</div>
|
| 187 |
+
<div className="font-mono">
|
| 188 |
+
<span className="text-gray-500">Normalized: </span>
|
| 189 |
+
<span className="text-green-600 dark:text-green-400">{step.normalizedValue.toFixed(6)}</span>
|
| 190 |
+
</div>
|
| 191 |
+
<div className="font-mono">
|
| 192 |
+
<span className="text-gray-500">Output (0-255): </span>
|
| 193 |
+
<span className="text-purple-600 dark:text-purple-400">{Math.round(step.normalizedValue * 255)}</span>
|
| 194 |
+
</div>
|
| 195 |
+
<div className="font-mono sm:col-span-3">
|
| 196 |
+
<span className="text-gray-500">Progress: </span>
|
| 197 |
+
<span>{Math.round(step.progress * 100)}%</span>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</TabsContent>
|
| 203 |
+
|
| 204 |
+
<TabsContent value="patch" className="flex-1 overflow-auto">
|
| 205 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
| 206 |
+
Image Patch Under Kernel at ({inspectedPosition?.x}, {inspectedPosition?.y}) - Channel: {channelLabels[activeChannel]}
|
| 207 |
+
</h5>
|
| 208 |
+
<div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${kernel.length}, 1fr)` }}>
|
| 209 |
+
{step.inputPatch.map((row, i) =>
|
| 210 |
+
row.map((val, j) => (
|
| 211 |
+
<div
|
| 212 |
+
key={`${i}-${j}`}
|
| 213 |
+
className={cn(
|
| 214 |
+
'p-3 text-center text-sm font-mono rounded border aspect-square flex items-center justify-center',
|
| 215 |
+
val > 0.5 ? 'bg-blue-50 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 border-gray-200'
|
| 216 |
+
)}
|
| 217 |
+
>
|
| 218 |
+
<div className="text-xs text-gray-500">{val.toFixed(3)}</div>
|
| 219 |
+
<div className="text-lg font-bold">{val.toFixed(3)}</div>
|
| 220 |
+
</div>
|
| 221 |
+
))
|
| 222 |
+
)}
|
| 223 |
+
</div>
|
| 224 |
+
</TabsContent>
|
| 225 |
+
|
| 226 |
+
<TabsContent value="multiplications" className="flex-1 overflow-auto">
|
| 227 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
| 228 |
+
Element-wise Multiplication: Pixel × Kernel
|
| 229 |
+
</h5>
|
| 230 |
+
<div className="space-y-4">
|
| 231 |
+
{step.multiplications.map((row, i) =>
|
| 232 |
+
row.map((mult, j) => {
|
| 233 |
+
const pixelVal = step.inputPatch[i]?.[j] ?? 0
|
| 234 |
+
const kernelVal = step.kernel[i]?.[j] ?? 0
|
| 235 |
+
return (
|
| 236 |
+
<div key={`${i}-${j}`} className="flex items-center justify-center gap-4 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
| 237 |
+
<div className="w-20 text-right text-sm font-mono text-gray-600 dark:text-gray-400">
|
| 238 |
+
{pixelVal.toFixed(3)}
|
| 239 |
+
</div>
|
| 240 |
+
<span className="text-gray-400">×</span>
|
| 241 |
+
<div className="w-20 text-center text-sm font-mono" style={{ color: kernelVal > 0 ? '#16a34a' : kernelVal < 0 ? '#dc2626' : '#6b7280' }}>
|
| 242 |
+
{kernelVal.toFixed(2)}
|
| 243 |
+
</div>
|
| 244 |
+
<span className="text-gray-400">=</span>
|
| 245 |
+
<div className="w-24 text-left text-sm font-mono font-medium text-purple-600 dark:text-purple-400">
|
| 246 |
+
{mult.toFixed(6)}
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
)
|
| 250 |
+
})
|
| 251 |
+
)}
|
| 252 |
+
</div>
|
| 253 |
+
</TabsContent>
|
| 254 |
+
|
| 255 |
+
<TabsContent value="summation" className="flex-1 overflow-auto">
|
| 256 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
| 257 |
+
Summation: Σ(Pixel × Kernel)
|
| 258 |
+
</h5>
|
| 259 |
+
<div className="space-y-2 font-mono text-sm">
|
| 260 |
+
{step.multiplications.map((row, i) =>
|
| 261 |
+
row.map((mult, j) => {
|
| 262 |
+
const pixelVal = step.inputPatch[i]?.[j] ?? 0
|
| 263 |
+
const kernelVal = step.kernel[i]?.[j] ?? 0
|
| 264 |
+
return (
|
| 265 |
+
<div key={`${i}-${j}`} className="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
| 266 |
+
<span className="text-gray-400">({pixelVal.toFixed(3)} × {kernelVal.toFixed(2)})</span>
|
| 267 |
+
<span className="text-purple-600 dark:text-purple-400">= {mult.toFixed(6)}</span>
|
| 268 |
+
</div>
|
| 269 |
+
)
|
| 270 |
+
})
|
| 271 |
+
)}
|
| 272 |
+
<div className="border-t border-gray-300 dark:border-gray-600 pt-2 mt-2 flex items-center justify-between">
|
| 273 |
+
<span className="font-medium">Sum:</span>
|
| 274 |
+
<span className="font-bold text-blue-600 dark:text-blue-400">{step.sum.toFixed(6)}</span>
|
| 275 |
+
</div>
|
| 276 |
+
<div className="flex items-center justify-between text-green-600 dark:text-green-400">
|
| 277 |
+
<span>After normalization (÷ {kernel.flat().reduce((a, b) => a + b, 0).toFixed(normalize ? 0 : 2)}):</span>
|
| 278 |
+
<span className="font-bold">{step.normalizedValue.toFixed(6)}</span>
|
| 279 |
+
</div>
|
| 280 |
+
<div className="flex items-center justify-between text-purple-600 dark:text-purple-400 border-t border-gray-300 dark:border-gray-600 pt-2 mt-2">
|
| 281 |
+
<span className="font-medium">Final Output (0-255):</span>
|
| 282 |
+
<span className="font-bold text-lg">{Math.round(step.normalizedValue * 255)}</span>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
</TabsContent>
|
| 286 |
+
|
| 287 |
+
<TabsContent value="padding" className="flex-1 overflow-auto">
|
| 288 |
+
<h5 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
| 289 |
+
Padded Image Preview ({padding} padding) - Kernel centered at ({inspectedPosition?.x}, {inspectedPosition?.y})
|
| 290 |
+
</h5>
|
| 291 |
+
{paddedPreview && (
|
| 292 |
+
<div className="space-y-4">
|
| 293 |
+
<div className="text-sm text-gray-500 dark:text-gray-400">
|
| 294 |
+
Padded dimensions: {paddedPreview.width} × {paddedPreview.height} (original: {imageWidth} × {imageHeight})
|
| 295 |
+
<br />
|
| 296 |
+
Kernel center position in padded image: ({centerPx}, {centerPy})
|
| 297 |
+
</div>
|
| 298 |
+
<div className="grid gap-0.5" style={{ gridTemplateColumns: `repeat(${paddedPreview.width}, 1fr)` }}>
|
| 299 |
+
{Array.from({ length: paddedPreview.height }, (_, py) =>
|
| 300 |
+
Array.from({ length: paddedPreview.width }, (_, px) => {
|
| 301 |
+
// Show only first 3 channels (RGB) as grayscale
|
| 302 |
+
const idx = (py * paddedPreview.width + px) * 4
|
| 303 |
+
const r = paddedPreview.data[idx] ?? 0
|
| 304 |
+
const g = paddedPreview.data[idx + 1] ?? 0
|
| 305 |
+
const b = paddedPreview.data[idx + 2] ?? 0
|
| 306 |
+
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b)
|
| 307 |
+
const isCenter = px === centerPx && py === centerPy
|
| 308 |
+
const isKernelArea =
|
| 309 |
+
px >= centerPx - halfKernel &&
|
| 310 |
+
px < centerPx + halfKernel + 1 &&
|
| 311 |
+
py >= centerPy - halfKernel &&
|
| 312 |
+
py < centerPy + halfKernel + 1
|
| 313 |
+
|
| 314 |
+
return (
|
| 315 |
+
<div
|
| 316 |
+
key={`${px}-${py}`}
|
| 317 |
+
className={cn(
|
| 318 |
+
'aspect-square text-center text-xs font-mono rounded border flex items-center justify-center',
|
| 319 |
+
isCenter
|
| 320 |
+
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-400 font-bold'
|
| 321 |
+
: isKernelArea
|
| 322 |
+
? 'bg-yellow-50 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 border-yellow-300'
|
| 323 |
+
: px < halfKernel || px >= paddedPreview.width - halfKernel || py < halfKernel || py >= paddedPreview.height - halfKernel
|
| 324 |
+
? 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 border-gray-200'
|
| 325 |
+
: 'bg-white text-gray-800 dark:bg-gray-900 dark:text-gray-200 border-gray-200'
|
| 326 |
+
)}
|
| 327 |
+
title={`Padded[${px},${py}] = ${gray}`}
|
| 328 |
+
>
|
| 329 |
+
{gray}
|
| 330 |
+
</div>
|
| 331 |
+
)
|
| 332 |
+
})
|
| 333 |
+
)}
|
| 334 |
+
</div>
|
| 335 |
+
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
| 336 |
+
<span className="flex items-center gap-1">
|
| 337 |
+
<span className="w-3 h-3 rounded border bg-blue-100 dark:bg-blue-900/30" />
|
| 338 |
+
Kernel Center
|
| 339 |
+
</span>
|
| 340 |
+
<span className="flex items-center gap-1">
|
| 341 |
+
<span className="w-3 h-3 rounded border bg-yellow-50 dark:bg-yellow-900/30" />
|
| 342 |
+
Kernel Area
|
| 343 |
+
</span>
|
| 344 |
+
<span className="flex items-center gap-1">
|
| 345 |
+
<span className="w-3 h-3 rounded border bg-gray-100 dark:bg-gray-800" />
|
| 346 |
+
Padding Area
|
| 347 |
+
</span>
|
| 348 |
+
<span className="flex items-center gap-1">
|
| 349 |
+
<span className="w-3 h-3 rounded border bg-white dark:bg-gray-900" />
|
| 350 |
+
Original Image
|
| 351 |
+
</span>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
)}
|
| 355 |
+
</TabsContent>
|
| 356 |
+
</Tabs>
|
| 357 |
+
)}
|
| 358 |
+
|
| 359 |
+
{!inspectedPosition && (
|
| 360 |
+
<div className="flex-1 flex items-center justify-center text-gray-400">
|
| 361 |
+
<p className="text-center">Click on the image to inspect a convolution step</p>
|
| 362 |
+
</div>
|
| 363 |
+
)}
|
| 364 |
+
</CardContent>
|
| 365 |
+
</Card>
|
| 366 |
+
)
|
| 367 |
+
}
|
src/components/ui/Tabs.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import * as React from 'react'
|
| 4 |
+
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
| 5 |
+
import { cn } from '@/utils/helpers'
|
| 6 |
+
|
| 7 |
+
const Tabs = TabsPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const TabsList = React.forwardRef<
|
| 10 |
+
React.ElementRef<typeof TabsPrimitive.List>,
|
| 11 |
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
| 12 |
+
>(({ className, ...props }, ref) => (
|
| 13 |
+
<TabsPrimitive.List
|
| 14 |
+
ref={ref}
|
| 15 |
+
className={cn(
|
| 16 |
+
'inline-flex h-10 items-center justify-center rounded-md bg-gray-100 p-1 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
/>
|
| 21 |
+
))
|
| 22 |
+
TabsList.displayName = TabsPrimitive.List.displayName
|
| 23 |
+
|
| 24 |
+
const TabsTrigger = React.forwardRef<
|
| 25 |
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
| 26 |
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
| 27 |
+
>(({ className, ...props }, ref) => (
|
| 28 |
+
<TabsPrimitive.Trigger
|
| 29 |
+
ref={ref}
|
| 30 |
+
className={cn(
|
| 31 |
+
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm dark:data-[state=active]:bg-gray-900 dark:data-[state=active]:text-white',
|
| 32 |
+
className
|
| 33 |
+
)}
|
| 34 |
+
{...props}
|
| 35 |
+
/>
|
| 36 |
+
))
|
| 37 |
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
| 38 |
+
|
| 39 |
+
const TabsContent = React.forwardRef<
|
| 40 |
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
| 41 |
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
| 42 |
+
>(({ className, ...props }, ref) => (
|
| 43 |
+
<TabsPrimitive.Content
|
| 44 |
+
ref={ref}
|
| 45 |
+
className={cn(
|
| 46 |
+
'mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:ring-offset-gray-900',
|
| 47 |
+
className
|
| 48 |
+
)}
|
| 49 |
+
{...props}
|
| 50 |
+
/>
|
| 51 |
+
))
|
| 52 |
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
| 53 |
+
|
| 54 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
src/components/ui/index.ts
CHANGED
|
@@ -4,4 +4,5 @@ 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'
|
|
|
|
|
|
| 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'
|
| 8 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs'
|
src/hooks/useStaticConvolution.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useMemo } from 'react'
|
| 2 |
+
import { useAppStore } from '@/store'
|
| 3 |
+
import { computeConvolutionStep, getPixel } from '@/lib/convolution/engine'
|
| 4 |
+
import type { ImageDataExtended, ConvolutionStep, PaddingMode } from '@/types'
|
| 5 |
+
|
| 6 |
+
export function useStaticConvolution() {
|
| 7 |
+
const {
|
| 8 |
+
originalImageData,
|
| 9 |
+
selectedKernel,
|
| 10 |
+
customKernelMatrix,
|
| 11 |
+
customKernelNormalize,
|
| 12 |
+
settings,
|
| 13 |
+
} = useAppStore()
|
| 14 |
+
|
| 15 |
+
const kernel = selectedKernel?.matrix ?? customKernelMatrix
|
| 16 |
+
const normalize = selectedKernel?.normalize ?? customKernelNormalize
|
| 17 |
+
const padding = settings.padding
|
| 18 |
+
|
| 19 |
+
// Create extended image data for the engine
|
| 20 |
+
const extendedImage = useMemo((): ImageDataExtended | null => {
|
| 21 |
+
if (!originalImageData) return null
|
| 22 |
+
return {
|
| 23 |
+
data: originalImageData.data,
|
| 24 |
+
width: originalImageData.width,
|
| 25 |
+
height: originalImageData.height,
|
| 26 |
+
channels: 4,
|
| 27 |
+
}
|
| 28 |
+
}, [originalImageData])
|
| 29 |
+
|
| 30 |
+
const computeStep = useCallback(
|
| 31 |
+
(x: number, y: number, channel: number = 0): ConvolutionStep | null => {
|
| 32 |
+
if (!extendedImage || !kernel) return null
|
| 33 |
+
|
| 34 |
+
// Clamp coordinates to valid range
|
| 35 |
+
const clampedX = Math.max(0, Math.min(x, extendedImage.width - 1))
|
| 36 |
+
const clampedY = Math.max(0, Math.min(y, extendedImage.height - 1))
|
| 37 |
+
const clampedChannel = Math.max(0, Math.min(channel, extendedImage.channels - 1))
|
| 38 |
+
|
| 39 |
+
return computeConvolutionStep(extendedImage, kernel, padding, normalize, clampedX, clampedY, clampedChannel)
|
| 40 |
+
},
|
| 41 |
+
[extendedImage, kernel, padding, normalize]
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
const getPixelValue = useCallback(
|
| 45 |
+
(x: number, y: number, channel: number = 0): number => {
|
| 46 |
+
if (!extendedImage) return 0
|
| 47 |
+
const clampedX = Math.max(0, Math.min(x, extendedImage.width - 1))
|
| 48 |
+
const clampedY = Math.max(0, Math.min(y, extendedImage.height - 1))
|
| 49 |
+
const clampedChannel = Math.max(0, Math.min(channel, extendedImage.channels - 1))
|
| 50 |
+
return getPixel(extendedImage, clampedX, clampedY, clampedChannel, padding)
|
| 51 |
+
},
|
| 52 |
+
[extendedImage, padding]
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
const getPaddedImagePreview = useCallback(
|
| 56 |
+
(kernelSize: number): { width: number; height: number; data: number[] } | null => {
|
| 57 |
+
if (!extendedImage) return null
|
| 58 |
+
|
| 59 |
+
const halfKernel = Math.floor(kernelSize / 2)
|
| 60 |
+
const paddedWidth = extendedImage.width + 2 * halfKernel
|
| 61 |
+
const paddedHeight = extendedImage.height + 2 * halfKernel
|
| 62 |
+
const data: number[] = []
|
| 63 |
+
|
| 64 |
+
for (let py = 0; py < paddedHeight; py++) {
|
| 65 |
+
for (let px = 0; px < paddedWidth; px++) {
|
| 66 |
+
const origX = px - halfKernel
|
| 67 |
+
const origY = py - halfKernel
|
| 68 |
+
// Sample each channel
|
| 69 |
+
for (let c = 0; c < Math.min(3, extendedImage.channels); c++) {
|
| 70 |
+
data.push(getPixel(extendedImage, origX, origY, c, padding))
|
| 71 |
+
}
|
| 72 |
+
// Alpha channel
|
| 73 |
+
if (extendedImage.channels > 3) {
|
| 74 |
+
data.push(getPixel(extendedImage, origX, origY, 3, padding))
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return { width: paddedWidth, height: paddedHeight, data }
|
| 80 |
+
},
|
| 81 |
+
[extendedImage, padding]
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
return {
|
| 85 |
+
computeStep,
|
| 86 |
+
getPixelValue,
|
| 87 |
+
getPaddedImagePreview,
|
| 88 |
+
kernel,
|
| 89 |
+
normalize,
|
| 90 |
+
padding,
|
| 91 |
+
imageWidth: extendedImage?.width ?? 0,
|
| 92 |
+
imageHeight: extendedImage?.height ?? 0,
|
| 93 |
+
imageChannels: extendedImage?.channels ?? 0,
|
| 94 |
+
hasImage: !!extendedImage,
|
| 95 |
+
hasKernel: !!kernel,
|
| 96 |
+
}
|
| 97 |
+
}
|
src/lib/convolution/engine.ts
CHANGED
|
@@ -1,5 +1,68 @@
|
|
| 1 |
import { PaddingMode, ConvolutionStep, ImageDataExtended } from '@/types'
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export function getPixel(
|
| 4 |
image: ImageDataExtended,
|
| 5 |
x: number,
|
|
|
|
| 1 |
import { PaddingMode, ConvolutionStep, ImageDataExtended } from '@/types'
|
| 2 |
|
| 3 |
+
/**
|
| 4 |
+
* Computes a single convolution step for a specific pixel position.
|
| 5 |
+
* Useful for static visualization/inspection without running the full animation.
|
| 6 |
+
*/
|
| 7 |
+
export function computeConvolutionStep(
|
| 8 |
+
image: ImageDataExtended,
|
| 9 |
+
kernel: number[][],
|
| 10 |
+
padding: PaddingMode,
|
| 11 |
+
normalize: boolean,
|
| 12 |
+
x: number,
|
| 13 |
+
y: number,
|
| 14 |
+
channel: number
|
| 15 |
+
): ConvolutionStep {
|
| 16 |
+
const { width, height, channels } = image
|
| 17 |
+
const kernelSize = kernel.length
|
| 18 |
+
const halfKernel = Math.floor(kernelSize / 2)
|
| 19 |
+
|
| 20 |
+
const kernelSum = kernel.flat().reduce((a, b) => a + b, 0)
|
| 21 |
+
const normalizeFactor = normalize && kernelSum !== 0 ? 1 / kernelSum : 1
|
| 22 |
+
|
| 23 |
+
const inputPatch: number[][] = []
|
| 24 |
+
const multiplications: number[][] = []
|
| 25 |
+
let sum = 0
|
| 26 |
+
|
| 27 |
+
for (let ky = 0; ky < kernelSize; ky++) {
|
| 28 |
+
const patchRow: number[] = []
|
| 29 |
+
const multRow: number[] = []
|
| 30 |
+
for (let kx = 0; kx < kernelSize; kx++) {
|
| 31 |
+
const px = x + kx - halfKernel
|
| 32 |
+
const py = y + ky - halfKernel
|
| 33 |
+
const pixelValue = getPixel(image, px, py, channel, padding)
|
| 34 |
+
const kernelValue = kernel[ky][kx]
|
| 35 |
+
patchRow.push(pixelValue)
|
| 36 |
+
const mult = pixelValue * kernelValue
|
| 37 |
+
multRow.push(mult)
|
| 38 |
+
sum += mult
|
| 39 |
+
}
|
| 40 |
+
inputPatch.push(patchRow)
|
| 41 |
+
multiplications.push(multRow)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const outputValue = sum
|
| 45 |
+
const normalizedValue = sum * normalizeFactor
|
| 46 |
+
|
| 47 |
+
// Calculate progress (for consistency with generator)
|
| 48 |
+
const totalPixels = width * height * channels
|
| 49 |
+
const processedPixels = (y * width + x) * channels + channel + 1
|
| 50 |
+
const progress = processedPixels / totalPixels
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
x,
|
| 54 |
+
y,
|
| 55 |
+
channel,
|
| 56 |
+
inputPatch,
|
| 57 |
+
kernel,
|
| 58 |
+
multiplications,
|
| 59 |
+
sum: outputValue,
|
| 60 |
+
outputValue: normalizedValue,
|
| 61 |
+
normalizedValue,
|
| 62 |
+
progress,
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
export function getPixel(
|
| 67 |
image: ImageDataExtended,
|
| 68 |
x: number,
|
updates.md
CHANGED
|
@@ -74,4 +74,36 @@
|
|
| 74 |
- Convolution engine implements all 4 padding modes
|
| 75 |
- Generator-based step-by-step execution for educational visualization
|
| 76 |
- State management handles complex interactions between components
|
| 77 |
-
- Ready for Phase 2 optimization and Phase 3 visualization enhancements
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
- Convolution engine implements all 4 padding modes
|
| 75 |
- Generator-based step-by-step execution for educational visualization
|
| 76 |
- State management handles complex interactions between components
|
| 77 |
+
- Ready for Phase 2 optimization and Phase 3 visualization enhancements
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## Version 0.3.0 - Phase 2 Complete: Static Convolution Visualization (2026-08-06)
|
| 82 |
+
|
| 83 |
+
### Completed
|
| 84 |
+
- [x] Added `computeConvolutionStep()` function to convolution engine for single-pixel inspection
|
| 85 |
+
- [x] Created `useStaticConvolution` hook for static inspection capabilities
|
| 86 |
+
- [x] Built `StaticConvolutionView` component with 5 educational tabs:
|
| 87 |
+
- Overview: Input patch, kernel values, computation summary
|
| 88 |
+
- Image Patch: Detailed patch view at clicked position
|
| 89 |
+
- Multiplications: Element-wise pixel × kernel breakdown
|
| 90 |
+
- Summation: Step-by-step Σ(pixel × kernel) with normalization
|
| 91 |
+
- Padding: Visual demonstration of padding strategies at borders
|
| 92 |
+
- [x] Implemented click-to-inspect on main ConvolutionCanvas
|
| 93 |
+
- [x] Created reusable Tabs UI component with Radix UI
|
| 94 |
+
- [x] Integrated static inspector into right sidebar (replaces kernel info when active)
|
| 95 |
+
- [x] Added channel selector (Red/Green/Blue/Alpha) for per-channel inspection
|
| 96 |
+
- [x] All 4 padding modes (zero, reflect, replicate, wrap) working in static view
|
| 97 |
+
|
| 98 |
+
### Technical Achievements
|
| 99 |
+
- Build passes: `npm run build` ✓
|
| 100 |
+
- TypeScript passes: `npm run type-check` ✓
|
| 101 |
+
- Dev server runs without errors ✓
|
| 102 |
+
- Efficient single-pixel computation without full generator execution
|
| 103 |
+
- Reuses existing ConvolutionStep type for consistency
|
| 104 |
+
- Handles edge cases (clamped coordinates, invalid channels)
|
| 105 |
+
|
| 106 |
+
### In Progress
|
| 107 |
+
- [ ] Phase 3: Animation & Visualization
|
| 108 |
+
- [ ] Phase 4: Interactive Controls
|
| 109 |
+
- [ ] Phase 5: Comparison & Analysis
|