import { ObservablePoint } from '../../maths/point/ObservablePoint';
import { deprecation, v8_0_0 } from '../../utils/logging/deprecation';
import { Bounds } from '../container/bounds/Bounds';
import { Container } from '../container/Container';
import type { Size } from '../../maths/misc/Size';
import type { PointData } from '../../maths/point/PointData';
import type { View } from '../../rendering/renderers/shared/view/View';
import type { ContainerOptions } from '../container/Container';
import type { Optional } from '../container/container-mixins/measureMixin';
import type { DestroyOptions } from '../container/destroyTypes';
import type { HTMLTextStyle, HTMLTextStyleOptions } from '../text-html/HtmlTextStyle';
import type { TextStyle, TextStyleOptions } from './TextStyle';
/**
* A string or number that can be used as text.
* @memberof text
*/
export type TextString = string | number | { toString: () => string };
/**
* A union of all text styles, including HTML, Bitmap and Canvas text styles.
* @memberof text
* @see text.TextStyle
* @see text.HTMLTextStyle
*/
export type AnyTextStyle = TextStyle | HTMLTextStyle;
/**
* A union of all text style options, including HTML, Bitmap and Canvas text style options.
* @memberof text
* @see text.TextStyleOptions
* @see text.HTMLTextStyleOptions
*/
export type AnyTextStyleOptions = TextStyleOptions | HTMLTextStyleOptions;
/**
* Options for the Text class.
* @example
* const text = new Text({
* text: 'Hello Pixi!',
* style: {
* fontFamily: 'Arial',
* fontSize: 24,
* fill: 0xff1010,
* align: 'center',
* }
* });
* @memberof text
*/
export interface TextOptions<
TEXT_STYLE extends TextStyle = TextStyle,
TEXT_STYLE_OPTIONS extends TextStyleOptions = TextStyleOptions,
> extends ContainerOptions
{
/** The anchor point of the text. */
anchor?: PointData | number;
/** The copy for the text object. To split a line you can use '\n'. */
text?: TextString;
/** The resolution of the text. */
resolution?: number;
/**
* The text style
* @type {
* text.TextStyle |
* Partial<text.TextStyle> |
* text.TextStyleOptions |
* text.HTMLTextStyle |
* Partial<text.HTMLTextStyle> |
* text.HTMLTextStyleOptions
* }
*/
style?: TEXT_STYLE | TEXT_STYLE_OPTIONS;
/** Whether or not to round the x/y position. */
roundPixels?: boolean;
}
/**
* An abstract Text class, used by all text type in Pixi. This includes Canvas, HTML, and Bitmap Text.
* @see scene.Text
* @see scene.BitmapText
* @see scene.HTMLText
* @memberof scene
*/
export abstract class AbstractText<
TEXT_STYLE extends TextStyle = TextStyle,
TEXT_STYLE_OPTIONS extends TextStyleOptions = TextStyleOptions,
> extends Container implements View
{
public abstract readonly renderPipeId: string;
public batched = true;
public _anchor: ObservablePoint;
public _resolution: number = null;
public _autoResolution: boolean = true;
public _style: TEXT_STYLE;
public _didTextUpdate = true;
public _roundPixels: 0 | 1 = 0;
protected _bounds: Bounds = new Bounds();
protected _boundsDirty = true;
protected _text: string;
private readonly _styleClass: new (options: TEXT_STYLE_OPTIONS) => TEXT_STYLE;
constructor(
options: TextOptions<TEXT_STYLE, TEXT_STYLE_OPTIONS>,
styleClass: new (options: TEXT_STYLE_OPTIONS) => TEXT_STYLE
)
{
const { text, resolution, style, anchor, width, height, roundPixels, ...rest } = options;
super({
...rest
});
this._styleClass = styleClass;
this.text = text ?? '';
this.style = style;
this.resolution = resolution ?? null;
this.allowChildren = false;
this._anchor = new ObservablePoint(
{
_onUpdate: () =>
{
this.onViewUpdate();
},
},
);
if (anchor) this.anchor = anchor;
this.roundPixels = roundPixels ?? false;
// needs to be set after the container has initiated
if (width) this.width = width;
if (height) this.height = height;
}
/**
* The anchor sets the origin point of the text.
* The default is `(0,0)`, this means the text's origin is the top left.
*
* Setting the anchor to `(0.5,0.5)` means the text's origin is centered.
*
* Setting the anchor to `(1,1)` would mean the text's origin point will be the bottom right corner.
*
* If you pass only single parameter, it will set both x and y to the same value as shown in the example below.
* @example
* import { Text } from 'pixi.js';
*
* const text = new Text('hello world');
* text.anchor.set(0.5); // This will set the origin to center. (0.5) is same as (0.5, 0.5).
*/
get anchor(): ObservablePoint
{
return this._anchor;
}
set anchor(value: PointData | number)
{
typeof value === 'number' ? this._anchor.set(value) : this._anchor.copyFrom(value);
}
/**
* Whether or not to round the x/y position of the text.
* @type {boolean}
*/
get roundPixels()
{
return !!this._roundPixels;
}
set roundPixels(value: boolean)
{
this._roundPixels = value ? 1 : 0;
}
/** Set the copy for the text object. To split a line you can use '\n'. */
set text(value: TextString)
{
// check its a string
value = value.toString();
if (this._text === value) return;
this._text = value as string;
this.onViewUpdate();
}
get text(): string
{
return this._text;
}
/**
* The resolution / device pixel ratio of the canvas.
* @default 1
*/
set resolution(value: number)
{
this._autoResolution = value === null;
this._resolution = value;
this.onViewUpdate();
}
get resolution(): number
{
return this._resolution;
}
get style(): TEXT_STYLE
{
return this._style;
}
/**
* Set the style of the text.
*
* Set up an event listener to listen for changes on the style object and mark the text as dirty.
*
* If setting the `style` can also be partial AnyTextStyleOptions.
* @type {
* text.TextStyle |
* Partial<text.TextStyle> |
* text.TextStyleOptions |
* text.HTMLTextStyle |
* Partial<text.HTMLTextStyle> |
* text.HTMLTextStyleOptions
* }
*/
set style(style: TEXT_STYLE | Partial<TEXT_STYLE> | TEXT_STYLE_OPTIONS)
{
style = style || {};
this._style?.off('update', this.onViewUpdate, this);
if (style instanceof this._styleClass)
{
this._style = style as TEXT_STYLE;
}
else
{
this._style = new this._styleClass(style as TEXT_STYLE_OPTIONS);
}
this._style.on('update', this.onViewUpdate, this);
this.onViewUpdate();
}
/**
* The local bounds of the Text.
* @type {rendering.Bounds}
*/
get bounds()
{
if (this._boundsDirty)
{
this._updateBounds();
this._boundsDirty = false;
}
return this._bounds;
}
/** The width of the sprite, setting this will actually modify the scale to achieve the value set. */
override get width(): number
{
return Math.abs(this.scale.x) * this.bounds.width;
}
override set width(value: number)
{
this._setWidth(value, this.bounds.width);
}
/** The height of the sprite, setting this will actually modify the scale to achieve the value set. */
override get height(): number
{
return Math.abs(this.scale.y) * this.bounds.height;
}
override set height(value: number)
{
this._setHeight(value, this.bounds.height);
}
/**
* Retrieves the size of the Text as a Size object.
* This is faster than get the width and height separately.
* @param out - Optional object to store the size in.
* @returns - The size of the Text.
*/
public override getSize(out?: Size): Size
{
if (!out)
{
out = {} as Size;
}
out.width = Math.abs(this.scale.x) * this.bounds.width;
out.height = Math.abs(this.scale.y) * this.bounds.height;
return out;
}
/**
* Sets the size of the Text to the specified width and height.
* This is faster than setting the width and height separately.
* @param value - This can be either a number or a Size object.
* @param height - The height to set. Defaults to the value of `width` if not provided.
*/
public override setSize(value: number | Optional<Size, 'height'>, height?: number)
{
let convertedWidth: number;
let convertedHeight: number;
if (typeof value !== 'object')
{
convertedWidth = value;
convertedHeight = height ?? value;
}
else
{
convertedWidth = value.width;
convertedHeight = value.height ?? value.width;
}
if (convertedWidth !== undefined)
{
this._setWidth(convertedWidth, this.bounds.width);
}
if (convertedHeight !== undefined)
{
this._setHeight(convertedHeight, this.bounds.height);
}
}
/**
* Adds the bounds of this text to the bounds object.
* @param bounds - The output bounds object.
*/
public addBounds(bounds: Bounds)
{
const _bounds = this.bounds;
bounds.addFrame(
_bounds.minX,
_bounds.minY,
_bounds.maxX,
_bounds.maxY,
);
}
/**
* Checks if the text contains the given point.
* @param point - The point to check
*/
public containsPoint(point: PointData)
{
const width = this.bounds.width;
const height = this.bounds.height;
const x1 = -width * this.anchor.x;
let y1 = 0;
if (point.x >= x1 && point.x <= x1 + width)
{
y1 = -height * this.anchor.y;
if (point.y >= y1 && point.y <= y1 + height) return true;
}
return false;
}
public onViewUpdate()
{
this._didChangeId += 1 << 12;
this._boundsDirty = true;
if (this.didViewUpdate) return;
this.didViewUpdate = true;
this._didTextUpdate = true;
const renderGroup = this.renderGroup || this.parentRenderGroup;
if (renderGroup)
{
renderGroup.onChildViewUpdate(this);
}
}
public _getKey(): string
{
return `${this.text}:${this._style.styleKey}:${this._resolution}`;
}
protected abstract _updateBounds(): void;
/**
* Destroys this text renderable and optionally its style texture.
* @param options - Options parameter. A boolean will act as if all options
* have been set to that value
* @param {boolean} [options.texture=false] - Should it destroy the texture of the text style
* @param {boolean} [options.textureSource=false] - Should it destroy the textureSource of the text style
* @param {boolean} [options.style=false] - Should it destroy the style of the text
*/
public destroy(options: DestroyOptions = false): void
{
super.destroy(options);
(this as any).owner = null;
this._bounds = null;
this._anchor = null;
if (typeof options === 'boolean' ? options : options?.style)
{
this._style.destroy(options);
}
this._style = null;
this._text = null;
}
}
export function ensureOptions<
TEXT_STYLE extends TextStyle,
TEXT_STYLE_OPTIONS extends TextStyleOptions
>(
args: any[],
name: string
): TextOptions<TEXT_STYLE, TEXT_STYLE_OPTIONS>
{
let options = (args[0] ?? {}) as TextOptions<TEXT_STYLE, TEXT_STYLE_OPTIONS>;
// @deprecated
if (typeof options === 'string' || args[1])
{
// #if _DEBUG
deprecation(v8_0_0, `use new ${name}({ text: "hi!", style }) instead`);
// #endif
options = {
text: options,
style: args[1],
} as TextOptions<TEXT_STYLE, TEXT_STYLE_OPTIONS>;
}
return options;
}