Skip to content

Commit a1df879

Browse files
committed
msdfonts: base64 distributed msdf fonts
1 parent 3f76da1 commit a1df879

19 files changed

+6618
-17016
lines changed

.github/workflows/packages.yml

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ jobs:
5959
- name: Set publishing config
6060
run: pnpm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_TOKEN }}"
6161

62+
- name: Deploy msdfonts Package
63+
working-directory: ./packages/msdfonts
64+
run: pnpm publish --access public --no-git-checks --tag ${{ steps.gitversion.outputs.preReleaseLabel == '' && 'latest' || steps.gitversion.outputs.preReleaseLabel }}
65+
6266
- name: Deploy Uikit Vanilla Package
6367
working-directory: ./packages/uikit
6468
run: pnpm publish --access public --no-git-checks --tag ${{ steps.gitversion.outputs.preReleaseLabel == '' && 'latest' || steps.gitversion.outputs.preReleaseLabel }}

.github/workflows/static.yml

+10-10
Original file line numberDiff line numberDiff line change
@@ -41,60 +41,60 @@ jobs:
4141

4242
# Fonts
4343
- name: Make Dist Dir
44-
working-directory: ./packages/fonts
44+
working-directory: ./packages/msdfonts/docker-volume
4545
run: mkdir dist
4646

