@lightningtv/renderer
Version:
Lightning 3 Renderer
300 lines (266 loc) • 8.38 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 { CoreNode } from '../../CoreNode.js';
import { SubTexture } from '../../textures/SubTexture.js';
import { TextureType, type Texture } from '../../textures/Texture.js';
import type { CoreContextTexture } from '../CoreContextTexture.js';
import {
CoreRenderer,
type CoreRendererOptions,
type QuadOptions,
} from '../CoreRenderer.js';
import { CanvasTexture } from './CanvasTexture.js';
import {
parseColor,
parseToAbgrString,
parseToRgbaString,
} from './internal/ColorUtils.js';
import { assertTruthy } from '../../../utils.js';
import { CanvasShaderNode, type CanvasShaderType } from './CanvasShaderNode.js';
import type { CoreShaderType } from '../CoreShaderNode.js';
export class CanvasRenderer extends CoreRenderer {
private context: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement;
private pixelRatio: number;
private clearColor: string;
public renderToTextureActive = false;
activeRttNode: CoreNode | null = null;
private parsedColorCache: Map<number, string> = new Map();
constructor(options: CoreRendererOptions) {
super(options);
this.mode = 'canvas';
const { canvas } = options;
this.canvas = canvas as HTMLCanvasElement;
this.context = canvas.getContext('2d') as CanvasRenderingContext2D;
this.pixelRatio = this.stage.pixelRatio;
this.clearColor = this.getParsedColor(this.stage.clearColor);
}
reset(): void {
this.canvas.width = this.canvas.width; // quick reset canvas
const ctx = this.context;
if (this.clearColor) {
ctx.fillStyle = this.clearColor;
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
ctx.scale(this.pixelRatio, this.pixelRatio);
}
render(): void {
// noop
}
addQuad(quad: QuadOptions): void {
const ctx = this.context;
const { tx, ty, ta, tb, tc, td, clippingRect } = quad;
let texture = quad.texture;
const textureType = texture?.type;
assertTruthy(textureType, 'Texture type is not defined');
// The Canvas2D renderer only supports image and color textures
if (
textureType !== TextureType.image &&
textureType !== TextureType.color &&
textureType !== TextureType.subTexture &&
textureType !== TextureType.noise
) {
return;
}
if (texture) {
if (texture instanceof SubTexture) {
texture = texture.parentTexture;
}
if (texture.state === 'freed') {
return;
}
if (texture.state !== 'loaded') {
return;
}
}
const hasTransform = ta !== 1;
const hasClipping = clippingRect.width !== 0 && clippingRect.height !== 0;
const hasShader = quad.shader !== null;
let saveAndRestore = hasTransform === true || hasClipping === true;
if (hasShader === true) {
saveAndRestore =
saveAndRestore || (quad.shader as CanvasShaderNode).applySNR;
}
if (saveAndRestore) {
ctx.save();
}
if (hasClipping === true) {
const path = new Path2D();
const { x, y, width, height } = clippingRect;
path.rect(x, y, width, height);
ctx.clip(path);
}
if (hasTransform === true) {
// Quad transform:
// | ta tb tx |
// | tc td ty |
// | 0 0 1 |
// C2D transform:
// | a c e |
// | b d f |
// | 0 0 1 |
const scale = this.pixelRatio;
ctx.setTransform(ta, tc, tb, td, tx * scale, ty * scale);
ctx.scale(scale, scale);
ctx.translate(-tx, -ty);
}
if (hasShader === true) {
let renderContext: (() => void) | null = () => {
this.renderContext(quad);
};
(quad.shader as CanvasShaderNode).render(ctx, quad, renderContext);
renderContext = null;
} else {
this.renderContext(quad);
}
if (saveAndRestore) {
ctx.restore();
}
}
renderContext(quad: QuadOptions) {
const color = quad.colorTl;
const textureType = quad.texture?.type;
if (
(textureType === TextureType.image ||
textureType === TextureType.subTexture ||
textureType === TextureType.noise) &&
quad.texture?.ctxTexture
) {
const tintColor = parseColor(color);
const image = (quad.texture.ctxTexture as CanvasTexture).getImage(
tintColor,
);
this.context.globalAlpha = tintColor.a ?? quad.alpha;
if (textureType === TextureType.subTexture) {
this.context.drawImage(
image,
(quad.texture as SubTexture).props.x,
(quad.texture as SubTexture).props.y,
(quad.texture as SubTexture).props.width,
(quad.texture as SubTexture).props.height,
quad.tx,
quad.ty,
quad.width,
quad.height,
);
} else {
try {
this.context.drawImage(
image,
quad.tx,
quad.ty,
quad.width,
quad.height,
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// noop
}
}
this.context.globalAlpha = 1;
return;
}
const hasGradient =
quad.colorTl !== quad.colorTr || quad.colorTl !== quad.colorBr;
if (textureType === TextureType.color && hasGradient) {
let endX: number = quad.tx;
let endY: number = quad.ty;
let endColor: number;
if (quad.colorTl === quad.colorTr) {
// vertical
endX = quad.tx;
endY = quad.ty + quad.height;
endColor = quad.colorBr;
} else {
// horizontal
endX = quad.tx + quad.width;
endY = quad.ty;
endColor = quad.colorTr;
}
const gradient = this.context.createLinearGradient(
quad.tx,
quad.ty,
endX,
endY,
);
gradient.addColorStop(0, this.getParsedColor(color));
gradient.addColorStop(1, this.getParsedColor(endColor));
this.context.fillStyle = gradient;
this.context.fillRect(quad.tx, quad.ty, quad.width, quad.height);
} else if (textureType === TextureType.color) {
this.context.fillStyle = this.getParsedColor(color);
this.context.fillRect(quad.tx, quad.ty, quad.width, quad.height);
}
}
createShaderNode(
shaderKey: string,
shaderType: Readonly<CanvasShaderType>,
props?: Record<string, any>,
) {
return new CanvasShaderNode(shaderKey, shaderType, this.stage, props);
}
createShaderProgram(shaderConfig) {
return null;
}
override supportsShaderType(shaderType: Readonly<CanvasShaderType>): boolean {
return shaderType.render !== undefined;
}
createCtxTexture(textureSource: Texture): CoreContextTexture {
return new CanvasTexture(this.stage.txMemManager, textureSource);
}
renderRTTNodes(): void {
// noop
}
removeRTTNode(node: CoreNode): void {
// noop
}
renderToTexture(node: CoreNode): void {
// noop
}
getBufferInfo(): null {
return null;
}
getQuadCount(): null {
return null;
}
getParsedColor(color: number, isRGBA: boolean = false) {
let out = this.parsedColorCache.get(color);
if (out !== undefined) {
return out;
}
if (isRGBA) {
out = parseToRgbaString(color);
} else {
out = parseToAbgrString(color);
}
this.parsedColorCache.set(color, out);
return out;
}
/**
* Updates the clear color of the canvas renderer.
*
* @param color - The color to set as the clear color.
*/
updateClearColor(color: number) {
this.clearColor = this.getParsedColor(color);
}
override getDefaultShaderNode() {
return null;
}
}