Skip to content

Commit

Permalink
feat: introducing Text (#37)
Browse files Browse the repository at this point in the history
* feat: introducing `Text`

* chore: update readme

* chore: remove dep to peer dependency

---------

Co-authored-by: VishaL <119810373+vis-prime@users.noreply.github.com>
  • Loading branch information
alexzhang1030 and vis-prime authored Sep 10, 2023
1 parent 60726e1 commit 7858fb0
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 2 deletions.
Binary file added .storybook/public/font/Kalam-Regular.ttf
Binary file not shown.
26 changes: 26 additions & 0 deletions .storybook/stories/Billboard.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Meta } from '@storybook/html'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { GUI } from 'lil-gui'
import { Billboard, BillboardProps, BillboardType } from '../../src/core/Billboard'
import { Text } from '../../src/core/Text'

export default {
title: 'Abstractions/Billboard',
Expand All @@ -13,13 +14,15 @@ let gui: GUI

let globalBillboards: BillboardType


const billboardParams = {
follow: true,
lockX: false,
lockY: false,
lockZ: false,
} as BillboardProps


const getTorusMesh = () => {
const geometry = new THREE.TorusKnotGeometry(1, 0.35, 100, 32)
const mat = new THREE.MeshStandardMaterial({
Expand All @@ -32,6 +35,21 @@ const getTorusMesh = () => {
return torusMesh
}

const setupText = (pos: [number, number, number]) => {
const billboard = Billboard({
...billboardParams,
})
const text = Text({
text: 'Hello World',
fontSize: 1,
color: 'red',
})
text.mesh.position.fromArray(pos)
billboard.group.add(text.mesh)
globalBillboards.push(billboard)
return billboard.group
}

const setupLight = () => {
const dirLight = new THREE.DirectionalLight(0xabcdef, 5)
dirLight.position.set(15, 15, 15)
Expand Down Expand Up @@ -77,6 +95,10 @@ export const BillboardStory = async () => {
plane.position.set(-3, 2, 0)
globalBillboards.group.add(plane)

scene.add(setupText([0, 2, 0]))
scene.add(setupText([8, 2, 0]))
scene.add(setupText([-8, 2, 0]))

render(() => {
globalBillboards.update(camera)
})
Expand All @@ -102,4 +124,8 @@ const addOutlineGui = () => {
folder.add(params, 'lockZ').onChange((value: boolean) => {
globalBillboards.updateProps({ lockZ: value })
})
folder.add(params, 'follow')
folder.add(params, 'lockX')
folder.add(params, 'lockY')
folder.add(params, 'lockZ')
}
119 changes: 119 additions & 0 deletions .storybook/stories/Text.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Setup } from '../Setup'
import { Meta } from '@storybook/html'
import { OrbitControls } from 'three-stdlib'
import { GUI } from 'lil-gui'
import { Text, TextProps, TextType } from '../../src/core/Text'

export default {
title: 'Abstractions/Text',
} as Meta

let gui: GUI

let textGlobal: TextType
let runtimeParams = {
animate: false,
}

const textParams: TextProps = {
color: '#ff0000',
text: `
LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT,
SED DO EIUSMOD TEMPOR INCIDIDUNT UT LABORE ET DOLORE MAGNA ALIQUA.
UT ENIM AD MINIM VENIAM, QUIS NOSTRUD EXERCITATION ULLAMCO LABORIS
NISI UT ALIQUIP EX EA COMMODO CONSEQUAT. DUIS AUTE IRURE DOLOR IN
REPREHENDERIT IN VOLUPTATE VELIT ESSE CILLUM DOLORE EU FUGIAT NULLA PARIATUR.
EXCEPTEUR SINT OCCAECAT CUPIDATAT NON PROIDENT,
SUNT IN CULPA QUI OFFICIA DESERUNT MOLLIT ANIM ID EST LABORUM.`,
fontSize: 2,
maxWidth: 40,
lineHeight: 1,
outlineWidth: 0.2,
outlineColor: '#ffffff',
outlineBlur: 0,
strokeWidth: 0,
strokeColor: '#0000ff',
}

const setupText = () => {
const text = (textGlobal = Text(textParams))

const mesh = text.mesh

return mesh
}

export const TextStory = async () => {
gui = new GUI({ title: 'Text Story', closeFolders: true })
const { renderer, scene, camera, render } = Setup()
renderer.shadowMap.enabled = true
camera.position.set(0, 0, 70)
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()

const textMesh = setupText()
scene.add(textMesh)

render(() => {
if (runtimeParams.animate) textMesh.rotation.y += 0.01
})

addOutlineGui()
}

TextStory.storyName = 'Default'

const addOutlineGui = () => {
const params = Object.assign({}, textParams)
const folder = gui.addFolder('T E X T')
folder.open().onChange(() => {
textGlobal.updateProps(params)
})
folder.add(runtimeParams, 'animate')
folder.addColor(params, 'color')
folder.add(params, 'fontSize', 0, 4, 0.1)
folder.add(params, 'maxWidth', 0, 100, 1)
folder.add(params, 'outlineWidth', 0, 10, 0.1)
folder.addColor(params, 'outlineColor')
folder.add(params, 'strokeWidth', 0, 10, 0.1)
folder.addColor(params, 'strokeColor')
folder.add(params, 'outlineBlur', 0, 4, 0.1)
}

const preloadParams: TextProps = {
color: '#ff0000',
text: `
LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT,
SED DO EIUSMOD TEMPOR INCIDIDUNT UT LABORE ET DOLORE MAGNA ALIQUA.
UT ENIM AD MINIM VENIAM, QUIS NOSTRUD EXERCITATION ULLAMCO LABORIS
NISI UT ALIQUIP EX EA COMMODO CONSEQUAT. DUIS AUTE IRURE DOLOR IN
REPREHENDERIT IN VOLUPTATE VELIT ESSE CILLUM DOLORE EU FUGIAT NULLA PARIATUR.
EXCEPTEUR SINT OCCAECAT CUPIDATAT NON PROIDENT,
SUNT IN CULPA QUI OFFICIA DESERUNT MOLLIT ANIM ID EST LABORUM.`,
fontSize: 2,
maxWidth: 40,
lineHeight: 1,
font: '/font/Kalam-Regular.ttf',
characters: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!?.,:;\'"()[]{}<>|/@\\^$-%+=#_&~*',
onPreloadEnd: () => {
console.log('loaded')
},
}

export const TextPreloadFontStory = () => {
const { renderer, scene, camera, render } = Setup()
renderer.shadowMap.enabled = true
camera.position.set(0, 0, 70)
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()

const text = Text(preloadParams)

const mesh = text.mesh

scene.add(mesh)
}

TextPreloadFontStory.storyName = 'PreloadFont'
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,86 @@ export type BillboardType = {
}
```


#### Text

[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/abstractions-text--text-story)

[drei counterpart](https://github.com/pmndrs/drei#text)

Hi-quality text rendering w/ signed distance fields (SDF) and antialiasing, using [troika-3d-text](https://github.com/protectwise/troika/tree/master/packages/troika-3d-text). All of troikas props are valid!

> Required `troika-three-text` >= `0.46.4`
```ts
export type TextProps = {
characters?: string
color?: number | string
// the text content
text: string
/** Font size, default: 1 */
fontSize?: number
maxWidth?: number
lineHeight?: number
letterSpacing?: number
textAlign?: 'left' | 'right' | 'center' | 'justify'
font?: string
anchorX?: number | 'left' | 'center' | 'right'
anchorY?: number | 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom'
clipRect?: [number, number, number, number]
depthOffset?: number
direction?: 'auto' | 'ltr' | 'rtl'
overflowWrap?: 'normal' | 'break-word'
whiteSpace?: 'normal' | 'overflowWrap' | 'nowrap'
outlineWidth?: number | string
outlineOffsetX?: number | string
outlineOffsetY?: number | string
outlineBlur?: number | string
outlineColor?: number | string
outlineOpacity?: number
strokeWidth?: number | string
strokeColor?: number | string
strokeOpacity?: number
fillOpacity?: number
sdfGlyphSize?: number
debugSDF?: boolean
onSync?: (troika: any) => void
onPreloadEnd?: () => void
}
```
Usage
```jsx
const text = Text({
text: 'Hello World',
})
const mesh = new THREE.Mesh(geometry, material)
mesh.add(text.mesh)
```

Text function returns the following

```jsx
export type TextType = {
mesh: THREE.Mesh
updateProps: (newProps: Partial<TextProps>) => void
dispose: () => void
}
```

You can preload the font and characters:

```ts
const preloadRelatedParams = {
// support ttf/otf/woff(woff2 is not supported)
font: '/your/font/path',
characters: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!?.,:;\'"()[]{}<>|/@\\^$-%+=#_&~*',
onPreloadEnd: () => {
// this is the callback when font and characters are loaded
},
=======

#### Sprite Animator

[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/misc-spriteanimator--sprite-animator-story)
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@
"three-stdlib": "^2.21.8",
"ts-node": "^10.9.1",
"typescript": "^4.7.4",
"yarn": "^1.22.17"
"yarn": "^1.22.17",
"troika-three-text": "^0.46.4"
},
"peerDependencies": {
"three": ">=0.137"
"three": ">=0.137",
"troika-three-text": ">=0.46.4"
}
}
98 changes: 98 additions & 0 deletions src/core/Text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// @ts-ignore
import { Text as TextMeshImpl, preloadFont } from 'troika-three-text'

export type TextProps = {
/** The content of text */
text: string
characters?: string
color?: number | string
/** Font size, default: 1 */
fontSize?: number
maxWidth?: number
lineHeight?: number
letterSpacing?: number
textAlign?: 'left' | 'right' | 'center' | 'justify'
font?: string
anchorX?: number | 'left' | 'center' | 'right'
anchorY?: number | 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom'
clipRect?: [number, number, number, number]
depthOffset?: number
direction?: 'auto' | 'ltr' | 'rtl'
overflowWrap?: 'normal' | 'break-word'
whiteSpace?: 'normal' | 'overflowWrap' | 'nowrap'
outlineWidth?: number | string
outlineOffsetX?: number | string
outlineOffsetY?: number | string
outlineBlur?: number | string
outlineColor?: number | string
outlineOpacity?: number
strokeWidth?: number | string
strokeColor?: number | string
strokeOpacity?: number
fillOpacity?: number
sdfGlyphSize?: number
debugSDF?: boolean
onSync?: (troika: any) => void
onPreloadEnd?: () => void
}

const externalProps = ['onSync', 'onPreloadEnd', 'characters']

function removeExternalProps(props: Partial<TextProps>) {
return Object.keys(props).reduce((result, key) => {
if (externalProps.indexOf(key) === -1) {
result[key] = props[key]
}
return result
}, {} as Partial<TextProps>)
}

export type TextType = {
mesh: THREE.Mesh
updateProps: (newProps: Partial<TextProps>) => void
dispose: () => void
}

export const Text = ({
sdfGlyphSize = 64,
anchorX = 'center',
anchorY = 'middle',
fontSize = 1,
...restProps
}: TextProps): TextType => {
const props: TextProps = {
sdfGlyphSize,
anchorX,
anchorY,
fontSize,
...restProps,
}
const troikaMesh = new TextMeshImpl()

Object.assign(troikaMesh, removeExternalProps(props))

if (props.font && props.characters) {
preloadFont(
{
font: props.font,
characters: props.characters,
},
() => {
props.onPreloadEnd && props.onPreloadEnd()
}
)
}

return {
mesh: troikaMesh,
updateProps(newProps) {
Object.assign(troikaMesh, removeExternalProps(newProps))
troikaMesh.sync(() => {
props.onSync && props.onSync(troikaMesh)
})
},
dispose() {
troikaMesh.dispose()
},
}
}
Loading

0 comments on commit 7858fb0

Please sign in to comment.