diff --git a/package-lock.json b/package-lock.json index a520359d2..4f656f570 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,6 @@ "@vitest/ui": "^4.1.5", "autoprefixer": "^10.5.0", "benchmark": "^2.1.4", - "canvas": "^3.2.3", "concurrently": "^9.2.1", "cross-env": "^10.1.0", "d3": "^7.9.0", @@ -84,8 +83,10 @@ "prettier": "^3.8.3", "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-sh": "^0.18.1", + "skia-canvas": "^3.0.8", "tailwindcss": "^4.2.4", "tsconfig-paths": "^4.2.0", + "twemoji-colr-font": "^15.0.3", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "vite": "^8.0.10", @@ -3881,6 +3882,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -4472,28 +4482,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", - "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": "^18.12.0 || >= 20.9.0" - } - }, - "node_modules/canvas/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -5528,22 +5516,6 @@ "dev": true, "license": "MIT" }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -5591,11 +5563,10 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -5780,16 +5751,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -6250,16 +6211,6 @@ "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", "dev": true }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -6543,6 +6494,26 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6575,13 +6546,6 @@ "node": ">= 0.8" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6712,13 +6676,6 @@ "omggif": "^1.0.10" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -6916,6 +6873,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -8260,19 +8230,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -8309,13 +8266,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, "node_modules/moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -8391,13 +8341,6 @@ "node": "^18 || >=20" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8431,19 +8374,6 @@ "tslib": "^2.0.3" } }, - "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-html-parser": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", @@ -8651,6 +8581,12 @@ "tslib": "^2.0.3" } }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "dev": true + }, "node_modules/parse-bmfont-ascii": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", @@ -9016,33 +8952,6 @@ "node": ">=0.10.0" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9135,17 +9044,6 @@ "node": ">= 0.10" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9795,53 +9693,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/simple-xml-to-json": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", @@ -9866,6 +9717,19 @@ "node": ">=18" } }, + "node_modules/skia-canvas": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/skia-canvas/-/skia-canvas-3.0.8.tgz", + "integrity": "sha512-FSYKxp8Ng2vOeeOBiyPhnn6ui6FirPJXMyjk4PKl8N/OWzVrkMawUgY9zubIWHMdYtyWFn0gfX3QlRwg6HBmdg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "detect-libc": "^2.1.1", + "follow-redirects": "^1.15.11", + "https-proxy-agent": "^7.0.6", + "string-split-by": "^1.0.0" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -9967,6 +9831,15 @@ "node": ">=0.6.19" } }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "dev": true, + "dependencies": { + "parenthesis": "^3.1.5" + } + }, "node_modules/string-width": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", @@ -10104,95 +9977,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/tar-stream/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/terser": { "version": "5.42.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.42.0.tgz", @@ -10576,18 +10360,12 @@ "fsevents": "~2.3.3" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } + "node_modules/twemoji-colr-font": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/twemoji-colr-font/-/twemoji-colr-font-15.0.3.tgz", + "integrity": "sha512-UsK4JUpaczeVoMGeYMnKaMTxKt7fujg1nQOk4NaC0teZmdOo+uAai0DIuaSzsMkShtG4J75F0OEsVdGJ+Q1pHQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index 3b0f78ee1..d7837534e 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@vitest/ui": "^4.1.5", "autoprefixer": "^10.5.0", "benchmark": "^2.1.4", - "canvas": "^3.2.3", "concurrently": "^9.2.1", "cross-env": "^10.1.0", "d3": "^7.9.0", @@ -71,8 +70,10 @@ "prettier": "^3.8.3", "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-sh": "^0.18.1", + "skia-canvas": "^3.0.8", "tailwindcss": "^4.2.4", "tsconfig-paths": "^4.2.0", + "twemoji-colr-font": "^15.0.3", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "vite": "^8.0.10", diff --git a/resources/images/namelayer-emojis.png b/resources/images/namelayer-emojis.png index 869440c22..304fc0bea 100644 Binary files a/resources/images/namelayer-emojis.png and b/resources/images/namelayer-emojis.png differ diff --git a/resources/images/namelayer-icons.png b/resources/images/namelayer-icons.png index ff3902741..cf61307b5 100644 Binary files a/resources/images/namelayer-icons.png and b/resources/images/namelayer-icons.png differ diff --git a/scripts/build-namelayer-assets.mjs b/scripts/build-namelayer-assets.mjs index c409ffeff..5debd25ca 100644 --- a/scripts/build-namelayer-assets.mjs +++ b/scripts/build-namelayer-assets.mjs @@ -2,6 +2,7 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { Canvas, FontLibrary, loadImage } from "skia-canvas"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); @@ -12,6 +13,10 @@ const imagesDir = path.join(root, "resources", "images"); const fontPng = "namelayer_overpass.png"; const fontXml = "namelayer_overpass.xml"; const fontFace = "namelayer_overpass"; +const emojiFontFamily = "NameLayerEmoji"; +const emojiFontPath = require.resolve("twemoji-colr-font/twemoji.woff2"); +const emojiFontSize = 96; +const atlasFramePaddingRatio = 1 / 16; const fontSourceCandidates = [ "overpass-regular.otf", "overpass-regular.ttf", @@ -46,45 +51,12 @@ const iconSources = [ fs.mkdirSync(fontsDir, { recursive: true }); fs.mkdirSync(imagesDir, { recursive: true }); -const canvasApi = await loadCanvasApi(); +FontLibrary.use(emojiFontFamily, emojiFontPath); await buildMsdfFont(); await buildIconAtlas(); await buildEmojiAtlas(); -async function loadCanvasApi() { - try { - const api = await import("canvas"); - const fontPath = fontSourceCandidates - .map((fileName) => path.join(fontsDir, fileName)) - .find((candidate) => fs.existsSync(candidate)); - try { - if (!fontPath) { - throw new Error( - `No Overpass font source found. Tried: ${fontSourceCandidates.join( - ", ", - )}`, - ); - } - api.registerFont(fontPath, { - family: "OverpassNameLayer", - }); - } catch (error) { - console.warn( - "Could not register Overpass; using canvas fallback font", - error, - ); - } - return api; - } catch (error) { - console.warn( - "canvas native bindings are unavailable; writing deterministic fallback NameLayer assets", - error, - ); - return null; - } -} - async function buildMsdfFont() { const fontPath = fontSourceCandidates .map((fileName) => path.join(fontsDir, fileName)) @@ -147,16 +119,10 @@ async function buildMsdfFont() { } async function buildIconAtlas() { - if (!canvasApi) { - writeFallbackAtlas("namelayer-icons", iconSources); - return; - } - - const { createCanvas, loadImage } = canvasApi; const cell = 256; const cols = 4; const rows = Math.ceil(iconSources.length / cols); - const canvas = createCanvas(cols * cell, rows * cell); + const canvas = new Canvas(cols * cell, rows * cell); const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); const frames = {}; @@ -168,8 +134,10 @@ async function buildIconAtlas() { const x = col * cell; const y = row * cell; try { - const img = await loadIconImage(path.join(imagesDir, source), loadImage); - ctx.drawImage(img, x, y, cell, cell); + const img = await loadIconImage(path.join(imagesDir, source)); + drawPackedAtlasFrame(ctx, x, y, cell, (scratchCtx, scratchSize) => { + drawContainedImage(scratchCtx, img, 0, 0, scratchSize, scratchSize); + }); } catch (error) { console.warn( `Could not pack ${source}; leaving empty atlas frame`, @@ -185,9 +153,14 @@ async function buildIconAtlas() { }; } + validateAtlasFramesPixels(ctx, canvas.width, canvas.height, frames, { + label: "icon", + requireColor: false, + }); + fs.writeFileSync( path.join(imagesDir, "namelayer-icons.png"), - canvas.toBuffer("image/png"), + await canvas.toBuffer("png"), ); fs.writeFileSync( path.join(imagesDir, "namelayer-icons.json"), @@ -208,7 +181,7 @@ async function buildIconAtlas() { ); } -async function loadIconImage(sourcePath, loadImage) { +async function loadIconImage(sourcePath) { if (path.extname(sourcePath).toLowerCase() !== ".svg") { return loadImage(sourcePath); } @@ -225,30 +198,17 @@ async function loadIconImage(sourcePath, loadImage) { ); } - return loadImage( - `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`, - ); + return loadImage(Buffer.from(svg, "utf8")); } async function buildEmojiAtlas() { - if (!canvasApi) { - const emojis = readEmojiTable(); - writeFallbackAtlas("namelayer-emojis", emojis); - return; - } - - const { createCanvas } = canvasApi; const emojis = readEmojiTable(); const cell = 128; const cols = 8; const rows = Math.max(1, Math.ceil(emojis.length / cols)); - const canvas = createCanvas(cols * cell, rows * cell); + const canvas = new Canvas(cols * cell, rows * cell); const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.font = - '96px "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif'; const frames = {}; emojis.forEach((emoji, index) => { @@ -256,7 +216,12 @@ async function buildEmojiAtlas() { const row = Math.floor(index / cols); const x = col * cell; const y = row * cell; - ctx.fillText(emoji, x + cell / 2, y + cell / 2); + drawPackedAtlasFrame(ctx, x, y, cell, (scratchCtx, scratchSize) => { + scratchCtx.textAlign = "center"; + scratchCtx.textBaseline = "middle"; + scratchCtx.font = `${emojiFontSize}px ${emojiFontFamily}`; + scratchCtx.fillText(emoji, scratchSize / 2, scratchSize / 2); + }); frames[emoji] = { frame: { x, y, w: cell, h: cell }, rotated: false, @@ -266,9 +231,14 @@ async function buildEmojiAtlas() { }; }); + validateAtlasFramesPixels(ctx, canvas.width, canvas.height, frames, { + label: "emoji", + requireColor: true, + }); + fs.writeFileSync( path.join(imagesDir, "namelayer-emojis.png"), - canvas.toBuffer("image/png"), + await canvas.toBuffer("png"), ); fs.writeFileSync( path.join(imagesDir, "namelayer-emojis.json"), @@ -289,6 +259,120 @@ async function buildEmojiAtlas() { ); } +function drawPackedAtlasFrame(targetCtx, x, y, cell, drawSource) { + const scratchSize = cell * 2; + const scratch = new Canvas(scratchSize, scratchSize); + const scratchCtx = scratch.getContext("2d"); + scratchCtx.clearRect(0, 0, scratchSize, scratchSize); + drawSource(scratchCtx, scratchSize); + + const bounds = findAlphaBounds( + scratchCtx.getImageData(0, 0, scratchSize, scratchSize).data, + scratchSize, + scratchSize, + ); + if (!bounds) { + throw new Error("NameLayer atlas frame source rendered empty"); + } + + const sourceWidth = bounds.maxX - bounds.minX + 1; + const sourceHeight = bounds.maxY - bounds.minY + 1; + const padding = Math.round(cell * atlasFramePaddingRatio); + const maxSize = cell - padding * 2; + const scale = Math.min(maxSize / sourceWidth, maxSize / sourceHeight, 1); + const drawWidth = Math.ceil(sourceWidth * scale); + const drawHeight = Math.ceil(sourceHeight * scale); + const drawX = x + Math.floor((cell - drawWidth) / 2); + const drawY = y + Math.floor((cell - drawHeight) / 2); + + targetCtx.drawImage( + scratch, + bounds.minX, + bounds.minY, + sourceWidth, + sourceHeight, + drawX, + drawY, + drawWidth, + drawHeight, + ); +} + +function drawContainedImage(ctx, image, x, y, width, height) { + const sourceWidth = image.width ?? width; + const sourceHeight = image.height ?? height; + const scale = Math.min(width / sourceWidth, height / sourceHeight); + const drawWidth = sourceWidth * scale; + const drawHeight = sourceHeight * scale; + ctx.drawImage( + image, + x + (width - drawWidth) / 2, + y + (height - drawHeight) / 2, + drawWidth, + drawHeight, + ); +} + +function findAlphaBounds(data, width, height) { + let minX = width; + let minY = height; + let maxX = -1; + let maxY = -1; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (data[(y * width + x) * 4 + 3] === 0) { + continue; + } + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + + return maxX >= minX && maxY >= minY ? { minX, minY, maxX, maxY } : null; +} + +function validateAtlasFramesPixels( + ctx, + width, + height, + frames, + { label, requireColor }, +) { + const data = ctx.getImageData(0, 0, width, height).data; + let colorfulPixels = 0; + + for (const [key, { frame }] of Object.entries(frames)) { + let alphaPixels = 0; + for (let y = frame.y; y < frame.y + frame.h; y++) { + for (let x = frame.x; x < frame.x + frame.w; x++) { + const offset = (y * width + x) * 4; + const r = data[offset]; + const g = data[offset + 1]; + const b = data[offset + 2]; + const a = data[offset + 3]; + if (a === 0) { + continue; + } + alphaPixels++; + if (Math.max(r, g, b) - Math.min(r, g, b) > 12) { + colorfulPixels++; + } + } + } + + if (alphaPixels === 0) { + throw new Error(`NameLayer ${label} atlas frame is empty: ${key}`); + } + } + + if (requireColor && colorfulPixels === 0) { + throw new Error(`NameLayer ${label} atlas rendered without color pixels`); + } +} + function readEmojiTable() { const utilPath = path.join(root, "src", "core", "Util.ts"); const utilSource = fs.readFileSync(utilPath, "utf8"); @@ -306,40 +390,3 @@ function readEmojiTable() { return Array.from(match[1].matchAll(/"([^"]+)"/g), (match) => match[1]); } - -function writeFallbackAtlas(name, keys) { - const transparentPng = Buffer.from( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lD6N7wAAAABJRU5ErkJggg==", - "base64", - ); - const frames = {}; - - for (const key of keys) { - frames[key] = { - frame: { x: 0, y: 0, w: 1, h: 1 }, - rotated: false, - trimmed: false, - spriteSourceSize: { x: 0, y: 0, w: 1, h: 1 }, - sourceSize: { w: 1, h: 1 }, - }; - } - - fs.writeFileSync(path.join(imagesDir, `${name}.png`), transparentPng); - fs.writeFileSync( - path.join(imagesDir, `${name}.json`), - `${JSON.stringify( - { - frames, - meta: { - app: "scripts/build-namelayer-assets.mjs", - image: `${name}.png`, - format: "RGBA8888", - size: { w: 1, h: 1 }, - scale: "1", - }, - }, - null, - 2, - )}\n`, - ); -}