diff --git a/eslint.config.js b/eslint.config.js index 3168893d5b..e75c2f38c4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,6 +26,7 @@ export default [ allowDefaultProject: [ "__mocks__/fileMock.js", "eslint.config.js", + "scripts/build-namelayer-assets.mjs", "scripts/sync-assets.mjs", ], }, diff --git a/package-lock.json b/package-lock.json index f86c893793..13068118bb 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", @@ -78,11 +77,13 @@ "lit": "^3.3.2", "lit-markdown": "^1.3.2", "mrmime": "^2.0.1", + "msdf-bmfont-xml": "^2.8.0", "pixi-filters": "^6.1.5", "pixi.js": "^8.18.1", "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", "typescript": "^6.0.3", @@ -204,6 +205,16 @@ "node": ">=18" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1127,6 +1138,562 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jimp/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", + "dev": true, + "dependencies": { + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^21.3.3", + "mime": "3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/diff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", + "dev": true, + "dependencies": { + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/file-ops": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-bmp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-gif": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-jpeg": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-png": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-tiff": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "utif2": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-color/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", + "dev": true, + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/types": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", + "dev": true, + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@jimp/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", + "dev": true, + "dependencies": { + "@jimp/types": "1.6.1", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1523,6 +2090,47 @@ "dev": true, "license": "MIT" }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -2171,6 +2779,29 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3250,6 +3881,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", @@ -3267,6 +3907,44 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -3309,6 +3987,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "dev": true + }, + "node_modules/arabic-persian-reshaper": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arabic-persian-reshaper/-/arabic-persian-reshaper-1.0.1.tgz", + "integrity": "sha512-VYBjkhz6o4W1Xt4mD2LAReljJpLSw5CUZMqSBDIQRvFgUSlTKEYghapgBWvkeMWF4W+KF3Fm+/z8EywJU4PBeg==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3353,6 +4043,16 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, "node_modules/autoprefixer": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", @@ -3390,6 +4090,15 @@ "postcss": "^8.1.0" } }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3468,6 +4177,12 @@ "readable-stream": "^4.2.0" } }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "dev": true + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -3514,6 +4229,84 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -3655,6 +4448,18 @@ "tslib": "^2.0.3" } }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001791", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", @@ -3676,28 +4481,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", @@ -3738,6 +4521,18 @@ "node": ">= 10.0" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -3754,6 +4549,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-progress/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cli-progress/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-truncate": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", @@ -4039,6 +4875,34 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/configstore": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz", + "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==", + "dev": true, + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -4651,22 +5515,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", @@ -4714,11 +5562,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" } @@ -4798,6 +5645,21 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -4888,16 +5750,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", @@ -5025,6 +5877,18 @@ "node": ">=6" } }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -5340,15 +6204,11 @@ "node": ">=0.8.x" } }, - "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/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true }, "node_modules/expect-type": { "version": "1.3.0", @@ -5514,6 +6374,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -5615,6 +6493,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", @@ -5647,13 +6545,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", @@ -5774,12 +6665,15 @@ "js-binary-schema-parser": "^2.0.3" } }, - "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==", + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", "dev": true, - "license": "MIT" + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } }, "node_modules/glob": { "version": "13.0.6", @@ -5812,6 +6706,30 @@ "node": ">=10.13.0" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/globals": { "version": "17.6.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", @@ -5844,6 +6762,27 @@ "dev": true, "license": "ISC" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5933,6 +6872,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", @@ -5993,6 +6945,21 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "dev": true, + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "dev": true + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6099,6 +7066,58 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "dev": true, + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-invalid-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-1.0.2.tgz", + "integrity": "sha512-6KLcFrPCEP3AFXMfnWrIFkZpYNBVzZAoBJJDEZKtI3LXkaDjM3uFMJQjxiizUuZTZ9Oh9FNv/soXbx5TcpaDmA==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6109,6 +7128,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -6215,6 +7246,44 @@ "node": ">=10" } }, + "node_modules/jimp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "dev": true, + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -6234,6 +7303,12 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, "node_modules/js-binary-schema-parser": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", @@ -6260,6 +7335,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-5.0.0.tgz", + "integrity": "sha512-ckXs0Fzd6icWurbeAXuqo+3Mhq2m8pOPygsQjTPh8K5UWgKaUgDSHrdDxAfexmT11xvBKOQ6sgYwPkYc5RW/bg==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", @@ -6364,6 +7448,33 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/latest-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", + "dev": true, + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6970,6 +8081,24 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "license": "ISC" }, + "node_modules/map-limit": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", + "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", + "dev": true, + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -6992,6 +8121,12 @@ "node": ">= 0.4" } }, + "node_modules/maxrects-packer": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/maxrects-packer/-/maxrects-packer-2.7.3.tgz", + "integrity": "sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==", + "dev": true + }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -7044,6 +8179,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -7082,19 +8229,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", @@ -7131,13 +8265,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", @@ -7164,6 +8291,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msdf-bmfont-xml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/msdf-bmfont-xml/-/msdf-bmfont-xml-2.8.0.tgz", + "integrity": "sha512-VK6US7QqNhY9K5sq6TKlpKNlbBch1M2P1vLirf8mZLHK3j7X86fM4sqEyVnwCBYwZ/xiTbJeUWMEv5Ji+jQQMQ==", + "dev": true, + "dependencies": { + "arabic-persian-reshaper": "^1.0.1", + "cli-progress": "^3.12.0", + "commander": "^14.0.0", + "handlebars": "^4.7.8", + "is-invalid-path": "^1.0.2", + "jimp": "^1.6.0", + "js2xmlparser": "^5.0.0", + "map-limit": "0.0.1", + "maxrects-packer": "^2.7.3", + "opentype.js": "^1.3.4", + "update-notifier": "^7.3.1" + }, + "bin": { + "msdf-bmfont": "cli.js" + } + }, + "node_modules/msdf-bmfont-xml/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/nanoid": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", @@ -7182,13 +8340,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", @@ -7205,6 +8356,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -7216,19 +8373,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", @@ -7290,6 +8434,12 @@ ], "license": "MIT" }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "dev": true + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -7345,6 +8495,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opentype.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", + "integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==", + "dev": true, + "dependencies": { + "string.prototype.codepointat": "^0.2.1", + "tiny-inflate": "^1.0.3" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7379,6 +8545,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", + "dev": true, + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -7390,6 +8580,34 @@ "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", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -7558,6 +8776,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "dev": true, + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/pixi-filters": { "version": "6.1.5", "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.1.5.tgz", @@ -7605,6 +8844,15 @@ "dev": true, "license": "MIT" }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", @@ -7703,33 +8951,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", @@ -7803,6 +9024,12 @@ "node": ">= 0.6.0" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7816,17 +9043,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", @@ -7837,6 +9053,21 @@ "node": ">=6" } }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "dev": true, + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", @@ -7956,6 +9187,33 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "dev": true, + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -8198,6 +9456,15 @@ "entities": "^4.4.0" } }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -8425,51 +9692,13 @@ "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==", + "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", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", "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" + "engines": { + "node": ">=20.12.2" } }, "node_modules/sirv": { @@ -8487,6 +9716,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", @@ -8588,6 +9830,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", @@ -8634,6 +9885,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "dev": true + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -8647,6 +9904,37 @@ "node": ">=8" } }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8688,95 +9976,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", @@ -8827,6 +10026,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "dev": true + }, "node_modules/tiny-lru": { "version": "11.4.7", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz", @@ -8844,6 +10049,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true + }, "node_modules/tinyexec": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", @@ -8954,6 +10165,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -9130,19 +10359,6 @@ "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9156,6 +10372,18 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -9207,6 +10435,31 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -9273,6 +10526,42 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-notifier": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", + "dev": true, + "dependencies": { + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -9283,6 +10572,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "dev": true, + "dependencies": { + "pako": "^1.0.11" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9746,6 +11044,12 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9779,6 +11083,65 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "dev": true, + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -9853,6 +11216,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -9958,6 +11327,18 @@ } } }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -9968,6 +11349,34 @@ "node": ">=18" } }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -9975,6 +11384,12 @@ "dev": true, "license": "MIT" }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 7ea8684627..42b60ef013 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "docs:map-generator": "cd map-generator && go doc -cmd -u -all", "tunnel": "npm run build-prod && npm run start:server", "test": "vitest run && vitest run tests/server", + "build:namelayer-assets": "node scripts/build-namelayer-assets.mjs", "perf": "npx tsx tests/perf/run-all.ts", "test:coverage": "vitest run --coverage", "format": "prettier --ignore-unknown --write .", @@ -49,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", @@ -64,11 +64,13 @@ "lit": "^3.3.2", "lit-markdown": "^1.3.2", "mrmime": "^2.0.1", + "msdf-bmfont-xml": "^2.8.0", "pixi-filters": "^6.1.5", "pixi.js": "^8.18.1", "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", "typescript": "^6.0.3", diff --git a/resources/fonts/namelayer_overpass.png b/resources/fonts/namelayer_overpass.png new file mode 100644 index 0000000000..2dfb1db7b5 Binary files /dev/null and b/resources/fonts/namelayer_overpass.png differ diff --git a/resources/fonts/namelayer_overpass.xml b/resources/fonts/namelayer_overpass.xml new file mode 100644 index 0000000000..506c88f576 --- /dev/null +++ b/resources/fonts/namelayer_overpass.xml @@ -0,0 +1,1158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/fonts/overpass-OFL.txt b/resources/fonts/overpass-OFL.txt new file mode 100644 index 0000000000..2739ed0cfc --- /dev/null +++ b/resources/fonts/overpass-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/fonts/overpass-regular.otf b/resources/fonts/overpass-regular.otf new file mode 100644 index 0000000000..3a7c095fab Binary files /dev/null and b/resources/fonts/overpass-regular.otf differ diff --git a/resources/fonts/twemoji-colr-OFL.txt b/resources/fonts/twemoji-colr-OFL.txt new file mode 100644 index 0000000000..787fc8ffd4 --- /dev/null +++ b/resources/fonts/twemoji-colr-OFL.txt @@ -0,0 +1,121 @@ +# License for the font file + +SIL OPEN FONT LICENSE +Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting — in part or in whole — any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +# License for the Visual Design + +The emoji art comes from [Twemoji](https://twitter.github.io/twemoji), +and is used and redistributed under the CC-BY-4.0 [license terms](https://github.com/twitter/twemoji#license) +offered by the Twemoji project. + +### Creative Commons Attribution 4.0 International (CC BY 4.0) +https://creativecommons.org/licenses/by/4.0/legalcode +or for the human readable summary: https://creativecommons.org/licenses/by/4.0/ + + +#### You are free to: +**Share** — copy and redistribute the material in any medium or format + +**Adapt** — remix, transform, and build upon the material for any purpose, even commercially. + +The licensor cannot revoke these freedoms as long as you follow the license terms. + + +#### Under the following terms: +**Attribution** — You must give appropriate credit, provide a link to the license, +and indicate if changes were made. +You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + +**No additional restrictions** — You may not apply legal terms or **technological measures** +that legally restrict others from doing anything the license permits. + +#### Notices: +You do not have to comply with the license for elements of the material in the public domain +or where your use is permitted by an applicable exception or limitation. No warranties are given. +The license may not give you all of the permissions necessary for your intended use. +For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. + diff --git a/resources/fonts/twemoji-colr.woff2 b/resources/fonts/twemoji-colr.woff2 new file mode 100644 index 0000000000..6cc58b6aa8 Binary files /dev/null and b/resources/fonts/twemoji-colr.woff2 differ diff --git a/resources/images/namelayer-emojis.json b/resources/images/namelayer-emojis.json new file mode 100644 index 0000000000..bd7f38fc8e --- /dev/null +++ b/resources/images/namelayer-emojis.json @@ -0,0 +1,1214 @@ +{ + "frames": { + "😀": { + "frame": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😊": { + "frame": { + "x": 128, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🥰": { + "frame": { + "x": 256, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😇": { + "frame": { + "x": 384, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😎": { + "frame": { + "x": 512, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😞": { + "frame": { + "x": 640, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🥺": { + "frame": { + "x": 768, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😭": { + "frame": { + "x": 896, + "y": 0, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😱": { + "frame": { + "x": 0, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😡": { + "frame": { + "x": 128, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "😈": { + "frame": { + "x": 256, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🤡": { + "frame": { + "x": 384, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🥱": { + "frame": { + "x": 512, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🫡": { + "frame": { + "x": 640, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🖕": { + "frame": { + "x": 768, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "👋": { + "frame": { + "x": 896, + "y": 128, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "👏": { + "frame": { + "x": 0, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "✋": { + "frame": { + "x": 128, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🙏": { + "frame": { + "x": 256, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "💪": { + "frame": { + "x": 384, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "👍": { + "frame": { + "x": 512, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "👎": { + "frame": { + "x": 640, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🫴": { + "frame": { + "x": 768, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🤌": { + "frame": { + "x": 896, + "y": 256, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🤦‍♂️": { + "frame": { + "x": 0, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🤝": { + "frame": { + "x": 128, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🆘": { + "frame": { + "x": 256, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🕊️": { + "frame": { + "x": 384, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🏳️": { + "frame": { + "x": 512, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⏳": { + "frame": { + "x": 640, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🔥": { + "frame": { + "x": 768, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "💥": { + "frame": { + "x": 896, + "y": 384, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "💀": { + "frame": { + "x": 0, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "☢️": { + "frame": { + "x": 128, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⚠️": { + "frame": { + "x": 256, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "↖️": { + "frame": { + "x": 384, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⬆️": { + "frame": { + "x": 512, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "↗️": { + "frame": { + "x": 640, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "👑": { + "frame": { + "x": 768, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🥇": { + "frame": { + "x": 896, + "y": 512, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⬅️": { + "frame": { + "x": 0, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🎯": { + "frame": { + "x": 128, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "➡️": { + "frame": { + "x": 256, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🥈": { + "frame": { + "x": 384, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🥉": { + "frame": { + "x": 512, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "↙️": { + "frame": { + "x": 640, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⬇️": { + "frame": { + "x": 768, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "↘️": { + "frame": { + "x": 896, + "y": 640, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "❤️": { + "frame": { + "x": 0, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "💔": { + "frame": { + "x": 128, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "💰": { + "frame": { + "x": 256, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⚓": { + "frame": { + "x": 384, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "⛵": { + "frame": { + "x": 512, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🏡": { + "frame": { + "x": 640, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🛡️": { + "frame": { + "x": 768, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🏭": { + "frame": { + "x": 896, + "y": 768, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🚂": { + "frame": { + "x": 0, + "y": 896, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "❓": { + "frame": { + "x": 128, + "y": 896, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🐔": { + "frame": { + "x": 256, + "y": 896, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + }, + "🐀": { + "frame": { + "x": 384, + "y": 896, + "w": 128, + "h": 128 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 128, + "h": 128 + }, + "sourceSize": { + "w": 128, + "h": 128 + } + } + }, + "meta": { + "app": "scripts/build-namelayer-assets.mjs", + "image": "namelayer-emojis.png", + "format": "RGBA8888", + "size": { + "w": 1024, + "h": 1024 + }, + "scale": "1" + } +} diff --git a/resources/images/namelayer-emojis.png b/resources/images/namelayer-emojis.png new file mode 100644 index 0000000000..304fc0bead Binary files /dev/null and b/resources/images/namelayer-emojis.png differ diff --git a/resources/images/namelayer-icons.json b/resources/images/namelayer-icons.json new file mode 100644 index 0000000000..f7cec5ee98 --- /dev/null +++ b/resources/images/namelayer-icons.json @@ -0,0 +1,274 @@ +{ + "frames": { + "AllianceIcon.svg": { + "frame": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "AllianceIconFaded.svg": { + "frame": { + "x": 256, + "y": 0, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "AllianceRequestBlackIcon.svg": { + "frame": { + "x": 512, + "y": 0, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "AllianceRequestWhiteIcon.svg": { + "frame": { + "x": 768, + "y": 0, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "CrownIcon.svg": { + "frame": { + "x": 0, + "y": 256, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "DisconnectedIcon.svg": { + "frame": { + "x": 256, + "y": 256, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "EmbargoBlackIcon.svg": { + "frame": { + "x": 512, + "y": 256, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "EmbargoWhiteIcon.svg": { + "frame": { + "x": 768, + "y": 256, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "NukeIconRed.svg": { + "frame": { + "x": 0, + "y": 512, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "NukeIconWhite.svg": { + "frame": { + "x": 256, + "y": 512, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "QuestionMarkIcon.svg": { + "frame": { + "x": 512, + "y": 512, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "TargetIcon.svg": { + "frame": { + "x": 768, + "y": 512, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + }, + "TraitorIcon.svg": { + "frame": { + "x": 0, + "y": 768, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + } + } + }, + "meta": { + "app": "scripts/build-namelayer-assets.mjs", + "image": "namelayer-icons.png", + "format": "RGBA8888", + "size": { + "w": 1024, + "h": 1024 + }, + "scale": "1" + } +} diff --git a/resources/images/namelayer-icons.png b/resources/images/namelayer-icons.png new file mode 100644 index 0000000000..cf61307b53 Binary files /dev/null and b/resources/images/namelayer-icons.png differ diff --git a/scripts/build-namelayer-assets.mjs b/scripts/build-namelayer-assets.mjs new file mode 100644 index 0000000000..a0110149e9 --- /dev/null +++ b/scripts/build-namelayer-assets.mjs @@ -0,0 +1,398 @@ +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); +const root = path.resolve(__dirname, ".."); +const fontsDir = path.join(root, "resources", "fonts"); +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 = path.join(fontsDir, "twemoji-colr.woff2"); +const emojiFontSize = 96; +const atlasFramePaddingRatio = 1 / 16; +const colorDetectionThreshold = 12; +const fontSourceCandidates = [ + "overpass-regular.otf", + "overpass-regular.ttf", + "overpass.otf", + "overpass.ttf", + "overpass.woff", +]; +const glyphs = Array.from( + new Set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ \u00fc\u00dc.[]+-=(),':!?/@#$%&\"".split( + "", + ), + ), +); + +const iconSources = [ + "AllianceIcon.svg", + "AllianceIconFaded.svg", + "AllianceRequestBlackIcon.svg", + "AllianceRequestWhiteIcon.svg", + "CrownIcon.svg", + "DisconnectedIcon.svg", + "EmbargoBlackIcon.svg", + "EmbargoWhiteIcon.svg", + "NukeIconRed.svg", + "NukeIconWhite.svg", + "QuestionMarkIcon.svg", + "TargetIcon.svg", + "TraitorIcon.svg", +]; + +fs.mkdirSync(fontsDir, { recursive: true }); +fs.mkdirSync(imagesDir, { recursive: true }); + +const overpassFontPath = findFontSource(); +FontLibrary.use(emojiFontFamily, [emojiFontPath]); + +await buildMsdfFont(); +await buildIconAtlas(); +await buildEmojiAtlas(); + +async function buildMsdfFont() { + if (!overpassFontPath) { + const fallbackXml = fs + .readFileSync(path.join(fontsDir, "round_6x6_modified.xml"), "utf8") + .replace(/face="round_6x6_modified"/g, `face="${fontFace}"`) + .replace(/file="round_6x6_modified\.png"/g, `file="${fontPng}"`); + fs.writeFileSync( + path.join(fontsDir, fontPng), + fs.readFileSync(path.join(fontsDir, "round_6x6_modified.png")), + ); + fs.writeFileSync(path.join(fontsDir, fontXml), fallbackXml); + return; + } + + const generateBMFont = require("msdf-bmfont-xml"); + const { textures, font } = await new Promise((resolve, reject) => { + generateBMFont( + overpassFontPath, + { + filename: path.join(fontsDir, path.basename(fontPng, ".png")), + outputType: "xml", + charset: glyphs, + fontSize: 64, + textureSize: [2048, 2048], + texturePadding: 2, + distanceRange: 8, + fieldType: "msdf", + smartSize: true, + pot: true, + roundDecimal: 0, + }, + (error, textures, font) => { + if (error) { + reject(error); + return; + } + resolve({ textures, font }); + }, + { + log: () => {}, + warn: (message) => console.warn(`NameLayer MSDF font: ${message}`), + error: (message) => console.error(`NameLayer MSDF font: ${message}`), + }, + ); + }); + + for (const texture of textures) { + fs.writeFileSync(`${texture.filename}.png`, texture.texture); + } + + const xml = String(font.data).replace( + /(]*face=")[^"]+(")/, + `$1${fontFace}$2`, + ); + fs.writeFileSync(path.join(fontsDir, fontXml), xml); +} + +async function buildIconAtlas() { + const cell = 256; + const cols = 4; + const rows = Math.ceil(iconSources.length / cols); + const canvas = new Canvas(cols * cell, rows * cell); + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + const frames = {}; + + for (let i = 0; i < iconSources.length; i++) { + const source = iconSources[i]; + const col = i % cols; + const row = Math.floor(i / cols); + const x = col * cell; + const y = row * cell; + try { + 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`, + error, + ); + } + frames[source] = { + frame: { x, y, w: cell, h: cell }, + rotated: false, + trimmed: false, + spriteSourceSize: { x: 0, y: 0, w: cell, h: cell }, + sourceSize: { w: cell, h: cell }, + }; + } + + validateAtlasFramesPixels(ctx, canvas.width, canvas.height, frames, { + label: "icon", + requireColor: false, + }); + + fs.writeFileSync( + path.join(imagesDir, "namelayer-icons.png"), + await canvas.toBuffer("png"), + ); + fs.writeFileSync( + path.join(imagesDir, "namelayer-icons.json"), + `${JSON.stringify( + { + frames, + meta: { + app: "scripts/build-namelayer-assets.mjs", + image: "namelayer-icons.png", + format: "RGBA8888", + size: { w: canvas.width, h: canvas.height }, + scale: "1", + }, + }, + null, + 2, + )}\n`, + ); +} + +async function loadIconImage(sourcePath) { + if (path.extname(sourcePath).toLowerCase() !== ".svg") { + return loadImage(sourcePath); + } + + let svg = fs.readFileSync(sourcePath, "utf8"); + if (!/]*\swidth=/i.test(svg) || !/]*\sheight=/i.test(svg)) { + const viewBoxMatch = svg.match( + /viewBox=["']\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*["']/i, + ); + const width = viewBoxMatch?.[3] ?? 64; + const height = viewBoxMatch?.[4] ?? 64; + svg = svg.replace(/ { + const col = index % cols; + const row = Math.floor(index / cols); + const x = col * cell; + const y = row * cell; + drawPackedAtlasFrame(ctx, x, y, cell, (scratchCtx, scratchSize) => { + drawEmojiText(scratchCtx, scratchSize, emoji); + }); + frames[emoji] = { + frame: { x, y, w: cell, h: cell }, + rotated: false, + trimmed: false, + spriteSourceSize: { x: 0, y: 0, w: cell, h: cell }, + sourceSize: { w: cell, h: cell }, + }; + }); + + validateAtlasFramesPixels(ctx, canvas.width, canvas.height, frames, { + label: "emoji", + requireColor: true, + }); + + fs.writeFileSync( + path.join(imagesDir, "namelayer-emojis.png"), + await canvas.toBuffer("png"), + ); + fs.writeFileSync( + path.join(imagesDir, "namelayer-emojis.json"), + `${JSON.stringify( + { + frames, + meta: { + app: "scripts/build-namelayer-assets.mjs", + image: "namelayer-emojis.png", + format: "RGBA8888", + size: { w: canvas.width, h: canvas.height }, + scale: "1", + }, + }, + null, + 2, + )}\n`, + ); +} + +function drawEmojiText(ctx, size, emoji) { + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = `${emojiFontSize}px ${emojiFontFamily}`; + ctx.fillText(emoji, size / 2, size / 2); +} + +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) > colorDetectionThreshold) { + 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"); + const match = utilSource.match( + /export const emojiTable = \[([\s\S]*?)\] as const;/, + ); + if (!match?.[1]) { + throw new Error( + `emojiTable not found in utilSource (${utilPath}). Start of file: ${utilSource.slice( + 0, + 160, + )}`, + ); + } + + return Array.from(match[1].matchAll(/"([^"]+)"/g), (match) => match[1]); +} + +function findFontSource() { + return fontSourceCandidates + .map((fileName) => path.join(fontsDir, fileName)) + .find((candidate) => fs.existsSync(candidate)); +} diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 51f29e6fd5..110f4dbfcf 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -148,9 +148,9 @@ export function joinLobby( terrainLoad, terrainMapFileLoader, ) - .then((r) => { + .then(async (r) => { currentGameRunner = r; - r.start(); + await r.start(); }) .catch((e) => { console.error("error creating client game", e); @@ -373,7 +373,7 @@ export class ClientGameRunner { endGame(record); } - public start() { + public async start() { this.soundManager.playBackgroundMusic(); console.log("starting client game"); @@ -410,7 +410,7 @@ export class ClientGameRunner { this.doBreakAllianceUnderCursor.bind(this), ); - this.renderer.initialize(); + await this.renderer.initialize(); this.input.initialize(); this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => { if (this.lobby.gameStartInfo === undefined) { diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 4df65faccd..f41048fb64 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -353,9 +353,11 @@ export class GameRenderer { this.context = context; } - initialize() { + async initialize() { this.eventBus.on(RedrawGraphicsEvent, () => this.redraw()); - this.layers.forEach((l) => l.init?.()); + for (const layer of this.layers) { + await layer.init?.(); + } // only append the canvas if it's not already in the document to avoid reparenting side-effects if (!document.body.contains(this.canvas)) { diff --git a/src/client/graphics/PlayerIcons.ts b/src/client/graphics/PlayerIcons.ts index 935de93881..36615e0e22 100644 --- a/src/client/graphics/PlayerIcons.ts +++ b/src/client/graphics/PlayerIcons.ts @@ -341,6 +341,10 @@ export function updateAllianceProgressIconRefs( } export function computeAllianceClipPath(fraction: number): string { - const topCut = 20 + (1 - fraction) * 80 * 0.78; // min 20%, max 82.40% + const topCut = computeAllianceTopCutPercent(fraction); return `inset(${topCut.toFixed(2)}% -2px 0 -2px)`; } + +export function computeAllianceTopCutPercent(fraction: number): number { + return 20 + (1 - fraction) * 80 * 0.78; // min 20%, max 82.40% +} diff --git a/src/client/graphics/layers/Layer.ts b/src/client/graphics/layers/Layer.ts index 456648f794..332bcf73cc 100644 --- a/src/client/graphics/layers/Layer.ts +++ b/src/client/graphics/layers/Layer.ts @@ -1,5 +1,5 @@ export interface Layer { - init?: () => void; + init?: () => void | Promise; tick?: () => void; // Optional hint to throttle expensive ticks by wall-clock. // If omitted or <= 0, the layer ticks whenever GameRenderer ticks. @@ -7,4 +7,5 @@ export interface Layer { renderLayer?: (context: CanvasRenderingContext2D) => void; shouldTransform?: () => boolean; redraw?: () => void; + destroy?: () => void; } diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 701e09c261..1e61208c60 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -1,4 +1,5 @@ -import { assetUrl } from "src/core/AssetUrls"; +import * as PIXI from "pixi.js"; +import { assetUrl } from "../../../core/AssetUrls"; import { EventBus } from "../../../core/EventBus"; import { PseudoRandom } from "../../../core/PseudoRandom"; import { Config, Theme } from "../../../core/configuration/Config"; @@ -9,8 +10,7 @@ import { AlternateViewEvent } from "../../InputHandler"; import { renderTroops } from "../../Utils"; import { ALLIANCE_ICON_ID, - AllianceProgressIconRefs, - createAllianceProgressIconRefs, + computeAllianceTopCutPercent, EMOJI_ICON_KIND, getFirstPlacePlayer, getPlayerIcons, @@ -18,58 +18,83 @@ import { PlayerIconDescriptor, PlayerIconId, TRAITOR_ICON_ID, - updateAllianceProgressIconRefs, } from "../PlayerIcons"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; - -const PLAYER_NAME = "player-name"; -const PLAYER_NAME_SPAN = "player-name-span"; -const PLAYER_TROOPS = "player-troops"; -const PLAYER_ICONS = "player-icons"; -const PLAYER_FLAG = "player-flag"; +import { NameLayerAssets } from "./NameLayerAssets"; +import { + computeNameLayerLayout, + computeNameLayerScreenMetrics, + computeNameLayerVisible, + computeTraitorFlashAlpha, + replaceUnsupportedNameGlyphs, +} from "./NameLayerLayout"; + +const allianceIconFaded = assetUrl("images/AllianceIconFaded.svg"); +const questionMarkIcon = assetUrl("images/QuestionMarkIcon.svg"); + +type PixiRenderer = PIXI.Renderer | PIXI.WebGLRenderer | PIXI.WebGPURenderer; + +interface PixiIconRender { + container: PIXI.Container; + centered: boolean; + src?: string; + sprite?: PIXI.Sprite; + alliance?: { + base: PIXI.Sprite; + colored: PIXI.Sprite; + questionMark: PIXI.Sprite; + mask: PIXI.Graphics; + }; +} class RenderInfo { - public icons: Map = new Map(); - public allianceIconRefs: AllianceProgressIconRefs | null = null; + public icons: Map = new Map(); + public location: Cell | null = null; + public baseSize = 1; + public fontSize = 0; + public iconSize = 0; + public fontColor = ""; + public flagSrc = ""; + public flagSprite: PIXI.Sprite | null = null; + public lastDisplayName = ""; + public lastTroopsText = ""; constructor( public player: PlayerView, public lastRenderCalc: number, - public location: Cell | null, - public fontSize: number, - public fontColor: string, - public element: HTMLElement, - public nameDiv: HTMLDivElement, - public nameSpan: HTMLSpanElement, - public troopsDiv: HTMLDivElement, - public flagImg: HTMLImageElement, - public iconsDiv: HTMLDivElement, - public lastTransform: string = "", + public container: PIXI.Container, + public nameText: PIXI.BitmapText, + public troopsText: PIXI.BitmapText, ) {} } export class NameLayer implements Layer { private config: Config; private lastChecked = 0; - private renderCheckRate = 100; - private renderRefreshRate = 500; - private rand = new PseudoRandom(10); - private renders: RenderInfo[] = []; - private seenPlayers: Set = new Set(); - private container: HTMLDivElement; + private readonly renderCheckRate = 100; + private readonly renderRefreshRate = 500; + private readonly rand = new PseudoRandom(10); + private readonly renders: RenderInfo[] = []; + private readonly seenPlayers: Set = new Set(); + private readonly rootStage: PIXI.Container = new PIXI.Container(); + private readonly labelStage: PIXI.Container = new PIXI.Container(); + private readonly assets = new NameLayerAssets(); private theme: Theme; private userSettings: UserSettings = new UserSettings(); - private isVisible: boolean = true; + private isVisible = true; private firstPlace: PlayerView | null = null; private allianceDuration: number; - private alliancesDisabled: boolean = false; + private alliancesDisabled = false; private myPlayer: PlayerView | null = null; - private lastContainerTransform: string = ""; - private basePlayerTemplate: HTMLDivElement; - private iconTemplate: HTMLImageElement; - private iconCenterTemplate: HTMLImageElement; - private emojiTemplate: HTMLDivElement; + private readonly pixiCanvas: HTMLCanvasElement = + document.createElement("canvas"); + private readonly onWindowResize = () => this.resizeCanvas(); + private readonly onAlternateViewHandler = (e: AlternateViewEvent) => + this.onAlternateViewChange(e); + private renderer: PixiRenderer | null = null; + private rendererInitialized = false; + private rebuildPending = false; constructor( private game: GameView, @@ -81,86 +106,48 @@ export class NameLayer implements Layer { return false; } - redraw() {} // not affected by Canvas/WebGL context loss as this layer is DOM-based - - public init() { - this.container = document.createElement("div"); - this.container.style.position = "fixed"; - this.container.style.left = "50%"; - this.container.style.top = "50%"; - this.container.style.pointerEvents = "none"; - this.container.style.zIndex = "2"; - document.body.appendChild(this.container); - - // Add CSS keyframes for traitor icon flashing animation - // Append to container instead of document.head to keep styles scoped to this component - const style = document.createElement("style"); - style.textContent = ` - @keyframes traitorFlash { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.3; - } - } - `; - this.container.appendChild(style); - + async init() { this.myPlayer = this.game.myPlayer(); this.config = this.game.config(); this.theme = this.config.theme(); - this.alliancesDisabled = this.config.disableAlliances(); this.allianceDuration = Math.max(1, this.config.allianceDuration()); - this.basePlayerTemplate = this.createBasePlayerElement(); - - this.iconTemplate = document.createElement("img"); - - this.iconCenterTemplate = document.createElement("img"); - this.iconCenterTemplate.style.position = "absolute"; - this.iconCenterTemplate.style.top = "50%"; - this.iconCenterTemplate.style.transform = "translateY(-50%)"; + this.rootStage.addChild(this.labelStage); + this.rootStage.position.set(0, 0); - this.emojiTemplate = document.createElement("div"); - this.emojiTemplate.style.position = "absolute"; - this.emojiTemplate.style.top = "50%"; - this.emojiTemplate.style.transform = "translateY(-50%)"; + this.eventBus.on(AlternateViewEvent, this.onAlternateViewHandler); + window.addEventListener("resize", this.onWindowResize); - this.eventBus.on(AlternateViewEvent, (e) => this.onAlternateViewChange(e)); + await this.setupRenderer(); + this.resizeCanvas(); } - private onAlternateViewChange(event: AlternateViewEvent) { - this.isVisible = !event.alternateView; - // Update visibility of all name elements immediately - for (const render of this.renders) { - this.updateElementVisibility(render); - } - } - - private updateElementVisibility(render: RenderInfo, baseSize?: number) { - if (!render.player.nameLocation() || !render.player.isAlive()) { + async redraw() { + if (this.rebuildPending) { return; } - - baseSize = - baseSize ?? Math.max(1, Math.floor(render.player.nameLocation().size)); - const size = this.transformHandler.scale * baseSize; - const isOnScreen = render.location - ? this.transformHandler.isOnScreen(render.location) - : false; - const maxZoomScale = 17; - - const display = - !this.isVisible || - size < 7 || - (this.transformHandler.scale > maxZoomScale && size > 100) || - !isOnScreen - ? "none" - : "flex"; - if (render.element.style.display !== display) { - render.element.style.display = display; + this.rebuildPending = true; + try { + if (!this.renderer || this.renderer.name === "webgpu") { + this.rendererInitialized = false; + await this.setupRenderer(); + } + this.resizeCanvas(); + for (const render of this.renders) { + render.container.destroy({ children: true }); + } + this.renders.length = 0; + this.seenPlayers.clear(); + } catch (error) { + console.error("NameLayer redraw failed; retrying next frame", error); + this.renderer = null; + this.rendererInitialized = false; + requestAnimationFrame(() => { + void this.redraw(); + }); + } finally { + this.rebuildPending = false; } } @@ -168,385 +155,636 @@ export class NameLayer implements Layer { return 1000; } - public tick() { - // Precompute the first-place player for performance + tick() { this.firstPlace = getFirstPlacePlayer(this.game); for (const player of this.game.playerViews()) { - if (player.isAlive()) { - if (!this.seenPlayers.has(player)) { - this.seenPlayers.add(player); - this.renders.push(this.createPlayerElement(player)); + if (player.isAlive() && !this.seenPlayers.has(player)) { + this.seenPlayers.add(player); + const render = this.createPlayerRender(player); + if (render) { + this.renders.push(render); } } } } - public renderLayer() { - const screenPosOld = this.transformHandler.worldToScreenCoordinates( - new Cell(0, 0), - ); - const screenPos = new Cell( - screenPosOld.x - window.innerWidth / 2, - screenPosOld.y - window.innerHeight / 2, - ); - const newTransform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})`; - if (this.lastContainerTransform !== newTransform) { - this.container.style.transform = newTransform; - this.lastContainerTransform = newTransform; + renderLayer(mainContext: CanvasRenderingContext2D) { + if (this.rendererOrGLContextLost()) { + return; } + this.myPlayer ??= this.game.myPlayer(); + this.updateTransformsAndVisibility(); + const now = Date.now(); if (now > this.lastChecked + this.renderCheckRate) { this.lastChecked = now; - - this.myPlayer ??= this.game.myPlayer(); const transitiveTargets = this.myPlayer?.transitiveTargets() ?? []; - - for (const render of this.renders) { - this.renderPlayerInfo(render, transitiveTargets); + for (const render of [...this.renders]) { + this.renderPlayerInfo(render, transitiveTargets, now); } } - } - private createBasePlayerElement(): HTMLDivElement { - const element = document.createElement("div"); - element.style.position = "absolute"; - element.style.flexDirection = "column"; - element.style.alignItems = "center"; - element.style.gap = "0px"; - // Start off invisible so it doesn't flash at 0,0 - element.style.display = "none"; - - const iconsDiv = document.createElement("div"); - iconsDiv.classList.add(PLAYER_ICONS); - iconsDiv.style.display = "flex"; - iconsDiv.style.gap = "4px"; - iconsDiv.style.justifyContent = "center"; - iconsDiv.style.alignItems = "center"; - iconsDiv.style.zIndex = "2"; - iconsDiv.style.opacity = "0.8"; - element.appendChild(iconsDiv); - - const nameDiv = document.createElement("div"); - nameDiv.classList.add(PLAYER_NAME); - nameDiv.style.whiteSpace = "nowrap"; - nameDiv.style.textOverflow = "ellipsis"; - nameDiv.style.zIndex = "3"; - nameDiv.style.display = "flex"; - nameDiv.style.justifyContent = "flex-end"; - nameDiv.style.alignItems = "center"; - - const flagImg = document.createElement("img"); - flagImg.classList.add(PLAYER_FLAG); - flagImg.style.opacity = "0.8"; - flagImg.style.zIndex = "1"; - flagImg.style.objectFit = "contain"; - flagImg.style.display = "none"; - nameDiv.appendChild(flagImg); - - const nameSpan = document.createElement("span"); - nameSpan.classList.add(PLAYER_NAME_SPAN); - nameDiv.appendChild(nameSpan); - element.appendChild(nameDiv); - - const troopsDiv = document.createElement("div"); - troopsDiv.classList.add(PLAYER_TROOPS); - troopsDiv.setAttribute("translate", "no"); - troopsDiv.style.zIndex = "3"; - troopsDiv.style.marginTop = "-5%"; - element.appendChild(troopsDiv); - - return element; + this.renderer?.render(this.rootStage); + if (this.renderer) { + mainContext.drawImage( + this.renderer.canvas, + 0, + 0, + this.renderer.canvas.width, + this.renderer.canvas.height, + 0, + 0, + mainContext.canvas.width, + mainContext.canvas.height, + ); + } } - private createPlayerElement(player: PlayerView): RenderInfo { - const element = this.basePlayerTemplate.cloneNode(true) as HTMLDivElement; - - // Queryselector expensive but this runs only once per player and better maintainable - const nameDiv = element.querySelector(`.${PLAYER_NAME}`) as HTMLDivElement; - const nameSpan = element.querySelector( - `.${PLAYER_NAME_SPAN}`, - ) as HTMLSpanElement; - const troopsDiv = element.querySelector( - `.${PLAYER_TROOPS}`, - ) as HTMLDivElement; - const flagImg = element.querySelector( - `.${PLAYER_FLAG}`, - ) as HTMLImageElement; - const iconsDiv = element.querySelector( - `.${PLAYER_ICONS}`, - ) as HTMLDivElement; - - const font = this.theme.font(); - nameDiv.style.fontFamily = font; - - const flag = player.cosmetics.flag; - if (flag) { - flagImg.src = assetUrl(flag); - flagImg.style.display = "block"; - } - - const renderInfo = new RenderInfo( - player, - 0, - null, - 0, - "", - element, - nameDiv, - nameSpan, - troopsDiv, - flagImg, - iconsDiv, - ); + private async setupRenderer() { + if (this.renderer) { + this.renderer.destroy(false); + this.renderer = null; + this.rendererInitialized = false; + this.labelStage.removeChildren(); + } + + await this.assets.preload(); + + const resolution = window.devicePixelRatio || 1; + this.resizePixiCanvasElement(resolution); - this.container.appendChild(element); - return renderInfo; + const renderer = await PIXI.autoDetectRenderer({ + canvas: this.pixiCanvas, + resolution, + width: window.innerWidth, + height: window.innerHeight, + antialias: false, + clearBeforeRender: true, + backgroundAlpha: 0, + backgroundColor: 0x00000000, + }); + + console.info(`Using ${renderer.name} for name layer`); + this.renderer = renderer; + + if (this.renderer.name === "webgpu") { + const gpuRenderer = this.renderer as PIXI.WebGPURenderer; + gpuRenderer.gpu.device.lost.then(() => { + // device.lost is a one-time Promise; setupRenderer() intentionally + // re-attaches this handler on rebuild so future losses are observed. + void this.redraw(); + }); + } + + if (this.renderer.name === "webgl") { + this.renderer.runners.contextChange.add({ + contextChange: () => { + requestAnimationFrame(() => { + void this.redraw(); + }); + }, + }); + } + + this.rendererInitialized = true; } - renderPlayerInfo(render: RenderInfo, transitiveTargets: PlayerView[]) { - if (!render.player.nameLocation()) { - return; + private rendererOrGLContextLost(): boolean { + if (!this.renderer || !this.rendererInitialized) return true; + if (this.renderer.name === "webgl") { + return (this.renderer as PIXI.WebGLRenderer).context?.isLost === true; } - if (!render.player.isAlive()) { - this.renders = this.renders.filter((r) => r !== render); - render.element.remove(); + return false; + } + + private resizeCanvas() { + if (this.rendererOrGLContextLost()) { return; } + const resolution = window.devicePixelRatio || 1; + this.resizePixiCanvasElement(resolution); + this.renderer?.resize(window.innerWidth, window.innerHeight, resolution); + } - // Update location and size, show or hide dependent on those - const nameLocation = render.player.nameLocation(); - const newX = nameLocation.x; - const newY = nameLocation.y; + private resizePixiCanvasElement(resolution: number) { + this.pixiCanvas.width = Math.ceil(window.innerWidth * resolution); + this.pixiCanvas.height = Math.ceil(window.innerHeight * resolution); + this.pixiCanvas.style.width = `${window.innerWidth}px`; + this.pixiCanvas.style.height = `${window.innerHeight}px`; + } - if ( - !render.location || - render.location.x !== newX || - render.location.y !== newY - ) { - render.location = new Cell(newX, newY); + private onAlternateViewChange(event: AlternateViewEvent) { + this.isVisible = !event.alternateView; + this.updateTransformsAndVisibility(); + } + + private createPlayerRender(player: PlayerView): RenderInfo | null { + if (!this.assets.fontReady) { + return null; } - const baseSize = Math.max(1, Math.floor(nameLocation.size)); - this.updateElementVisibility(render, baseSize); + const container = new PIXI.Container(); + container.visible = false; - if (render.element.style.display === "none") { - return; - } + const nameText = this.createBitmapText(""); + const troopsText = this.createBitmapText(""); - // Throttle further updates - const now = Date.now(); - if (now - render.lastRenderCalc <= this.renderRefreshRate) { - return; + container.addChild(nameText, troopsText); + this.labelStage.addChild(container); + + const render = new RenderInfo(player, 0, container, nameText, troopsText); + this.updateFlag(render); + return render; + } + + private createBitmapText(text: string): PIXI.BitmapText { + if (!this.assets.fontReady || !this.assets.fontFamily) { + throw new Error("NameLayer bitmap font is not ready"); } - render.lastRenderCalc = now + this.rand.nextInt(0, 100); - // Update text sizes, content and color - render.fontSize = Math.max(4, Math.floor(baseSize * 0.4)); - render.nameDiv.style.fontSize = `${render.fontSize}px`; - render.nameDiv.style.lineHeight = `${render.fontSize}px`; - render.flagImg.style.height = `${render.fontSize}px`; - render.troopsDiv.style.fontSize = `${render.fontSize}px`; + const bitmapText = new PIXI.BitmapText({ + text, + style: { + fontFamily: this.assets.fontFamily, + fontSize: 12, + fill: "#ffffff", + }, + }); + bitmapText.anchor.set(0.5); + return bitmapText; + } - render.nameSpan.textContent = render.player.displayName(); - render.troopsDiv.textContent = renderTroops(render.player.troops()); + private updateTransformsAndVisibility() { + const now = performance.now(); + for (const render of this.renders) { + const nameLocation = render.player.nameLocation(); + if (!nameLocation || !render.player.isAlive()) { + render.container.visible = false; + continue; + } - const fontColor = this.theme.textColor(render.player); - if (render.fontColor !== fontColor) { - render.fontColor = fontColor; - render.nameDiv.style.color = fontColor; - render.troopsDiv.style.color = fontColor; + render.baseSize = Math.max(1, Math.floor(nameLocation.size)); + const metrics = computeNameLayerScreenMetrics( + render.baseSize, + this.transformHandler.scale, + ); + if ( + render.fontSize !== metrics.fontSize || + render.iconSize !== metrics.iconSize + ) { + render.fontSize = metrics.fontSize; + render.iconSize = metrics.iconSize; + this.updateText(render); + this.resizeIcons(render, render.iconSize); + this.layoutRender(render, render.iconSize); + } + render.location = new Cell(nameLocation.x, nameLocation.y); + const isOnScreen = this.transformHandler.isOnScreen(render.location); + render.container.visible = computeNameLayerVisible({ + isLayerVisible: this.isVisible, + transformScale: this.transformHandler.scale, + baseSize: render.baseSize, + isOnScreen, + }); + + if (!render.container.visible) { + continue; + } + + const screenPos = this.transformHandler.worldToCanvasCoordinates( + render.location, + ); + render.container.position.set(screenPos.x, screenPos.y); + render.container.scale.set(1); + this.updateTraitorAlpha(render, now); + } + } + + private renderPlayerInfo( + render: RenderInfo, + transitiveTargets: PlayerView[], + now: number, + ) { + if (!render.player.nameLocation()) { + return; + } + if (!render.player.isAlive()) { + this.deleteRender(render); + return; + } + if (!render.container.visible) { + return; } + if (now - render.lastRenderCalc <= this.renderRefreshRate) { + return; + } + render.lastRenderCalc = now + this.rand.nextInt(0, 100); - // Handle icons - const iconSize = Math.min(render.fontSize * 1.5, 48); - const darkMode = this.userSettings.darkMode(); - const darkModeStr = darkMode.toString(); + this.updateText(render); + this.updateFlag(render); - // Compute which icons should be shown for this player using shared logic const icons = getPlayerIcons({ game: this.game, player: render.player, includeAllianceIcon: true, firstPlace: this.firstPlace, - darkMode: darkMode, + darkMode: this.userSettings.darkMode(), alliancesDisabled: this.alliancesDisabled, - transitiveTargets: transitiveTargets, + transitiveTargets, }); - // Build a set of desired icon IDs - const desiredIconIds = new Set(icons.map((icon) => icon.id)); - - // Remove any icons that are no longer needed - for (const [id, element] of render.icons) { - if (!desiredIconIds.has(id)) { - if (id === ALLIANCE_ICON_ID) { - render.allianceIconRefs?.wrapper.remove(); - render.allianceIconRefs = null; - render.icons.delete(ALLIANCE_ICON_ID); - } else { - element.remove(); - render.icons.delete(id); - } + this.updateIcons(render, icons, render.iconSize); + this.layoutRender(render, render.iconSize); + } + + private updateText(render: RenderInfo) { + if (!this.assets.fontFamily) { + return; + } + + const displayName = replaceUnsupportedNameGlyphs( + render.player.displayName(), + ); + const troopsText = replaceUnsupportedNameGlyphs( + renderTroops(render.player.troops()), + ); + const fontColor = this.theme.textColor(render.player); + const prevFontColor = render.fontColor; + + if ( + render.lastDisplayName !== displayName || + prevFontColor !== fontColor || + render.nameText.style.fontSize !== render.fontSize || + render.nameText.style.fontFamily !== this.assets.fontFamily + ) { + render.nameText.text = displayName; + render.nameText.style = { + fontFamily: this.assets.fontFamily, + fontSize: render.fontSize, + fill: fontColor, + }; + render.lastDisplayName = displayName; + } + + if ( + render.lastTroopsText !== troopsText || + prevFontColor !== fontColor || + render.troopsText.style.fontSize !== render.fontSize || + render.troopsText.style.fontFamily !== this.assets.fontFamily + ) { + render.troopsText.text = troopsText; + render.troopsText.style = { + fontFamily: this.assets.fontFamily, + fontSize: render.fontSize, + fill: fontColor, + }; + render.lastTroopsText = troopsText; + } + + render.fontColor = fontColor; + } + + private updateFlag(render: RenderInfo) { + const flag = render.player.cosmetics.flag; + const src = flag ? assetUrl(flag) : ""; + if (!src) { + this.hideFlag(render, true); + return; + } + + if (src !== render.flagSrc) { + this.hideFlag(render, true); + } + + const texture = this.assets.getTexture(src); + if (!texture) { + this.hideFlag(render, false); + return; + } + + if (!render.flagSprite) { + render.flagSprite = new PIXI.Sprite(texture); + render.flagSprite.anchor.set(0.5); + render.flagSprite.alpha = 0.8; + render.container.addChild(render.flagSprite); + } else if (render.flagSprite.texture !== texture) { + render.flagSprite.texture = texture; + } + + render.flagSrc = src; + render.flagSprite.visible = true; + } + + private hideFlag(render: RenderInfo, clearSource: boolean) { + render.flagSprite?.destroy(); + render.flagSprite = null; + if (clearSource) { + render.flagSrc = ""; + } + } + + private updateIcons( + render: RenderInfo, + icons: PlayerIconDescriptor[], + size: number, + ) { + const desiredIds = new Set(icons.map((icon) => icon.id)); + for (const [id, iconRender] of render.icons) { + if (!desiredIds.has(id)) { + iconRender.container.destroy({ children: true }); + render.icons.delete(id); } } - // Add or update icons that should be shown for (const icon of icons) { - if (icon.kind === EMOJI_ICON_KIND && icon.text) { - this.handleEmojiIcon(render, icon, iconSize); - continue; - } else if (!(icon.kind === IMAGE_ICON_KIND && icon.src)) { - continue; + if (icon.kind === EMOJI_ICON_KIND) { + this.updateEmojiIcon(render, icon, size); + } else if (icon.id === ALLIANCE_ICON_ID) { + this.updateAllianceIcon(render, icon, size); + } else if (icon.kind === IMAGE_ICON_KIND && icon.src) { + this.updateImageIcon(render, icon, size); + } + } + } + + private resizeIcons(render: RenderInfo, size: number) { + for (const iconRender of render.icons.values()) { + if (iconRender.sprite) { + iconRender.sprite.width = size; + iconRender.sprite.height = size; } - // Special handling for alliance icon with progress indicator - if (icon.id === ALLIANCE_ICON_ID) { - this.handleAllianceIcons(render, iconSize, darkModeStr); - continue; // Skip regular image handling + if (iconRender.alliance) { + const refs = iconRender.alliance; + refs.base.width = size; + refs.base.height = size; + refs.colored.width = size; + refs.colored.height = size; + refs.questionMark.width = size; + refs.questionMark.height = size; + this.updateAllianceProgressMask(render, refs, size); } + } + } - const imgElement = this.handleOtherIcons( - render, - icon, - iconSize, - darkModeStr, - ); + private updateImageIcon( + render: RenderInfo, + icon: PlayerIconDescriptor, + size: number, + ) { + const src = icon.src; + if (!src) { + return; + } + + let iconRender = render.icons.get(icon.id); + if (!iconRender || iconRender.src !== src || !iconRender.sprite) { + iconRender?.container.destroy({ children: true }); + const container = new PIXI.Container(); + container.alpha = 0.8; + const sprite = new PIXI.Sprite(); + sprite.anchor.set(0.5); + container.addChild(sprite); + render.container.addChild(container); + iconRender = { + container, + centered: icon.center ?? false, + src, + sprite, + }; + render.icons.set(icon.id, iconRender); + } + + iconRender.centered = icon.center ?? false; + const texture = this.assets.getTexture(src); + iconRender.container.visible = texture !== null; + if (!texture) { + return; + } + + iconRender.sprite!.texture = texture; + iconRender.sprite!.width = size; + iconRender.sprite!.height = size; + } - // Traitor flashing - smooth speed increase starting at 15s - if (icon.id === TRAITOR_ICON_ID) { - this.handleTraitorIconFlashing(render.player, imgElement); + private updateEmojiIcon( + render: RenderInfo, + icon: PlayerIconDescriptor, + size: number, + ) { + const text = icon.text ?? ""; + const texture = text ? this.assets.getEmojiTexture(text) : null; + if (!texture) { + const existing = render.icons.get(icon.id); + if (existing) { + existing.container.visible = false; } + return; } - // Position element with scale - // Don't require nameLocation to be changed: Scale update otherwise sometimes only happens after seconds which looks buggy. - // Because of sometimes overlapping delays of 20 ticks for nameLocation() (largestClusterBoundingBox in PlayerExecution) - // and the 500ms renderRefreshRate in here. - const scale = Math.min(baseSize * 0.25, 3); - const transform = `translate(${newX}px, ${newY}px) translate(-50%, -50%) scale(${scale})`; - if (render.lastTransform !== transform) { - render.element.style.transform = transform; - render.lastTransform = transform; + let iconRender = render.icons.get(icon.id); + if (!iconRender || iconRender.src !== text || !iconRender.sprite) { + iconRender?.container.destroy({ children: true }); + const container = new PIXI.Container(); + container.alpha = 0.8; + const sprite = new PIXI.Sprite(texture); + sprite.anchor.set(0.5); + container.addChild(sprite); + render.container.addChild(container); + iconRender = { + container, + centered: icon.center ?? false, + src: text, + sprite, + }; + render.icons.set(icon.id, iconRender); } + + iconRender.centered = icon.center ?? false; + iconRender.sprite!.texture = texture; + iconRender.sprite!.width = size; + iconRender.sprite!.height = size; + iconRender.container.visible = true; } - private handleEmojiIcon( + private updateAllianceIcon( render: RenderInfo, icon: PlayerIconDescriptor, size: number, ) { - let emojiDiv = render.icons.get(icon.id) as HTMLDivElement | undefined; + let iconRender = render.icons.get(icon.id); + if (!iconRender || !iconRender.alliance) { + iconRender?.container.destroy({ children: true }); + const container = new PIXI.Container(); + container.alpha = 0.8; + const base = new PIXI.Sprite(); + const colored = new PIXI.Sprite(); + const questionMark = new PIXI.Sprite(); + const mask = new PIXI.Graphics(); + for (const sprite of [base, colored, questionMark]) { + sprite.anchor.set(0.5); + container.addChild(sprite); + } + colored.mask = mask; + container.addChild(mask); + render.container.addChild(container); + iconRender = { + container, + centered: false, + src: icon.src, + alliance: { base, colored, questionMark, mask }, + }; + render.icons.set(icon.id, iconRender); + } - if (!emojiDiv) { - emojiDiv = this.emojiTemplate.cloneNode(true) as HTMLDivElement; - render.iconsDiv.appendChild(emojiDiv); - render.icons.set(icon.id, emojiDiv); + const baseTexture = this.assets.getTexture(allianceIconFaded); + const coloredTexture = icon.src ? this.assets.getTexture(icon.src) : null; + const questionTexture = this.assets.getTexture(questionMarkIcon); + iconRender.container.visible = + baseTexture !== null && coloredTexture !== null; + if (!baseTexture || !coloredTexture) { + return; } - emojiDiv.textContent = icon.text ?? ""; - emojiDiv.style.fontSize = `${size}px`; + const refs = iconRender.alliance!; + refs.base.texture = baseTexture; + refs.colored.texture = coloredTexture; + iconRender.src = icon.src; + refs.base.width = size; + refs.base.height = size; + refs.colored.width = size; + refs.colored.height = size; + + this.updateAllianceProgressMask(render, refs, size); + + refs.questionMark.visible = + this.hasAllianceExtensionRequest(render) && questionTexture !== null; + if (questionTexture) { + refs.questionMark.texture = questionTexture; + refs.questionMark.width = size; + refs.questionMark.height = size; + } } - private handleAllianceIcons( + private updateAllianceProgressMask( render: RenderInfo, + refs: PixiIconRender["alliance"], size: number, - darkMode: string, ) { + if (!refs) { + return; + } + this.myPlayer ??= this.game.myPlayer(); const allianceView = this.myPlayer ?.alliances() .find((a) => a.other === render.player.id()); + const remaining = allianceView + ? Math.max(0, allianceView.expiresAt - this.game.ticks()) + : 0; + const fraction = Math.max( + 0, + Math.min(1, remaining / this.allianceDuration), + ); + const topCut = (computeAllianceTopCutPercent(fraction) / 100) * size; + refs.mask.clear(); + // computeAllianceTopCutPercent can intentionally make the visible alliance + // height zero when remaining / this.allianceDuration is depleted; PIXI v8 + // tolerates the zero-area refs.mask.rect and the Math.max guard preserves it. + refs.mask + .rect(-size / 2, -size / 2 + topCut, size, Math.max(0, size - topCut)) + .fill(0xffffff); + } - let fraction = 0; - let hasExtensionRequest = false; - if (allianceView) { - const remaining = Math.max(0, allianceView.expiresAt - this.game.ticks()); - fraction = Math.max(0, Math.min(1, remaining / this.allianceDuration)); - hasExtensionRequest = allianceView.hasExtensionRequest; - } + private hasAllianceExtensionRequest(render: RenderInfo): boolean { + this.myPlayer ??= this.game.myPlayer(); + return ( + this.myPlayer?.alliances().find((a) => a.other === render.player.id()) + ?.hasExtensionRequest === true + ); + } - if (!render.allianceIconRefs) { - render.allianceIconRefs = createAllianceProgressIconRefs( - size, - fraction, - hasExtensionRequest, - darkMode, - ); + private layoutRender(render: RenderInfo, iconSize: number) { + const regularIcons = Array.from(render.icons.values()).filter( + (icon) => !icon.centered && icon.container.visible, + ); + const centeredIcons = Array.from(render.icons.values()).filter( + (icon) => icon.centered && icon.container.visible, + ); + const flagTexture = render.flagSprite?.visible + ? render.flagSprite.texture + : null; + const flagAspectRatio = + flagTexture && flagTexture.height > 0 + ? flagTexture.width / flagTexture.height + : 1; + + const layout = computeNameLayerLayout({ + fontSize: render.fontSize, + iconSize, + iconCount: regularIcons.length, + centeredIconCount: centeredIcons.length, + hasFlag: render.flagSprite?.visible === true, + flagAspectRatio, + nameWidth: render.nameText.width, + troopWidth: render.troopsText.width, + }); - render.iconsDiv.appendChild(render.allianceIconRefs.wrapper); - render.icons.set(ALLIANCE_ICON_ID, render.allianceIconRefs.wrapper); - } else { - updateAllianceProgressIconRefs( - render.allianceIconRefs, - size, - fraction, - hasExtensionRequest, - darkMode, - ); + regularIcons.forEach((icon, index) => { + const pos = layout.iconPositions[index]; + icon.container.position.set(pos.x, pos.y); + }); + centeredIcons.forEach((icon, index) => { + const pos = layout.centeredIconPositions[index]; + icon.container.position.set(pos.x, pos.y); + }); + + if (render.flagSprite && layout.flag) { + render.flagSprite.position.set(layout.flag.x, layout.flag.y); + render.flagSprite.width = layout.flag.width; + render.flagSprite.height = layout.flag.height; + render.flagSprite.visible = true; + } else if (render.flagSprite) { + render.flagSprite.visible = false; } - return; + + render.nameText.position.set(layout.nameText.x, layout.nameText.y); + render.troopsText.position.set(layout.troopText.x, layout.troopText.y); } - private handleOtherIcons( - render: RenderInfo, - icon: PlayerIconDescriptor, - size: number, - darkMode: string, - ): HTMLImageElement { - let imgElement = render.icons.get(icon.id) as HTMLImageElement | undefined; - - if (!imgElement) { - imgElement = icon.center - ? (this.iconCenterTemplate.cloneNode(true) as HTMLImageElement) - : (this.iconTemplate.cloneNode(true) as HTMLImageElement); - - imgElement.src = icon.src ?? ""; - imgElement.style.width = `${size}px`; - imgElement.style.height = `${size}px`; - imgElement.setAttribute("dark-mode", darkMode); - render.iconsDiv.appendChild(imgElement); - render.icons.set(icon.id, imgElement); - } else { - // Update src if it changed (e.g., nuke red/white or dark-mode icons) - if (imgElement.src !== icon.src) { - imgElement.src = icon.src ?? ""; - } + private updateTraitorAlpha(render: RenderInfo, nowMs: number) { + const traitorIcon = render.icons.get(TRAITOR_ICON_ID); + if (!traitorIcon) { + return; + } + traitorIcon.container.alpha = + computeTraitorFlashAlpha( + render.player.getTraitorRemainingTicks(), + nowMs, + ) * 0.8; + } - imgElement.style.width = `${size}px`; - imgElement.style.height = `${size}px`; - imgElement.setAttribute("dark-mode", darkMode); + private deleteRender(render: RenderInfo) { + const index = this.renders.indexOf(render); + if (index >= 0) { + this.renders.splice(index, 1); } - return imgElement; + this.seenPlayers.delete(render.player); + render.container.destroy({ children: true }); } - private handleTraitorIconFlashing( - player: PlayerView, - icon: HTMLImageElement, - ) { - const remainingTicks = player.getTraitorRemainingTicks(); - // Use precise seconds (not rounded) for smoother transitions, rounded to 0.5s intervals - const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2; - - if (remainingSeconds <= 15) { - // Smooth transition: starts at 1s at 15 seconds, decreases to 0.2s at 0 seconds - // Using cubic ease-out for slower, more gradual acceleration - const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds)); - const normalizedTime = clampedSeconds / 15; // 0 to 1 (1 = 15s remaining, 0 = 0s remaining) - - // Cubic ease-out: slower acceleration, smoother transition - const easedProgress = 1 - Math.pow(1 - normalizedTime, 3); - const maxDuration = 1.0; // Slow flash at 15 seconds - const minDuration = 0.2; // Fast flash at 0 seconds - const duration = - minDuration + (maxDuration - minDuration) * easedProgress; - const animationDuration = `${duration.toFixed(2)}s`; - - icon.style.animation = `traitorFlash ${animationDuration} infinite`; - icon.style.animationTimingFunction = "ease-in-out"; - } else { - // Don't flash if more than 15 seconds remaining - icon.style.animation = "none"; + destroy() { + this.eventBus.off(AlternateViewEvent, this.onAlternateViewHandler); + window.removeEventListener("resize", this.onWindowResize); + for (const render of this.renders) { + render.container.destroy({ children: true }); } + this.renders.length = 0; + this.seenPlayers.clear(); + this.rootStage.removeChildren(); + this.renderer?.destroy(true); + this.renderer = null; + this.rendererInitialized = false; } } diff --git a/src/client/graphics/layers/NameLayerAssets.ts b/src/client/graphics/layers/NameLayerAssets.ts new file mode 100644 index 0000000000..51dbff7d49 --- /dev/null +++ b/src/client/graphics/layers/NameLayerAssets.ts @@ -0,0 +1,164 @@ +import * as PIXI from "pixi.js"; +import { assetUrl } from "../../../core/AssetUrls"; + +const nameLayerFont = assetUrl("fonts/namelayer_overpass.xml"); +const fallbackFont = assetUrl("fonts/round_6x6_modified.xml"); +const iconAtlas = assetUrl("images/namelayer-icons.json"); +const emojiAtlas = assetUrl("images/namelayer-emojis.json"); + +export const NAME_LAYER_FONT_FAMILY = "namelayer_overpass"; +export const NAME_LAYER_FALLBACK_FONT_FAMILY = "round_6x6_modified"; + +export class NameLayerAssets { + public fontFamily: string | null = null; + public fontReady = false; + + private readonly textures = new Map(); + private readonly atlasTextures = new Map(); + private readonly emojiTextures = new Map(); + private readonly pendingTextures = new Map>(); + private readonly failedTextures = new Set(); + private readonly warnedTextureFailures = new Set(); + private readonly warnedMissingEmojis = new Set(); + private preloadPromise: Promise | null = null; + + preload(): Promise { + this.preloadPromise ??= this.loadBaseAssets(); + return this.preloadPromise; + } + + getTexture(src: string): PIXI.Texture | null { + const atlasTexture = this.atlasTextures.get(textureKeyFromSrc(src)); + if (atlasTexture) { + return atlasTexture; + } + + const cached = this.textures.get(src); + if (cached) { + return cached; + } + + if (this.failedTextures.has(src)) { + return null; + } + + if (!this.pendingTextures.has(src)) { + this.pendingTextures.set( + src, + PIXI.Assets.load(src) + .then((texture: PIXI.Texture) => { + this.textures.set(src, texture); + }) + .catch((error) => { + this.textures.delete(src); + this.failedTextures.add(src); + this.warnTextureFailure(src, error); + }) + .finally(() => { + this.pendingTextures.delete(src); + }), + ); + } + + return null; + } + + getEmojiTexture(emoji: string): PIXI.Texture | null { + const texture = this.emojiTextures.get(emoji); + if (texture) { + return texture; + } + if (!this.warnedMissingEmojis.has(emoji)) { + this.warnedMissingEmojis.add(emoji); + console.warn(`NameLayer emoji omitted; atlas frame missing: ${emoji}`); + } + return null; + } + + preloadTextures(srcs: Iterable): void { + for (const src of srcs) { + this.getTexture(src); + } + } + + resetWarningsForTests(): void { + this.warnedTextureFailures.clear(); + this.warnedMissingEmojis.clear(); + this.failedTextures.clear(); + this.preloadPromise = null; + this.fontReady = false; + this.fontFamily = null; + } + + private async loadBaseAssets(): Promise { + await this.loadFont(); + await Promise.all([ + this.loadOptionalAtlas( + iconAtlas, + "static icon atlas", + this.atlasTextures, + ), + this.loadOptionalAtlas(emojiAtlas, "emoji atlas", this.emojiTextures), + ]); + } + + private async loadFont(): Promise { + try { + await PIXI.Assets.load(nameLayerFont); + this.fontFamily = NAME_LAYER_FONT_FAMILY; + this.fontReady = true; + return; + } catch (error) { + console.warn( + "NameLayer generated bitmap font unavailable; using fallback font", + error, + ); + } + + try { + await PIXI.Assets.load(fallbackFont); + this.fontFamily = NAME_LAYER_FALLBACK_FONT_FAMILY; + this.fontReady = true; + } catch (error) { + this.fontFamily = null; + this.fontReady = false; + console.error("NameLayer failed to load bitmap font", error); + } + } + + private async loadOptionalAtlas( + src: string, + label: string, + target: Map, + ): Promise { + try { + const atlas = (await PIXI.Assets.load(src)) as { + textures?: Record; + }; + for (const [key, texture] of Object.entries(atlas.textures ?? {})) { + target.set(key, texture); + } + } catch (error) { + console.warn(`NameLayer ${label} unavailable`, error); + } + } + + private warnTextureFailure(src: string, error: unknown): void { + if (this.warnedTextureFailures.has(src)) { + return; + } + this.warnedTextureFailures.add(src); + console.warn(`NameLayer texture omitted after load failure: ${src}`, error); + } +} + +function textureKeyFromSrc(src: string): string { + const clean = src.split(/[?#]/, 1)[0] ?? src; + const slash = clean.lastIndexOf("/"); + const key = slash >= 0 ? clean.slice(slash + 1) : clean; + try { + return decodeURIComponent(key); + } catch { + return key; + } +} diff --git a/src/client/graphics/layers/NameLayerLayout.ts b/src/client/graphics/layers/NameLayerLayout.ts new file mode 100644 index 0000000000..36e02a8f98 --- /dev/null +++ b/src/client/graphics/layers/NameLayerLayout.ts @@ -0,0 +1,248 @@ +export const NAME_LAYER_ICON_GAP = 4; +export const NAME_LAYER_MAX_ZOOM_SCALE = 17; +export const NAME_LAYER_TROOP_MARGIN_RATIO = -0.05; + +export interface NameLayerVisibilityInput { + isLayerVisible: boolean; + transformScale: number; + baseSize: number; + isOnScreen: boolean; +} + +export interface NameLayerLayoutInput { + fontSize: number; + iconSize: number; + iconCount: number; + centeredIconCount: number; + hasFlag: boolean; + flagAspectRatio: number; + nameWidth: number; + troopWidth: number; +} + +export interface NameLayerLayout { + flag: { x: number; y: number; width: number; height: number } | null; + nameText: { x: number; y: number }; + troopText: { x: number; y: number }; + iconPositions: { x: number; y: number }[]; + centeredIconPositions: { x: number; y: number }[]; + height: number; + width: number; + rows: { iconsY: number | null; nameY: number; troopsY: number }; +} + +export interface NameLayerScreenMetrics { + fontSize: number; + iconSize: number; +} + +const SUPPORTED_TEXT_CHARS = new Set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ \u00fc\u00dc.[]+-=(),':!?/@#$%&\"".split( + "", + ), +); + +const warnedUnsupportedGlyphs = new Set(); + +type IntlSegmenterConstructor = new ( + locales?: string | string[], + options?: { granularity: "grapheme" }, +) => { segment(value: string): Iterable<{ segment: string }> }; + +export function computeNameLayerVisible({ + isLayerVisible, + transformScale, + baseSize, + isOnScreen, +}: NameLayerVisibilityInput): boolean { + const size = transformScale * baseSize; + return ( + isLayerVisible && + size >= 7 && + !(transformScale > NAME_LAYER_MAX_ZOOM_SCALE && size > 100) && + isOnScreen + ); +} + +export function computeNameLayerScale(baseSize: number): number { + return Math.min(baseSize * 0.25, 3); +} + +export function computeNameLayerWorldScale( + baseSize: number, + transformScale: number, +): number { + return computeNameLayerScale(baseSize) * transformScale; +} + +export function computeNameLayerFontSize(baseSize: number): number { + return Math.max(4, Math.floor(baseSize * 0.4)); +} + +export function computeNameLayerScreenMetrics( + baseSize: number, + transformScale: number, +): NameLayerScreenMetrics { + const worldScale = computeNameLayerWorldScale(baseSize, transformScale); + const localFontSize = computeNameLayerFontSize(baseSize); + const localIconSize = Math.min(localFontSize * 1.5, 48); + return { + fontSize: Math.max(1, localFontSize * worldScale), + iconSize: Math.max(1, localIconSize * worldScale), + }; +} + +export function computeNameLayerLayout({ + fontSize, + iconSize, + iconCount, + centeredIconCount, + hasFlag, + flagAspectRatio, + nameWidth, + troopWidth, +}: NameLayerLayoutInput): NameLayerLayout { + const visibleIconCount = Math.max(0, iconCount); + const iconRowHeight = visibleIconCount > 0 ? iconSize : 0; + const iconRowWidth = + visibleIconCount > 0 + ? visibleIconCount * iconSize + + (visibleIconCount - 1) * NAME_LAYER_ICON_GAP + : 0; + const flagHeight = hasFlag ? fontSize : 0; + const flagWidth = hasFlag ? Math.max(0, flagHeight * flagAspectRatio) : 0; + const nameRowHeight = fontSize; + const troopMargin = fontSize * NAME_LAYER_TROOP_MARGIN_RATIO; + const troopHeight = fontSize; + const nameRowWidth = flagWidth + nameWidth; + const totalHeight = iconRowHeight + nameRowHeight + troopMargin + troopHeight; + const width = Math.max(iconRowWidth, nameRowWidth, troopWidth); + + let cursorY = -totalHeight / 2; + const iconsY = visibleIconCount > 0 ? cursorY + iconRowHeight / 2 : null; + cursorY += iconRowHeight; + const nameY = cursorY + nameRowHeight / 2; + cursorY += nameRowHeight + troopMargin; + const troopsY = cursorY + troopHeight / 2; + + const iconPositions: { x: number; y: number }[] = []; + if (visibleIconCount > 0 && iconsY !== null) { + const startX = -iconRowWidth / 2 + iconSize / 2; + for (let i = 0; i < visibleIconCount; i++) { + iconPositions.push({ + x: startX + i * (iconSize + NAME_LAYER_ICON_GAP), + y: iconsY, + }); + } + } + + const nameStartX = -nameRowWidth / 2; + const flag = hasFlag + ? { + x: nameStartX + flagWidth / 2, + y: nameY, + width: flagWidth, + height: flagHeight, + } + : null; + const nameTextX = nameStartX + flagWidth + nameWidth / 2; + const visibleCenteredIconCount = Math.max(0, centeredIconCount); + const centeredIconRowWidth = + visibleCenteredIconCount > 0 + ? visibleCenteredIconCount * iconSize + + (visibleCenteredIconCount - 1) * NAME_LAYER_ICON_GAP + : 0; + const centeredIconPositions = Array.from( + { length: visibleCenteredIconCount }, + (_, index) => ({ + x: + -centeredIconRowWidth / 2 + + iconSize / 2 + + index * (iconSize + NAME_LAYER_ICON_GAP), + y: nameY, + }), + ); + + return { + flag, + nameText: { x: nameTextX, y: nameY }, + troopText: { x: 0, y: troopsY }, + iconPositions, + centeredIconPositions, + height: totalHeight, + width, + rows: { iconsY, nameY, troopsY }, + }; +} + +export function computeTraitorFlashDurationSeconds( + remainingTicks: number, +): number | null { + const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2; + if (remainingSeconds > 15) { + return null; + } + + const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds)); + const normalizedTime = clampedSeconds / 15; + const easedProgress = 1 - Math.pow(1 - normalizedTime, 3); + return 0.2 + (1.0 - 0.2) * easedProgress; +} + +export function computeTraitorFlashAlpha( + remainingTicks: number, + nowMs: number, +): number { + const duration = computeTraitorFlashDurationSeconds(remainingTicks); + if (duration === null) { + return 1; + } + + const durationMs = Math.max(1, duration * 1000); + const phase = (nowMs % durationMs) / durationMs; + const wave = phase < 0.5 ? phase / 0.5 : (1 - phase) / 0.5; + const eased = 0.5 - Math.cos(wave * Math.PI) / 2; + return 1 - eased * 0.7; +} + +export function replaceUnsupportedNameGlyphs( + value: string, + warn: (message: string) => void = console.warn, +): string { + let changed = false; + let result = ""; + + const segments = segmentGraphemes(value); + for (const segment of segments) { + if (segment.length === 1 && SUPPORTED_TEXT_CHARS.has(segment)) { + result += segment; + continue; + } + + changed = true; + result += "?"; + if (!warnedUnsupportedGlyphs.has(segment)) { + warnedUnsupportedGlyphs.add(segment); + warn(`NameLayer unsupported glyph replaced with ?: ${segment}`); + } + } + + return changed ? result : value; +} + +function segmentGraphemes(value: string): string[] { + const Segmenter = ( + Intl as typeof Intl & { Segmenter?: IntlSegmenterConstructor } + ).Segmenter; + if (typeof Segmenter === "function") { + const segmenter = new Segmenter(undefined, { + granularity: "grapheme", + }); + return Array.from(segmenter.segment(value), ({ segment }) => segment); + } + return Array.from(value); +} + +export function resetNameLayerGlyphWarningsForTests(): void { + warnedUnsupportedGlyphs.clear(); +} diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 76dd7e2bac..6c7e2c1c53 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -135,17 +135,17 @@ export class StructureIconsLayer implements Layer { } this.pixicanvas = document.createElement("canvas"); - this.pixicanvas.width = window.innerWidth; - this.pixicanvas.height = window.innerHeight; + const resolution = window.devicePixelRatio || 1; + this.resizePixiCanvasElement(resolution); // This will prefer WebGL, eventually WebGPU, and fallback to Canvas // Restrict using 'preferences: ["WebGPU", "WebGL"]' or // 'preferences: "WebGPU"' later if needed const renderer = await PIXI.autoDetectRenderer({ canvas: this.pixicanvas, - resolution: 1, - width: this.pixicanvas.width, - height: this.pixicanvas.height, + resolution, + width: window.innerWidth, + height: window.innerHeight, antialias: false, clearBeforeRender: true, backgroundAlpha: 0, @@ -293,9 +293,16 @@ export class StructureIconsLayer implements Layer { if (this.rendererOrGLContextLost()) { return; } - this.pixicanvas.width = window.innerWidth; - this.pixicanvas.height = window.innerHeight; - this.renderer?.resize(innerWidth, innerHeight, 1); + const resolution = window.devicePixelRatio || 1; + this.resizePixiCanvasElement(resolution); + this.renderer?.resize(window.innerWidth, window.innerHeight, resolution); + } + + private resizePixiCanvasElement(resolution: number) { + this.pixicanvas.width = Math.ceil(window.innerWidth * resolution); + this.pixicanvas.height = Math.ceil(window.innerHeight * resolution); + this.pixicanvas.style.width = `${window.innerWidth}px`; + this.pixicanvas.style.height = `${window.innerHeight}px`; } tick() { @@ -351,7 +358,17 @@ export class StructureIconsLayer implements Layer { this.levelsStage!.visible = scale > ZOOM_THRESHOLD && this.renderSprites; if (this.renderer) { this.renderer.render(this.rootStage); - mainContext.drawImage(this.renderer.canvas, 0, 0); + mainContext.drawImage( + this.renderer.canvas, + 0, + 0, + this.renderer.canvas.width, + this.renderer.canvas.height, + 0, + 0, + mainContext.canvas.width, + mainContext.canvas.height, + ); } } diff --git a/tests/NameLayer.test.ts b/tests/NameLayer.test.ts index 2337e78a1a..049d64a184 100644 --- a/tests/NameLayer.test.ts +++ b/tests/NameLayer.test.ts @@ -1,4 +1,16 @@ -import { computeAllianceClipPath } from "../src/client/graphics/PlayerIcons"; +import { + computeAllianceClipPath, + computeAllianceTopCutPercent, +} from "../src/client/graphics/PlayerIcons"; +import { + computeNameLayerLayout, + computeNameLayerScreenMetrics, + computeNameLayerWorldScale, + computeTraitorFlashAlpha, + computeTraitorFlashDurationSeconds, + replaceUnsupportedNameGlyphs, + resetNameLayerGlyphWarningsForTests, +} from "../src/client/graphics/layers/NameLayerLayout"; describe("PlayerIcons", () => { describe("computeAllianceClipPath", () => { @@ -37,5 +49,114 @@ describe("PlayerIcons", () => { expect(result).toContain("-2px"); expect(result.match(/-2px/g)).toHaveLength(2); // Should appear twice (left and right) }); + + test("shares numeric top-cut helper with Pixi masks", () => { + expect(computeAllianceTopCutPercent(1.0)).toBeCloseTo(20); + expect(computeAllianceTopCutPercent(0.5)).toBeCloseTo(51.2); + expect(computeAllianceTopCutPercent(0.0)).toBeCloseTo(82.4); + }); + }); +}); + +describe("NameLayerLayout", () => { + test("computes DOM-compatible local row positions with flag and icon gaps", () => { + const layout = computeNameLayerLayout({ + fontSize: 10, + iconSize: 15, + iconCount: 2, + centeredIconCount: 1, + hasFlag: true, + flagAspectRatio: 2, + nameWidth: 40, + troopWidth: 30, + }); + + expect(layout.iconPositions).toEqual([ + { x: -9.5, y: -9.75 }, + { x: 9.5, y: -9.75 }, + ]); + expect(layout.flag).toEqual({ x: -20, y: 2.75, width: 20, height: 10 }); + expect(layout.nameText).toEqual({ x: 10, y: 2.75 }); + expect(layout.troopText).toEqual({ x: 0, y: 12.25 }); + expect(layout.centeredIconPositions).toEqual([{ x: 0, y: 2.75 }]); + }); + + test("keeps no-flag names centered on the text width", () => { + const layout = computeNameLayerLayout({ + fontSize: 12, + iconSize: 18, + iconCount: 0, + centeredIconCount: 0, + hasFlag: false, + flagAspectRatio: 1, + nameWidth: 60, + troopWidth: 24, + }); + + expect(layout.flag).toBeNull(); + expect(layout.nameText.x).toBe(0); + expect(layout.width).toBe(60); + }); + + test("combines local label scale with camera scale for world-stable labels", () => { + expect(computeNameLayerWorldScale(8, 2)).toBeCloseTo(4); + expect(computeNameLayerWorldScale(20, 2)).toBeCloseTo(6); + }); + + test("computes final screen-space text and icon sizes", () => { + expect(computeNameLayerScreenMetrics(8, 2)).toEqual({ + fontSize: 16, + iconSize: 24, + }); + expect(computeNameLayerScreenMetrics(20, 2)).toEqual({ + fontSize: 48, + iconSize: 72, + }); + }); + + test("matches traitor flash duration thresholds and alpha extrema", () => { + expect(computeTraitorFlashDurationSeconds(156)).toBeNull(); + expect(computeTraitorFlashDurationSeconds(150)).toBeCloseTo(1); + expect(computeTraitorFlashDurationSeconds(0)).toBeCloseTo(0.2); + expect(computeTraitorFlashAlpha(150, 0)).toBeCloseTo(1); + expect(computeTraitorFlashAlpha(150, 250)).toBeCloseTo(0.65); + expect(computeTraitorFlashAlpha(150, 500)).toBeCloseTo(0.3); + }); + + test("spreads multiple centered icons instead of stacking them", () => { + const layout = computeNameLayerLayout({ + fontSize: 10, + iconSize: 15, + iconCount: 0, + centeredIconCount: 2, + hasFlag: false, + flagAspectRatio: 1, + nameWidth: 40, + troopWidth: 30, + }); + + expect(layout.centeredIconPositions).toEqual([ + { x: -9.5, y: -4.75 }, + { x: 9.5, y: -4.75 }, + ]); + }); + + test("replaces unsupported glyphs once per glyph", () => { + resetNameLayerGlyphWarningsForTests(); + const warn = vi.fn(); + + expect(replaceUnsupportedNameGlyphs("A🙂🙂B", warn)).toBe("A??B"); + expect(replaceUnsupportedNameGlyphs("🙂", warn)).toBe("?"); + expect(warn).toHaveBeenCalledTimes(1); + }); + + test("replaces unsupported grapheme clusters with one fallback glyph", () => { + resetNameLayerGlyphWarningsForTests(); + const warn = vi.fn(); + + expect( + replaceUnsupportedNameGlyphs("A\u{1F469}\u200D\u{1F4BB}B", warn), + ).toBe("A?B"); + expect(warn).toHaveBeenCalledTimes(1); }); });