@lightningjs/renderer
Version:
Lightning 3 Renderer
257 lines • 10.2 kB
JavaScript
/*
* 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 { Texture, TextureType } from './Texture.js';
import { isCompressedTextureContainer, loadCompressedTexture, } from '../lib/textureCompression.js';
import { convertUrlToAbsolute, dataURIToBlob, isBase64Image, } from '../lib/utils.js';
import { isSvgImage, loadSvg } from '../lib/textureSvg.js';
import { fetchJson } from '../text-rendering/font-face-types/utils.js';
/**
* Texture consisting of an image loaded from a URL
*
* @remarks
* The ImageTexture's {@link ImageTextureProps.src} prop defines the image URL
* to be downloaded.
*
* By default, the texture's alpha values will be premultiplied into its color
* values which is generally the desired setting before they are sent to the
* texture's associated {@link Shader}. However, in special cases you may want
* the Shader to receive straight (non-premultiplied) values. In that case you
* can disable the default behavior by setting the
* {@link ImageTextureProps.premultiplyAlpha} prop to `false`.
*/
export class ImageTexture extends Texture {
type = TextureType.image;
props;
constructor(txManager, props) {
const resolvedProps = ImageTexture.resolveDefaults(props);
super(txManager);
this.props = resolvedProps;
this.maxRetryCount =
resolvedProps.maxRetryCount ?? this.txManager.maxRetryCount;
}
hasAlphaChannel(mimeType) {
return mimeType.indexOf('image/png') !== -1;
}
async loadImageFallback(src, hasAlpha) {
const img = new Image();
if (typeof src === 'string' && isBase64Image(src) === false) {
img.crossOrigin = 'anonymous';
}
return new Promise((resolve, reject) => {
img.onload = () => {
resolve({ data: img, premultiplyAlpha: hasAlpha });
};
img.onerror = (err) => {
const errorMessage = err instanceof Error
? err.message
: err instanceof Event
? `Image loading failed for ${img.src}`
: 'Unknown image loading error';
reject(new Error(`Image loading failed: ${errorMessage}`));
};
if (src instanceof Blob) {
img.src = URL.createObjectURL(src);
}
else {
img.src = src;
}
});
}
async createImageBitmap(blob, premultiplyAlpha, sx, sy, sw, sh) {
const hasAlphaChannel = premultiplyAlpha ?? blob.type.includes('image/png');
const imageBitmapSupported = this.txManager.imageBitmapSupported;
if (imageBitmapSupported.full === true && sw !== null && sh !== null) {
// createImageBitmap with crop
try {
const bitmap = await createImageBitmap(blob, sx || 0, sy || 0, sw, sh, {
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
});
return { data: bitmap, premultiplyAlpha: hasAlphaChannel };
}
catch (error) {
throw new Error(`Failed to create image bitmap with crop: ${error}`);
}
}
else if (imageBitmapSupported.basic === true) {
// basic createImageBitmap without options or crop
// this is supported for Chrome v50 to v52/54 that doesn't support options
try {
return {
data: await createImageBitmap(blob),
premultiplyAlpha: hasAlphaChannel,
};
}
catch (error) {
throw new Error(`Failed to create basic image bitmap: ${error}`);
}
}
// default createImageBitmap without crop but with options
try {
const bitmap = await createImageBitmap(blob, {
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
});
return { data: bitmap, premultiplyAlpha: hasAlphaChannel };
}
catch (error) {
throw new Error(`Failed to create image bitmap with options: ${error}`);
}
}
async loadImage(src) {
const { premultiplyAlpha, sx, sy, sw, sh } = this.props;
if (this.txManager.hasCreateImageBitmap === true) {
if (isBase64Image(src) === false &&
this.txManager.hasWorker === true &&
this.txManager.imageWorkerManager !== null) {
try {
return this.txManager.imageWorkerManager.getImage(src, premultiplyAlpha, sx, sy, sw, sh);
}
catch (error) {
throw new Error(`Failed to load image via worker: ${error}`);
}
}
let blob;
if (isBase64Image(src) === true) {
blob = dataURIToBlob(src);
}
else {
try {
blob = (await fetchJson(src, 'blob'));
}
catch (error) {
throw new Error(`Failed to fetch image blob from ${src}: ${error}`);
}
}
return this.createImageBitmap(blob, premultiplyAlpha, sx, sy, sw, sh);
}
return this.loadImageFallback(src, premultiplyAlpha ?? true);
}
async getTextureSource() {
let resp;
try {
resp = await this.determineImageTypeAndLoadImage();
}
catch (e) {
this.setState('failed', e);
return {
data: null,
};
}
if (resp.data === null) {
this.setState('failed', Error('ImageTexture: No image data'));
return {
data: null,
};
}
return {
data: resp.data,
premultiplyAlpha: this.props.premultiplyAlpha ?? true,
};
}
determineImageTypeAndLoadImage() {
const { src, premultiplyAlpha, type } = this.props;
if (src === null) {
return {
data: null,
};
}
if (typeof src !== 'string') {
if (src instanceof Blob) {
if (this.txManager.hasCreateImageBitmap === true) {
const { sx, sy, sw, sh } = this.props;
return this.createImageBitmap(src, premultiplyAlpha, sx, sy, sw, sh);
}
else {
return this.loadImageFallback(src, premultiplyAlpha ?? true);
}
}
if (src instanceof ImageData) {
return {
data: src,
premultiplyAlpha,
};
}
return {
data: src(),
premultiplyAlpha,
};
}
const absoluteSrc = convertUrlToAbsolute(src);
if (type === 'regular') {
return this.loadImage(absoluteSrc);
}
if (type === 'svg') {
return loadSvg(absoluteSrc, this.props.width, this.props.height, this.props.sx, this.props.sy, this.props.sw, this.props.sh);
}
if (isSvgImage(src) === true) {
return loadSvg(absoluteSrc, this.props.width, this.props.height, this.props.sx, this.props.sy, this.props.sw, this.props.sh);
}
if (type === 'compressed') {
return loadCompressedTexture(absoluteSrc);
}
if (isCompressedTextureContainer(src) === true) {
return loadCompressedTexture(absoluteSrc);
}
// default
return this.loadImage(absoluteSrc);
}
/**
* Generates a cache key for the ImageTexture based on the provided props.
* @param props - The props used to generate the cache key.
* @returns The cache key as a string, or `false` if the key cannot be generated.
*/
static makeCacheKey(props) {
const resolvedProps = ImageTexture.resolveDefaults(props);
// Only cache key-able textures; prioritise key
const key = resolvedProps.key || resolvedProps.src;
if (typeof key !== 'string') {
return false;
}
// if we have source dimensions, cache the texture separately
let dimensionProps = '';
if (resolvedProps.sh !== null && resolvedProps.sw !== null) {
dimensionProps += ',';
dimensionProps += resolvedProps.sx ?? '';
dimensionProps += resolvedProps.sy ?? '';
dimensionProps += resolvedProps.sw || '';
dimensionProps += resolvedProps.sh || '';
}
return `ImageTexture,${key},${resolvedProps.premultiplyAlpha ?? 'true'}${dimensionProps}`;
}
static resolveDefaults(props) {
return {
src: props.src ?? '',
premultiplyAlpha: props.premultiplyAlpha ?? true, // null,
key: props.key ?? null,
type: props.type ?? null,
width: props.width ?? null,
height: props.height ?? null,
sx: props.sx ?? null,
sy: props.sy ?? null,
sw: props.sw ?? null,
sh: props.sh ?? null,
maxRetryCount: props.maxRetryCount ?? null,
};
}
static z$__type__Props;
}
//# sourceMappingURL=ImageTexture.js.map