Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions src/core/platforms/web/WebPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
*/

import { Platform, type PlatformSettings } from '../Platform.js';
import { ImageWorkerManager } from './lib/ImageWorker.js';
import {
ImageWorkerManager,
type ImageWorkerFactory,
} from './lib/ImageWorker.js';
import { createImageWorker } from './lib/ImageWorkerDefault.js';
import type { Stage } from '../../Stage.js';
import {
dataURIToBlob,
Expand All @@ -37,7 +41,7 @@ import type { GlContextWrapper } from '../GlContextWrapper.js';
* make fontface add not show errors
*/
interface FontFaceSetWithAdd extends FontFaceSet {
add(font: FontFace): void;
add(font: FontFace): this;
}

export class WebPlatform extends Platform {
Expand All @@ -52,10 +56,23 @@ export class WebPlatform extends Platform {
this.useImageWorker = numImageWorkers > 0 && this.hasWorker;

if (this.useImageWorker === true) {
this.imageWorkerManager = new ImageWorkerManager(numImageWorkers);
this.imageWorkerManager = this.createImageWorkerManager(numImageWorkers);
}
}

protected createImageWorkerManager(
numImageWorkers: number,
): ImageWorkerManager {
return new ImageWorkerManager(
numImageWorkers,
this.getImageWorkerFactory(),
);
}

protected getImageWorkerFactory(): ImageWorkerFactory {
return createImageWorker;
}

////////////////////////
// Platform-specific methods
////////////////////////
Expand All @@ -65,7 +82,11 @@ export class WebPlatform extends Platform {
}

override createContext(): GlContextWrapper {
const gl = createWebGLContext(this.canvas!, this.settings.forceWebGL2);
if (this.canvas === null) {
throw new Error('Canvas has not been created yet.');
}

const gl = createWebGLContext(this.canvas, this.settings.forceWebGL2);
this.glw = new WebGlContextWrapper(gl);
return this.glw;
}
Expand Down Expand Up @@ -159,12 +180,16 @@ export class WebPlatform extends Platform {
override fetch(url: string): Promise<Blob> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = '';
xhr.responseType = 'blob';
xhr.onreadystatechange = function () {
if (xhr.readyState == XMLHttpRequest.DONE) {
// On most devices like WebOS and Tizen, the file protocol returns 0 while http(s) protocol returns 200
if (xhr.status === 0 || xhr.status === 200) {
resolve(xhr.response);
if (xhr.response instanceof Blob) {
resolve(xhr.response);
} else {
reject(new Error('Expected blob response while loading image.'));
}
} else {
reject(xhr.statusText);
}
Expand Down Expand Up @@ -234,7 +259,7 @@ export class WebPlatform extends Platform {

// fallback to main thread loading
let blob: Blob;
if (isBase64Image(src) === true) {
if (isBase64 === true) {
blob = dataURIToBlob(src);
} else {
blob = await this.fetch(absoluteSrc);
Expand Down
6 changes: 6 additions & 0 deletions src/core/platforms/web/WebPlatformChrome50.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import { WebPlatform } from './WebPlatform.js';
import type { ImageResponse } from '../../textures/ImageTexture.js';
import type { ImageWorkerFactory } from './lib/ImageWorker.js';
import { createImageWorkerNoOptions } from './lib/ImageWorkerNoOptions.js';

/**
* Chrome 50 Web Platform implementation with limited createImageBitmap support
Expand All @@ -33,6 +35,10 @@ import type { ImageResponse } from '../../textures/ImageTexture.js';
* - Image workers can still be used if enabled via settings
*/
export class WebPlatformChrome50 extends WebPlatform {
protected override getImageWorkerFactory(): ImageWorkerFactory {
return createImageWorkerNoOptions;
}

override async createImage(
blob: Blob,
premultiplyAlpha: boolean | null,
Expand Down
14 changes: 12 additions & 2 deletions src/core/platforms/web/WebPlatformLegacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import { WebPlatform } from './WebPlatform.js';
import type { PlatformSettings } from '../Platform.js';
import type { ImageResponse } from '../../textures/ImageTexture.js';
import type { ImageWorkerFactory } from './lib/ImageWorker.js';
import { createImageWorkerLegacy } from './lib/ImageWorkerLegacy.js';
import {
isBase64Image,
dataURIToBlob,
Expand All @@ -37,8 +39,11 @@ import {
*/
export class WebPlatformLegacy extends WebPlatform {
constructor(settings: PlatformSettings = {}) {
// Force image workers to be disabled in legacy mode
super({ ...settings, numImageWorkers: 0 });
super({ ...settings, numImageWorkers: settings.numImageWorkers ?? 0 });
}

protected override getImageWorkerFactory(): ImageWorkerFactory {
return createImageWorkerLegacy;
}

override async loadImage(
Expand All @@ -50,6 +55,11 @@ export class WebPlatformLegacy extends WebPlatform {
sh?: number | null,
): Promise<ImageResponse> {
const isBase64 = isBase64Image(src);

if (isBase64 === false && this.settings.numImageWorkers > 0) {
return super.loadImage(src, premultiplyAlpha, sx, sy, sw, sh);
}

const absoluteSrc = convertUrlToAbsolute(src);

// For base64 images, use blob conversion
Expand Down
173 changes: 59 additions & 114 deletions src/core/platforms/web/lib/ImageWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,136 +19,68 @@

import type { ImageResponse } from '../../../textures/ImageTexture.js';

type MessageCallback = [(value: any) => void, (reason: any) => void];
export type ImageWorkerFactory = () => void;

type MessageCallback = [
(value: ImageResponse) => void,
(reason: unknown) => void,
];

interface ImageWorkerLegacyResponse {
data: Blob;
premultiplyAlpha: boolean | null;
}

interface ImageWorkerMessage {
id: number;
src: string;
data: ImageResponse;
data: ImageResponse | ImageWorkerLegacyResponse;
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.
*
* The createImageWorker function is a web worker that fetches an image from a URL and returns an ImageBitmap object.
* The eslint @typescript rule is disabled for the entire function because the function is converted to a blob and used as the
* js of the web worker thread, so the typescript syntax is not valid in this context.
*/
export class ImageWorkerManager {
imageWorkersEnabled = true;
messageManager: Record<number, MessageCallback> = {};
workers: Worker[] = [];
workerLoad: number[] = [];
nextId = 0;

/* eslint-disable */
function createImageWorker() {
function hasAlphaChannel(mimeType: string) {
return mimeType.indexOf('image/png') !== -1;
constructor(numImageWorkers: number, workerFactory: ImageWorkerFactory) {
this.workers = this.createWorkers(numImageWorkers, workerFactory);
this.workers.forEach((worker, index) => {
worker.onmessage = (event) => this.handleMessage(event, index);
});
}

function getImage(
src: string,
private isLegacyResponse(
data: ImageResponse | ImageWorkerLegacyResponse,
): data is ImageWorkerLegacyResponse {
return data.data instanceof Blob;
}

private createImageFromBlob(
blob: Blob,
premultiplyAlpha: boolean | null,
x: number | null,
y: number | null,
width: number | null,
height: number | null,
): Promise<ImageResponse> {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'blob';

xhr.onload = function () {
// On most devices like WebOS and Tizen, the file protocol returns 0 while http(s) protocol returns 200
if (xhr.status !== 200 && xhr.status !== 0) {
return reject(
new Error(
`Image loading failed. HTTP status code: ${
xhr.status || 'N/A'
}. URL: ${src}`,
),
);
}
return new Promise((resolve, reject) => {
const objectUrl = URL.createObjectURL(blob);
const image = new Image();

var blob = xhr.response;
var withAlphaChannel =
premultiplyAlpha !== undefined
? premultiplyAlpha
: hasAlphaChannel(blob.type);

// createImageBitmap with crop and options
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;
} else {
createImageBitmap(blob, {
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
})
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
})
.catch(function (error) {
reject(error);
});
}
image.onload = () => {
URL.revokeObjectURL(objectUrl);
resolve({ data: image, premultiplyAlpha });
};

xhr.onerror = function () {
reject(
new Error('Network error occurred while trying to fetch the image.'),
);
image.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Image loading failed for legacy worker response.'));
};

xhr.send();
});
}

self.onmessage = (event) => {
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, x, y, width, height)
.then(function (data) {
// @ts-ignore ts has wrong postMessage signature
self.postMessage({ id: id, src: src, data: data }, [data.data]);
})
.catch(function (error) {
self.postMessage({ id: id, src: src, error: error.message });
});
};
}
/* eslint-enable */

export class ImageWorkerManager {
imageWorkersEnabled = true;
messageManager: Record<number, MessageCallback> = {};
workers: Worker[] = [];
workerLoad: number[] = [];
nextId = 0;

constructor(numImageWorkers: number) {
this.workers = this.createWorkers(numImageWorkers);
this.workers.forEach((worker, index) => {
worker.onmessage = (event) => this.handleMessage(event, index);
image.src = objectUrl;
});
}

Expand All @@ -165,14 +97,21 @@ export class ImageWorkerManager {
delete this.messageManager[id];
if (error) {
reject(new Error(error));
} else if (this.isLegacyResponse(data)) {
this.createImageFromBlob(data.data, data.premultiplyAlpha)
.then(resolve)
.catch(reject);
} else {
resolve(data);
}
}
}

private createWorkers(numWorkers = 1): Worker[] {
let workerCode = `(${createImageWorker.toString()})()`;
private createWorkers(
numWorkers = 1,
workerFactory: ImageWorkerFactory,
): Worker[] {
let workerCode = `(${workerFactory.toString()})()`;

workerCode = workerCode.replace('"use strict";', '');
const blob: Blob = new Blob([workerCode], {
Expand Down Expand Up @@ -226,9 +165,15 @@ export class ImageWorkerManager {

if (nextWorkerIndex !== -1) {
const worker = this.workers[nextWorkerIndex];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.workerLoad[nextWorkerIndex]!++;
worker!.postMessage({
if (worker === undefined) {
delete this.messageManager[id];
reject(new Error('No image worker available.'));
return;
}

this.workerLoad[nextWorkerIndex] =
(this.workerLoad[nextWorkerIndex] ?? 0) + 1;
worker.postMessage({
id,
src: src,
premultiplyAlpha,
Expand Down
Loading
Loading