From 4255c8cb878200311334579879c80461aaa79097 Mon Sep 17 00:00:00 2001 From: Terncode Date: Wed, 12 Feb 2025 20:21:33 +0100 Subject: [PATCH] Add pixel statistics --- package-lock.json | 610 +++++++++++++++++++++++++++++- package.json | 1 + src/UI/components/main.tsx | 11 +- src/UI/components/menu.tsx | 28 +- src/UI/components/pixelCharts.tsx | 133 +++++++ src/UI/components/pixelCount.tsx | 17 +- src/UI/importMural.tsx | 2 +- src/index.ts | 4 +- src/inject.ts | 113 +++++- src/lib/interceptor.ts | 86 +++++ src/lib/pixelPlaced.ts | 43 ++- src/lib/store.ts | 137 ++++++- 12 files changed, 1127 insertions(+), 58 deletions(-) create mode 100644 src/UI/components/pixelCharts.tsx create mode 100644 src/lib/interceptor.ts diff --git a/package-lock.json b/package-lock.json index 1ff2370..00c4e39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "process": "^0.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", + "recharts": "^2.15.1", "rgbquant": "^1.1.2", "stream": "^0.0.3", "stream-browserify": "^3.0.0", @@ -50,6 +51,18 @@ "node": ">=0.10.0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", @@ -601,6 +614,69 @@ "@types/har-format": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/file-saver": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", @@ -1287,6 +1363,15 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1363,8 +1448,128 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/debug": { "version": "4.3.4", @@ -1383,6 +1588,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1447,6 +1658,16 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/es-abstract": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", @@ -1831,12 +2052,27 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2320,6 +2556,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -3233,6 +3478,37 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3258,6 +3534,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", @@ -3279,6 +3593,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -3730,6 +4050,12 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3889,6 +4215,28 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4012,6 +4360,14 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@babel/runtime": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", @@ -4299,6 +4655,60 @@ "@types/har-format": "*" } }, + "@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "requires": { + "@types/d3-color": "*" + } + }, + "@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "requires": { + "@types/d3-path": "*" + } + }, + "@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "@types/file-saver": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", @@ -4765,6 +5175,11 @@ } } }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4825,8 +5240,84 @@ "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "requires": { + "d3-path": "^3.1.0" + } + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" }, "debug": { "version": "4.3.4", @@ -4837,6 +5328,11 @@ "ms": "2.1.2" } }, + "decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4883,6 +5379,15 @@ "esutils": "^2.0.2" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "es-abstract": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", @@ -5181,12 +5686,22 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==" + }, "fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -5529,6 +6044,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -6164,6 +6684,27 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "requires": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -6182,6 +6723,36 @@ "picomatch": "^2.2.1" } }, + "recharts": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "requires": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + } + } + }, + "recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "requires": { + "decimal.js-light": "^2.4.1" + } + }, "reflect.getprototypeof": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", @@ -6197,6 +6768,11 @@ "which-builtin-type": "^1.1.3" } }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -6507,6 +7083,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6620,6 +7201,27 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "requires": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index bf312da..8d8a2f6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "process": "^0.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", + "recharts": "^2.15.1", "rgbquant": "^1.1.2", "stream": "^0.0.3", "stream-browserify": "^3.0.0", diff --git a/src/UI/components/main.tsx b/src/UI/components/main.tsx index ed82120..13bd4c4 100644 --- a/src/UI/components/main.tsx +++ b/src/UI/components/main.tsx @@ -11,6 +11,7 @@ import { debounce } from "lodash"; import { TC_LOGO } from "../../constants"; import styled from "styled-components"; import { MuralEx } from "../../interfaces"; +import { PieCharts } from "./pixelCharts"; const Img = styled.img` width: 25px; @@ -35,6 +36,7 @@ interface Props { interface State extends StoreSettings{ selected?: MuralEx; overlays: number[]; + showCharts: boolean; phantomOverlay: number; overlayModify?: OverlayReturn; } @@ -46,6 +48,7 @@ export class Main extends React.Component { constructor(props: Props) { super(props); this.state = { + showCharts: false, overlays: [], phantomOverlay: -1, opacity: 50, @@ -106,8 +109,14 @@ export class Main extends React.Component { render() { return <> - + {this.state.showCharts ? + + + + : null} + this.setState({showCharts: !this.state.showCharts})} onOpacityChange={this.opacityChange} cords={this.props.cords} store={this.props.store} diff --git a/src/UI/components/menu.tsx b/src/UI/components/menu.tsx index 9af37c6..9f4c543 100644 --- a/src/UI/components/menu.tsx +++ b/src/UI/components/menu.tsx @@ -1,4 +1,4 @@ -import { faCamera, faCaretDown, faCaretUp, faUpload } from "@fortawesome/free-solid-svg-icons"; +import { faCamera, faCaretDown, faCaretUp, faPieChart, faUpload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import styled from "styled-components"; @@ -76,6 +76,7 @@ interface Props { storage: Storage; opacity: number; collapsed: boolean; + showChart: () => void; onOpacityChange: (n: number) => void; onCollapsedChanged: (b: boolean) => void; } @@ -130,13 +131,30 @@ export class Menu extends React.Component { this.setState({takingCanvasShot: false}); }; render() { - return + return - - + + + + + + + this.props.showChart()} + title="Pixels graph"> + + {this.props.opacity}% - this.props.onCollapsedChanged(!this.props.collapsed)}> + this.props.onCollapsedChanged(!this.props.collapsed)}> + + {this.props.collapsed ? null : } ; diff --git a/src/UI/components/pixelCharts.tsx b/src/UI/components/pixelCharts.tsx new file mode 100644 index 0000000..57acc10 --- /dev/null +++ b/src/UI/components/pixelCharts.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import { Palette } from "../../lib/palette"; +import { Store, StoreEvents } from "../../lib/store"; +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import styled from "styled-components"; +import { formatNumber } from "../../lib/utils"; + +const ColorBlock = styled.div` + width: 20px; + height: 20px; + margin-right: 5px; + border: 1px solid black; + border-radius: 12px; +`; + +const ColorLine = styled.div` + padding: 4px; + display: flex; + flex-direction: row; + font-size: 15px; +`; +const FlexList = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + height: 100%; +`; +interface PixelData { + color: string; + value: number; +} + +interface State { + total: number; + groupData?: PixelData[]; +} + +interface Props { + store: Store; + palette: Palette; +} + +export class PieCharts extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { + total: 0, + }; + } + + componentDidMount () { + this.refresh(); + this.props.store.on(StoreEvents.PixelLog, this.refresh); + } + componentWillUnmount() { + this.props.store.off(StoreEvents.PixelLog, this.refresh); + } + private refresh = async () => { + const colors: string[] = []; + const groupData: PixelData[] = []; + let total = 0; + for (let i = 0; i < this.props.palette.hex.length; i++) { + const hex = this.props.palette.hex[i]; + colors.push(hex); + const logs = await this.props.store.fetchPixelsLogByColor(i); + if (logs.length) { + total += logs.length; + groupData.push({ + color: hex, + value: logs.length + }); + } + } + groupData.sort((a,b) => a.value > b.value ? -1 : 1); + this.setState(({ + groupData, + total + })); + }; + render(): React.ReactNode + { + if (!this.state.groupData ) { + return
Loading
; + } + + if (!this.state.groupData.length) { + return
No data! Please a pixel to see pixel chart
; + } + const style:React.CSSProperties = { + width: Math.min(250, window.innerWidth - 20), + height: Math.min(250, window.innerHeight - 20) + }; + + return
+
Total pixels: {formatNumber(this.state.total)}
+
+ + + + {this.state.groupData.map((entry, index) => ( + + ))} + + + + +
+
+ + {this.state.groupData.map((e, i) => { + return + {formatNumber(e.value)}; + })} + + +
+
; + } +} \ No newline at end of file diff --git a/src/UI/components/pixelCount.tsx b/src/UI/components/pixelCount.tsx index 9729137..9d040ae 100644 --- a/src/UI/components/pixelCount.tsx +++ b/src/UI/components/pixelCount.tsx @@ -4,6 +4,7 @@ import styled from "styled-components"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSquare } from "@fortawesome/free-solid-svg-icons"; import { formatNumber } from "../../lib/utils"; +import { StoreEvents } from "../../lib/store"; const CountBox = styled.div<{width: number}>` position: fixed; @@ -48,20 +49,16 @@ export class PixelCount extends React.Component { }; } componentDidMount() { - this.props.pixelCount.count.then(countMaybe => { - if (typeof countMaybe === "number") { - this.updateCount(countMaybe); - } - }); - this.props.pixelCount.on(this.updateCount); - window.addEventListener("resize", this.onResize); + this.updateCount(); + this.props.pixelCount.store.on(StoreEvents.PixelLog, this.updateCount); + window.addEventListener("resize", this.onResize); } componentWillUnmount() { - this.props.pixelCount.on(this.updateCount); + this.props.pixelCount.store.off(StoreEvents.PixelLog, this.updateCount); window.removeEventListener("resize", this.onResize); } - updateCount = (count: number) => { - this.setState({count}); + updateCount = async () => { + this.setState({count: await this.props.pixelCount.getCount()}); }; onResize = () => { this.setState({ width: window.innerWidth }); diff --git a/src/UI/importMural.tsx b/src/UI/importMural.tsx index 3cc2320..da30504 100644 --- a/src/UI/importMural.tsx +++ b/src/UI/importMural.tsx @@ -98,7 +98,7 @@ export async function importArtWorks( const pixels = await imageToMural(img, palette); const mural = await new Promise((resolve, reject)=> { store.setOverlayModify({ - pixels, + mural, muralObj: { name: img.alt, x: Math.floor(cords.ux - (img.width / 2)), diff --git a/src/index.ts b/src/index.ts index d9e26cf..cc67a5e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,12 +14,12 @@ async function main() { const palette = new Palette(); while(!palette.init()) { await waitForDraw();} const storage = new Storage(ENVIRONMENT === "browser-extension"); - const pixelPlaced = PixelPlaced.create(storage); const store = new Store(storage, palette); await store.load(); + const pixelPlaced = PixelPlaced.create(store); const coordinates = new Coordinates(); await coordinates.init(); - + createUI(store,storage, coordinates, palette, pixelPlaced); console.log( diff --git a/src/inject.ts b/src/inject.ts index f8ed392..74296e2 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -1,14 +1,105 @@ // Creating artificial API between two worlds -const log = console.log; -console.log = (...args: any) => { - const firstArg = args[0]; - if (firstArg && typeof firstArg === "object" && "pixelsPlaced" in firstArg) { - const pixelsPlaced = new CustomEvent("__pixelsPlaced", { - detail: firstArg.pixelsPlaced, - }); - document.dispatchEvent(pixelsPlaced); - } - log(...args); -}; +export type InterceptBefore = (requestInfo: string, init?: RequestInit) => Promise | void; +export type InterceptAfter = (requestInfo: string, response: Response, init?: RequestInit) => Promise | void; +export class Interceptor { + + private static fetch = window.fetch; + private static overwritten = false; + private static before = new Map(); + private static after = new Map(); + + private constructor() {} + + private static overrideFetch() { + if (this.overwritten) { + return; + } + this.overwritten = true; + (window as any).fetch = async (...args: any) => { + const url = ((args[0] as RequestInfo | URL) || "").toString(); + const init = args[1]; + + const befores = [...(this.before.get(url) || []), ...(this.before.get("*") || [])]; + if (befores) { + for (const before of befores) { + await before(url, init); + } + } + + const response = (this.fetch as any)(...args); + const afters = [...(this.after.get(url) || []), ...(this.after.get("*") || [])]; + if (afters) { + const result = await response; + for (const after of afters) { + try { + await after(url, result, init); + } catch (_) {} + } + return response; + } else { + return response; + } + }; + } + + static onAfter(url: string, cb: InterceptAfter) { + Interceptor.overrideFetch(); + const arr = Interceptor.after.get(url) || []; + if (!arr.includes(cb)) { + arr.push(cb); + } + Interceptor.after.set(url, arr); + } + static offAfter(url: string, cb: InterceptAfter) { + const arr = Interceptor.after.get(url) || []; + const index = arr.indexOf(cb); + if (index !== -1) { + arr.splice(index, 1); + } + if (arr.length) { + Interceptor.after.set(url, arr); + } else { + Interceptor.after.delete(url); + } + } + + static onBefore(url: string, cb: InterceptBefore) { + Interceptor.overrideFetch(); + const arr = Interceptor.before.get(url) || []; + if (!arr.includes(cb)) { + arr.push(cb); + } + Interceptor.before.set(url, arr); + } + + static offBefore(url: string, cb: InterceptBefore) { + const arr = Interceptor.before.get(url) || []; + const index = arr.indexOf(cb); + if (index !== -1) { + arr.splice(index, 1); + } + if (arr.length) { + Interceptor.before.set(url, arr); + } else { + Interceptor.before.delete(url); + } + } +} + +Interceptor.onAfter("/api/pixel", async (_, response, init) => { + try { + if (response.status === 200 && init && "body" in init) { + const body = JSON.parse(init.body as any); + if ("x" in body && "y" in body && "color" in body) { + const pixelsPlaced = new CustomEvent("__pixelsPlaced", { + detail: { color: body.color, x: body.x, y: body.y }, + }); + document.dispatchEvent(pixelsPlaced); + } + } + } catch (error) { + console.error(error); + } +}); diff --git a/src/lib/interceptor.ts b/src/lib/interceptor.ts new file mode 100644 index 0000000..e9ee7cd --- /dev/null +++ b/src/lib/interceptor.ts @@ -0,0 +1,86 @@ +export type InterceptBefore = (requestInfo: string, init?: RequestInit) => Promise | void; +export type InterceptAfter = (requestInfo: string, response: Response, init?: RequestInit) => Promise | void; + +export class Interceptor { + + private static fetch = window.fetch; + private static overwritten = false; + private static before = new Map(); + private static after = new Map(); + + private constructor() {} + + private static overrideFetch() { + if (this.overwritten) { + return; + } + this.overwritten = true; + (window as any).fetch = async (...args: any) => { + const url = ((args[0] as RequestInfo | URL) || "").toString(); + const init = args[1]; + + const befores = [...(this.before.get(url) || []), ...(this.before.get("*") || [])]; + if (befores) { + for (const before of befores) { + await before(url, init); + } + } + + const response = (this.fetch as any)(...args); + const afters = [...(this.after.get(url) || []), ...(this.after.get("*") || [])]; + if (afters) { + const result = await response; + for (const after of afters) { + try { + await after(url, result, init); + } catch (_) {} + } + return response; + } else { + return response; + } + }; + } + + static onAfter(url: string, cb: InterceptAfter) { + Interceptor.overrideFetch(); + const arr = Interceptor.after.get(url) || []; + if (!arr.includes(cb)) { + arr.push(cb); + } + Interceptor.after.set(url, arr); + } + static offAfter(url: string, cb: InterceptAfter) { + const arr = Interceptor.after.get(url) || []; + const index = arr.indexOf(cb); + if (index !== -1) { + arr.splice(index, 1); + } + if (arr.length) { + Interceptor.after.set(url, arr); + } else { + Interceptor.after.delete(url); + } + } + + static onBefore(url: string, cb: InterceptBefore) { + Interceptor.overrideFetch(); + const arr = Interceptor.before.get(url) || []; + if (!arr.includes(cb)) { + arr.push(cb); + } + Interceptor.before.set(url, arr); + } + static offBefore(url: string, cb: InterceptBefore) { + const arr = Interceptor.before.get(url) || []; + const index = arr.indexOf(cb); + if (index !== -1) { + arr.splice(index, 1); + } + if (arr.length) { + Interceptor.before.set(url, arr); + } else { + Interceptor.before.delete(url); + } + } +} diff --git a/src/lib/pixelPlaced.ts b/src/lib/pixelPlaced.ts index f617789..8be18e4 100644 --- a/src/lib/pixelPlaced.ts +++ b/src/lib/pixelPlaced.ts @@ -1,13 +1,16 @@ import { INJECT_SCRIPT_PIXEL_OBSERVER } from "../constants"; import { BasicEventEmitter } from "./eventEmitter"; -import { Storage } from "./storage"; +import { Store } from "./store"; +interface Pixel { + x: number; + y: number; + color: number; +} -export class PixelPlaced { - private emitter = new BasicEventEmitter(); +export class PixelPlaced { private static instance: PixelPlaced; - private readonly storageKey = "__pixelsPlaced"; - private constructor(private storage: Storage) { + private constructor(public readonly store: Store) { // The website is logging pixels as object onto pixelsPlaced onto console with .log. // With extension we need to cross 2 worlds. Adding artificial api to document @@ -26,28 +29,28 @@ export class PixelPlaced { } document.body.appendChild(api); - (document as any).addEventListener("__pixelsPlaced", (event: CustomEvent) => { - if (typeof event.detail === "number") { - this.storage.setItem(this.storageKey, event.detail); - this.emitter.emit(0, event.detail); + (document as any) + .addEventListener("__pixelsPlaced", (event: CustomEvent) => { + const body = event.detail; + console.log(body); + if (typeof body === "object" && "x" in body && "y" in body && "color" in body + && typeof body.color === "number" && typeof body.x === "number" + && typeof body.y === "number" + ) { + const data = event.detail; + this.store.addPixelLog(data.x, data.y, data.color); } }); } - static create(storage: Storage) { + static create(store: Store) { if (!this.instance) { - this.instance = new PixelPlaced(storage); + this.instance = new PixelPlaced(store); } return this.instance; } - - on(fn: (count: number) => void) { - this.emitter.on(0, fn); - } - off(fn: (count: number) => void) { - this.emitter.off(0, fn); - } - get count() { - return this.storage.getItem(this.storageKey); + getCount() { + return this.store.getPixelCount(); } + } \ No newline at end of file diff --git a/src/lib/store.ts b/src/lib/store.ts index cb955af..0d11263 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -16,6 +16,7 @@ export enum StoreEvents { MuralSelect, MuralOverlay, MuralPhantomOverlay, + PixelLog, Any, } @@ -25,23 +26,75 @@ export interface OverlayReturn { cb: (name: string, x: number, y: number, confirm?: boolean) => void; } +export interface PixelLog { + timestamp: string; + x: number; + y: number; + color: number; +} + +export interface PixelLogEx extends PixelLog { + id: number; +} + export class Store implements LoadUnload { private readonly STORAGE_KEY_MURAL = "_murals"; private readonly STORAGE_KEY_SELECTED = "_mural"; + private readonly STORAGE_KEY_LOG_STORE = "records"; + private readonly STORAGE_KEY_PIXEL_LOG = "_pixel-log"; private _murals: MuralEx[] = []; private _selected?: MuralEx; private emitter = new BasicEventEmitter(); private _overlayIndices: number[] = []; private _phantomOverlay = -1; private _overlayModify?: OverlayReturn; + private dbLog?: IDBDatabase; private db = localforage.createInstance({ name: "pixel-canvas-overlay" - }); - + }); constructor(private storage: Storage, private palette: Palette) {} async load() { - console.log("loaded"); + globalThis.store = this; + const pixelCanvasLog = "pixel-canvas-log"; + const logDb = indexedDB.open(pixelCanvasLog, 1); + + logDb.addEventListener("upgradeneeded", event => { + if (event) { + const db = (event?.target as any).result; + const store = db + .createObjectStore(this.STORAGE_KEY_LOG_STORE, + { keyPath: "id", autoIncrement: true } + ); + store.createIndex("by_timestamp", "timestamp"); + store.createIndex("by_color", "color"); + } + }); + + this.dbLog = await new Promise((resolve, reject) => { + logDb.addEventListener("success", event => { + const db = (event.target as any)?.result as IDBDatabase; + if (db) { + resolve(db); + } else { + reject(new Error("IndexDB did not return db object")); + } + }, { once: true }); + logDb.addEventListener("error", event => { + if (confirm( + "Unable to load data. Would you like to continue? Some feature will not work correctly" + )) { + indexedDB.deleteDatabase(pixelCanvasLog); + + resolve(undefined); + } else { + reject((event.target as IDBOpenDBRequest).error); + } + }, { once: true }); + }); + + + this._murals = []; const rawMurals = await this.db.getItem(this.STORAGE_KEY_MURAL) || []; @@ -77,6 +130,83 @@ export class Store implements LoadUnload { } } } + addPixelLog(x: number, y: number, color: number) { + const timestamp = new Date().toISOString(); + const data: PixelLog = { + timestamp, + color, + x, + y + }; + + return new Promise((resolve, reject) => { + if (!this.dbLog) { + resolve(null); + return; + } + const tx = this.dbLog.transaction(this.STORAGE_KEY_LOG_STORE, "readwrite"); + const store = tx.objectStore(this.STORAGE_KEY_LOG_STORE); + const request = store.add(data); + request.addEventListener("success", () => { + resolve(request.result); + this.emit(StoreEvents.PixelLog); + }, { once: true }); + request.addEventListener("error", () => { + reject(request.error); + }, { once: true }); + }); + } + async fetchPixelsLogByColor(color: number) { + if (!this.dbLog) return Promise.resolve([]); + const tx = this.dbLog.transaction("records", "readonly"); + const store = tx.objectStore("records"); + const index = store.index("by_color"); + const range = IDBKeyRange.only(color); + const request = index.openCursor(range); + return this.cursorIterator(request); + } + + async fetchPixelsLogByTime(startDate: Date, endDate: Date) { + if (!this.dbLog) return Promise.resolve([]); + const tx = this.dbLog.transaction(this.STORAGE_KEY_LOG_STORE, "readonly"); + const store = tx.objectStore(this.STORAGE_KEY_LOG_STORE); + const index = store.index("by_timestamp"); + const range = IDBKeyRange.bound(startDate.toISOString(), endDate.toISOString()); + const request = index.openCursor(range); + return this.cursorIterator(request); + } + + async getPixelCount() { + if (!this.dbLog) return Promise.resolve(0); + const tx = this.dbLog.transaction(this.STORAGE_KEY_LOG_STORE, "readonly"); + const store = tx.objectStore(this.STORAGE_KEY_LOG_STORE); + + return new Promise((resolve, reject) => { + const request = store.count(); + request.addEventListener("success", () => { + resolve(request.result); + }, { once: true }); + request.addEventListener("error", () => { + reject(request.error); + }, { once: true }); + }); + } + private cursorIterator(request: IDBRequest) { + const results: PixelLogEx[] = []; + return new Promise((resolve) => { + request.addEventListener("success", event => { + const cursor = (event.target as IDBRequest).result; + if (cursor) { + results.push(cursor.value); + cursor.continue(); + } else { + resolve(results); + } + + }); + }); + } + updateMural(_mural: Mural) { this.save(); this.emit(StoreEvents.MuralUpdated); @@ -90,7 +220,6 @@ export class Store implements LoadUnload { this.emit(StoreEvents.Any); this.save(); } - remove(mural: MuralEx) { this._overlayIndices = []; this._overlayModify = undefined;