4747
# Inter
4848
- name: Generate Inter Light
49-
working-directory: ./packages/fonts
49+
working-directory: ./packages/msdfonts/docker-volume
5050
run: |
5151
node download.js "Inter" 300
5252
fontforge -lang=ff -c 'Open($1); SelectAll(); RemoveOverlap(); Generate($2)' font.ttf inter-light.ttf
5353
pnpm msdf-bmfont -f json inter-light.ttf -i charset.txt -m 256,512 -o dist/inter-light -s 48
5454
5555
- name: Generate Inter Normal
56-
working-directory: ./packages/fonts
56+
working-directory: ./packages/msdfonts/docker-volume
5757
run: |
5858
node download.js "Inter" 400
5959
fontforge -lang=ff -c 'Open($1); SelectAll(); RemoveOverlap(); Generate($2)' font.ttf inter-normal.ttf
6060
pnpm msdf-bmfont -f json inter-normal.ttf -i charset.txt -m 256,512 -o dist/inter-normal -s 48
6161
6262
- name: Generate Inter Medium
63-
working-directory: ./packages/fonts
63+
working-directory: ./packages/msdfonts/docker-volume
6464
run: |
6565
node download.js "Inter" 500
6666
fontforge -lang=ff -c 'Open($1); SelectAll(); RemoveOverlap(); Generate($2)' font.ttf inter-medium.ttf
6767
pnpm msdf-bmfont -f json inter-medium.ttf -i charset.txt -m 256,512 -o dist/inter-medium -s 48
6868
6969
- name: Generate Inter Semi Bold
70-
working-directory: ./packages/fonts
70+
working-directory: ./packages/msdfonts/docker-volume
7171
run: |
7272
node download.js "Inter" 600
7373
fontforge -lang=ff -c 'Open($1); SelectAll(); RemoveOverlap(); Generate($2)' font.ttf inter-semi-bold.ttf
7474
pnpm msdf-bmfont -f json inter-semi-bold.ttf -i charset.txt -m 256,512 -o dist/inter-semi-bold -s 48
7575
7676
- name: Generate Inter Bold
77-
working-directory: ./packages/fonts
77+
working-directory: ./packages/msdfonts/docker-volume
7878
run: |
7979
node download.js "Inter" 700
8080
fontforge -lang=ff -c 'Open($1); SelectAll(); RemoveOverlap(); Generate($2)' font.ttf inter-bold.ttf
8181
pnpm msdf-bmfont -f json inter-bold.ttf -i charset.txt -m 256,512 -o dist/inter-bold -s 48
8282
8383
- name: Convert to Webp
84-
working-directory: ./packages/fonts
84+
working-directory: ./packages/msdfonts/docker-volume
8585
run: pnpm sharp --lossless -i dist/*.png -o dist/ -f webp
8686

8787
- name: Replace file png files
88-
working-directory: ./packages/fonts
88+
working-directory: ./packages/msdfonts/docker-volume
8989
run: |
9090
sed -i 's/png/webp/g' dist/*.json
9191
rm dist/*.png
9292
9393
- name: Copy font files
9494
run: |
9595
mkdir -p public/fonts
96-
cp ./packages/fonts/dist/* ./public/fonts
97-
cp ./packages/fonts/LICENSE public/fonts/LICENSE
96+
cp ./packages/msdfonts/docker-volume/dist/* ./public/fonts
97+
cp ./packages/msdfonts/LICENSE public/fonts/LICENSE
9898
9999
- name: Upload Artifact
100100
uses: actions/upload-artifact@v4

packages/fonts/package.json

-6
This file was deleted.
File renamed without changes.

packages/msdfonts/README.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# msdfonts
2+
3+
base64 msdf fonts distributed as npm packages
4+
5+
# usage with R3/uikit
6+
7+
```jsx
8+
import { inter } from '@pmndrs/msdfonts'
9+
10+
;<FontFamilyProvider inter={inter}>{...children}</FontFamilyProvider>
11+
```
12+
13+
# How to build
14+
15+
## First Step
16+
17+
`cd docker-volume`
18+
`docker build . -t msdfonts`
19+
`docker run -v ./docker-volume:/data/:rw -e GOOGLE_FONTS_API_KEY='<insert-api-key>' msdfonts`
20+
21+
for users on ARM architecture (e.g. Apple M-chips)
22+
23+
`cd docker-volume`
24+
`docker build . --platform linux/x86_64 -t msdfonts`
25+
`docker run -v ./docker-volume:/data/:rw -e GOOGLE_FONTS_API_KEY='<insert-api-key>' --platform linux/x86_64 msdfonts`
26+
27+
## Final Step
28+
29+
Now delete the file in `src/index.ts` and copy the file `docker-volume/index.ts` into `src/index.ts`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.json
2+
!package.json
3+
*.ttf
4+
*.png
5+
*.webp
6+
*.ts
File renamed without changes.

packages/fonts/download.js packages/msdfonts/docker-volume/download.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const https = require('https') // or 'https' for https:// URLs
2-
const http = require('http')
3-
const fs = require('fs')
1+
import https from 'https' // or 'https' for https:// URLs
2+
import http from 'http'
3+
import fs from 'fs'
44

55
const [, , fontFamily, variant] = process.argv
66

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import https from 'https'
2+
import http from 'http'
3+
import fs, { writeFileSync } from 'fs'
4+
import { exec } from 'child_process'
5+
import { dirname, resolve } from 'path'
6+
7+
async function generate(fontFamily, variant) {
8+
const response = await fetch(
9+
`https://www.googleapis.com/webfonts/v1/webfonts?family=${fontFamily}&key=${process.env.GOOGLE_FONTS_API_KEY}`,
10+
)
11+
const json = await response.json()
12+
if (json.error != null) {
13+
console.error('fetch font families', fontFamily, json.error)
14+
return
15+
}
16+
const result = Object.entries(json.items[0].files).find(
17+
([name]) => name.toLocaleLowerCase() === variant.toLocaleLowerCase(),
18+
)
19+
if (result == null) {
20+
return false
21+
}
22+
await download(result[1], 'font.ttf')
23+
await runCmd("fontforge -lang=ff -c 'Open($1); SelectAll(); RemoveOverlap(); Generate($2)' font.ttf fixed-font.ttf")
24+
await runCmd(`npm run msdf`)
25+
await runCmd(`npm run webp`)
26+
return true
27+
}
28+
29+
const variants = { light: '300', regular: '400', medium: '500', 'semi-bold': '600', bold: '700' }
30+
const fontFamilies = ['Inter']
31+
32+
async function main() {
33+
let result = ''
34+
for (const fontFamily of fontFamilies) {
35+
result += `export const ${fontFamily.toLowerCase()} = {`
36+
for (const [fontWeightName, fontWeightValue] of Object.entries(variants)) {
37+
if (!(await generate(fontFamily, fontWeightValue))) {
38+
continue
39+
}
40+
result += `\t"${fontWeightName}": ${fontToJson('fixed-font.json')},`
41+
}
42+
result += `}\n\n`
43+
}
44+
writeFileSync('./index.ts', result)
45+
}
46+
47+
main().catch(console.error)
48+
49+
function runCmd(cmd) {
50+
return new Promise((resolve, reject) =>
51+
exec(cmd, (error) => {
52+
if (error == null) {
53+
resolve()
54+
return
55+
}
56+
reject(error)
57+
}),
58+
)
59+
}
60+
61+
function download(url, to) {
62+
return new Promise((resolve, reject) => {
63+
const file = fs.createWriteStream(to)
64+
;(url.startsWith('https') ? https : http)
65+
.get(url, (response) => {
66+
response.pipe(file)
67+
file.on('finish', () => {
68+
file.close()
69+
resolve()
70+
})
71+
})
72+
.on('error', reject)
73+
})
74+
}
75+
76+
function fontToJson(jsonPath) {
77+
const json = JSON.parse(fs.readFileSync(jsonPath))
78+
79+
for (let i = 0; i < json.pages.length; i++) {
80+
const url = resolve(dirname(jsonPath), json.pages[i]).replace('.png', '.webp')
81+
json.pages[i] = toUrl(fs.readFileSync(url), 'image/webp')
82+
}
83+
84+
return JSON.stringify(json)
85+
}
86+
87+
function toUrl(buf, mimeType) {
88+
return `data:${mimeType};base64,${buf.toString('base64')}`
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "module",
3+
"dependencies": {
4+
"msdf-bmfont-xml": "^2.7.0",
5+
"sharp-cli": "4.1"
6+
},
7+
"scripts": {
8+
"msdf": "msdf-bmfont -f json fixed-font.ttf -i charset.txt -m 256,512 -o fixed-font -s 48",
9+
"webp": "sharp --nearLossless -i fixed-font.png -o fixed-font.webp -f webp"
10+
}
11+
}

packages/msdfonts/dockerfile

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM ubuntu:24.10
2+
RUN apt -y update
3+
RUN apt -y install nodejs
4+
RUN apt -y install npm
5+
RUN apt -y install -y fontforge
6+
WORKDIR /data/
7+
CMD npm install && node generate.js
8+
9+

packages/msdfonts/package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@pmndrs/msdfonts",
3+
"version": "0.0.0",
4+
"description": "base64 msdf fonts distributed as npm package",
5+
"files": [
6+
"dist"
7+
],
8+
"keywords": [
9+
"fonts",
10+
"uikit",
11+
"icons",
12+
"threejs",
13+
"r3f",
14+
"msdf"
15+
],
16+
"author": "Bela Bohlender",
17+
"scripts": {
18+
"build": "tsc"
19+
},
20+
"type": "module",
21+
"main": "dist/index.js"
22+
}
23+

packages/msdfonts/src/index.ts

+2
Large diffs are not rendered by default.

packages/msdfonts/tsconfig.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"declaration": true,
6+
"skipLibCheck": true
7+
},
8+
"include": ["src"]
9+
}

packages/react/src/font.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
Font,
33
FontFamilies,
4-
FontFamilyUrls,
4+
FontFamilyWeightMap,
55
FontWeight,
66
GlyphLayoutProperties,
77
Initializers,
@@ -19,7 +19,7 @@ import { useContext, createContext, ReactNode, useCallback, useEffect, useMemo }
1919
const FontFamiliesContext = createContext<FontFamilies>(null as any)
2020

2121
export function FontFamilyProvider<T extends string = never>(properties: {
22-
[Key in T]: Key extends 'children' ? ReactNode : FontFamilyUrls
22+
[Key in T]: Key extends 'children' ? ReactNode : FontFamilyWeightMap
2323
}) {
2424
let { children, ...fontFamilies } = properties as any
2525
const existinFontFamilyUrls = useContext(FontFamiliesContext)

packages/uikit/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"inline-style-parser": "^0.2.3",
4444
"node-html-parser": "^6.1.13",
4545
"tw-to-css": "^0.0.12",
46-
"yoga-layout": "^3.0.4"
46+
"yoga-layout": "^3.0.4",
47+
"@pmndrs/msdfonts": "workspace:^"
4748
},
4849
"devDependencies": {
4950
"@types/node": "^20.11.0"

packages/uikit/src/text/cache.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { TextureLoader, WebGLRenderer } from 'three'
22
import { Font, FontInfo } from './font.js'
33

4-
const fontCache = new Map<string, Set<(font: Font) => void> | Font>()
4+
const fontCache = new Map<string | FontInfo, Set<(font: Font) => void> | Font>()
55

66
const textureLoader = new TextureLoader()
77

8-
export function loadCachedFont(url: string, renderer: WebGLRenderer, onLoad: (font: Font) => void): void {
9-
let entry = fontCache.get(url)
8+
export function loadCachedFont(
9+
fontInfoOrUrl: string | FontInfo,
10+
renderer: WebGLRenderer,
11+
onLoad: (font: Font) => void,
12+
): void {
13+
let entry = fontCache.get(fontInfoOrUrl)
1014
if (entry instanceof Set) {
1115
entry.add(onLoad)
1216
return
@@ -18,26 +22,29 @@ export function loadCachedFont(url: string, renderer: WebGLRenderer, onLoad: (fo
1822

1923
const set = new Set<(font: Font) => void>()
2024
set.add(onLoad)
21-
fontCache.set(url, set)
25+
fontCache.set(fontInfoOrUrl, set)
2226

23-
loadFont(url, renderer)
27+
loadFont(fontInfoOrUrl, renderer)
2428
.then((font) => {
2529
for (const fn of set) {
2630
fn(font)
2731
}
28-
fontCache.set(url, font)
32+
fontCache.set(fontInfoOrUrl, font)
2933
})
3034
.catch(console.error)
3135
}
3236

33-
async function loadFont(url: string, renderer: WebGLRenderer): Promise<Font> {
34-
const info: FontInfo = await (await fetch(url)).json()
37+
async function loadFont(fontInfoOrUrl: string | FontInfo, renderer: WebGLRenderer): Promise<Font> {
38+
const info: FontInfo = typeof fontInfoOrUrl === 'object' ? fontInfoOrUrl : await (await fetch(fontInfoOrUrl)).json()
3539

3640
if (info.pages.length !== 1) {
3741
throw new Error('only supporting exactly 1 page')
3842
}
3943

40-
const page = await textureLoader.loadAsync(new URL(info.pages[0], new URL(url, window.location.href)).href)
44+
const page = await textureLoader.loadAsync(
45+
new URL(info.pages[0], typeof fontInfoOrUrl === 'string' ? new URL(fontInfoOrUrl, window.location.href) : undefined)
46+
.href,
47+
)
4148

4249
page.anisotropy = renderer.capabilities.getMaxAnisotropy()
4350
page.flipY = false

0 commit comments

Comments
 (0)