@lightningjs/renderer
Version:
Lightning 3 Renderer
216 lines • 8.69 kB
JavaScript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2026 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 { Platform } from '../Platform.js';
import { ImageWorkerManager, } from './lib/ImageWorker.js';
import { createImageWorker } from './lib/ImageWorkerDefault.js';
import { dataURIToBlob, isBase64Image, convertUrlToAbsolute, createWebGLContext, } from './lib/utils.js';
import { loadSvg } from './lib/textureSvg.js';
import { loadCompressedTexture } from './lib/textureCompression.js';
import { WebGlContextWrapper } from './WebGlContextWrapper.js';
export class WebPlatform extends Platform {
useImageWorker;
imageWorkerManager = null;
hasWorker = !!self.Worker;
stopped = false;
constructor(settings = {}) {
super(settings);
const numImageWorkers = settings.numImageWorkers ?? 0;
this.useImageWorker = numImageWorkers > 0 && this.hasWorker;
if (this.useImageWorker === true) {
this.imageWorkerManager = this.createImageWorkerManager(numImageWorkers);
}
}
createImageWorkerManager(numImageWorkers) {
return new ImageWorkerManager(numImageWorkers, this.getImageWorkerFactory());
}
getImageWorkerFactory() {
return createImageWorker;
}
////////////////////////
// Platform-specific methods
////////////////////////
createCanvas() {
return document.createElement('canvas');
}
createContext() {
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;
}
getElementById(id) {
return document.getElementById(id);
}
////////////////////////
// Update loop
////////////////////////
startLoop(stage) {
this.stopped = false;
let isIdle = false;
let lastFrameTime = 0;
const runLoop = (currentTime = 0) => {
if (this.stopped)
return;
const targetFrameTime = stage.targetFrameTime;
// Check if we should throttle this frame
if (targetFrameTime > 0 &&
currentTime - lastFrameTime < targetFrameTime) {
// Too early for next frame, schedule with setTimeout for precise timing
const delay = targetFrameTime - (currentTime - lastFrameTime);
setTimeout(() => requestAnimationFrame(runLoop), delay);
return;
}
stage.updateFrameTime();
stage.updateAnimations();
if (!stage.hasSceneUpdates()) {
// We still need to calculate the fps else it looks like the app is frozen
stage.calculateFps();
if (targetFrameTime > 0) {
// Use setTimeout for throttled idle frames
setTimeout(() => requestAnimationFrame(runLoop), Math.max(targetFrameTime, 16.666666666666668));
}
else {
// Use standard idle timeout when not throttling
setTimeout(() => requestAnimationFrame(runLoop), 16.666666666666668);
}
if (isIdle === false) {
stage.shManager.cleanup();
stage.eventBus.emit('idle');
isIdle = true;
}
if (stage.txMemManager.checkCleanup() === true) {
stage.txMemManager.cleanup();
}
stage.flushFrameEvents();
return;
}
if (isIdle === true) {
stage.eventBus.emit('active');
isIdle = false;
}
stage.drawFrame();
stage.flushFrameEvents();
// Schedule next frame
if (targetFrameTime > 0) {
// Use setTimeout + rAF combination for precise FPS control
const nextFrameDelay = Math.max(0, targetFrameTime - (performance.now() - currentTime));
setTimeout(() => requestAnimationFrame(runLoop), nextFrameDelay);
}
else {
// Use standard rAF when not throttling
requestAnimationFrame(runLoop);
}
};
requestAnimationFrame(runLoop);
}
stopLoop() {
this.stopped = true;
if (this.imageWorkerManager !== null) {
for (const worker of this.imageWorkerManager.workers) {
worker.terminate();
}
this.imageWorkerManager = null;
}
}
////////////////////////
// Image handling
////////////////////////
fetch(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
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) {
if (xhr.response instanceof Blob) {
resolve(xhr.response);
}
else {
reject(new Error('Expected blob response while loading image.'));
}
}
else {
reject(xhr.statusText);
}
}
};
xhr.open('GET', url, true);
xhr.send(null);
});
}
async createImage(blob, premultiplyAlpha, sx, sy, sw, sh) {
const hasAlphaChannel = premultiplyAlpha ?? blob.type.includes('image/png');
if (sw !== null && sh !== null) {
// createImageBitmap with crop
const bitmap = await createImageBitmap(blob, sx || 0, sy || 0, sw, sh, {
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
});
return { data: bitmap, premultiplyAlpha: hasAlphaChannel };
}
// default createImageBitmap without crop but with options
const bitmap = await createImageBitmap(blob, {
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
});
return { data: bitmap, premultiplyAlpha: hasAlphaChannel };
}
async loadImage(src, premultiplyAlpha, sx, sy, sw, sh) {
const isBase64 = isBase64Image(src);
const absoluteSrc = convertUrlToAbsolute(src);
const x = sx ?? null;
const y = sy ?? null;
const width = sw ?? null;
const height = sh ?? null;
// check if image worker is enabled
if (this.imageWorkerManager !== null && isBase64 === false) {
return this.imageWorkerManager.getImage(absoluteSrc, premultiplyAlpha, x, y, width, height);
}
// fallback to main thread loading
let blob;
if (isBase64 === true) {
blob = dataURIToBlob(src);
}
else {
blob = await this.fetch(absoluteSrc);
}
return this.createImage(blob, premultiplyAlpha, x, y, width, height);
}
async loadSvg(src, width, height, sx, sy, sw, sh) {
return loadSvg(convertUrlToAbsolute(src), width, height, sx ?? null, sy ?? null, sw ?? null, sh ?? null);
}
async loadCompressedTexture(src) {
return loadCompressedTexture(convertUrlToAbsolute(src));
}
////////////////////////
// Utilities
////////////////////////
getTimeStamp() {
return performance ? performance.now() : Date.now();
}
addFont(font) {
document.fonts.add(font);
}
}
//# sourceMappingURL=WebPlatform.js.map