first commit
This commit is contained in:
commit
fa27a24bb6
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
210
bun.lock
Normal file
210
bun.lock
Normal file
@ -0,0 +1,210 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "chaos-tracker-online",
|
||||
"dependencies": {
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
"vue": "^3.5.21",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.6",
|
||||
"vue-tsc": "^3.0.7",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.29", "", {}, "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.2", "", { "os": "android", "cpu": "arm" }, "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.2", "", { "os": "android", "cpu": "arm64" }, "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.2", "", { "os": "linux", "cpu": "arm" }, "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.2", "", { "os": "linux", "cpu": "arm" }, "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.50.2", "", { "os": "linux", "cpu": "none" }, "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.2", "", { "os": "linux", "cpu": "none" }, "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.2", "", { "os": "linux", "cpu": "none" }, "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.2", "", { "os": "linux", "cpu": "x64" }, "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.2", "", { "os": "linux", "cpu": "x64" }, "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.2", "", { "os": "none", "cpu": "arm64" }, "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.2", "", { "os": "win32", "cpu": "x64" }, "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA=="],
|
||||
|
||||
"@techstark/opencv-js": ["@techstark/opencv-js@4.10.0-release.1", "", {}, "sha512-S4XELidRiQeA0q1s9VQLo540wCxUo24r1O4C+LqZ6llX+sPCXvZCPv3Ice8dEIr0uavyZ8YZeKXSBdDgMXSXjw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.29" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw=="],
|
||||
|
||||
"@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="],
|
||||
|
||||
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
|
||||
|
||||
"@volar/typescript": ["@volar/typescript@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.21", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/shared": "3.5.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.21", "", { "dependencies": { "@vue/compiler-core": "3.5.21", "@vue/shared": "3.5.21" } }, "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.21", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/compiler-core": "3.5.21", "@vue/compiler-dom": "3.5.21", "@vue/compiler-ssr": "3.5.21", "@vue/shared": "3.5.21", "estree-walker": "^2.0.2", "magic-string": "^0.30.18", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/shared": "3.5.21" } }, "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w=="],
|
||||
|
||||
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
|
||||
|
||||
"@vue/language-core": ["@vue/language-core@3.0.7", "", { "dependencies": { "@volar/language-core": "2.4.23", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^2.0.5", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-0sqqyqJ0Gn33JH3TdIsZLCZZ8Gr4kwlg8iYOnOrDDkJKSjFurlQY/bEFQx5zs7SX2C/bjMkmPYq/NiyY1fTOkw=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.21", "", { "dependencies": { "@vue/shared": "3.5.21" } }, "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.21", "", { "dependencies": { "@vue/reactivity": "3.5.21", "@vue/shared": "3.5.21" } }, "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.21", "", { "dependencies": { "@vue/reactivity": "3.5.21", "@vue/runtime-core": "3.5.21", "@vue/shared": "3.5.21", "csstype": "^3.1.3" } }, "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.21", "", { "dependencies": { "@vue/compiler-ssr": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "vue": "3.5.21" } }, "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.21", "", {}, "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw=="],
|
||||
|
||||
"@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="],
|
||||
|
||||
"alien-signals": ["alien-signals@2.0.7", "", {}, "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"rollup": ["rollup@4.50.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.2", "@rollup/rollup-android-arm64": "4.50.2", "@rollup/rollup-darwin-arm64": "4.50.2", "@rollup/rollup-darwin-x64": "4.50.2", "@rollup/rollup-freebsd-arm64": "4.50.2", "@rollup/rollup-freebsd-x64": "4.50.2", "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", "@rollup/rollup-linux-arm-musleabihf": "4.50.2", "@rollup/rollup-linux-arm64-gnu": "4.50.2", "@rollup/rollup-linux-arm64-musl": "4.50.2", "@rollup/rollup-linux-loong64-gnu": "4.50.2", "@rollup/rollup-linux-ppc64-gnu": "4.50.2", "@rollup/rollup-linux-riscv64-gnu": "4.50.2", "@rollup/rollup-linux-riscv64-musl": "4.50.2", "@rollup/rollup-linux-s390x-gnu": "4.50.2", "@rollup/rollup-linux-x64-gnu": "4.50.2", "@rollup/rollup-linux-x64-musl": "4.50.2", "@rollup/rollup-openharmony-arm64": "4.50.2", "@rollup/rollup-win32-arm64-msvc": "4.50.2", "@rollup/rollup-win32-ia32-msvc": "4.50.2", "@rollup/rollup-win32-x64-msvc": "4.50.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"vite": ["vite@7.1.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="],
|
||||
|
||||
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||
|
||||
"vue": ["vue@3.5.21", "", { "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", "@vue/runtime-dom": "3.5.21", "@vue/server-renderer": "3.5.21", "@vue/shared": "3.5.21" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA=="],
|
||||
|
||||
"vue-tsc": ["vue-tsc@3.0.7", "", { "dependencies": { "@volar/typescript": "2.4.23", "@vue/language-core": "3.0.7" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-BSMmW8GGEgHykrv7mRk6zfTdK+tw4MBZY/x6fFa7IkdXK3s/8hQRacPjG9/8YKFDIWGhBocwi6PlkQQ/93OgIQ=="],
|
||||
}
|
||||
}
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "chaos-tracker-online",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
"vue": "^3.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.6",
|
||||
"vue-tsc": "^3.0.7"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
396
src/App.vue
Normal file
396
src/App.vue
Normal file
@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="stage">
|
||||
<!-- 隐藏视频元素仅用于抓帧 -->
|
||||
<video
|
||||
ref="videoEl"
|
||||
playsinline
|
||||
muted
|
||||
autoplay
|
||||
class="hidden-video"
|
||||
></video>
|
||||
|
||||
<!-- 可见的绘制画布 -->
|
||||
<canvas ref="canvasEl" class="canvas"></canvas>
|
||||
|
||||
<!-- HUD 面板 -->
|
||||
<div class="hud">
|
||||
<div class="row">
|
||||
<button class="btn" @click="toggle()">
|
||||
{{ running ? '停止' : '启动' }}
|
||||
</button>
|
||||
<button class="btn" @click="switchFacing()">
|
||||
切换摄像头({{ facingModeLabel }})
|
||||
</button>
|
||||
</div>
|
||||
<div class="row small">
|
||||
<span>FPS: {{ fps.toFixed(1) }}</span>
|
||||
<span v-if="err" class="err">错误:{{ err }}</span>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="group">
|
||||
<h4>Green</h4>
|
||||
<div>x: {{ fmt(metrics.greenRectCenterX) }}, y: {{ fmt(metrics.greenRectCenterY) }}</div>
|
||||
<div>θ: {{ fmt(metrics.greenRectRotation) }}°</div>
|
||||
<div>L×W: {{ fmt(metrics.greenRectLength) }} × {{ fmt(metrics.greenRectWidth) }}</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<h4>Blue</h4>
|
||||
<div>x: {{ fmt(metrics.blueRectCenterX) }}, y: {{ fmt(metrics.blueRectCenterY) }}</div>
|
||||
<div>θ: {{ fmt(metrics.blueRectRotation) }}°</div>
|
||||
<div>L×W: {{ fmt(metrics.blueRectLength) }} × {{ fmt(metrics.blueRectWidth) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">
|
||||
提示:蓝色矩形基于红色/绿色关系构造;若未检测到所需目标,相关字段会为空(null),画面也不会绘制对应图形。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount, ref, reactive, computed } from 'vue'
|
||||
import cv from '@techstark/opencv-js'
|
||||
import { processImage } from './lib/processImage' // ← 根据你的实际路径调整
|
||||
|
||||
type Metrics = ReturnType<typeof processImage>
|
||||
|
||||
// 画布/视频引用
|
||||
const videoEl = ref<HTMLVideoElement | null>(null)
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null)
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
|
||||
// 状态
|
||||
const running = ref(false)
|
||||
const fps = ref(0)
|
||||
const err = ref<string | null>(null)
|
||||
// OpenCV 就绪标记:仅在完成 WASM 初始化后才进行图像处理
|
||||
const cvReady = ref(false)
|
||||
const metrics = reactive<Metrics>({
|
||||
greenRectCenterX: null,
|
||||
greenRectCenterY: null,
|
||||
blueRectCenterX: null,
|
||||
blueRectCenterY: null,
|
||||
greenRectRotation: null,
|
||||
blueRectRotation: null,
|
||||
greenRectLength: null,
|
||||
blueRectLength: null,
|
||||
greenRectWidth: null,
|
||||
blueRectWidth: null
|
||||
})
|
||||
|
||||
// 摄像头方向
|
||||
const facingMode = ref<'environment' | 'user'>('environment')
|
||||
const facingModeLabel = computed(() =>
|
||||
facingMode.value === 'environment' ? '后置' : '前置'
|
||||
)
|
||||
|
||||
// RAF 相关
|
||||
let rafId = 0
|
||||
let lastT = 0
|
||||
|
||||
// 资源引用
|
||||
let stream: MediaStream | null = null
|
||||
|
||||
function fmt(n: number | null) {
|
||||
return n == null ? '—' : n.toFixed(1)
|
||||
}
|
||||
|
||||
function drawRotRect(
|
||||
g: CanvasRenderingContext2D,
|
||||
cx: number,
|
||||
cy: number,
|
||||
longLen: number,
|
||||
shortLen: number,
|
||||
angleDeg: number,
|
||||
color: string
|
||||
) {
|
||||
const rad = (angleDeg * Math.PI) / 180
|
||||
const dir = { x: Math.cos(rad), y: Math.sin(rad) }
|
||||
const perp = { x: -dir.y, y: dir.x }
|
||||
const hl = longLen / 2
|
||||
const hw = shortLen / 2
|
||||
|
||||
const p0 = { x: cx - dir.x * hl - perp.x * hw, y: cy - dir.y * hl - perp.y * hw }
|
||||
const p1 = { x: cx + dir.x * hl - perp.x * hw, y: cy + dir.y * hl - perp.y * hw }
|
||||
const p2 = { x: cx + dir.x * hl + perp.x * hw, y: cy + dir.y * hl + perp.y * hw }
|
||||
const p3 = { x: cx - dir.x * hl + perp.x * hw, y: cy - dir.y * hl + perp.y * hw }
|
||||
|
||||
g.save()
|
||||
g.lineWidth = 2
|
||||
g.strokeStyle = color
|
||||
g.beginPath()
|
||||
g.moveTo(p0.x, p0.y)
|
||||
g.lineTo(p1.x, p1.y)
|
||||
g.lineTo(p2.x, p2.y)
|
||||
g.lineTo(p3.x, p3.y)
|
||||
g.closePath()
|
||||
g.stroke()
|
||||
|
||||
// 中心点 + 朝向线
|
||||
g.fillStyle = color
|
||||
g.beginPath()
|
||||
g.arc(cx, cy, 4, 0, Math.PI * 2)
|
||||
g.fill()
|
||||
g.beginPath()
|
||||
g.moveTo(cx, cy)
|
||||
g.lineTo(cx + dir.x * (hl * 0.8), cy + dir.y * (hl * 0.8))
|
||||
g.stroke()
|
||||
g.restore()
|
||||
}
|
||||
|
||||
function clearCanvas(g: CanvasRenderingContext2D, w: number, h: number) {
|
||||
g.clearRect(0, 0, w, h)
|
||||
}
|
||||
|
||||
function resizeCanvasToVideo(video: HTMLVideoElement, canvas: HTMLCanvasElement) {
|
||||
const vw = video.videoWidth || 640
|
||||
const vh = video.videoHeight || 480
|
||||
// 实际像素尺寸
|
||||
canvas.width = vw
|
||||
canvas.height = vh
|
||||
// CSS 自适应
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = 'auto'
|
||||
}
|
||||
|
||||
function copyMetrics(next: Metrics) {
|
||||
;(Object.keys(metrics) as (keyof Metrics)[]).forEach((k) => {
|
||||
metrics[k] = next[k] as any
|
||||
})
|
||||
}
|
||||
|
||||
async function ensureCVReady(): Promise<void> {
|
||||
// @techstark/opencv-js 在 WASM 初始化完成后可调用 getBuildInformation
|
||||
try {
|
||||
if ((cv as any)?.getBuildInformation) {
|
||||
cvReady.value = true
|
||||
return
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
;(cv as any).onRuntimeInitialized = () => {
|
||||
cvReady.value = true
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// 忽略异常,让后续帧内判断 cvReady 决定是否处理
|
||||
}
|
||||
}
|
||||
|
||||
async function startStream() {
|
||||
try {
|
||||
err.value = null
|
||||
// 关闭旧流
|
||||
await stopStream()
|
||||
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: facingMode.value }, width: { ideal: 1280 }, height: { ideal: 720 } },
|
||||
audio: false
|
||||
})
|
||||
if (!videoEl.value) return
|
||||
videoEl.value.srcObject = stream
|
||||
|
||||
// 等待元数据并尝试播放(兼容 Safari/部分浏览器自动播放策略)
|
||||
await new Promise<void>((res) => {
|
||||
if (!videoEl.value) return res()
|
||||
const v = videoEl.value
|
||||
if (v.readyState >= 1) return res()
|
||||
const onMeta = () => {
|
||||
v.removeEventListener('loadedmetadata', onMeta)
|
||||
res()
|
||||
}
|
||||
v.addEventListener('loadedmetadata', onMeta)
|
||||
})
|
||||
try {
|
||||
await videoEl.value.play()
|
||||
} catch {
|
||||
// 若被策略拦截,依赖用户点击“启动”后再次尝试
|
||||
}
|
||||
|
||||
if (!canvasEl.value || !videoEl.value) return
|
||||
ctx = canvasEl.value.getContext('2d')
|
||||
if (!ctx) throw new Error('CanvasRenderingContext2D 获取失败')
|
||||
resizeCanvasToVideo(videoEl.value, canvasEl.value)
|
||||
|
||||
running.value = true
|
||||
lastT = performance.now()
|
||||
loop()
|
||||
} catch (e: any) {
|
||||
err.value = e?.message ?? String(e)
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stopStream() {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = 0
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((t) => t.stop())
|
||||
stream = null
|
||||
}
|
||||
running.value = false
|
||||
}
|
||||
|
||||
function loop() {
|
||||
if (!running.value || !videoEl.value || !canvasEl.value || !ctx) return
|
||||
|
||||
const now = performance.now()
|
||||
const dt = now - lastT
|
||||
lastT = now
|
||||
fps.value = 1000 / (dt || 16.7)
|
||||
|
||||
// 将视频帧绘制到画布
|
||||
const vw = canvasEl.value.width
|
||||
const vh = canvasEl.value.height
|
||||
ctx.drawImage(videoEl.value, 0, 0, vw, vh)
|
||||
|
||||
try {
|
||||
// 取像素、识别
|
||||
const imgData = ctx.getImageData(0, 0, vw, vh)
|
||||
if (cvReady.value) {
|
||||
const res = processImage(imgData)
|
||||
copyMetrics(res)
|
||||
}
|
||||
|
||||
// 覆盖绘制识别结果
|
||||
if (metrics.greenRectCenterX != null && metrics.greenRectCenterY != null && metrics.greenRectLength && metrics.greenRectWidth && metrics.greenRectRotation != null) {
|
||||
drawRotRect(
|
||||
ctx,
|
||||
metrics.greenRectCenterX,
|
||||
metrics.greenRectCenterY,
|
||||
metrics.greenRectLength,
|
||||
metrics.greenRectWidth,
|
||||
metrics.greenRectRotation,
|
||||
'#00ff6a'
|
||||
)
|
||||
}
|
||||
if (metrics.blueRectCenterX != null && metrics.blueRectCenterY != null && metrics.blueRectLength && metrics.blueRectWidth && metrics.blueRectRotation != null) {
|
||||
drawRotRect(
|
||||
ctx,
|
||||
metrics.blueRectCenterX,
|
||||
metrics.blueRectCenterY,
|
||||
metrics.blueRectLength,
|
||||
metrics.blueRectWidth,
|
||||
metrics.blueRectRotation,
|
||||
'#2e6bff'
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 单帧异常不中断,显示在 HUD
|
||||
err.value = e?.message ?? String(e)
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
if (running.value) {
|
||||
await stopStream()
|
||||
} else {
|
||||
// 不阻塞摄像头启动,让 CV 就绪在后台进行
|
||||
ensureCVReady()
|
||||
await startStream()
|
||||
}
|
||||
}
|
||||
|
||||
async function switchFacing() {
|
||||
facingMode.value = facingMode.value === 'environment' ? 'user' : 'environment'
|
||||
if (running.value) {
|
||||
await startStream()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 后台等待 CV 就绪
|
||||
ensureCVReady()
|
||||
// 自动启动(可改为手动)
|
||||
await startStream()
|
||||
})
|
||||
|
||||
onBeforeUnmount(async () => {
|
||||
await stopStream()
|
||||
ctx = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.stage {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #0b0f19;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.hidden-video {
|
||||
display: none; /* 仅用于抓帧即可 */
|
||||
}
|
||||
.canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
.hud {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
background: rgba(0,0,0,0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
color: #e9eef7;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
min-width: 220px;
|
||||
}
|
||||
.hud .row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.hud .row.small {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.metrics .group h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.err { color: #ffb4b4; }
|
||||
.btn {
|
||||
appearance: none;
|
||||
background: #1a5cff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn:hover { filter: brightness(1.05); }
|
||||
.hint {
|
||||
color: #5f6b7b;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
41
src/components/HelloWorld.vue
Normal file
41
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
292
src/lib/processImage.ts
Normal file
292
src/lib/processImage.ts
Normal file
@ -0,0 +1,292 @@
|
||||
// tsconfig tip: set "skipLibCheck": true if your bundler yells about opencv.js types.
|
||||
import cv from "@techstark/opencv-js";
|
||||
|
||||
export type ProcessImageResult = {
|
||||
greenRectCenterX: number | null;
|
||||
greenRectCenterY: number | null;
|
||||
blueRectCenterX: number | null;
|
||||
blueRectCenterY: number | null;
|
||||
greenRectRotation: number | null; // degrees in [0,180)
|
||||
blueRectRotation: number | null; // degrees in [0,180)
|
||||
greenRectLength: number | null; // long side
|
||||
blueRectLength: number | null; // long side (ray length)
|
||||
greenRectWidth: number | null; // short side
|
||||
blueRectWidth: number | null; // short side = red short side
|
||||
};
|
||||
|
||||
// Geometry helpers use fixed-length tuples to satisfy TS with noUncheckedIndexedAccess
|
||||
type Vec2 = [number, number];
|
||||
type Box4 = [Vec2, Vec2, Vec2, Vec2];
|
||||
|
||||
type RectProps = {
|
||||
box: Box4; // 4 points [x,y]
|
||||
center: Vec2; // [x,y]
|
||||
longLen: number;
|
||||
shortLen: number;
|
||||
dirLong: Vec2; // unit vector along long edge
|
||||
};
|
||||
|
||||
export function processImage(imgData: ImageData): ProcessImageResult {
|
||||
const src = cv.matFromImageData(imgData); // CV_8UC4 RGBA
|
||||
const toFree: cv.Mat[] = [src];
|
||||
|
||||
const rgb = new cv.Mat(); toFree.push(rgb);
|
||||
cv.cvtColor(src, rgb, cv.COLOR_RGBA2RGB);
|
||||
const hsv = new cv.Mat(); toFree.push(hsv);
|
||||
cv.cvtColor(rgb, hsv, cv.COLOR_RGB2HSV);
|
||||
const gray = new cv.Mat(); toFree.push(gray);
|
||||
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
|
||||
|
||||
// ---- color masks (HSV) ----
|
||||
const kernel3 = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(3, 3));
|
||||
// red = two ranges
|
||||
const redMask = (() => {
|
||||
const low1 = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [0, 80, 80, 0]);
|
||||
const up1 = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [5, 255, 255, 0]);
|
||||
const low2 = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [175, 80, 80, 0]);
|
||||
const up2 = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [179, 255, 255, 0]);
|
||||
const m1 = new cv.Mat(); const m2 = new cv.Mat();
|
||||
cv.inRange(hsv, low1, up1, m1);
|
||||
cv.inRange(hsv, low2, up2, m2);
|
||||
const rm = new cv.Mat();
|
||||
cv.bitwise_or(m1, m2, rm);
|
||||
// morphology open -> close
|
||||
cv.morphologyEx(rm, rm, cv.MORPH_OPEN, kernel3);
|
||||
cv.morphologyEx(rm, rm, cv.MORPH_CLOSE, kernel3);
|
||||
low1.delete(); up1.delete(); low2.delete(); up2.delete(); m1.delete(); m2.delete();
|
||||
toFree.push(rm);
|
||||
return rm;
|
||||
})();
|
||||
|
||||
const grnMask = (() => {
|
||||
const low = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [35, 80, 80, 0]);
|
||||
const up = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [85, 255, 255, 0]);
|
||||
const gm = new cv.Mat();
|
||||
cv.inRange(hsv, low, up, gm);
|
||||
cv.morphologyEx(gm, gm, cv.MORPH_OPEN, kernel3);
|
||||
cv.morphologyEx(gm, gm, cv.MORPH_CLOSE, kernel3);
|
||||
low.delete(); up.delete();
|
||||
toFree.push(gm);
|
||||
return gm;
|
||||
})();
|
||||
|
||||
// ---- detect largest rect (by area) within mask ----
|
||||
const detectLargestBox = (graySrc: cv.Mat, mask: cv.Mat): RectProps | null => {
|
||||
const masked = new cv.Mat(); toFree.push(masked);
|
||||
cv.bitwise_and(graySrc, graySrc, masked, mask);
|
||||
|
||||
const blur = new cv.Mat(); toFree.push(blur);
|
||||
cv.GaussianBlur(masked, blur, new cv.Size(0, 0), 3.0, 3.0, cv.BORDER_DEFAULT);
|
||||
|
||||
const high = new cv.Mat(); toFree.push(high);
|
||||
// unsharp: (1+alpha)*I + (-alpha)*blur
|
||||
cv.addWeighted(masked, 3.0 /*1+2*/, blur, -2.0, 0, high);
|
||||
|
||||
const edges = new cv.Mat(); toFree.push(edges);
|
||||
cv.Canny(high, edges, 40, 120, 3, true);
|
||||
|
||||
const edgesD = new cv.Mat(); toFree.push(edgesD);
|
||||
cv.dilate(edges, edgesD, kernel3);
|
||||
|
||||
const contours = new cv.MatVector(); const hierarchy = new cv.Mat();
|
||||
cv.findContours(edgesD, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
let bestArea = 0;
|
||||
let bestRect: any = null; // rotated rect (object)
|
||||
for (let i = 0; i < contours.size(); i++) {
|
||||
const cnt = contours.get(i);
|
||||
const area = cv.contourArea(cnt);
|
||||
if (area > 300 && area > bestArea) {
|
||||
bestArea = area;
|
||||
bestRect = cv.minAreaRect(cnt); // {center:{x,y}, size:{width,height}, angle:number}
|
||||
}
|
||||
cnt.delete();
|
||||
}
|
||||
contours.delete(); hierarchy.delete();
|
||||
|
||||
if (!bestRect) return null;
|
||||
|
||||
const box = rotatedRectToPoints(bestRect); // Float32Array[2] * 4
|
||||
const props = rectPropsFromBox(box);
|
||||
return props;
|
||||
};
|
||||
|
||||
const redProps = detectLargestBox(gray, redMask);
|
||||
const grnProps = detectLargestBox(gray, grnMask);
|
||||
|
||||
// ---- build blue rectangle from rules ----
|
||||
let blueCenter: Vec2 | null = null;
|
||||
let blueDir: Vec2 | null = null;
|
||||
let blueLen: number | null = null;
|
||||
let blueW: number | null = null;
|
||||
|
||||
if (redProps && grnProps) {
|
||||
const rBox = orderBoxClockwise(redProps.box);
|
||||
const gCenter = grnProps.center;
|
||||
|
||||
// midpoints of 4 edges (typed as Box4 to avoid undefined indexes)
|
||||
const [r0, r1, r2, r3] = rBox;
|
||||
const mids: Box4 = [
|
||||
midpoint(r0, r1),
|
||||
midpoint(r1, r2),
|
||||
midpoint(r2, r3),
|
||||
midpoint(r3, r0)
|
||||
];
|
||||
|
||||
// farthest midpoint from green center
|
||||
let farIdx = 0, maxD2 = -1;
|
||||
mids.forEach((m, i) => {
|
||||
const dx = m[0] - gCenter[0];
|
||||
const dy = m[1] - gCenter[1];
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 > maxD2) { maxD2 = d2; farIdx = i; }
|
||||
});
|
||||
const [m0, m1, m2, m3] = mids;
|
||||
const startPt: Vec2 = farIdx === 0 ? m0 : farIdx === 1 ? m1 : farIdx === 2 ? m2 : m3;
|
||||
|
||||
// long direction of red; point toward projection to green center
|
||||
const baseDir = redProps.dirLong;
|
||||
let dir: Vec2 = [baseDir[0], baseDir[1]];
|
||||
const toG: Vec2 = v2(gCenter[0] - startPt[0], gCenter[1] - startPt[1]);
|
||||
if (dot(toG, dir) < 0) dir = scale(dir, -1);
|
||||
|
||||
const proj = dot(toG, dir); // scalar projection
|
||||
if (proj > 1.0) {
|
||||
blueLen = proj;
|
||||
blueW = redProps.shortLen;
|
||||
const bb = blueBoxFromRay(startPt, dir, blueLen, blueW);
|
||||
blueCenter = bb.center;
|
||||
blueDir = dir;
|
||||
// const blueBox = bb.box; // (available if you later want corners)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- assemble metrics ----
|
||||
const res: ProcessImageResult = {
|
||||
greenRectCenterX: grnProps ? grnProps.center[0] : null,
|
||||
greenRectCenterY: grnProps ? grnProps.center[1] : null,
|
||||
blueRectCenterX: blueCenter ? blueCenter[0] : null,
|
||||
blueRectCenterY: blueCenter ? blueCenter[1] : null,
|
||||
greenRectRotation: grnProps ? angleDegFromDir(grnProps.dirLong) : null,
|
||||
blueRectRotation: blueDir ? angleDegFromDir(blueDir) : null,
|
||||
greenRectLength: grnProps ? grnProps.longLen : null,
|
||||
blueRectLength: blueLen,
|
||||
greenRectWidth: grnProps ? grnProps.shortLen : null,
|
||||
blueRectWidth: blueW
|
||||
};
|
||||
|
||||
// ---- cleanup ----
|
||||
try {
|
||||
// nothing else
|
||||
} finally {
|
||||
kernel3.delete();
|
||||
toFree.forEach(m => { try { m.delete(); } catch { /* noop */ } });
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/* ----------------------- helpers ----------------------- */
|
||||
|
||||
function v2(x: number, y: number): Vec2 { return [x, y]; }
|
||||
function f32(arr: readonly number[]): Vec2 { return [arr[0] ?? 0, arr[1] ?? 0]; }
|
||||
|
||||
function dot(a: Vec2, b: Vec2): number { return a[0] * b[0] + a[1] * b[1]; }
|
||||
function norm(a: Vec2): number { return Math.hypot(a[0], a[1]); }
|
||||
function normalize(a: Vec2): Vec2 {
|
||||
const n = norm(a) + 1e-9;
|
||||
return f32([a[0]/n, a[1]/n]);
|
||||
}
|
||||
function scale(a: Vec2, k: number): Vec2 { return f32([a[0]*k, a[1]*k]); }
|
||||
function add(a: Vec2, b: Vec2): Vec2 { return f32([a[0]+b[0], a[1]+b[1]]); }
|
||||
function sub(a: Vec2, b: Vec2): Vec2 { return f32([a[0]-b[0], a[1]-b[1]]); }
|
||||
|
||||
function midpoint(a: Vec2, b: Vec2): Vec2 {
|
||||
return f32([(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5]);
|
||||
}
|
||||
|
||||
function angleDegFromDir(d: Vec2): number {
|
||||
let ang = Math.atan2(d[1], d[0]) * 180 / Math.PI;
|
||||
ang = (ang + 360) % 180; // [0,180)
|
||||
return ang;
|
||||
}
|
||||
|
||||
function orderBoxClockwise(box: Box4): Box4 {
|
||||
const c = centroid(box);
|
||||
const withAng = box.map(p => ({ p, a: Math.atan2(p[1] - c[1], p[0] - c[0]) }));
|
||||
withAng.sort((u, v) => u.a - v.a); // -pi..pi
|
||||
const sorted = withAng.map(o => o.p) as Vec2[];
|
||||
// ensure Box4 length 4 (non-null assertion since input is Box4)
|
||||
return [sorted[0]!, sorted[1]!, sorted[2]!, sorted[3]!];
|
||||
}
|
||||
|
||||
function centroid(pts: ReadonlyArray<Vec2>): Vec2 {
|
||||
let x = 0, y = 0;
|
||||
for (const p of pts) { x += p[0]; y += p[1]; }
|
||||
return f32([x / pts.length, y / pts.length]);
|
||||
}
|
||||
|
||||
function edgeLengths(box: Box4): number[] {
|
||||
const [p0, p1, p2, p3] = box;
|
||||
return [
|
||||
Math.hypot(p1[0] - p0[0], p1[1] - p0[1]),
|
||||
Math.hypot(p2[0] - p1[0], p2[1] - p1[1]),
|
||||
Math.hypot(p3[0] - p2[0], p3[1] - p2[1]),
|
||||
Math.hypot(p0[0] - p3[0], p0[1] - p3[1])
|
||||
];
|
||||
}
|
||||
|
||||
function rectPropsFromBox(boxIn: Box4): RectProps {
|
||||
const box = orderBoxClockwise(boxIn);
|
||||
const center = centroid(box);
|
||||
const L = edgeLengths(box);
|
||||
const longLen = Math.max(...L);
|
||||
const shortLen = Math.min(...L);
|
||||
const idx = L.indexOf(longLen) as 0 | 1 | 2 | 3;
|
||||
const [p0, p1, p2, p3] = box;
|
||||
let a: Vec2, b: Vec2;
|
||||
switch (idx) {
|
||||
case 0: a = p0; b = p1; break;
|
||||
case 1: a = p1; b = p2; break;
|
||||
case 2: a = p2; b = p3; break;
|
||||
case 3: a = p3; b = p0; break;
|
||||
}
|
||||
const dirLong = normalize(sub(b, a));
|
||||
return { box, center, longLen, shortLen, dirLong };
|
||||
}
|
||||
|
||||
function rotatedRectToPoints(rr: { center: { x: number, y: number }, size: { width: number, height: number }, angle: number }): Box4 {
|
||||
const cx = rr.center.x, cy = rr.center.y;
|
||||
const w = rr.size.width, h = rr.size.height;
|
||||
const ang = rr.angle * Math.PI / 180.0;
|
||||
const cosA = Math.cos(ang), sinA = Math.sin(ang);
|
||||
const hw = w * 0.5, hh = h * 0.5;
|
||||
|
||||
const local: [number, number][] = [
|
||||
[-hw, -hh], [ hw, -hh],
|
||||
[ hw, hh], [-hw, hh]
|
||||
];
|
||||
|
||||
const pts: Vec2[] = [];
|
||||
for (const [lx, ly] of local) {
|
||||
const x = cx + lx * cosA - ly * sinA;
|
||||
const y = cy + lx * sinA + ly * cosA;
|
||||
pts.push(f32([x, y]));
|
||||
}
|
||||
return [pts[0]!, pts[1]!, pts[2]!, pts[3]!];
|
||||
}
|
||||
|
||||
function blueBoxFromRay(startPt: Vec2, dirUnitIn: Vec2, length: number, width: number): { box: Box4, center: Vec2 } {
|
||||
const dir = normalize(dirUnitIn);
|
||||
const perp = f32([-dir[1], dir[0]]);
|
||||
const c = add(startPt, scale(dir, length * 0.5));
|
||||
const halfL = scale(dir, length * 0.5);
|
||||
const halfW = scale(perp, width * 0.5);
|
||||
|
||||
const p0 = sub(sub(c, halfL), halfW);
|
||||
const p1 = add(sub(c, halfW), halfL);
|
||||
const p2 = add(add(c, halfL), halfW);
|
||||
const p3 = add(sub(c, halfW), scale(halfL, -1));
|
||||
|
||||
return { box: [p0, p1, p2, p3], center: c };
|
||||
}
|
||||
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
0
src/style.css
Normal file
0
src/style.css
Normal file
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
15
tsconfig.app.json
Normal file
15
tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user