Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using TTFLoader in useFont #2303

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/public/fonts/helvetiker_regular.jsonfont

Large diffs are not rendered by default.

Binary file added .storybook/public/fonts/lemon-round.ttf
Binary file not shown.
23 changes: 21 additions & 2 deletions .storybook/stories/Text3D.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default {
</Setup>
),
],
args: {
bevelEnabled: true,
bevelSize: 0.05,
},
} satisfies Meta<typeof Text3D>

type Story = StoryObj<typeof Text3D>
Expand All @@ -36,9 +40,24 @@ function Text3DScene(props: React.ComponentProps<typeof Text3D>) {
export const Text3DSt = {
args: {
font: '/fonts/helvetiker_regular.typeface.json',
bevelEnabled: true,
bevelSize: 0.05,
},
render: (args) => <Text3DScene {...args} />,
name: 'Default',
} satisfies Story

export const Text3DCustomExtSt = {
args: {
font: '/fonts/helvetiker_regular.jsonfont',
},
render: (args) => <Text3DScene {...args} />,
name: 'Custom Extension (JSON)',
} satisfies Story

export const Text3DTtfSt = {
args: {
font: '/fonts/lemon-round.ttf',
ttfLoader: true,
},
render: (args) => <Text3DScene {...args} />,
name: 'TTF',
} satisfies Story
2 changes: 2 additions & 0 deletions docs/abstractions/text3d.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Render 3D text using ThreeJS's `TextGeometry`.

Text3D will suspend while loading the font data. Text3D requires fonts in JSON format generated through [typeface.json](http://gero3.github.io/facetype.js), either as a path to a JSON file or a JSON object. If you face display issues try checking "Reverse font direction" in the typeface tool.

Alternatively, the path can point to a font file of a type supported by [opentype.js](https://github.com/opentypejs/opentype.js) (for example OTF or TTF). Then, after setting `ttfLoader={true}`, the conversion to the JSON format will be done automatically at load time.

```jsx
<Text3D font={fontUrl} {...textOptions}>
Hello world!
Expand Down
12 changes: 10 additions & 2 deletions docs/loaders/use-font.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ title: useFont
sourcecode: src/core/useFont.tsx
---

Uses THREE.FontLoader to load a font and returns a `THREE.Font` object. It also accepts a JSON object as a parameter. You can use this to preload or share a font across multiple components.
Uses `THREE.FontLoader` to load a font and returns a `THREE.Font` object. It also accepts a JSON object as a parameter. You can use this to preload or share a font across multiple components.

```jsx
const font = useFont('/fonts/helvetiker_regular.typeface.json')
return <Text3D font={font} />
return <Text3D font={font.data} />
```

In order to preload you do this:

```jsx
useFont.preload('/fonts/helvetiker_regular.typeface.json')
```

If the second argument, `ttfLoader`, is set to `true`, `THREE.TTFLoader` is used to try parsing the response as a font file in a standard format (TTF, OTF etc).
However, keep in mind that the on-the-fly conversion to the JSON format will impact the loading time.

```jsx
const font = useFont('/fonts/helvetiker_regular.ttf', true)
return <Text3D font={font.data} />
```
13 changes: 8 additions & 5 deletions src/core/Text3D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as React from 'react'
import * as THREE from 'three'
import { extend, MeshProps, Node } from '@react-three/fiber'
import { useMemo } from 'react'
import { suspend } from 'suspend-react'
import { mergeVertices, TextGeometry, TextGeometryParameters, FontLoader } from 'three-stdlib'
import { useFont, FontData } from './useFont'
import { mergeVertices, TextGeometry, TextGeometryParameters } from 'three-stdlib'
import { useFont } from './useFont'
import { ForwardRefComponent } from '../helpers/ts-utils'

declare global {
Expand All @@ -15,8 +14,11 @@ declare global {
}
}

type UseFontParams = Parameters<typeof useFont>

type Text3DProps = {
font: FontData | string
font: UseFontParams[0]
ttfLoader?: UseFontParams[1]
bevelSegments?: number
smooth?: number
} & Omit<TextGeometryParameters, 'font'> &
Expand All @@ -43,6 +45,7 @@ export const Text3D: ForwardRefComponent<
(
{
font: _font,
ttfLoader = false,
letterSpacing = 0,
lineHeight = 1,
size = 1,
Expand All @@ -62,7 +65,7 @@ export const Text3D: ForwardRefComponent<
React.useMemo(() => extend({ RenamedTextGeometry: TextGeometry }), [])

const ref = React.useRef<THREE.Mesh>(null!)
const font = useFont(_font)
const font = useFont(_font, ttfLoader)

const opts = useMemo(() => {
return {
Expand Down
43 changes: 31 additions & 12 deletions src/core/useFont.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FontLoader } from 'three-stdlib'
import { FontLoader, TTFLoader } from 'three-stdlib'
import { suspend, preload, clear } from 'suspend-react'

export type Glyph = {
Expand All @@ -22,10 +22,7 @@ export type FontData = {
type FontInput = string | FontData

let fontLoader: FontLoader | null = null

async function loadFontData(font: FontInput): Promise<FontData> {
return typeof font === 'string' ? await (await fetch(font)).json() : font
}
let ttfLoader: TTFLoader | null = null

function parseFontData(fontData: FontData) {
if (!fontLoader) {
Expand All @@ -34,14 +31,36 @@ function parseFontData(fontData: FontData) {
return fontLoader.parse(fontData)
}

async function loader(font: FontInput) {
const data = await loadFontData(font)
return parseFontData(data)
function parseTtfArrayBuffer(ttfData: ArrayBuffer) {
if (!ttfLoader) {
ttfLoader = new TTFLoader()
}
return ttfLoader.parse(ttfData) as FontData
}

async function loadFontData(font: FontInput, ttfLoader: boolean) {
if (typeof font === 'string') {
const res = await fetch(font)

if (ttfLoader) {
const arrayBuffer = await res.arrayBuffer()
return parseTtfArrayBuffer(arrayBuffer)
} else {
return (await res.json()) as FontData
}
} else {
return font
}
}

async function loader(font: FontInput, ttfLoader: boolean) {
const fontData = await loadFontData(font, ttfLoader)
return parseFontData(fontData)
}

export function useFont(font: FontInput) {
return suspend(loader, [font])
export function useFont(font: FontInput, ttfLoader: boolean = false) {
return suspend(loader, [font, ttfLoader])
}

useFont.preload = (font: FontInput) => preload(loader, [font])
useFont.clear = (font: FontInput) => clear([font])
useFont.preload = (font: FontInput, ttfLoader: boolean = false) => preload(loader, [font, ttfLoader])
useFont.clear = (font: FontInput, ttfLoader: boolean = false) => clear([font, ttfLoader])
Loading