@lightningjs/renderer
Version:
Lightning 3 Renderer
235 lines (207 loc) • 6.48 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 { assertTruthy } from '../../utils.js';
import type { Stage } from '../Stage.js';
import type {
CanvasRenderInfo,
FontHandler,
TextLineStruct,
TextRenderInfo,
} from './TextRenderer.js';
import * as CanvasFontHandler from './CanvasFontHandler.js';
import type { CoreTextNodeProps } from '../CoreTextNode.js';
import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
import { mapTextLayout } from './TextLayoutEngine.js';
const MAX_TEXTURE_DIMENSION = 4096;
const type = 'canvas' as const;
const font: FontHandler = CanvasFontHandler;
let canvas: HTMLCanvasElement | OffscreenCanvas | null = null;
let context:
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D
| null = null;
// Separate canvas and context for text measurements
let measureCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
let measureContext:
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D
| null = null;
// Cache for text layout calculations
const renderInfoCache = new Map<string, CanvasRenderInfo>();
// Initialize the Text Renderer
const init = (stage: Stage): void => {
const dpr = stage.options.devicePhysicalPixelRatio;
// Drawing canvas and context
canvas = stage.platform.createCanvas() as HTMLCanvasElement | OffscreenCanvas;
context = canvas.getContext('2d', { willReadFrequently: true }) as
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D;
context.setTransform(dpr, 0, 0, dpr, 0, 0);
context.textRendering = 'optimizeSpeed';
// Separate measuring canvas and context
measureCanvas = stage.platform.createCanvas() as
| HTMLCanvasElement
| OffscreenCanvas;
measureContext = measureCanvas.getContext('2d') as
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D;
measureContext.setTransform(dpr, 0, 0, dpr, 0, 0);
measureContext.textRendering = 'optimizeSpeed';
// Set up a minimal size for the measuring canvas since we only use it for measurements
measureCanvas.width = 1;
measureCanvas.height = 1;
CanvasFontHandler.init(context, measureContext);
};
/**
* Canvas text renderer
*
* @param stage - Stage instance for font resolution
* @param props - Text rendering properties
* @returns Object containing ImageData and dimensions
*/
const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
assertTruthy(canvas, 'Canvas is not initialized');
assertTruthy(context, 'Canvas context is not available');
assertTruthy(measureContext, 'Canvas measureContext is not available');
const cacheKey = getLayoutCacheKey(props);
let layout = renderInfoCache.get(cacheKey);
if (layout !== undefined) {
return layout;
}
// Extract already normalized properties
const {
text,
fontFamily,
fontStyle,
fontSize,
textAlign,
maxLines,
lineHeight,
verticalAlign,
overflowSuffix,
maxWidth,
maxHeight,
wordBreak,
} = props;
const font = `${fontStyle} ${fontSize}px Unknown, ${fontFamily}`;
// Get font metrics and calculate line height
measureContext.font = font;
measureContext.textBaseline = 'hanging';
const metrics = CanvasFontHandler.getFontMetrics(fontFamily, fontSize);
const letterSpacing = props.letterSpacing;
const [
lines,
remainingLines,
hasRemainingText,
bareLineHeight,
lineHeightPx,
effectiveWidth,
effectiveHeight,
] = mapTextLayout(
CanvasFontHandler.measureText,
metrics,
text,
textAlign,
fontFamily,
lineHeight,
overflowSuffix,
wordBreak,
letterSpacing,
maxLines,
maxWidth,
maxHeight,
);
const lineAmount = lines.length;
const canvasW = Math.ceil(effectiveWidth);
const canvasH = Math.ceil(effectiveHeight);
canvas.width = canvasW;
canvas.height = canvasH;
context.fillStyle = 'white';
context.font = font;
context.textBaseline = 'hanging';
// Performance optimization for large fonts
if (fontSize >= 128) {
context.globalAlpha = 0.01;
context.fillRect(0, 0, 0.01, 0.01);
context.globalAlpha = 1.0;
}
for (let i = 0; i < lineAmount; i++) {
const line = lines[i] as TextLineStruct;
const textLine = line[0];
let currentX = Math.ceil(line[3]);
const currentY = Math.ceil(line[4]);
if (letterSpacing === 0) {
context.fillText(textLine, currentX, currentY);
} else {
const textLineLength = textLine.length;
for (let j = 0; j < textLineLength; j++) {
const char = textLine.charAt(j);
if (hasZeroWidthSpace(char) === true) {
continue;
}
context.fillText(char, currentX, currentY);
currentX += CanvasFontHandler.measureText(
char,
fontFamily,
letterSpacing,
);
}
}
}
// Extract image data
let imageData: ImageData | null = null;
if (canvas.width > 0 && canvas.height > 0) {
imageData = context.getImageData(0, 0, canvasW, canvasH);
}
const renderInfo = {
type,
imageData,
width: effectiveWidth,
height: effectiveHeight,
remainingLines,
hasRemainingText,
} as CanvasRenderInfo;
renderInfoCache.set(cacheKey, renderInfo);
return renderInfo;
};
/**
* Clear layout cache for memory management
*/
const clearCache = (): void => {
renderInfoCache.clear();
};
/**
* Render quads for Canvas renderer (Canvas doesn't use quad-based rendering)
*/
const renderQuads = (): void => {
// Canvas renderer doesn't use quad-based rendering
// This method is for interface compatibility only
};
/**
* Canvas Text Renderer - implements TextRenderer interface
*/
const CanvasTextRenderer = {
type,
font,
renderText,
renderQuads,
init,
clearCache,
};
export default CanvasTextRenderer;