NgtsText
import { ChangeDetectionStrategy, Component, ElementRef, inject, signal } from '@angular/core';import { SobaWrapper } from '@soba/wrapper.ts';import { TweakpaneNumber, TweakpanePane } from 'angular-three-tweakpane';import { NgtCanvas, provideNgtRenderer } from 'angular-three/dom';import { SceneGraph } from './scene-graph';
@Component({ selector: 'app-soba-text', template: ` <ngt-canvas [camera]="{ position: [0, 2, 50], fov: 90 }"> <app-soba-wrapper *canvasContent [grid]="false" [controls]="null"> <app-scene-graph [count]="count()" [radius]="radius()" /> </app-soba-wrapper> </ngt-canvas>
<tweakpane-pane title="Text" [container]="host"> <tweakpane-number [(value)]="count" label="word count param" [params]="{ min: 1, max: 10, step: 1 }" /> <tweakpane-number [(value)]="radius" label="sphere radius" [params]="{ min: 10, max: 100, step: 1 }" /> </tweakpane-pane> `, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgtCanvas, SobaWrapper, SceneGraph, TweakpanePane, TweakpaneNumber], host: { class: 'text-demo relative block h-full' },})export default class Text { static clientProviders = [provideNgtRenderer()];
protected host = inject(ElementRef);
protected count = signal(8); protected radius = signal(20);}
import { DOCUMENT } from '@angular/common';import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, inject, input, signal, viewChild,} from '@angular/core';import { generateWord } from '@soba/random-words.ts';import { beforeRender, NgtArgs, type NgtVector3 } from 'angular-three';import { NgtsBillboard, NgtsText } from 'angular-three-soba/abstractions';import { NgtsTrackballControls } from 'angular-three-soba/controls';import * as THREE from 'three';
/** * Credits: https://codesandbox.io/p/sandbox/yup2o */
@Component({ selector: 'app-word', template: ` <ngts-billboard [options]="{ position: position() }"> <ngts-text [text]="text()" [options]="{ fontSize: 2.5, letterSpacing: -0.05, lineHeight: 1, 'material.toneMapped': false }" (pointerover)="$event.stopPropagation(); hovered.set(true)" (pointerout)="hovered.set(false)" /> </ngts-billboard> `, schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgtsBillboard, NgtsText],})export class Word { position = input<NgtVector3>([0, 0, 0]); text = input.required<string>();
private textRef = viewChild.required(NgtsText);
private document = inject(DOCUMENT);
protected hovered = signal(false);
constructor() { effect((onCleanup) => { const hovered = this.hovered(); if (hovered) this.document.body.style.cursor = 'pointer'; onCleanup(() => (this.document.body.style.cursor = 'auto')); });
const color = new THREE.Color(); beforeRender(() => { const textObject = this.textRef().troikaMesh; textObject.material.color.lerp(color.set(this.hovered() ? 'mediumpurple' : 'white'), 0.1); }); }}
@Component({ selector: 'app-cloud', template: ` @for (word of words(); track $index) { <app-word [position]="word.position" [text]="word.text" /> } `, schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, imports: [Word],})export class Cloud { count = input(4); radius = input(20);
protected words = computed(() => { const [count, radius] = [this.count(), this.radius()];
const words: { position: NgtVector3; text: string }[] = []; const spherical = new THREE.Spherical(); const phiSpan = Math.PI / (count + 1); const thetaSpan = (Math.PI * 2) / count; for (let i = 1; i < count + 1; i++) { for (let j = 0; j < count; j++) { words.push({ position: new THREE.Vector3().setFromSpherical(spherical.set(radius, phiSpan * i, thetaSpan * j)), text: generateWord(), }); } } return words; });}
@Component({ selector: 'app-scene-graph', template: ` <ngt-fog *args="['#202025', 0, 80]" attach="fog" />
<ngt-group [rotation]="[10, 10.5, 10]"> <app-cloud [count]="count()" [radius]="radius()" /> </ngt-group>
<ngts-trackball-controls /> `, schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgtArgs, NgtsTrackballControls, Cloud],})export class SceneGraph { count = input(8); radius = input(20);}
Hi-quality text rendering w/ signed distance fields (SDF) and antialiasing, using troika-three-text.
Usage
import { NgtsText } from 'angular-three-soba/abstractions';
<ngts-text [text]="text" [options]="options" />
Custom Material
NgtsText
can accept custom material(s) via content projection.
<ngts-text [text]="text" [options]="options"> <ngt-mesh-basic-material [color]="color()" /></ngts-text>
Prevent FOUC
By default, NgtsText
uses every characters from the font. If you want to use only a subset of characters, you can pass the characters
option.
<ngts-text [text]="text" [options]="{ characters: 'abcdef' }" />
Inputs
name | type | description | required |
---|---|---|---|
text | string | text to render | yes |
Options
options
input accepts any properties from THREE.Mesh
in addition to the following:
Properties
name | type | description |
---|---|---|
characters | string | Characters to extract from font. Useful to optimize SDF texture and prevent FOUC |
color | THREE.ColorRepresentation | This is a shortcut for setting the color of the text's material. You can use this if you don't want to specify a whole custom material and just want to change its color |
fontSize | number | The em-height at which to render the font, in local world units. Default: 1 |
fontWeight | number | string | A numeric font weight, 'normal', or 'bold'. Currently only used to select the preferred weight for the fallback Unicode fonts |
fontStyle | string | Either 'italic' or 'normal'. Currently only used to select the preferred style for the fallback Unicode fonts |
maxWidth | number | The maximum width of the text block, above which text may start wrapping according to the whiteSpace and overflowWrap properties. |
lineHeight | number | Sets the height of each line of text. Can be 'normal' which chooses a reasonable height based on the chosen font's ascender/descender metrics, or a number that is interpreted as a multiple of the fontSize |
letterSpacing | number | Sets a uniform adjustment to spacing between letters after kerning is applied, in local world units. Positive numbers increase spacing and negative numbers decrease it |
textAlign | string | The horizontal alignment of each line of text within the overall text bounding box. Can be one of 'left', 'right', 'center', or 'justify'. |
font | string | The URL of a custom font file to be used. Supported font formats are: .ttf, .otf, .woff (.woff2 is not supported) |
anchorX | number | string | Defines the horizontal position in the text block that should line up with the local origin. Can be specified as a numeric x position in local units, a string percentage of the total text block width e.g. "25%", or one of the following keyword strings: "left", "center", or "right". Default: center |
anchorY | number | string | Defines the vertical position in the text block that should line up with the local origin. Can be specified as a numeric y position in local units (note: down is negative y), a string percentage of the total text block height e.g. "25%", or one of the following keyword strings: "top", "top-baseline", "top-cap", "top-ex", "middle", "bottom-baseline", or "bottom". Default: middle |
clipRect | [number, number, number, number] | If specified, defines the [minX, minY, maxX, maxY] of a rectangle outside of which all pixels will be discarded. This can be used for example to clip overflowing text when whiteSpace="nowrap" |
depthOffset | number | This is a shortcut for setting the material's polygonOffset and related properties, which can be useful in preventing z-fighting when this text is laid on top of another plane in the scene. |
direction | string | Sets the base direction for the text. The default value of 'auto' will choose a direction based on the text's content according to the bidi spec. A value of 'ltr' or 'rtl' will force the direction |
overflowWrap | string | Defines how text wraps if the whiteSpace property is 'normal'. Can be either 'normal' to break at whitespace characters, or 'break-word' to allow breaking within words. |
whiteSpace | string | Defines whether text should wrap when a line reaches the maxWidth. Can be either 'normal' to allow wrapping, or 'nowrap' to prevent wrapping. Note that 'normal' honors newline characters. |
outlineWidth | number | string | The width of an outline/halo drawn around each text glyph. Can be specified as an absolute number in local units, or as a percentage string e.g. '10%' of the fontSize |
outlineOffsetX | number | string | Horizontal offset of the text outline. Can be specified as an absolute number in local units, or as a percentage string e.g. '12%' of the fontSize |
outlineOffsetY | number | string | Vertical offset of the text outline. Can be specified as an absolute number in local units, or as a percentage string e.g. '12%' of the fontSize |
outlineBlur | number | string | Blur radius applied to the outer edge of the text's outlineWidth. Can be specified as an absolute number in local units, or as a percentage string e.g. '12%' of the fontSize |
outlineColor | THREE.ColorRepresentation | The color to use for the text outline when outlineWidth, outlineBlur, and/or outlineOffsetX/Y are set. |
outlineOpacity | number | Sets the opacity of a configured text outline, in the range 0 to 1. |
strokeWidth | number | string | Sets the width of a stroke drawn inside the edge of each text glyph. Can be specified as an absolute number in local units, or as a percentage string e.g. '10%' of the fontSize |
strokeColor | THREE.ColorRepresentation | The color of the text stroke, when strokeWidth is nonzero. |
strokeOpacity | number | The opacity of the text stroke, when strokeWidth is nonzero. Accepts a number from 0 to 1. |
fillOpacity | number | Controls the opacity of just the glyph's fill area, separate from any configured strokeOpacity, outlineOpacity, and the material's opacity. |
sdfGlyphSize | number | The size of each glyph's SDF (signed distance field) used for rendering. Must be a power-of-two number. Default: 64 |
debugSDF | boolean | debug sdf |
glyphGeometryDetail | number | The number of vertical/horizontal segments that make up each glyph's rectangular plane. Can be increased to provide more geometrical detail for custom vertex shader effects. |