UNPKG

@lightningtv/renderer

Version:
551 lines (520 loc) 14.9 kB
/* * 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 { Dimensions } from '../../../common/CommonTypes.js'; import type { EventEmitter } from '../../../common/EventEmitter.js'; import type { CoreTextNode } from '../../CoreTextNode.js'; import type { Stage } from '../../Stage.js'; import type { Matrix3d } from '../../lib/Matrix3d.js'; import type { Rect, RectWithValid } from '../../lib/utils.js'; import type { TrFontFace, TrFontFaceDescriptors, } from '../font-face-types/TrFontFace.js'; import type { TextBaseline, TextVerticalAlign, } from './LightningTextTextureRenderer.js'; /** * Augmentable map of text renderer type IDs to text renderer types. * * @example * ```ts * declare module './TextRenderer' { * interface TextRendererMap { * canvas: CanvasTextRenderer; * } * } * ``` */ export interface TextRendererMap {} export interface TextRendererState { props: TrProps; /** * Whether or not the text renderer state is scheduled to be updated * via queueMicrotask. */ updateScheduled: boolean; status: 'initialState' | 'loading' | 'loaded' | 'failed' | 'destroyed'; /** * Event emitter for the text renderer */ emitter: EventEmitter; /** * Force a full layout pass for the calculation of the * total dimensions of the text */ forceFullLayoutCalc: boolean; textW: number | undefined; textH: number | undefined; isRenderable: boolean; debugData: { updateCount: number; layoutCount: number; drawCount: number; lastLayoutNumCharacters: number; layoutSum: number; drawSum: number; bufferSize: number; }; } export interface TextRendererDebugProps { showRenderWindow: boolean; showVisibleRect: boolean; showElementRect: boolean; disableScissor: boolean; printLayoutTime: boolean; } /** * Text renderer properties that are used in resolving appropriate font faces * * @remarks * Extended by {@link TrProps} */ export interface TrFontProps { /** * Font Family * * @internalRemarks * `fontFamily` is defined currently as single string, but in the future we may want to * support multiple font family fallbacks, as this is supported by CSS / Canvas2d. We can * do this in a backwards compatible way by unioning an array of strings to the * `fontFamily` property. */ fontFamily: string; /** * Font Weight * * @remarks * The font weight to use when looking up the font face. This can be a numeric * value between 1 and 1000, or one of the following strings: * - `'normal'` - same as 400 * - `'bold'` - same as 700 * - `'bolder'` - (Not yet properly supported) */ fontWeight: TrFontFaceDescriptors['weight'] | 'bolder' | 'lighter'; /** * Font Style * * @remarks * The font style to use when looking up the font face. This can be one of the * following strings: * - `'normal'` * - `'italic'` * - `'oblique'` */ fontStyle: TrFontFaceDescriptors['style']; /** * Font Stretch * * @remarks * The font stretch to use when looking up the font face. This can be one of the * following strings: * - `'normal'` * - `'ultra-condensed'` * - `'extra-condensed'` * - `'condensed'` * - `'semi-condensed'` * - `'semi-expanded'` * - `'expanded'` * - `'extra-expanded'` * - `'ultra-expanded'` * * @default 'normal' */ fontStretch: TrFontFaceDescriptors['stretch']; /** * Font Size * * @remarks * The font size to use when looking up the font face. * * The font size is specified in pixels and is the height of the font's * em-square. The em-square is essentially the height of the capital letters * for the font. The actual height of the text can be larger than the * specified font size, as the font may have ascenders and descenders that * extend beyond the em-square. * * @default 16 */ fontSize: number; } export interface TrProps extends TrFontProps { /** * Text to display * * @default '' */ text: string; /** * Text alignment * * @remarks * Alignment of the text relative to it's contained bounds. For best results, * use {@link contain} mode `'width'` or `'both'` and a set an explicit * {@link width} for the text to be aligned within. * * @default 'left' */ textAlign: 'left' | 'center' | 'right'; /** * Color of text * * @remarks * The color value is a number in the format 0xRRGGBBAA, where RR is the red * component, GG is the green component, BB is the blue component, and AA is * the alpha component. * * @default 0xffffffff (opaque white) */ color: number; x: number; y: number; /** * Contain mode for text * * @remarks * The contain mode determines how the text is contained within the bounds * of the Text Node. The default value is `'none'`, which means that the * Text Renderer will not constrain the text in any way. `'width'` mode will * constrain the text to the set width wrapping lines as necessary, and * `'both'` mode will constrain the text to both the set width and height * wrapping lines and truncating text as necessary. * * ## Text Auto-size Behavior * Depending on the set contain mode, after the text 'loaded' event is emitted, * the text node may have either its {@link width} and {@link height} updated * to match the rendered size of the text. * * When contain mode is 'none', both the {@link width} and {@link height} * properties are updated. * * When contain mode is 'width', only the {@link height} property is updated. * * When contain mode is 'both', neither property is updated. * * @default 'none' */ contain: 'none' | 'width' | 'both'; width: number; height: number; /** * Whether or not the text is scrollable * * @remarks * If `scrollable` is `true`, the text can be scrolled vertically within the * bounds of the Text Node. You can set the scroll position using the * {@link scrollY} property. * * @default false */ scrollable: boolean; /** * Vertical scroll position for text * * @remarks * The vertical scroll position of the text. This property is only used if * {@link scrollable} is `true`. * * @default 0 */ scrollY: number; /** * Vertical offset for text * * @remarks * The vertical offset of the text. This property is only used if * {@link scrollable} is `true`. * * @default 0 */ offsetY: number; /** * Letter spacing for text (in pixels) * * @remarks * This property sets additional (or reduced, if value is negative) spacing * between characters in the text. * * @default 0 */ letterSpacing: number; /** * Line height for text (in pixels) * * @remarks * This property sets the height of each line. If set to `undefined`, the * line height will be calculated based on the font and font size to be the * minimal height required to completely contain a line of text. * * See: https://github.com/lightning-js/renderer/issues/170 * * @default `undefined` */ lineHeight: number | undefined; /** * Max lines for text * * @remarks * This property sets max number of lines of a text paragraph. * Not yet implemented in the SDF renderer. * * @default 0 */ maxLines: number; /** * Baseline for text * * @remarks * This property sets the text baseline used when drawing text. * Not yet implemented in the SDF renderer. * * @default alphabetic */ textBaseline: TextBaseline; /** * Vertical Align for text when lineHeight > fontSize * * @remarks * This property sets the vertical align of the text. * Not yet implemented in the SDF renderer. * * @default middle */ verticalAlign: TextVerticalAlign; /** * Overflow Suffix for text * * @remarks * The suffix to be added when text is cropped due to overflow. * Not yet implemented in the SDF renderer. * * @default "..." */ overflowSuffix: string; zIndex: number; debug: Partial<TextRendererDebugProps>; } export type TrPropSetters<StateT = TextRendererState> = { [key in keyof TrProps]: (state: StateT, value: TrProps[key]) => void; }; const trPropSetterDefaults: TrPropSetters = { x: (state, value) => { state.props.x = value; }, y: (state, value) => { state.props.y = value; }, width: (state, value) => { state.props.width = value; }, height: (state, value) => { state.props.height = value; }, color: (state, value) => { state.props.color = value; }, zIndex: (state, value) => { state.props.zIndex = value; }, fontFamily: (state, value) => { state.props.fontFamily = value; }, fontWeight: (state, value) => { state.props.fontWeight = value; }, fontStyle: (state, value) => { state.props.fontStyle = value; }, fontStretch: (state, value) => { state.props.fontStretch = value; }, fontSize: (state, value) => { state.props.fontSize = value; }, text: (state, value) => { state.props.text = value; }, textAlign: (state, value) => { state.props.textAlign = value; }, contain: (state, value) => { state.props.contain = value; }, offsetY: (state, value) => { state.props.offsetY = value; }, scrollable: (state, value) => { state.props.scrollable = value; }, scrollY: (state, value) => { state.props.scrollY = value; }, letterSpacing: (state, value) => { state.props.letterSpacing = value; }, lineHeight: (state, value) => { state.props.lineHeight = value; }, maxLines: (state, value) => { state.props.maxLines = value; }, textBaseline: (state, value) => { state.props.textBaseline = value; }, verticalAlign: (state, value) => { state.props.verticalAlign = value; }, overflowSuffix: (state, value) => { state.props.overflowSuffix = value; }, debug: (state, value) => { state.props.debug = value; }, }; /** * Event handler for when text is loaded * * @remarks * Emitted by state.emitter */ export type TrLoadedEventHandler = (target: any) => void; /** * Event handler for when text failed to load * * @remarks * Emitted by state.emitter */ export type TrFailedEventHandler = (target: any, error: Error) => void; export abstract class TextRenderer< StateT extends TextRendererState = TextRendererState, > { readonly set: Readonly<TrPropSetters<StateT>>; abstract type: 'canvas' | 'sdf'; constructor(protected stage: Stage) { const propSetters = { ...trPropSetterDefaults, ...this.getPropertySetters(), }; // For each prop setter add a wrapper method that checks if the prop is // different before calling the setter const propSet = {}; Object.keys(propSetters).forEach((key) => { Object.defineProperty(propSet, key, { value: (state: StateT, value: TrProps[keyof TrProps]) => { // Check if the current prop value is different before calling the setter if (state.props[key as keyof TrProps] !== value) { propSetters[key as keyof TrPropSetters](state, value as never); // Assume any prop change will require a render // This ensures that renders are triggered appropriately even with RAF paused this.stage.requestRender(); } }, writable: false, // Prevents property from being changed configurable: false, // Prevents property from being deleted }); }); this.set = propSet as Readonly<TrPropSetters<StateT>>; } setStatus(state: StateT, status: StateT['status'], error?: Error) { // Don't emit the same status twice if (state.status === status) { return; } state.status = status; state.emitter.emit(status, error); } /** * Allows the CoreTextNode to communicate changes to the isRenderable state of * the itself. * * @param state * @param renderable */ setIsRenderable(state: StateT, renderable: boolean) { state.isRenderable = renderable; } /** * Called by constructor to get a map of property setter functions for this renderer. */ abstract getPropertySetters(): Partial<TrPropSetters<StateT>>; /** * Given text renderer properties (particularly the specific properties related to font selection) * returns whether or not the renderer can render it. * * @param props */ abstract canRenderFont(props: TrFontProps): boolean; /** * Called by the TrFontManager to find out if a newly added font face is supported * by this renderer. * * @param fontFace */ abstract isFontFaceSupported(fontFace: TrFontFace): boolean; /** * Called by the TrFontManager to add a font face to the renderer's font registry. * * @remarks * This method MUST ONLY be called for a fontFace that previously passed the * {@link isFontFaceSupported} check. * * @param fontFace */ abstract addFontFace(fontFace: TrFontFace): void; abstract createState(props: TrProps, node: CoreTextNode): StateT; /** * Destroy/Clean up the state object * * @remarks * Opposite of createState(). Frees any event listeners / resources held by * the state that may not reliably get garbage collected. * * @param state */ destroyState(state: StateT) { this.setStatus(state, 'destroyed'); state.emitter.removeAllListeners(); } /** * Schedule a state update via queueMicrotask * * @remarks * This method is used to schedule a state update via queueMicrotask. This * method should be called whenever a state update is needed, and it will * ensure that the state is only updated once per microtask. * @param state * @returns */ scheduleUpdateState(state: StateT): void { if (state.updateScheduled) { return; } state.updateScheduled = true; queueMicrotask(() => { // If the state has been destroyed, don't update it if (state.status === 'destroyed') { return; } state.updateScheduled = false; this.updateState(state); }); } abstract updateState(state: StateT): void; renderQuads?(node: CoreTextNode): void; }