diff --git a/eslint.config.js b/eslint.config.js
index 3168893d5..e75c2f38c 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.json b/package.json
index e0e3cecc2..5c234e22b 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 .",
diff --git a/resources/fonts/namelayer_overpass.png b/resources/fonts/namelayer_overpass.png
new file mode 100644
index 000000000..fd0364875
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 000000000..2b316fe9d
--- /dev/null
+++ b/resources/fonts/namelayer_overpass.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/fonts/overpass-OFL.txt b/resources/fonts/overpass-OFL.txt
new file mode 100644
index 000000000..2739ed0cf
--- /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 000000000..3a7c095fa
Binary files /dev/null and b/resources/fonts/overpass-regular.otf differ
diff --git a/resources/images/namelayer-emojis.json b/resources/images/namelayer-emojis.json
new file mode 100644
index 000000000..655ac5e79
--- /dev/null
+++ b/resources/images/namelayer-emojis.json
@@ -0,0 +1,1214 @@
+{
+ "frames": {
+ "π": {
+ "frame": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 64,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯°": {
+ "frame": {
+ "x": 128,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 192,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 256,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 320,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯Ί": {
+ "frame": {
+ "x": 384,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 448,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π±": {
+ "frame": {
+ "x": 0,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π‘": {
+ "frame": {
+ "x": 64,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 128,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π€‘": {
+ "frame": {
+ "x": 192,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯±": {
+ "frame": {
+ "x": 256,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π«‘": {
+ "frame": {
+ "x": 320,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 384,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 448,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 0,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β": {
+ "frame": {
+ "x": 64,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 128,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "πͺ": {
+ "frame": {
+ "x": 192,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 256,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 320,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π«΄": {
+ "frame": {
+ "x": 384,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π€": {
+ "frame": {
+ "x": 448,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π€¦ββοΈ": {
+ "frame": {
+ "x": 0,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π€": {
+ "frame": {
+ "x": 64,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 128,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "ποΈ": {
+ "frame": {
+ "x": 192,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π³οΈ": {
+ "frame": {
+ "x": 256,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β³": {
+ "frame": {
+ "x": 320,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯": {
+ "frame": {
+ "x": 384,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯": {
+ "frame": {
+ "x": 448,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 0,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β’οΈ": {
+ "frame": {
+ "x": 64,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β οΈ": {
+ "frame": {
+ "x": 128,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "βοΈ": {
+ "frame": {
+ "x": 192,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β¬οΈ": {
+ "frame": {
+ "x": 256,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "βοΈ": {
+ "frame": {
+ "x": 320,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 384,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯": {
+ "frame": {
+ "x": 448,
+ "y": 256,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β¬
οΈ": {
+ "frame": {
+ "x": 0,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π―": {
+ "frame": {
+ "x": 64,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β‘οΈ": {
+ "frame": {
+ "x": 128,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯": {
+ "frame": {
+ "x": 192,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π₯": {
+ "frame": {
+ "x": 256,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "βοΈ": {
+ "frame": {
+ "x": 320,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β¬οΈ": {
+ "frame": {
+ "x": 384,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "βοΈ": {
+ "frame": {
+ "x": 448,
+ "y": 320,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β€οΈ": {
+ "frame": {
+ "x": 0,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 64,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π°": {
+ "frame": {
+ "x": 128,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β": {
+ "frame": {
+ "x": 192,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β΅": {
+ "frame": {
+ "x": 256,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π‘": {
+ "frame": {
+ "x": 320,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π‘οΈ": {
+ "frame": {
+ "x": 384,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 448,
+ "y": 384,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 0,
+ "y": 448,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "β": {
+ "frame": {
+ "x": 64,
+ "y": 448,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 128,
+ "y": 448,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "π": {
+ "frame": {
+ "x": 192,
+ "y": 448,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ }
+ },
+ "meta": {
+ "app": "scripts/build-namelayer-assets.mjs",
+ "image": "namelayer-emojis.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 512,
+ "h": 512
+ },
+ "scale": "1"
+ }
+}
diff --git a/resources/images/namelayer-emojis.png b/resources/images/namelayer-emojis.png
new file mode 100644
index 000000000..03e5276ac
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 000000000..91124e703
--- /dev/null
+++ b/resources/images/namelayer-icons.json
@@ -0,0 +1,274 @@
+{
+ "frames": {
+ "AllianceIcon.svg": {
+ "frame": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "AllianceIconFaded.svg": {
+ "frame": {
+ "x": 64,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "AllianceRequestBlackIcon.svg": {
+ "frame": {
+ "x": 128,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "AllianceRequestWhiteIcon.svg": {
+ "frame": {
+ "x": 192,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "CrownIcon.svg": {
+ "frame": {
+ "x": 0,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "DisconnectedIcon.svg": {
+ "frame": {
+ "x": 64,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "EmbargoBlackIcon.svg": {
+ "frame": {
+ "x": 128,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "EmbargoWhiteIcon.svg": {
+ "frame": {
+ "x": 192,
+ "y": 64,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "NukeIconRed.svg": {
+ "frame": {
+ "x": 0,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "NukeIconWhite.svg": {
+ "frame": {
+ "x": 64,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "QuestionMarkIcon.svg": {
+ "frame": {
+ "x": 128,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "TargetIcon.svg": {
+ "frame": {
+ "x": 192,
+ "y": 128,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "TraitorIcon.svg": {
+ "frame": {
+ "x": 0,
+ "y": 192,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ }
+ },
+ "meta": {
+ "app": "scripts/build-namelayer-assets.mjs",
+ "image": "namelayer-icons.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 256,
+ "h": 256
+ },
+ "scale": "1"
+ }
+}
diff --git a/resources/images/namelayer-icons.png b/resources/images/namelayer-icons.png
new file mode 100644
index 000000000..8d85df2d1
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 000000000..8727a8a60
--- /dev/null
+++ b/scripts/build-namelayer-assets.mjs
@@ -0,0 +1,351 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = path.dirname(fileURLToPath(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 fontSourceCandidates = [
+ "overpass-regular.otf",
+ "overpass-regular.ttf",
+ "overpass.otf",
+ "overpass.ttf",
+ "overpass.woff",
+];
+const glyphs = Array.from(
+ new Set(
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ ΓΌΓ.[]+-=(),':!?/@#$%&\"".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 canvasApi = await loadCanvasApi();
+
+await buildBitmapFont();
+await buildIconAtlas();
+await buildEmojiAtlas();
+
+async function loadCanvasApi() {
+ try {
+ const api = await import("canvas");
+ const fontPath = fontSourceCandidates
+ .map((fileName) => path.join(fontsDir, fileName))
+ .find((candidate) => fs.existsSync(candidate));
+ try {
+ if (!fontPath) {
+ throw new Error(
+ `No Overpass font source found. Tried: ${fontSourceCandidates.join(
+ ", ",
+ )}`,
+ );
+ }
+ api.registerFont(fontPath, {
+ family: "OverpassNameLayer",
+ });
+ } catch (error) {
+ console.warn(
+ "Could not register Overpass; using canvas fallback font",
+ error,
+ );
+ }
+ return api;
+ } catch (error) {
+ console.warn(
+ "canvas native bindings are unavailable; writing deterministic fallback NameLayer assets",
+ error,
+ );
+ return null;
+ }
+}
+
+async function buildBitmapFont() {
+ if (!canvasApi) {
+ 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 { createCanvas } = canvasApi;
+ const cell = 64;
+ const cols = 16;
+ const rows = Math.ceil(glyphs.length / cols);
+ const canvas = createCanvas(cols * cell, rows * cell);
+ const ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = "#ffffff";
+ ctx.textBaseline = "alphabetic";
+ ctx.textAlign = "left";
+ ctx.font = '48px "OverpassNameLayer", Arial, sans-serif';
+
+ const chars = [];
+ glyphs.forEach((glyph, index) => {
+ const col = index % cols;
+ const row = Math.floor(index / cols);
+ const x = col * cell;
+ const y = row * cell;
+ const metrics = ctx.measureText(glyph);
+ const advance = glyph === " " ? 16 : Math.max(16, Math.ceil(metrics.width));
+ const drawX = x + 4;
+ const drawY = y + 48;
+ if (glyph !== " ") {
+ ctx.fillText(glyph, drawX, drawY);
+ }
+ chars.push({
+ id: glyph.codePointAt(0),
+ x,
+ y,
+ width: cell,
+ height: cell,
+ xadvance: advance,
+ xoffset: 0,
+ yoffset: 0,
+ label: glyph,
+ });
+ });
+
+ const xml = `
+
+
+
+
+
+
+
+${chars
+ .map(
+ (char) =>
+ ` `,
+ )
+ .join("\n")}
+
+
+`;
+
+ fs.writeFileSync(path.join(fontsDir, fontPng), canvas.toBuffer("image/png"));
+ fs.writeFileSync(path.join(fontsDir, fontXml), xml);
+}
+
+async function buildIconAtlas() {
+ if (!canvasApi) {
+ writeFallbackAtlas("namelayer-icons", iconSources);
+ return;
+ }
+
+ const { createCanvas, loadImage } = canvasApi;
+ const cell = 64;
+ const cols = 4;
+ const rows = Math.ceil(iconSources.length / cols);
+ const canvas = createCanvas(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), loadImage);
+ ctx.drawImage(img, x, y, cell, cell);
+ } 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 },
+ };
+ }
+
+ fs.writeFileSync(
+ path.join(imagesDir, "namelayer-icons.png"),
+ canvas.toBuffer("image/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,
+ ),
+ );
+}
+
+async function loadIconImage(sourcePath, loadImage) {
+ if (path.extname(sourcePath).toLowerCase() !== ".svg") {
+ return loadImage(sourcePath);
+ }
+
+ let svg = fs.readFileSync(sourcePath, "utf8");
+ if (!/