diff --git a/examples/assets/elevator.svg b/examples/assets/elevator.svg
new file mode 100644
index 00000000..2a050e6c
--- /dev/null
+++ b/examples/assets/elevator.svg
@@ -0,0 +1,95 @@
+
+
diff --git a/examples/assets/lightning.svg b/examples/assets/lightning.svg
new file mode 100644
index 00000000..0a50c2ae
--- /dev/null
+++ b/examples/assets/lightning.svg
@@ -0,0 +1,47 @@
+
+
+
diff --git a/examples/assets/rocko.svg b/examples/assets/rocko.svg
new file mode 100644
index 00000000..ebde6300
--- /dev/null
+++ b/examples/assets/rocko.svg
@@ -0,0 +1,170 @@
+
+
diff --git a/examples/assets/rocko2.svg b/examples/assets/rocko2.svg
new file mode 100644
index 00000000..ebde6300
--- /dev/null
+++ b/examples/assets/rocko2.svg
@@ -0,0 +1,170 @@
+
+
diff --git a/examples/assets/testscreen.svg b/examples/assets/testscreen.svg
new file mode 100644
index 00000000..b1adb69f
--- /dev/null
+++ b/examples/assets/testscreen.svg
@@ -0,0 +1,608 @@
+
+
+
diff --git a/examples/tests/texture-source.ts b/examples/tests/texture-source.ts
new file mode 100644
index 00000000..6a820841
--- /dev/null
+++ b/examples/tests/texture-source.ts
@@ -0,0 +1,141 @@
+/*
+ * 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 INode,
+ type Dimensions,
+ type NodeLoadedEventHandler,
+} from '@lightningjs/renderer';
+import rockoPng from '../assets/rocko.png';
+import lightningPng from '../assets/lightning.png';
+import elevatorPng from '../assets/elevator.png';
+import type { ExampleSettings } from '../common/ExampleSettings.js';
+
+export async function automation(settings: ExampleSettings) {
+ await test(settings);
+ await settings.snapshot();
+}
+
+export default async function test({ renderer, testRoot }: ExampleSettings) {
+ const FONT_SIZE = 45;
+ const BEGIN_Y = FONT_SIZE;
+
+ const header = renderer.createTextNode({
+ fontFamily: 'Ubuntu',
+ text: `PNG Source Test`,
+ fontSize: FONT_SIZE,
+ parent: testRoot,
+ });
+
+ const curX = 0;
+ let curY = BEGIN_Y;
+ let curTest = 1;
+
+ const rocko = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: rockoPng,
+ parent: testRoot,
+ });
+
+ await execLoadingTest(rocko, 181, 218);
+
+ const elevator = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: elevatorPng,
+ parent: testRoot,
+ srcX: 120,
+ srcY: 0,
+ srcHeight: 268,
+ srcWidth: 100,
+ });
+
+ await execLoadingTest(elevator, 100, 268);
+
+ const lightningNode = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: lightningPng,
+ srcHeight: 100,
+ srcWidth: 100,
+ parent: testRoot,
+ });
+
+ await execLoadingTest(lightningNode, 100, 100);
+
+ function waitForTxLoaded(imgNode: INode) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject(new Error('TIMEOUT'));
+ }, 1000);
+ imgNode.once('loaded', ((target, payload) => {
+ resolve(payload.dimensions);
+ }) satisfies NodeLoadedEventHandler);
+ });
+ }
+
+ async function execLoadingTest(
+ imgNode: INode,
+ expectedWidth: number,
+ expectedHeight: number,
+ ) {
+ const textNode = renderer.createTextNode({
+ fontFamily: 'Ubuntu',
+ x: curX,
+ text: '',
+ fontSize: FONT_SIZE,
+ parent: testRoot,
+ });
+
+ let exception: string | false = false;
+ let dimensions: Dimensions = { width: 0, height: 0 };
+ try {
+ dimensions = await waitForTxLoaded(imgNode);
+ } catch (e: unknown) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ exception = (e as any)?.message ?? 'Unknown';
+ }
+
+ imgNode.width = dimensions.width;
+ imgNode.height = dimensions.height;
+
+ textNode.y = imgNode.y + imgNode.height;
+ let result = 'Fail';
+ let expectedPostfix = '';
+ if (
+ !exception &&
+ imgNode.width === expectedWidth &&
+ imgNode.height === expectedHeight
+ ) {
+ textNode.color = 0x00ff00ff;
+ result = 'Pass';
+ } else {
+ textNode.color = 0xff0000ff;
+ if (exception) {
+ expectedPostfix = ` (exception: ${exception})`;
+ } else {
+ expectedPostfix = ` (expected ${expectedWidth}x${expectedHeight})`;
+ }
+ }
+ textNode.text = `${curTest}. Loaded Event Test: ${result} (${imgNode.width}x${imgNode.height})${expectedPostfix}`;
+ curY = textNode.y + FONT_SIZE;
+ curTest++;
+ }
+}
diff --git a/examples/tests/texture-svg.ts b/examples/tests/texture-svg.ts
new file mode 100644
index 00000000..8e2f4c4a
--- /dev/null
+++ b/examples/tests/texture-svg.ts
@@ -0,0 +1,209 @@
+/*
+ * 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 INode,
+ type Dimensions,
+ type NodeLoadedEventHandler,
+ type NodeFailedEventHandler,
+ Texture,
+} from '@lightningjs/renderer';
+import rockoSvg from '../assets/rocko.svg';
+import lightning from '../assets/lightning.svg';
+import elevatorSvg from '../assets/elevator.svg';
+import rockoSvg2 from '../assets/rocko2.svg';
+import type { ExampleSettings } from '../common/ExampleSettings.js';
+
+export async function automation(settings: ExampleSettings) {
+ await test(settings);
+ await settings.snapshot();
+}
+
+export default async function test({ renderer, testRoot }: ExampleSettings) {
+ const FONT_SIZE = 45;
+ const BEGIN_Y = FONT_SIZE;
+
+ const header = renderer.createTextNode({
+ fontFamily: 'Ubuntu',
+ text: `SVG Test`,
+ fontSize: FONT_SIZE,
+ parent: testRoot,
+ });
+
+ const curX = 0;
+ let curY = BEGIN_Y;
+ let curTest = 1;
+
+ const rocko = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: rockoSvg,
+ parent: testRoot,
+ });
+
+ await execLoadingTest(rocko, 181, 218);
+
+ const elevator = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: elevatorSvg,
+ parent: testRoot,
+ });
+
+ await execLoadingTest(elevator, 200, 268);
+
+ const lightningNode = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: lightning,
+ height: 25,
+ width: 125,
+ parent: testRoot,
+ });
+
+ await execLoadingTest(lightningNode, 125, 25);
+
+ const partialSvg = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: rockoSvg2,
+ srcX: 100,
+ srcY: 0,
+ srcHeight: 218,
+ srcWidth: 81,
+ parent: testRoot,
+ });
+
+ await execLoadingTest(partialSvg, 81, 218);
+
+ // Test: Check that we capture a texture load failure
+ const failure = renderer.createNode({
+ x: curX,
+ y: curY,
+ src: 'does-not-exist.svg',
+ parent: testRoot,
+ });
+
+ await execFailureTest(failure);
+
+ function waitForTxLoaded(imgNode: INode) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject(new Error('TIMEOUT'));
+ }, 1000);
+ imgNode.once('loaded', ((target, payload) => {
+ resolve(payload.dimensions);
+ }) satisfies NodeLoadedEventHandler);
+ });
+ }
+
+ function waitForTxFailed(imgNode: INode) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject(new Error('TIMEOUT'));
+ }, 1000);
+ imgNode.once('failed', (() => {
+ resolve(true);
+ }) satisfies NodeFailedEventHandler);
+ });
+ }
+
+ async function execLoadingTest(
+ imgNode: INode,
+ expectedWidth: number,
+ expectedHeight: number,
+ ) {
+ const textNode = renderer.createTextNode({
+ fontFamily: 'Ubuntu',
+ x: curX,
+ text: '',
+ fontSize: FONT_SIZE,
+ parent: testRoot,
+ });
+
+ let exception: string | false = false;
+ let dimensions: Dimensions = { width: 0, height: 0 };
+ try {
+ dimensions = await waitForTxLoaded(imgNode);
+ } catch (e: unknown) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ exception = (e as any)?.message ?? 'Unknown';
+ }
+
+ imgNode.width = dimensions.width;
+ imgNode.height = dimensions.height;
+
+ textNode.y = imgNode.y + imgNode.height;
+ let result = 'Fail';
+ let expectedPostfix = '';
+ if (
+ !exception &&
+ imgNode.width === expectedWidth &&
+ imgNode.height === expectedHeight
+ ) {
+ textNode.color = 0x00ff00ff;
+ result = 'Pass';
+ } else {
+ textNode.color = 0xff0000ff;
+ if (exception) {
+ expectedPostfix = ` (exception: ${exception})`;
+ } else {
+ expectedPostfix = ` (expected ${expectedWidth}x${expectedHeight})`;
+ }
+ }
+ textNode.text = `${curTest}. Loaded Event Test: ${result} (${imgNode.width}x${imgNode.height})${expectedPostfix}`;
+ curY = textNode.y + FONT_SIZE;
+ curTest++;
+ }
+
+ async function execFailureTest(imgNode: INode) {
+ const textNode = renderer.createTextNode({
+ fontFamily: 'Ubuntu',
+ x: curX,
+ text: '',
+ fontSize: FONT_SIZE,
+ parent: testRoot,
+ });
+
+ let failureTestPass = false;
+ let exception: string | false = false;
+ try {
+ failureTestPass = await waitForTxFailed(imgNode);
+ } catch (e: unknown) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ exception = (e as any)?.message ?? 'Unknown';
+ }
+
+ textNode.y = imgNode.y + imgNode.height;
+ let result = '';
+ if (!exception && failureTestPass) {
+ textNode.color = 0x00ff00ff;
+ result = 'Pass';
+ } else {
+ textNode.color = 0xff0000ff;
+ result = 'Fail';
+ if (exception) {
+ result += ` (exception: ${exception})`;
+ }
+ }
+ textNode.text = `${curTest}. Failure Event Test: ${result}`;
+ curY = textNode.y + FONT_SIZE;
+ curTest++;
+ }
+}
diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts
index 79db1024..67048b42 100644
--- a/src/core/CoreNode.ts
+++ b/src/core/CoreNode.ts
@@ -613,6 +613,47 @@ export interface CoreNodeProps {
* @default `undefined`
*/
data?: CustomDataMap;
+
+ /**
+ * Image Type to explicitly set the image type that is being loaded
+ *
+ * @remarks
+ * This property must be used with a `src` that points at an image. In some cases
+ * the extension doesn't provide a reliable representation of the image type. In such
+ * cases set the ImageType explicitly.
+ *
+ * `regular` is used for normal images such as png, jpg, etc
+ * `compressed` is used for ETC1/ETC2 compressed images with a PVR or KTX container
+ * `svg` is used for scalable vector graphics
+ *
+ * @default `undefined`
+ */
+ imageType?: 'regular' | 'compressed' | 'svg' | null;
+
+ /**
+ * She width of the rectangle from which the Image Texture will be extracted.
+ * This value can be negative. If not provided, the image's source natural
+ * width will be used.
+ */
+ srcWidth?: number;
+ /**
+ * The height of the rectangle from which the Image Texture will be extracted.
+ * This value can be negative. If not provided, the image's source natural
+ * height will be used.
+ */
+ srcHeight?: number;
+ /**
+ * The x coordinate of the reference point of the rectangle from which the Texture
+ * will be extracted. `width` and `height` are provided. And only works when
+ * createImageBitmap is available. Only works when createImageBitmap is supported on the browser.
+ */
+ srcX?: number;
+ /**
+ * The y coordinate of the reference point of the rectangle from which the Texture
+ * will be extracted. Only used when source `srcWidth` width and `srcHeight` height
+ * are provided. Only works when createImageBitmap is supported on the browser.
+ */
+ srcY?: number;
}
/**
@@ -1845,9 +1886,60 @@ export class CoreNode extends EventEmitter {
this.texture = this.stage.txManager.loadTexture('ImageTexture', {
src: imageUrl,
+ width: this.props.width,
+ height: this.props.height,
+ type: this.props.imageType,
+ sx: this.props.srcX,
+ sy: this.props.srcY,
+ sw: this.props.srcWidth,
+ sh: this.props.srcHeight,
});
}
+ set imageType(type: 'regular' | 'compressed' | 'svg' | null) {
+ if (this.props.imageType === type) {
+ return;
+ }
+
+ this.props.imageType = type;
+ }
+
+ get imageType() {
+ return this.props.imageType || null;
+ }
+
+ get srcHeight(): number | undefined {
+ return this.props.srcHeight;
+ }
+
+ set srcHeight(value: number) {
+ this.props.srcHeight = value;
+ }
+
+ get srcWidth(): number | undefined {
+ return this.props.srcWidth;
+ }
+
+ set srcWidth(value: number) {
+ this.props.srcWidth = value;
+ }
+
+ get srcX(): number | undefined {
+ return this.props.srcX;
+ }
+
+ set srcX(value: number) {
+ this.props.srcX = value;
+ }
+
+ get srcY(): number | undefined {
+ return this.props.srcY;
+ }
+
+ set srcY(value: number) {
+ this.props.srcY = value;
+ }
+
/**
* Returns the framebuffer dimensions of the node.
* If the node has a render texture, the dimensions are the same as the node's dimensions.
diff --git a/src/core/Stage.ts b/src/core/Stage.ts
index b306edae..f9c18805 100644
--- a/src/core/Stage.ts
+++ b/src/core/Stage.ts
@@ -550,6 +550,10 @@ export class Stage {
// Since setting the `src` will trigger a texture load, we need to set it after
// we set the texture. Otherwise, problems happen.
src: props.src ?? null,
+ srcHeight: props.srcHeight,
+ srcWidth: props.srcWidth,
+ srcX: props.srcX,
+ srcY: props.srcY,
scale: props.scale ?? null,
scaleX: props.scaleX ?? props.scale ?? 1,
scaleY: props.scaleY ?? props.scale ?? 1,
@@ -562,6 +566,7 @@ export class Stage {
rotation: props.rotation ?? 0,
rtt: props.rtt ?? false,
data: data,
+ imageType: props.imageType,
};
}
}
diff --git a/src/core/animations/CoreAnimation.ts b/src/core/animations/CoreAnimation.ts
index 29cb4706..af78737d 100644
--- a/src/core/animations/CoreAnimation.ts
+++ b/src/core/animations/CoreAnimation.ts
@@ -60,7 +60,8 @@ export class CoreAnimation extends EventEmitter {
this.propValuesMap['props'] = {};
}
this.propValuesMap['props'][key] = {
- start: node[key as keyof Omit],
+ start:
+ node[key as keyof Omit] || 0,
target: props[
key as keyof Omit
] as number,
diff --git a/src/core/lib/ImageWorker.ts b/src/core/lib/ImageWorker.ts
index c212a7f3..087cf44f 100644
--- a/src/core/lib/ImageWorker.ts
+++ b/src/core/lib/ImageWorker.ts
@@ -25,6 +25,17 @@ interface getImageReturn {
premultiplyAlpha: boolean | null;
}
+interface ImageWorkerMessage {
+ id: number;
+ src: string;
+ data: getImageReturn;
+ error: string;
+ sx: number | null;
+ sy: number | null;
+ sw: number | null;
+ sh: number | null;
+}
+
/**
* Note that, within the createImageWorker function, we must only use ES5 code to keep it ES5-valid after babelifying, as
* the converted code of this section is converted to a blob and used as the js of the web worker thread.
@@ -43,6 +54,10 @@ function createImageWorker() {
function getImage(
src: string,
premultiplyAlpha: boolean | null,
+ x: number | null,
+ y: number | null,
+ width: number | null,
+ height: number | null,
): Promise {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
@@ -60,6 +75,21 @@ function createImageWorker() {
? premultiplyAlpha
: hasAlphaChannel(blob.type);
+ if (width !== null && height !== null) {
+ createImageBitmap(blob, x || 0, y || 0, width, height, {
+ premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none',
+ colorSpaceConversion: 'none',
+ imageOrientation: 'none',
+ })
+ .then(function (data) {
+ resolve({ data, premultiplyAlpha: premultiplyAlpha });
+ })
+ .catch(function (error) {
+ reject(error);
+ });
+ return;
+ }
+
createImageBitmap(blob, {
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
@@ -87,8 +117,12 @@ function createImageWorker() {
var src = event.data.src;
var id = event.data.id;
var premultiplyAlpha = event.data.premultiplyAlpha;
+ var x = event.data.sx;
+ var y = event.data.sy;
+ var width = event.data.sw;
+ var height = event.data.sh;
- getImage(src, premultiplyAlpha)
+ getImage(src, premultiplyAlpha, x, y, width, height)
.then(function (data) {
self.postMessage({ id: id, src: src, data: data });
})
@@ -114,12 +148,7 @@ export class ImageWorkerManager {
}
private handleMessage(event: MessageEvent) {
- const { id, data, error } = event.data as {
- id: number;
- src: string;
- data?: any;
- error?: string;
- };
+ const { id, data, error } = event.data as ImageWorkerMessage;
const msg = this.messageManager[id];
if (msg) {
const [resolve, reject] = msg;
@@ -155,6 +184,10 @@ export class ImageWorkerManager {
getImage(
src: string,
premultiplyAlpha: boolean | null,
+ sx: number | null,
+ sy: number | null,
+ sw: number | null,
+ sh: number | null,
): Promise {
return new Promise((resolve, reject) => {
try {
@@ -167,6 +200,10 @@ export class ImageWorkerManager {
id,
src: src,
premultiplyAlpha,
+ sx,
+ sy,
+ sw,
+ sh,
});
}
}
diff --git a/src/core/lib/textureSvg.ts b/src/core/lib/textureSvg.ts
new file mode 100644
index 00000000..04b04000
--- /dev/null
+++ b/src/core/lib/textureSvg.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 TextureData } from '../textures/Texture.js';
+
+/**
+ * Tests if the given location is a SVG
+ * @param url
+ * @remarks
+ * This function is used to determine if the given image url is a SVG
+ * image
+ * @returns
+ */
+export function isSvgImage(url: string): boolean {
+ return /\.(svg)$/.test(url);
+}
+
+/**
+ * Loads a SVG image
+ * @param url
+ * @returns
+ */
+export const loadSvg = (
+ url: string,
+ width: number | null,
+ height: number | null,
+ sx: number | null,
+ sy: number | null,
+ sw: number | null,
+ sh: number | null,
+): Promise => {
+ return new Promise((resolve, reject) => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ assertTruthy(ctx);
+
+ ctx.imageSmoothingEnabled = true;
+ const img = new Image();
+ img.onload = () => {
+ const x = sx ?? 0;
+ const y = sy ?? 0;
+ const w = width || img.width;
+ const h = height || img.height;
+
+ canvas.width = w;
+ canvas.height = h;
+ ctx.drawImage(img, 0, 0, w, h);
+
+ resolve({
+ data: ctx.getImageData(x, y, sw ?? w, sh ?? h),
+ premultiplyAlpha: false,
+ });
+ };
+
+ img.onerror = (err) => {
+ reject(err);
+ };
+
+ img.src = url;
+ });
+};
diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts
index b521a3f9..6d05fefd 100644
--- a/src/core/textures/ImageTexture.ts
+++ b/src/core/textures/ImageTexture.ts
@@ -24,6 +24,7 @@ import {
loadCompressedTexture,
} from '../lib/textureCompression.js';
import { convertUrlToAbsolute } from '../lib/utils.js';
+import { isSvgImage, loadSvg } from '../lib/textureSvg.js';
/**
* Properties of the {@link ImageTexture}
@@ -55,6 +56,52 @@ export interface ImageTextureProps {
* `ImageData` textures are not cached unless a `key` is provided
*/
key?: string | null;
+ /**
+ * Width of the image to be used as a texture. If not provided, the image's
+ * natural width will be used.
+ */
+ width?: number | null;
+ /**
+ * Height of the image to be used as a texture. If not provided, the image's
+ * natural height will be used.
+ */
+ height?: number | null;
+ /**
+ * Type, indicate an image type for overriding type detection
+ *
+ * @default null
+ */
+ type?: 'regular' | 'compressed' | 'svg' | null;
+ /**
+ * The width of the rectangle from which the ImageBitmap will be extracted. This value
+ * can be negative. Only works when createImageBitmap is supported on the browser.
+ *
+ * @default null
+ */
+ sw?: number | null;
+ /**
+ * The height of the rectangle from which the ImageBitmap will be extracted. This value
+ * can be negative. Only works when createImageBitmap is supported on the browser.
+ *
+ * @default null
+ */
+ sh?: number | null;
+ /**
+ * The y coordinate of the reference point of the rectangle from which the ImageBitmap
+ * will be extracted. Only used when `sw` and `sh` are provided. And only works when
+ * createImageBitmap is available.
+ *
+ * @default null
+ */
+ sx?: number | null;
+ /**
+ * The x coordinate of the reference point of the rectangle from which the
+ * ImageBitmap will be extracted. Only used when source `sw` width and `sh` height
+ * are provided. Only works when createImageBitmap is supported on the browser.
+ *
+ * @default null
+ */
+ sy?: number | null;
}
/**
@@ -83,45 +130,35 @@ export class ImageTexture extends Texture {
return mimeType.indexOf('image/png') !== -1;
}
- override async getTextureData(): Promise {
- const { src, premultiplyAlpha } = this.props;
- if (!src) {
- return {
- data: null,
- };
- }
-
- if (typeof src !== 'string') {
- if (src instanceof ImageData) {
- return {
- data: src,
- premultiplyAlpha,
- };
- }
- return {
- data: src(),
- premultiplyAlpha,
- };
- }
-
- // Handle compressed textures
- if (isCompressedTextureContainer(src)) {
- return loadCompressedTexture(src);
- }
+ async loadImage(src: string) {
+ const { premultiplyAlpha, sx, sy, sw, sh, width, height } = this.props;
- // Convert relative URL to absolute URL
- const absoluteSrc = convertUrlToAbsolute(src);
-
- if (this.txManager.imageWorkerManager) {
+ if (this.txManager.imageWorkerManager !== null) {
return await this.txManager.imageWorkerManager.getImage(
- absoluteSrc,
+ src,
premultiplyAlpha,
+ sx,
+ sy,
+ sw,
+ sh,
);
- } else if (this.txManager.hasCreateImageBitmap) {
- const response = await fetch(absoluteSrc);
+ } else if (this.txManager.hasCreateImageBitmap === true) {
+ const response = await fetch(src);
const blob = await response.blob();
const hasAlphaChannel =
premultiplyAlpha ?? this.hasAlphaChannel(blob.type);
+
+ if (sw !== null && sh !== null) {
+ return {
+ data: await createImageBitmap(blob, sx ?? 0, sy ?? 0, sw, sh, {
+ premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
+ colorSpaceConversion: 'none',
+ imageOrientation: 'none',
+ }),
+ premultiplyAlpha: hasAlphaChannel,
+ };
+ }
+
return {
data: await createImageBitmap(blob, {
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
@@ -131,11 +168,11 @@ export class ImageTexture extends Texture {
premultiplyAlpha: hasAlphaChannel,
};
} else {
- const img = new Image();
+ const img = new Image(width || undefined, height || undefined);
if (!(src.substr(0, 5) === 'data:')) {
img.crossOrigin = 'Anonymous';
}
- img.src = absoluteSrc;
+ img.src = src;
await new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error(`Failed to load image`));
@@ -150,6 +187,73 @@ export class ImageTexture extends Texture {
}
}
+ override async getTextureData(): Promise {
+ const { src, premultiplyAlpha, type } = this.props;
+ if (src === null) {
+ return {
+ data: null,
+ };
+ }
+
+ if (typeof src !== 'string') {
+ 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 override makeCacheKey(props: ImageTextureProps): string | false {
const resolvedProps = ImageTexture.resolveDefaults(props);
// Only cache key-able textures; prioritise key
@@ -157,7 +261,20 @@ export class ImageTexture extends Texture {
if (typeof key !== 'string') {
return false;
}
- return `ImageTexture,${key},${resolvedProps.premultiplyAlpha ?? 'true'}`;
+
+ // 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 override resolveDefaults(
@@ -167,6 +284,13 @@ export class ImageTexture extends Texture {
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,
};
}
diff --git a/visual-regression/certified-snapshots/chromium-ci/texture-source-1.png b/visual-regression/certified-snapshots/chromium-ci/texture-source-1.png
new file mode 100644
index 00000000..fa3c78ef
Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/texture-source-1.png differ
diff --git a/visual-regression/certified-snapshots/chromium-ci/texture-svg-1.png b/visual-regression/certified-snapshots/chromium-ci/texture-svg-1.png
new file mode 100644
index 00000000..da29d34e
Binary files /dev/null and b/visual-regression/certified-snapshots/chromium-ci/texture-svg-1.png differ