mihirmithani commited on
Commit
25e7739
·
1 Parent(s): a320533
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
- <KernelInfoPanel />
264
- <OverlayControls />
265
- <PaddingControls />
 
 
 
 
 
 
 
 
 
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