概要
Three.jsのReact用ライブラリ react-three-fibar を使用して、文字の表示方法を3パターン実装します。
パッケージのバージョンやインストール方法は、以下を参照してください。
実装
立体感のある文字
厚みのある文字です。Boxオブジェクトなどと同様に影を落としたりできます。
import React, { VFC } from 'react';
import { FontLoader, TextGeometry } from 'three';
import threeFontJson from 'three/examples/fonts/helvetiker_bold.typeface.json';
import { OrbitControls } from '@react-three/drei';
import { TDirectionalLight } from './objects/TDirectionalLight';
import { TFloorPlane } from './objects/TFloorPlane';
import { TCanvas } from './TCanvas';
export const Text1: VFC = () => {
return (
<TCanvas>
<OrbitControls />
<TDirectionalLight position={[-1, 3, 5]} />
<TextObject text="Hello! Three.js" />
<TFloorPlane />
</TCanvas>
)
}
// ----------------------------------------------
const TextObject: VFC<{ text: string }> = ({ text }) => {
// 位置の調整
const textGeo = new TextGeometry(text, {
font: new FontLoader().parse(threeFontJson),
size: 1,
height: 0.1
})
textGeo.computeBoundingBox()
const centerOffset = -(textGeo.boundingBox!.max.x - textGeo.boundingBox!.min.x) / 2
return (
<mesh position={[centerOffset, 1, 0]} args={[textGeo]} castShadow receiveShadow>
{/* <textGeometry args={['Hello! Three.js', config]} /> */}
<meshPhongMaterial color="royalblue" />
</mesh>
)
}
TCanvas、TDirectionalLight、TFloorPlaneは、それぞれreact-three-fiberのCanvas、directionalLight、planeGeometryをラップしたコンポーネントです。
実装の詳細は、おまけ載せています。
テキストの表示位置の調整のために、TextGeometryのインスタンスを作成して、それを使ってオフセットを割り出しています。
[x, y] = [0, 0]
の位置がテキストの左・下になっているので、オフセットを使用して中心・下に調整します。
この調整が必要ない場合は、コメントアウトしているtextGeometryが使えます。
元ネタ
平面な文字
厚みのない平面な文字です。
import { loremIpsum } from 'lorem-ipsum';
import React, { VFC } from 'react';
import { Text } from '@react-three/drei';
import { useRotate } from './hooks/useRotate';
import { TCanvas } from './TCanvas';
export const Text2: VFC = () => {
const lorem = loremIpsum({ count: 6 })
return (
<TCanvas position={[0, 0, 10]}>
<TextObject text={lorem} />
</TCanvas>
)
}
// ----------------------------------------------
const TextObject: VFC<{ text: string }> = ({ text }) => {
const textRef = useRotate([0, 0.1, 0])
return (
<Text
ref={textRef}
color="#24ACF2"
fontSize={0.5}
maxWidth={10}
lineHeight={1}
letterSpacing={0.02}
textAlign={'left'}
font="https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff"
anchorX="center"
anchorY="middle">
{text}
</Text>
)
}
useRotateは、オブジェクトをフレーム単位で回転させるためのカスタムフックです。
実装の詳細は、おまけ載せています。
元ネタ
オブジェクトに追従する文字
オブジェクトに追従して、常に前面を向いている文字です。
また、カメラとオブジェクトの距離によって文字サイズも変化します。
import React, { VFC } from 'react';
import { css } from '@emotion/css';
import { Html, OrbitControls } from '@react-three/drei';
import { useRotate } from './hooks/useRotate';
import { TDirectionalLight } from './objects/TDirectionalLight';
import { TFloorPlane } from './objects/TFloorPlane';
import { TCanvas } from './TCanvas';
export const Text3: VFC = () => {
return (
<TCanvas>
<OrbitControls />
<TDirectionalLight position={[5, 5, 5]} />
<CubeWithText text="Hello! Three.js" />
<TFloorPlane />
</TCanvas>
)
}
// ----------------------------------------------
const CubeWithText: VFC<{ text: string }> = ({ text }) => {
const ref = useRotate()
const groupRef = useRotate([0, 0.5, 0])
return (
<group ref={groupRef}>
<mesh ref={ref} position={[2, 2, 2]} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshPhongMaterial color="royalblue" />
<Html className={styles.text} center distanceFactor={10}>
{text}
</Html>
</mesh>
</group>
)
}
const styles = {
text: css`
white-space: nowrap;
font-size: 2rem;
color: red;
margin-top: 100px;
user-select: none;
`
}
meshに回転アニメーションのrefを追加すると、そのオブジェクトが回転(自転)します。
meshをgroupで囲って、そのgroupに回転アニメーションのrefを追加すると、そのオブジェクトが中心軸まわりに回転(公転)します。
元ネタ
まとめ
文字の扱い方についてまとめました。(実装はほぼコピーですが...)
また違う表現方法を見つけたら追加します。
おまけ
TCanvas
Canvasのラッパーコンポーネントです。
import React, { VFC } from 'react';
import { css } from '@emotion/css';
import { Canvas } from '@react-three/fiber';
type Props = {
children: React.ReactNode
fov?: number
position?: [number, number, number]
}
export const TCanvas: VFC<Props> = props => {
const { children, fov = 50, position = [0, 3, 10] } = props
return (
<Canvas className={styles.canvas} camera={{ fov, position }} dpr={[1, 2]} shadows>
{children}
</Canvas>
)
}
const styles = {
canvas: css`
width: 100%;
height: 100%;
`
}
TDirectionalLight
光源です。オプションでHelperを表示することができます。
import React, { useRef, VFC } from 'react';
import { DirectionalLightHelper } from 'three';
import { useHelper } from '@react-three/drei';
type Props = {
position: [number, number, number]
isHelper?: boolean
}
export const TDirectionalLight: VFC<Props> = props => {
const { position, isHelper = false } = props
const lightRef = useRef()
useHelper(lightRef, DirectionalLightHelper)
return (
<>
{isHelper ? (
<directionalLight
ref={lightRef}
position={position}
intensity={1} // 光の強さ
shadow-mapSize-width={2048} // 描画精度
shadow-mapSize-height={2048}
castShadow
/>
) : (
<directionalLight
position={position}
intensity={1} // 光の強さ
shadow-mapSize-width={2048} // 描画精度
shadow-mapSize-height={2048}
castShadow
/>
)}
</>
)
}
TFloorPlane
床パネルです。
import React, { VFC } from 'react';
import { DoubleSide } from 'three';
type Props = {
size?: [number, number]
color?: string
isGridHelper?: boolean
}
export const TFloorPlane: VFC<Props> = props => {
const { size = [10, 10], color = 'white', isGridHelper = false } = props
return (
<>
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={size} />
<meshPhongMaterial color={color} side={DoubleSide} />
</mesh>
{isGridHelper && (
<gridHelper position={[0, 0.01, 0]} args={[size[0], size[0], 'red', 'black']} />
)}
</>
)
}
useRotate
オブジェクトを回転させるためのカスタムフックです。
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
export const useRotate = (speed = [1, 1, 1]) => {
const ref = useRef<any>(null)
useFrame(({ clock }) => {
const a = clock.getElapsedTime()
ref.current.rotation.x = a * speed[0]
ref.current.rotation.y = a * speed[1]
ref.current.rotation.z = a * speed[2]
})
return ref
}