UNPKG

@lightningjs/renderer

Version:
216 lines 8.69 kB
/* * 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