Skip to content

Commit 25e91d5

Browse files
committed
Keep text upright and facing the camera
1 parent b663b5c commit 25e91d5

File tree

2 files changed

+120
-68
lines changed

2 files changed

+120
-68
lines changed

src/Text.jsx

Lines changed: 91 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import React, { useMemo, useCallback } from 'react'
2-
import { useLoader, useThree } from 'react-three-fiber'
3-
import * as THREE from 'three'
1+
import React, { useMemo, useRef } from 'react'
2+
import { useLoader, useThree, useFrame } from 'react-three-fiber'
3+
import {
4+
Vector3,
5+
Font,
6+
TextureLoader,
7+
Color,
8+
DoubleSide,
9+
Quaternion,
10+
} from 'three'
411
import TextGeometry from './TextGeometry'
512
import robotoFont from './fonts/roboto/Roboto-Regular.json'
613
import robotoTexture from './fonts/roboto/Roboto-Regular.png'
@@ -59,7 +66,7 @@ export const Text = ({
5966
}) => {
6067
// Font Data
6168
const font = useMemo(() => {
62-
return new THREE.Font(fontData)
69+
return new Font(fontData)
6370
}, [fontData])
6471

6572
const hasBackground = useMemo(() => backgroundAlpha > 0, [backgroundAlpha])
@@ -69,15 +76,15 @@ export const Text = ({
6976
])
7077

7178
// Texture Data
72-
const texture = useLoader(THREE.TextureLoader, textureData)
79+
const texture = useLoader(TextureLoader, textureData)
7380

7481
// Uniforms for shader
7582
const uniforms = useMemo(() => {
76-
const textColorArray = new THREE.Color(textColor).toArray()
83+
const textColorArray = new Color(textColor).toArray()
7784
textColorArray.push(textAlpha)
78-
const borderColorArray = new THREE.Color(borderColor).toArray()
85+
const borderColorArray = new Color(borderColor).toArray()
7986
borderColorArray.push(borderAlpha)
80-
const backgroundColorArray = new THREE.Color(backgroundColor).toArray()
87+
const backgroundColorArray = new Color(backgroundColor).toArray()
8188
backgroundColorArray.push(backgroundAlpha)
8289

8390
const uniforms = {
@@ -104,77 +111,94 @@ export const Text = ({
104111
])
105112

106113
// Retrieve the viewport from the rendering engine
107-
const { viewport } = useThree()
108-
109-
// Calculate the scale of the font using the viewport factor
110-
const scale = useMemo(() => {
111-
const view = 1 / viewport.factor
112-
return (view / font.data.info.size) * fontSize
113-
}, [font.data.info.size, fontSize, viewport.factor])
114+
const { camera, size } = useThree()
114115

115116
// Calculate the desired width of the text (for wrapping) based on the "width" prop (percentage of the screen width)
116117
const adjustedTextWidth = useMemo(() => {
117-
return ((viewport.width / scale) * width) / 100
118-
}, [scale, viewport.width, width])
118+
return (size.width * font.data.info.size * (1 / fontSize) * width) / 100
119+
}, [size.width, font.data.info.size, fontSize, width])
119120

120121
// Create userData based on the text so that the screen will update if the text changes
121122
const userData = useMemo(() => {
122123
return { text }
123124
}, [text])
124125

126+
// Capture the camera postion so we can orient the txt towards it
127+
const cameraPosition = useMemo(() => {
128+
const vec = new Vector3()
129+
camera.getWorldPosition(vec)
130+
return vec
131+
}, [camera])
132+
133+
const worldQuaternion = useMemo(() => new Quaternion())
134+
135+
const meshRef = useRef()
136+
125137
// Called whenever the mesh updates. Here we calculate and set the postion of the text.
126-
const update = useCallback(
127-
(self) => {
128-
const box = self.geometry.boundingBox
129-
const sphere = self.geometry.boundingSphere
130-
131-
const anchorOffset = {
132-
x:
133-
anchorHorz === LEFT
134-
? -box.min.x
135-
: anchorHorz === CENTER
136-
? -sphere.center.x
137-
: anchorHorz === RIGHT
138-
? -box.max.x
139-
: 0,
140-
y:
141-
anchorVert === TOP
142-
? box.min.y
143-
: anchorVert === CENTER
144-
? sphere.center.y
145-
: anchorVert === BOTTOM
146-
? box.max.y
147-
: 0,
148-
}
149-
150-
const placementOffset = {
151-
x: (viewport.width * positionHorz) / 100 - viewport.width / 2,
152-
y: viewport.height / 2 - (viewport.height * positionVert) / 100,
153-
}
154-
155-
const position = [
156-
anchorOffset.x * scale + placementOffset.x,
157-
anchorOffset.y * scale + placementOffset.y,
158-
0,
159-
]
160-
161-
self.scale.set(scale, scale, scale)
162-
self.position.set(...position)
163-
self.rotation.set(Math.PI, 0, 0)
164-
},
165-
[
166-
anchorHorz,
167-
anchorVert,
168-
positionHorz,
169-
positionVert,
170-
scale,
171-
viewport.height,
172-
viewport.width,
138+
useFrame(() => {
139+
const self = meshRef.current
140+
141+
const box = self.geometry.boundingBox
142+
const sphere = self.geometry.boundingSphere
143+
144+
const anchorOffset = {
145+
x:
146+
anchorHorz === LEFT
147+
? -box.min.x
148+
: anchorHorz === CENTER
149+
? -sphere.center.x
150+
: anchorHorz === RIGHT
151+
? -box.max.x
152+
: 0,
153+
y:
154+
anchorVert === TOP
155+
? box.min.y
156+
: anchorVert === CENTER
157+
? sphere.center.y
158+
: anchorVert === BOTTOM
159+
? box.max.y
160+
: 0,
161+
}
162+
163+
const worldPosition = self.getWorldPosition(self.position)
164+
165+
const aspect = size.width / size.height
166+
const distance = cameraPosition.distanceTo(worldPosition)
167+
const fov = (camera.fov * Math.PI) / 180 // convert vertical fov to radians
168+
const viewHeight = 2 * Math.tan(fov / 2) * distance // visible
169+
const viewWidth = viewHeight * aspect
170+
const factor = size.width / viewWidth
171+
const scale = fontSize / (factor * font.data.info.size)
172+
173+
const placementOffset = {
174+
x: (viewWidth * positionHorz) / 100 - viewWidth / 2,
175+
y: viewHeight / 2 - (viewHeight * positionVert) / 100,
176+
}
177+
178+
const position = [
179+
anchorOffset.x * scale + placementOffset.x,
180+
anchorOffset.y * scale + placementOffset.y,
181+
0,
173182
]
174-
)
183+
184+
const upright = new Quaternion().setFromAxisAngle(
185+
new Vector3(1, 0, 0),
186+
Math.PI
187+
)
188+
189+
const rotation = self.parent
190+
.getWorldQuaternion(worldQuaternion)
191+
.conjugate()
192+
.multiply(camera.quaternion)
193+
.multiply(upright)
194+
195+
self.position.set(...position)
196+
self.setRotationFromQuaternion(rotation)
197+
self.scale.set(scale, scale, scale)
198+
})
175199

176200
return (
177-
<mesh name='Text' onUpdate={update} userData={userData}>
201+
<mesh name='Text' ref={meshRef} userData={userData}>
178202
<TextGeometry
179203
attach='geometry'
180204
text={text}
@@ -190,6 +214,7 @@ export const Text = ({
190214
<shaderMaterial
191215
attach='material'
192216
depthTest={depthTest}
217+
side={DoubleSide}
193218
args={[
194219
{
195220
transparent: true,

src/utils.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export const getUvs = (
113113
}
114114

115115
// get the geometric postions for the text, background and borders.
116+
// set 0,0,0 to the center
116117
export const getPositions = (
117118
glyphs,
118119
hasBack = false,
@@ -126,12 +127,38 @@ export const getPositions = (
126127
var positions = new Float32Array((glyphs.length + extras) * size)
127128
var i = extras * size
128129

130+
// Find the bounding box for the glyphs
131+
const { xmin, xmax, ymin, ymax } = glyphs.reduce(
132+
({ xmin, xmax, ymin, ymax }, glyph) => {
133+
var bitmap = glyph.data
134+
135+
// bottom left position
136+
var x = glyph.position[0] + bitmap.xoffset
137+
var y = glyph.position[1] + bitmap.yoffset
138+
139+
// quad size
140+
var w = bitmap.width
141+
var h = bitmap.height
142+
143+
return {
144+
xmin: xmin !== null ? Math.min(xmin, x) : x,
145+
xmax: xmax !== null ? Math.max(xmax, x + w) : x + w,
146+
ymin: ymin !== null ? Math.min(ymin, y) : y,
147+
ymax: ymax !== null ? Math.max(ymax, y + h) : y + h,
148+
}
149+
},
150+
{ xmin: null, xmax: null, ymin: null, ymax: null }
151+
)
152+
153+
const width = xmax - xmin
154+
const height = ymin - ymax
155+
129156
glyphs.forEach(function (glyph) {
130157
var bitmap = glyph.data
131158

132159
// bottom left position
133-
var x = glyph.position[0] + bitmap.xoffset
134-
var y = glyph.position[1] + bitmap.yoffset
160+
var x = glyph.position[0] + bitmap.xoffset - width / 2 - xmin
161+
var y = glyph.position[1] + bitmap.yoffset - height / 2 - ymin
135162

136163
// quad size
137164
var w = bitmap.width

0 commit comments

Comments
 (0)