@lightningtv/renderer
Version:
Lightning 3 Renderer
449 lines (370 loc) • 13 kB
text/typescript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
TextRenderer,
TextRendererMap,
TrProps,
TextRendererState,
TrFailedEventHandler,
TrLoadedEventHandler,
} from './text-rendering/renderers/TextRenderer.js';
import { CoreNode, UpdateType, type CoreNodeProps } from './CoreNode.js';
import type { Stage } from './Stage.js';
import type { CoreRenderer } from './renderers/CoreRenderer.js';
import type {
NodeTextFailedPayload,
NodeTextLoadedPayload,
} from '../common/CommonTypes.js';
import type { RectWithValid } from './lib/utils.js';
import { assertTruthy } from '../utils.js';
import { Matrix3d } from './lib/Matrix3d.js';
export interface CoreTextNodeProps extends CoreNodeProps, TrProps {
/**
* Force Text Node to use a specific Text Renderer
*
* @remarks
* By default, Text Nodes resolve the Text Renderer to use based on the font
* that is matched using the font family and other font selection properties.
*
* If two fonts supported by two separate Text Renderers are matched setting
* this override forces the Text Node to resolve to the Text Renderer defined
* here.
*
* @default null
*/
textRendererOverride: keyof TextRendererMap | null;
}
/**
* An CoreNode in the Renderer scene graph that renders text.
*
* @remarks
* A Text Node is the second graphical building block of the Renderer scene
* graph. It renders text using a specific text renderer that is automatically
* chosen based on the font requested and what type of fonts are installed
* into an app.
*
* The text renderer can be overridden by setting the `textRendererOverride`
*
* The `texture` and `shader` properties are managed by loaded text renderer and
* should not be set directly.
*
* For non-text rendering, see {@link CoreNode}.
*/
export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
textRenderer: TextRenderer;
trState: TextRendererState;
private _textRendererOverride: CoreTextNodeProps['textRendererOverride'] =
null;
constructor(
stage: Stage,
props: CoreTextNodeProps,
textRenderer: TextRenderer,
) {
super(stage, props);
this._textRendererOverride = props.textRendererOverride;
this.textRenderer = textRenderer;
const textRendererState = this.createState({
x: this.absX,
y: this.absY,
width: props.width,
height: props.height,
textAlign: props.textAlign,
color: props.color,
zIndex: props.zIndex,
contain: props.contain,
scrollable: props.scrollable,
scrollY: props.scrollY,
offsetY: props.offsetY,
letterSpacing: props.letterSpacing,
debug: props.debug,
fontFamily: props.fontFamily,
fontSize: props.fontSize,
fontStretch: props.fontStretch,
fontStyle: props.fontStyle,
fontWeight: props.fontWeight,
text: props.text,
lineHeight: props.lineHeight,
maxLines: props.maxLines,
textBaseline: props.textBaseline,
verticalAlign: props.verticalAlign,
overflowSuffix: props.overflowSuffix,
});
this.trState = textRendererState;
}
private onTextLoaded: TrLoadedEventHandler = () => {
const { contain } = this;
const setWidth = this.trState.props.width;
const setHeight = this.trState.props.height;
const calcWidth = this.trState.textW || 0;
const calcHeight = this.trState.textH || 0;
if (contain === 'both') {
this.props.width = setWidth;
this.props.height = setHeight;
} else if (contain === 'width') {
this.props.width = setWidth;
this.props.height = calcHeight;
} else if (contain === 'none') {
this.props.width = calcWidth;
this.props.height = calcHeight;
}
this.updateLocalTransform();
// Incase the RAF loop has been stopped already before text was loaded,
// we request a render so it can be drawn.
this.stage.requestRender();
this.emit('loaded', {
type: 'text',
dimensions: {
width: this.trState.textW || 0,
height: this.trState.textH || 0,
},
} satisfies NodeTextLoadedPayload);
};
private onTextFailed: TrFailedEventHandler = (target, error) => {
this.emit('failed', {
type: 'text',
error,
} satisfies NodeTextFailedPayload);
};
override get width(): number {
return this.props.width;
}
override set width(value: number) {
this.props.width = value;
this.textRenderer.set.width(this.trState, value);
// If not containing, we must update the local transform to account for the
// new width
if (this.contain === 'none') {
this.setUpdateType(UpdateType.Local);
}
}
override get height(): number {
return this.props.height;
}
override set height(value: number) {
this.props.height = value;
this.textRenderer.set.height(this.trState, value);
// If not containing in the horizontal direction, we must update the local
// transform to account for the new height
if (this.contain !== 'both') {
this.setUpdateType(UpdateType.Local);
}
}
override get color(): number {
return this.trState.props.color;
}
override set color(value: number) {
this.textRenderer.set.color(this.trState, value);
}
get text(): string {
return this.trState.props.text;
}
set text(value: string) {
this.textRenderer.set.text(this.trState, value);
}
get textRendererOverride(): CoreTextNodeProps['textRendererOverride'] {
return this._textRendererOverride;
}
set textRendererOverride(value: CoreTextNodeProps['textRendererOverride']) {
this._textRendererOverride = value;
this.textRenderer.destroyState(this.trState);
const textRenderer = this.stage.resolveTextRenderer(
this.trState.props,
this._textRendererOverride,
);
if (!textRenderer) {
console.warn(
'Text Renderer not found for font',
this.trState.props.fontFamily,
);
return;
}
this.textRenderer = textRenderer;
this.trState = this.createState(this.trState.props);
}
get fontSize(): CoreTextNodeProps['fontSize'] {
return this.trState.props.fontSize;
}
set fontSize(value: CoreTextNodeProps['fontSize']) {
this.textRenderer.set.fontSize(this.trState, value);
}
get fontFamily(): CoreTextNodeProps['fontFamily'] {
return this.trState.props.fontFamily;
}
set fontFamily(value: CoreTextNodeProps['fontFamily']) {
this.textRenderer.set.fontFamily(this.trState, value);
}
get fontStretch(): CoreTextNodeProps['fontStretch'] {
return this.trState.props.fontStretch;
}
set fontStretch(value: CoreTextNodeProps['fontStretch']) {
this.textRenderer.set.fontStretch(this.trState, value);
}
get fontStyle(): CoreTextNodeProps['fontStyle'] {
return this.trState.props.fontStyle;
}
set fontStyle(value: CoreTextNodeProps['fontStyle']) {
this.textRenderer.set.fontStyle(this.trState, value);
}
get fontWeight(): CoreTextNodeProps['fontWeight'] {
return this.trState.props.fontWeight;
}
set fontWeight(value: CoreTextNodeProps['fontWeight']) {
this.textRenderer.set.fontWeight(this.trState, value);
}
get textAlign(): CoreTextNodeProps['textAlign'] {
return this.trState.props.textAlign;
}
set textAlign(value: CoreTextNodeProps['textAlign']) {
this.textRenderer.set.textAlign(this.trState, value);
}
get contain(): CoreTextNodeProps['contain'] {
return this.trState.props.contain;
}
set contain(value: CoreTextNodeProps['contain']) {
this.textRenderer.set.contain(this.trState, value);
}
get scrollable(): CoreTextNodeProps['scrollable'] {
return this.trState.props.scrollable;
}
set scrollable(value: CoreTextNodeProps['scrollable']) {
this.textRenderer.set.scrollable(this.trState, value);
}
get scrollY(): CoreTextNodeProps['scrollY'] {
return this.trState.props.scrollY;
}
set scrollY(value: CoreTextNodeProps['scrollY']) {
this.textRenderer.set.scrollY(this.trState, value);
}
get offsetY(): CoreTextNodeProps['offsetY'] {
return this.trState.props.offsetY;
}
set offsetY(value: CoreTextNodeProps['offsetY']) {
this.textRenderer.set.offsetY(this.trState, value);
}
get letterSpacing(): CoreTextNodeProps['letterSpacing'] {
return this.trState.props.letterSpacing;
}
set letterSpacing(value: CoreTextNodeProps['letterSpacing']) {
this.textRenderer.set.letterSpacing(this.trState, value);
}
get lineHeight(): CoreTextNodeProps['lineHeight'] {
return this.trState.props.lineHeight;
}
set lineHeight(value: CoreTextNodeProps['lineHeight']) {
this.textRenderer.set.lineHeight(this.trState, value);
}
get maxLines(): CoreTextNodeProps['maxLines'] {
return this.trState.props.maxLines;
}
set maxLines(value: CoreTextNodeProps['maxLines']) {
this.textRenderer.set.maxLines(this.trState, value);
}
get textBaseline(): CoreTextNodeProps['textBaseline'] {
return this.trState.props.textBaseline;
}
set textBaseline(value: CoreTextNodeProps['textBaseline']) {
this.textRenderer.set.textBaseline(this.trState, value);
}
get verticalAlign(): CoreTextNodeProps['verticalAlign'] {
return this.trState.props.verticalAlign;
}
set verticalAlign(value: CoreTextNodeProps['verticalAlign']) {
this.textRenderer.set.verticalAlign(this.trState, value);
}
get overflowSuffix(): CoreTextNodeProps['overflowSuffix'] {
return this.trState.props.overflowSuffix;
}
set overflowSuffix(value: CoreTextNodeProps['overflowSuffix']) {
this.textRenderer.set.overflowSuffix(this.trState, value);
}
get debug(): CoreTextNodeProps['debug'] {
return this.trState.props.debug;
}
set debug(value: CoreTextNodeProps['debug']) {
this.textRenderer.set.debug(this.trState, value);
}
override update(delta: number, parentClippingRect: RectWithValid) {
super.update(delta, parentClippingRect);
assertTruthy(this.globalTransform);
// globalTransform is updated in super.update(delta)
this.textRenderer.set.x(this.trState, this.globalTransform.tx);
this.textRenderer.set.y(this.trState, this.globalTransform.ty);
}
override checkBasicRenderability() {
if (this.worldAlpha === 0 || this.isOutOfBounds() === true) {
return false;
}
if (this.trState && this.trState.props.text !== '') {
return true;
}
return false;
}
override setRenderable(isRenderable: boolean) {
super.setRenderable(isRenderable);
this.textRenderer.setIsRenderable(this.trState, isRenderable);
}
override renderQuads(renderer: CoreRenderer) {
assertTruthy(this.globalTransform);
// If the text renderer does not support rendering quads, fallback to the
// default renderQuads method
if (!this.textRenderer.renderQuads) {
super.renderQuads(renderer);
return;
}
// If the text renderer does support rendering quads, use it...
// Prevent quad rendering if parent has a render texture
// and this node is not the render texture
if (this.parentHasRenderTexture) {
if (!renderer.renderToTextureActive) {
return;
}
// Prevent quad rendering if parent render texture is not the active render texture
if (this.parentRenderTexture !== renderer.activeRttNode) {
return;
}
}
if (this.parentHasRenderTexture && this.props.parent?.rtt) {
this.globalTransform = Matrix3d.identity();
if (this.localTransform) {
this.globalTransform.multiply(this.localTransform);
}
}
assertTruthy(this.globalTransform);
this.textRenderer.renderQuads(this);
}
/**
* Destroy the node and cleanup all resources
*/
override destroy(): void {
super.destroy();
this.textRenderer.destroyState(this.trState);
}
/**
* Resolve a text renderer and a new state based on the current text renderer props provided
* @param props
* @returns
*/
private createState(props: TrProps) {
const textRendererState = this.textRenderer.createState(props, this);
textRendererState.emitter.on('loaded', this.onTextLoaded);
textRendererState.emitter.on('failed', this.onTextFailed);
this.textRenderer.scheduleUpdateState(textRendererState);
return textRendererState;
}
}