From 73d42fe1fbe69469be6e0417e58036a5cabc1e50 Mon Sep 17 00:00:00 2001 From: Frank Weindel <6070611+frank-weindel@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:51:13 -0400 Subject: [PATCH] NodeJS version / Doc Updates / JSON Adjustments --- .gitignore | 5 +- README.md | 72 +++++++++--- {public => font-src-sample}/charset.txt | 0 font-src-sample/overrides.json | 12 ++ package.json | 12 +- pnpm-lock.yaml | 139 +++++++++++++++++++++++- public/fonts/overrides.json | 2 - scripts/gen-fonts.sh | 121 --------------------- src/adjustFont.ts | 38 +++++++ src/genFont.ts | 96 ++++++++++++++++ src/index.ts | 31 ++++++ tsconfig.json | 24 ++++ 12 files changed, 408 insertions(+), 144 deletions(-) rename {public => font-src-sample}/charset.txt (100%) create mode 100644 font-src-sample/overrides.json delete mode 100644 public/fonts/overrides.json delete mode 100644 scripts/gen-fonts.sh create mode 100644 src/adjustFont.ts create mode 100644 src/genFont.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 02400b2..c423513 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -node_modules \ No newline at end of file +node_modules +font-src +font-dst +dist diff --git a/README.md b/README.md index 7afd708..9ba5121 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,71 @@ -# Generate MSDF Textures +# Lightning 3 SDF Font Generator -### Installation +This tool converts font files (.ttf, .otf, .woff, woff2) to Signed Distance Field (SDF) fonts for use with the Lightning 3's SDF text renderer. -Make sure you have Node.js installed on your system. Then, run the following command to install dependencies: +Both multi-channel (MSDF) and single-channel (SSDF) font files are generated. -`pnpm install` +See the following resources for more information about SDF font rendering: +- https://lightningjs.io/blogs/lng3FontRendering.html +- https://github.com/Chlumsky/msdfgen?tab=readme-ov-file#multi-channel-signed-distance-field-generator -### Instructions +This tool uses [msdf-bmfont-xml](https://github.com/soimy/msdf-bmfont-xml) to generate the font JSON data and texture atlases. And also applies some adjustments to that JSON data. -1. Copy Fonts: Place all the font files you want to convert to (m)sdf fonts into the `public/fonts` directory. -2. Generate MSDF Textures: Run the following command to generate MSDF textures from the font files: +## Setup + +1. Make sure you have Node.js installed on your system. Then, run the following command to install dependencies: + +``` +pnpm install +``` + +2. Copy the `font-src-sample` directory to `font-src`: + +``` +cp -R font-src-sample font-src +``` + +## Instructions + +1. Copy Fonts: Place all the font files you want to convert to SDF fonts into the `font-src` directory. + +2. Generate SDF Textures: Run the following command to generate SDF textures from the font files: ``` pnpm generate ``` -3. Access Generated Files: The generated (m)sdf font files will be available in the `public/sdf-fonts` folder. +3. Access Generated Files: The generated SDF font files will be available in the `font-dst` directory. + +## Adjusting the Charset + +The contents of `font-src/charset.txt` can be modified to adjust the characters +that are included into the SDF font. -## Supported Font Extensions +## Overrides (Advanced) -The script supports the following font file extensions: +By default this tool will generate SDF fonts with these properties: +- Font Size (pixels): 42 + - This is the size of the font that is rendered into the atlas texture PNG. + - Generally bigger values result in clearer fonts with less potential of artifacts. However, bigger values can also dramatically increase the size of the texture so keep this value as small as possible if you make adjustments. +- Distance Range: 4 + - The distance range defines number of pixels of used in rendering the actual signed-distance field of the atlas texture. + - Generally this value shouldn't have to be adjusted, but feel free to tweak along with the font size in order to get the highest quality text rendering with the smallest atlas texture size. It **must** be a multiple of 2. -- .ttf -- .otf -- .woff -- .woff2 +For each font file in the `font-src` directory you can define overrides for these values in the `font-src/overrides.json` file. + +Below is an example of overriding font size and distance range for the Ubuntu-Regular font. + +``` +{ + "Ubuntu-Regular": { + "msdf": { + "fontSize": 45 + "distanceRange": 6 + }, + "ssdf": { + "fontSize": 50 + "distanceRange": 6 + } + } +} +``` diff --git a/public/charset.txt b/font-src-sample/charset.txt similarity index 100% rename from public/charset.txt rename to font-src-sample/charset.txt diff --git a/font-src-sample/overrides.json b/font-src-sample/overrides.json new file mode 100644 index 0000000..c5ee47e --- /dev/null +++ b/font-src-sample/overrides.json @@ -0,0 +1,12 @@ +{ + "Comic-Sans-MS": { + "msdf": { + "fontSize": 50, + "distanceRange": 6 + }, + "ssdf": { + "fontSize": 70, + "distanceRange": 8 + } + } +} diff --git a/package.json b/package.json index cd1b506..b22b6e0 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,22 @@ "version": "1.0.0", "description": "", "main": "index.js", + "type": "module", "scripts": { - "generate":"bash ./scripts/gen-fonts.sh", + "start": "tsc && node dist/index.js", + "generate": "pnpm run start", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "msdf-bmfont-xml": "^2.7.0" + "@types/node": "^20.12.7", + "msdf-bmfont-xml": "^2.7.0", + "typescript": "^5.4.5" + }, + "dependencies": { + "chalk": "^5.3.0", + "execa": "^8.0.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f9f617..654850c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,24 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + chalk: + specifier: ^5.3.0 + version: 5.3.0 + execa: + specifier: ^8.0.1 + version: 8.0.1 + devDependencies: + '@types/node': + specifier: ^20.12.7 + version: 20.12.7 msdf-bmfont-xml: specifier: ^2.7.0 version: 2.7.0 + typescript: + specifier: ^5.4.5 + version: 5.4.5 packages: @@ -351,11 +365,11 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.11.16 + '@types/node': 20.12.7 dev: true - /@types/node@20.11.16: - resolution: {integrity: sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==} + /@types/node@20.12.7: + resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} dependencies: undici-types: 5.26.5 dev: true @@ -363,7 +377,7 @@ packages: /@types/responselike@1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 20.11.16 + '@types/node': 20.12.7 dev: true /ansi-align@3.0.1: @@ -452,6 +466,11 @@ packages: supports-color: 7.2.0 dev: true + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + /ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} dev: true @@ -507,6 +526,15 @@ packages: requiresBuild: true dev: true + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: false + /crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -558,6 +586,21 @@ packages: engines: {node: '>=8'} dev: true + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: false + /exif-parser@0.1.12: resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} dev: true @@ -581,6 +624,11 @@ packages: pump: 3.0.0 dev: true + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: false + /global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} @@ -645,6 +693,11 @@ packages: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} dev: true + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -712,6 +765,11 @@ packages: engines: {node: '>=8'} dev: true + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} dev: true @@ -720,6 +778,10 @@ packages: resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} dev: true + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: false + /jimp@0.3.11: resolution: {integrity: sha512-M+MWaCg/sJmeXSP5TRzKHmJUU2LpBWKEdoxiqczhY4FdMqz2k3Db4pdQjkcYR5ihW9MvtrjqarPAF6iTMGT34g==} dependencies: @@ -804,12 +866,21 @@ packages: resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==} dev: true + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false + /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true dev: true + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: false + /mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -863,6 +934,13 @@ packages: engines: {node: '>=8'} dev: true + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: false + /omggif@1.0.10: resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} dev: true @@ -879,6 +957,13 @@ packages: wrappy: 1.0.2 dev: true + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: false + /opentype.js@0.11.0: resolution: {integrity: sha512-Z9NkAyQi/iEKQYzCSa7/VJSqVIs33wknw8Z8po+DzuRUAqivJ+hJZ94mveg3xIeKwLreJdWTMyEO7x1K13l41Q==} hasBin: true @@ -925,6 +1010,16 @@ packages: resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} dev: true + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: false + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: false + /phin@2.9.3: resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} dev: true @@ -1023,10 +1118,27 @@ packages: lru-cache: 6.0.0 dev: true + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: false + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -1052,6 +1164,11 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: false + /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -1092,6 +1209,12 @@ packages: is-typedarray: 1.0.0 dev: true + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -1144,6 +1267,14 @@ packages: pako: 1.0.11 dev: true + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + /widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} diff --git a/public/fonts/overrides.json b/public/fonts/overrides.json deleted file mode 100644 index fc95028..0000000 --- a/public/fonts/overrides.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/scripts/gen-fonts.sh b/scripts/gen-fonts.sh deleted file mode 100644 index 6317baa..0000000 --- a/scripts/gen-fonts.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash - -# If not stated otherwise in this file or this component's LICENSE file the -# following copyright and licenses apply: -# -# Copyright 2024 Comcast Cable Communications Management, LLC. -# -# Licensed under the Apache License, Version 2.0 (the License); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -base_path=$(dirname $0) -cd $base_path - -public_path=../public -fonts_path=$public_path/fonts -sdf_fonts_path=$public_path/sdf-fonts -overrides_path=$fonts_path/overrides.json - -# Path to charset in public folder -charset_path=$public_path/charset.txt - -# Font file extensions -font_exts=(.ttf .otf .woff .woff2) - -# Overrides file schema -# All keys are optional. If a key is not present, the default value is used. -# { -# "": { -# "msdf": { -# "fontSize": number (default: 42), -# "distanceRange": number (default: 4), -# }, -# "ssdf": { -# "fontSize": number (default: 42), -# "distanceRange": number (default: 8), -# } -# } -# } - -# This function takes a font name and a font size and generates a font using msdf-bmfont -function gen_font { - # Name of font in fonts folder (with extension) - font_name=$1 - - # "msdf" or "ssdf" - field_type=$2 - - # Check that field_type is valid - if [ $field_type != "msdf" ] && [ $field_type != "ssdf" ]; then - echo "Invalid field type $field_type" - exit 1 - fi - - # Check that the font exists - if [ ! -f $fonts_path/$font_name ]; then - echo "Font $font_name does not exist" - exit 1 - fi - - bmfont_field_type=$field_type - - # If bmfont_field_type is "ssdf" change it to "sdf" - # since this is what is used by msdf-bmfont - if [ $bmfont_field_type = "ssdf" ]; then - bmfont_field_type="sdf" - fi - - # Remove the extension from the font name - font_name_no_ext=${font_name%.*} - - # Extract override data for font + field type, if exists - font_size=$(jq -r ".\"$font_name_no_ext\".$field_type.fontSize" $overrides_path) - distance_range=$(jq -r ".\"$font_name_no_ext\".$field_type.distanceRange" $overrides_path) - - # If override data does not exist, use default values - if [ $font_size = "null" ]; then - font_size=42 # msdf-bmfont default - fi - - if [ $distance_range = "null" ]; then - distance_range=4 # msdf-bmfont default - fi - - # Generate the font - msdf-bmfont \ - --field-type $bmfont_field_type \ - --output-type json \ - --round-decimal 6 \ - --smart-size \ - --pot \ - --font-size $font_size \ - --distance-range $distance_range \ - --charset-file $charset_path $fonts_path/$font_name && \ - mv $fonts_path/$font_name_no_ext.json $sdf_fonts_path/$font_name_no_ext.$field_type.json && \ - mv $fonts_path/$font_name_no_ext.png $sdf_fonts_path/$font_name_no_ext.$field_type.png -} - -# Make sure the sdf-fonts folder exists -mkdir -p $sdf_fonts_path - -# For every font file in the fonts folder -# Generate an msdf and ssdf font -for font in $fonts_path/*; do - for ext in "${font_exts[@]}"; do - if [[ $font == *$ext ]]; then - font_name=$(basename $font) - gen_font $font_name "msdf" - gen_font $font_name "ssdf" - fi - done -done diff --git a/src/adjustFont.ts b/src/adjustFont.ts new file mode 100644 index 0000000..221a498 --- /dev/null +++ b/src/adjustFont.ts @@ -0,0 +1,38 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; +import type { SdfFontInfo } from "./genFont.js"; + +/** + * Adjusts the font data for the generated fonts. + * + * @remarks + * A bug in the msdf-bmfont-xml package causes both the baseline and y-offsets + * of every character to be incorrect which results in the text being rendered + * out of intended alignment. This function corrects that data. + * + * See the following GitHub issue for more information: + * https://github.com/soimy/msdf-bmfont-xml/pull/93 + * + * @param font + */ +export function adjustFont(font: SdfFontInfo) { + console.log(chalk.magenta(`Adjusting ${chalk.bold(path.basename(font.jsonPath))}...`)); + const json = JSON.parse(fs.readFileSync(font.jsonPath, 'utf8')); + const distanceField = json.distanceField.distanceRange; + /** + * `pad` used by msdf-bmfont-xml + * + * (This is really just distanceField / 2 but guarantees a truncated integer result) + */ + const pad = (distanceField >> 1); + + // Remove 1x pad from the baseline + json.common.base = json.common.base - pad; + + // Remove 2x pad from the y-offset of every character + for (const char of json.chars) { + char.yoffset = char.yoffset - pad - pad; + } + fs.writeFileSync(font.jsonPath, JSON.stringify(json, null, 2)); +} \ No newline at end of file diff --git a/src/genFont.ts b/src/genFont.ts new file mode 100644 index 0000000..a34855f --- /dev/null +++ b/src/genFont.ts @@ -0,0 +1,96 @@ +import { execa } from 'execa'; +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +let fontSrcPath: string = ''; +let fontDstPath: string = ''; +let overrides_path = ''; +let charset_path = ''; + +/** + * Set the paths for the font source and destination directories. + * + * @param srcPath + * @param dstPath + */ +export function setGeneratePaths(srcPath: string, dstPath: string) { + fontSrcPath = srcPath; + fontDstPath = dstPath; + overrides_path = path.join(fontSrcPath, 'overrides.json'); + charset_path = path.join(fontSrcPath, 'charset.txt'); +} + +export interface SdfFontInfo { + fontName: string; + fieldType: 'ssdf' | 'msdf'; + jsonPath: string; + pngPath: string; +} + +/** + * Generates a font file in the specified field type. + * @param fontFileName - The name of the font. + * @param fieldType - The type of the font field (msdf or ssdf). + * @returns {Promise} - A promise that resolves when the font generation is complete. + */ +export async function genFont(fontFileName: string, fieldType: 'ssdf' | 'msdf'): Promise { + console.log(chalk.blue(`Generating ${fieldType} font from ${chalk.bold(fontFileName)}...`)); + if (fieldType !== 'msdf' && fieldType !== 'ssdf') { + console.log(`Invalid field type ${fieldType}`); + process.exit(1); + } + + if (!fs.existsSync(path.join(fontSrcPath, fontFileName))) { + console.log(`Font ${fontFileName} does not exist`); + process.exit(1); + } + + let bmfont_field_type: string = fieldType; + if (bmfont_field_type === 'ssdf') { + bmfont_field_type = 'sdf'; + } + + const fontNameNoExt = fontFileName.split('.')[0]!; + const overrides = JSON.parse(fs.readFileSync(overrides_path, 'utf8')); + const font_size = overrides[fontNameNoExt]?.[fieldType]?.fontSize || 42; + const distance_range = + overrides[fontNameNoExt]?.[fieldType]?.distanceRange || 4; + + await execa('msdf-bmfont', [ + '--field-type', + bmfont_field_type, + '--output-type', + 'json', + '--round-decimal', + '6', + '--smart-size', + '--pot', + '--font-size', + `${font_size}`, + '--distance-range', + `${distance_range}`, + '--charset-file', + charset_path, + path.join(fontSrcPath, fontFileName), + ]); + + const info = { + fontName: fontNameNoExt, + fieldType, + jsonPath: path.join(fontDstPath, `${fontNameNoExt}.${fieldType}.json`), + pngPath: path.join(fontDstPath, `${fontNameNoExt}.${fieldType}.png`), + }; + + fs.renameSync( + path.join(fontSrcPath, `${fontNameNoExt}.json`), + info.jsonPath, + ); + fs.renameSync( + path.join(fontSrcPath, `${fontNameNoExt}.png`), + info.pngPath, + ); + + return info; +} + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..74fdbf6 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,31 @@ + +import { adjustFont } from './adjustFont.js'; +import { genFont, setGeneratePaths, type SdfFontInfo } from './genFont.js'; +import fs from 'fs'; + +const fontSrcPath = 'font-src'; +const fontDstPath = 'font-dst'; +const font_exts = ['.ttf', '.otf', '.woff', '.woff2']; + +if (!fs.existsSync(fontDstPath)) { + fs.mkdirSync(fontDstPath, { recursive: true }); +} + +export async function generateFonts() { + const files = fs.readdirSync(fontSrcPath); + for (const file of files) { + for (const ext of font_exts) { + if (file.endsWith(ext)) { + await adjustFont(await genFont(file, 'msdf')); + await adjustFont(await genFont(file, 'ssdf')); + } + } + } +} + +(async () => { + setGeneratePaths(fontSrcPath, fontDstPath); + await generateFonts(); +})().catch((err) => { + console.log(err); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4681190 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "types": ["node"], + "lib": ["ES2022", "DOM"], + "outDir": "dist", + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "sourceMap": true, + "declaration": true, + + // Type Checking / Syntax Rules + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + }, + "include": [ + "src/**/*", + ] +}