@lightningjs/renderer
Advanced tools
Comparing version 2.8.0 to 2.9.0-beta1
@@ -9,2 +9,3 @@ import { ImageWorkerManager } from './lib/ImageWorker.js'; | ||
import type { Texture } from './textures/Texture.js'; | ||
import { EventEmitter } from '../common/EventEmitter.js'; | ||
/** | ||
@@ -25,2 +26,7 @@ * Augmentable map of texture class types | ||
} | ||
export interface CreateImageBitmapSupport { | ||
basic: boolean; | ||
options: boolean; | ||
full: boolean; | ||
} | ||
export type ExtractProps<Type> = Type extends { | ||
@@ -113,3 +119,3 @@ z$__type__Props: infer Props; | ||
} | ||
export declare class CoreTextureManager { | ||
export declare class CoreTextureManager extends EventEmitter { | ||
/** | ||
@@ -129,2 +135,7 @@ * Map of textures by cache key | ||
hasCreateImageBitmap: boolean; | ||
imageBitmapSupported: { | ||
basic: boolean; | ||
options: boolean; | ||
full: boolean; | ||
}; | ||
hasWorker: boolean; | ||
@@ -150,2 +161,3 @@ /** | ||
constructor(numImageWorkers: number); | ||
private validateCreateImageBitmap; | ||
registerTextureType<Type extends keyof TextureMap>(textureType: Type, textureClass: TextureMap[Type]): void; | ||
@@ -152,0 +164,0 @@ loadTexture<Type extends keyof TextureMap>(textureType: Type, props: ExtractProps<TextureMap[Type]>): InstanceType<TextureMap[Type]>; |
@@ -25,3 +25,4 @@ /* | ||
import { RenderTexture } from './textures/RenderTexture.js'; | ||
export class CoreTextureManager { | ||
import { EventEmitter } from '../common/EventEmitter.js'; | ||
export class CoreTextureManager extends EventEmitter { | ||
/** | ||
@@ -41,2 +42,7 @@ * Map of textures by cache key | ||
hasCreateImageBitmap = !!self.createImageBitmap; | ||
imageBitmapSupported = { | ||
basic: false, | ||
options: false, | ||
full: false, | ||
}; | ||
hasWorker = !!self.Worker; | ||
@@ -62,9 +68,26 @@ /** | ||
constructor(numImageWorkers) { | ||
// Register default known texture types | ||
if (this.hasCreateImageBitmap && this.hasWorker && numImageWorkers > 0) { | ||
this.imageWorkerManager = new ImageWorkerManager(numImageWorkers); | ||
} | ||
if (!this.hasCreateImageBitmap) { | ||
super(); | ||
this.validateCreateImageBitmap() | ||
.then((result) => { | ||
this.hasCreateImageBitmap = | ||
result.basic || result.options || result.full; | ||
this.imageBitmapSupported = result; | ||
if (!this.hasCreateImageBitmap) { | ||
console.warn('[Lightning] createImageBitmap is not supported on this browser. ImageTexture will be slower.'); | ||
} | ||
if (this.hasCreateImageBitmap && | ||
this.hasWorker && | ||
numImageWorkers > 0) { | ||
this.imageWorkerManager = new ImageWorkerManager(numImageWorkers, result); | ||
} | ||
else { | ||
console.warn('[Lightning] Imageworker is 0 or not supported on this browser. Image loading will be slower.'); | ||
} | ||
this.emit('initialized'); | ||
}) | ||
.catch((e) => { | ||
console.warn('[Lightning] createImageBitmap is not supported on this browser. ImageTexture will be slower.'); | ||
} | ||
// initialized without image worker manager and createImageBitmap | ||
this.emit('initialized'); | ||
}); | ||
this.registerTextureType('ImageTexture', ImageTexture); | ||
@@ -76,2 +99,135 @@ this.registerTextureType('ColorTexture', ColorTexture); | ||
} | ||
async validateCreateImageBitmap() { | ||
// Test if createImageBitmap is supported using a simple 1x1 PNG image | ||
// prettier-ignore (this is a binary PNG image) | ||
const pngBinaryData = new Uint8Array([ | ||
0x89, | ||
0x50, | ||
0x4e, | ||
0x47, | ||
0x0d, | ||
0x0a, | ||
0x1a, | ||
0x0a, // PNG signature | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x0d, // IHDR chunk length | ||
0x49, | ||
0x48, | ||
0x44, | ||
0x52, // "IHDR" chunk type | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x01, // Width: 1 | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x01, // Height: 1 | ||
0x01, // Bit depth: 1 | ||
0x03, // Color type: Indexed | ||
0x00, // Compression method: Deflate | ||
0x00, // Filter method: None | ||
0x00, // Interlace method: None | ||
0x25, | ||
0xdb, | ||
0x56, | ||
0xca, // CRC for IHDR | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x03, // PLTE chunk length | ||
0x50, | ||
0x4c, | ||
0x54, | ||
0x45, // "PLTE" chunk type | ||
0x00, | ||
0x00, | ||
0x00, // Palette entry: Black | ||
0xa7, | ||
0x7a, | ||
0x3d, | ||
0xda, // CRC for PLTE | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x01, // tRNS chunk length | ||
0x74, | ||
0x52, | ||
0x4e, | ||
0x53, // "tRNS" chunk type | ||
0x00, // Transparency for black: Fully transparent | ||
0x40, | ||
0xe6, | ||
0xd8, | ||
0x66, // CRC for tRNS | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x0a, // IDAT chunk length | ||
0x49, | ||
0x44, | ||
0x41, | ||
0x54, // "IDAT" chunk type | ||
0x08, | ||
0xd7, // Deflate header | ||
0x63, | ||
0x60, | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x02, | ||
0x00, | ||
0x01, // Zlib-compressed data | ||
0xe2, | ||
0x21, | ||
0xbc, | ||
0x33, // CRC for IDAT | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x00, // IEND chunk length | ||
0x49, | ||
0x45, | ||
0x4e, | ||
0x44, // "IEND" chunk type | ||
0xae, | ||
0x42, | ||
0x60, | ||
0x82, // CRC for IEND | ||
]); | ||
const support = { | ||
basic: false, | ||
options: false, | ||
full: false, | ||
}; | ||
// Test basic createImageBitmap support | ||
const blob = new Blob([pngBinaryData], { type: 'image/png' }); | ||
const bitmap = await createImageBitmap(blob); | ||
bitmap.close?.(); | ||
support.basic = true; | ||
// Test createImageBitmap with options support | ||
try { | ||
const options = { premultiplyAlpha: 'none' }; | ||
const bitmapWithOptions = await createImageBitmap(blob, options); | ||
bitmapWithOptions.close?.(); | ||
support.options = true; | ||
} | ||
catch (e) { | ||
/* ignore */ | ||
} | ||
// Test createImageBitmap with full options support | ||
try { | ||
const bitmapWithFullOptions = await createImageBitmap(blob, 0, 0, 1, 1, { | ||
premultiplyAlpha: 'none', | ||
}); | ||
bitmapWithFullOptions.close?.(); | ||
support.full = true; | ||
} | ||
catch (e) { | ||
/* ignore */ | ||
} | ||
return support; | ||
} | ||
registerTextureType(textureType, textureClass) { | ||
@@ -78,0 +234,0 @@ this.txConstructors[textureType] = textureClass; |
@@ -0,1 +1,2 @@ | ||
import type { CreateImageBitmapSupport } from '../CoreTextureManager.js'; | ||
import { type TextureData } from '../textures/Texture.js'; | ||
@@ -9,3 +10,3 @@ type MessageCallback = [(value: any) => void, (reason: any) => void]; | ||
nextId: number; | ||
constructor(numImageWorkers: number); | ||
constructor(numImageWorkers: number, createImageBitmapSupport: CreateImageBitmapSupport); | ||
private handleMessage; | ||
@@ -12,0 +13,0 @@ private createWorkers; |
@@ -30,2 +30,4 @@ /* | ||
function createImageWorker() { | ||
var supportsOptionsCreateImageBitmap = false; | ||
var supportsFullCreateImageBitmap = false; | ||
function hasAlphaChannel(mimeType) { | ||
@@ -47,3 +49,6 @@ return mimeType.indexOf('image/png') !== -1; | ||
: hasAlphaChannel(blob.type); | ||
if (width !== null && height !== null) { | ||
// createImageBitmap with crop and options | ||
if (supportsFullCreateImageBitmap === true && | ||
width !== null && | ||
height !== null) { | ||
createImageBitmap(blob, x || 0, y || 0, width, height, { | ||
@@ -62,7 +67,20 @@ premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', | ||
} | ||
createImageBitmap(blob, { | ||
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}) | ||
// createImageBitmap without crop but with options | ||
if (supportsOptionsCreateImageBitmap === true) { | ||
createImageBitmap(blob, { | ||
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}) | ||
.then(function (data) { | ||
resolve({ data, premultiplyAlpha: premultiplyAlpha }); | ||
}) | ||
.catch(function (error) { | ||
reject(error); | ||
}); | ||
return; | ||
} | ||
// Fallback for browsers that do not support createImageBitmap with options | ||
// this is supported for Chrome v50 to v52/54 that doesn't support options | ||
createImageBitmap(blob) | ||
.then(function (data) { | ||
@@ -105,4 +123,4 @@ resolve({ data, premultiplyAlpha: premultiplyAlpha }); | ||
nextId = 0; | ||
constructor(numImageWorkers) { | ||
this.workers = this.createWorkers(numImageWorkers); | ||
constructor(numImageWorkers, createImageBitmapSupport) { | ||
this.workers = this.createWorkers(numImageWorkers, createImageBitmapSupport); | ||
this.workers.forEach((worker) => { | ||
@@ -126,4 +144,9 @@ worker.onmessage = this.handleMessage.bind(this); | ||
} | ||
createWorkers(numWorkers = 1) { | ||
const workerCode = `(${createImageWorker.toString()})()`; | ||
createWorkers(numWorkers = 1, createImageBitmapSupport) { | ||
let workerCode = `(${createImageWorker.toString()})()`; | ||
// Replace placeholders with actual initialization values | ||
const supportsOptions = createImageBitmapSupport.options ? 'true' : 'false'; | ||
const supportsFull = createImageBitmapSupport.full ? 'true' : 'false'; | ||
workerCode = workerCode.replace('var supportsOptionsCreateImageBitmap = false;', `var supportsOptionsCreateImageBitmap = ${supportsOptions};`); | ||
workerCode = workerCode.replace('var supportsFullCreateImageBitmap = false;', `var supportsFullCreateImageBitmap = ${supportsFull};`); | ||
const blob = new Blob([workerCode.replace('"use strict";', '')], { | ||
@@ -135,3 +158,9 @@ type: 'application/javascript', | ||
for (let i = 0; i < numWorkers; i++) { | ||
workers.push(new Worker(blobURL)); | ||
const worker = new Worker(blobURL); | ||
// Pass `createImageBitmap` support level during worker initialization | ||
worker.postMessage({ | ||
type: 'init', | ||
support: createImageBitmapSupport, | ||
}); | ||
workers.push(worker); | ||
} | ||
@@ -138,0 +167,0 @@ return workers; |
@@ -71,2 +71,4 @@ /** | ||
readonly COLOR_ATTACHMENT0: 36064; | ||
readonly INVALID_ENUM: number; | ||
readonly INVALID_OPERATION: number; | ||
constructor(gl: WebGLRenderingContext | WebGL2RenderingContext); | ||
@@ -506,2 +508,10 @@ /** | ||
* ``` | ||
* gl.getError(type); | ||
* ``` | ||
* | ||
* @returns | ||
*/ | ||
getError(): number; | ||
/** | ||
* ``` | ||
* gl.createVertexArray(); | ||
@@ -508,0 +518,0 @@ * ``` |
@@ -81,2 +81,4 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
COLOR_ATTACHMENT0; | ||
INVALID_ENUM; | ||
INVALID_OPERATION; | ||
//#endregion WebGL Enums | ||
@@ -154,2 +156,4 @@ constructor(gl) { | ||
this.COLOR_ATTACHMENT0 = gl.COLOR_ATTACHMENT0; | ||
this.INVALID_ENUM = gl.INVALID_ENUM; | ||
this.INVALID_OPERATION = gl.INVALID_OPERATION; | ||
} | ||
@@ -785,2 +789,13 @@ /** | ||
* ``` | ||
* gl.getError(type); | ||
* ``` | ||
* | ||
* @returns | ||
*/ | ||
getError() { | ||
const { gl } = this; | ||
return gl.getError(); | ||
} | ||
/** | ||
* ``` | ||
* gl.createVertexArray(); | ||
@@ -787,0 +802,0 @@ * ``` |
@@ -23,3 +23,3 @@ /* | ||
import { CanvasCoreTexture } from './CanvasCoreTexture.js'; | ||
import { getBorder, getRadius, strokeLine } from './internal/C2DShaderUtils.js'; | ||
import { getBorder, getRadius, roundRect, strokeLine, } from './internal/C2DShaderUtils.js'; | ||
import { formatRgba, parseColorRgba, parseColor, } from './internal/ColorUtils.js'; | ||
@@ -121,3 +121,3 @@ import { UnsupportedShader } from './shaders/UnsupportedShader.js'; | ||
const path = new Path2D(); | ||
path.roundRect(tx, ty, width, height, radius); | ||
roundRect.call(path, tx, ty, width, height, radius); | ||
ctx.clip(path); | ||
@@ -171,3 +171,3 @@ } | ||
if (radius) { | ||
ctx.roundRect(tx + borderInnerWidth, ty + borderInnerWidth, width - borderWidth, height - borderWidth, radius); | ||
roundRect.call(ctx, tx + borderInnerWidth, ty + borderInnerWidth, width - borderWidth, height - borderWidth, radius); | ||
ctx.stroke(); | ||
@@ -174,0 +174,0 @@ } |
@@ -12,3 +12,4 @@ import type { QuadOptions } from '../../CoreRenderer.js'; | ||
export declare function getBorder(quad: QuadOptions, direction?: '' | Direction): BorderEffectProps | undefined; | ||
export declare function roundRect(this: CanvasRenderingContext2D | Path2D, x: number, y: number, width: number, height: number, radius: number | DOMPointInit | (number | DOMPointInit)[]): void; | ||
export declare function strokeLine(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, lineWidth: number | undefined, color: number | undefined, direction: Direction): void; | ||
export {}; |
@@ -61,2 +61,40 @@ /* | ||
} | ||
export function roundRect(x, y, width, height, radius) { | ||
const context = Object.getPrototypeOf(this); | ||
if (!context.roundRect) { | ||
const fixOverlappingCorners = (radii) => { | ||
const maxRadius = Math.min(width / 2, height / 2); | ||
const totalHorizontal = radii.topLeft + radii.topRight + radii.bottomRight + radii.bottomLeft; | ||
if (totalHorizontal > width || totalHorizontal > height) { | ||
const scale = maxRadius / | ||
Math.max(radii.topLeft, radii.topRight, radii.bottomRight, radii.bottomLeft); | ||
radii.topLeft *= scale; | ||
radii.topRight *= scale; | ||
radii.bottomRight *= scale; | ||
radii.bottomLeft *= scale; | ||
} | ||
}; | ||
const radii = typeof radius === 'number' | ||
? { | ||
topLeft: radius, | ||
topRight: radius, | ||
bottomRight: radius, | ||
bottomLeft: radius, | ||
} | ||
: { topLeft: 0, topRight: 0, bottomRight: 0, bottomLeft: 0, ...radius }; | ||
fixOverlappingCorners(radii); | ||
this.moveTo(x + radii.topLeft, y); | ||
this.lineTo(x + width - radii.topRight, y); | ||
this.ellipse(x + width - radii.topRight, y + radii.topRight, radii.topRight, radii.topRight, 0, 1.5 * Math.PI, 2 * Math.PI); | ||
this.lineTo(x + width, y + height - radii.bottomRight); | ||
this.ellipse(x + width - radii.bottomRight, y + height - radii.bottomRight, radii.bottomRight, radii.bottomRight, 0, 0, 0.5 * Math.PI); | ||
this.lineTo(x + radii.bottomLeft, y + height); | ||
this.ellipse(x + radii.bottomLeft, y + height - radii.bottomLeft, radii.bottomLeft, radii.bottomLeft, 0, 0.5 * Math.PI, Math.PI); | ||
this.lineTo(x, y + radii.topLeft); | ||
this.ellipse(x + radii.topLeft, y + radii.topLeft, radii.topLeft, radii.topLeft, 0, Math.PI, 1.5 * Math.PI); | ||
} | ||
else { | ||
this.roundRect(x, y, width, height, radius); | ||
} | ||
} | ||
export function strokeLine(ctx, x, y, width, height, lineWidth = 0, color, direction) { | ||
@@ -63,0 +101,0 @@ if (!lineWidth) { |
@@ -28,3 +28,3 @@ /* | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any | ||
MAX_VIEWPORT_DIMS: 0, | ||
MAX_VIEWPORT_DIMS: 0, // Code below will replace this with an Int32Array | ||
MAX_VERTEX_TEXTURE_IMAGE_UNITS: 0, | ||
@@ -31,0 +31,0 @@ MAX_TEXTURE_IMAGE_UNITS: 0, |
@@ -23,3 +23,4 @@ /* | ||
if (!shader) { | ||
throw new Error(`Unable to create shader type: ${type}. Source: ${source}`); | ||
const glError = glw.getError(); | ||
throw new Error(`Unable to create the shader: ${type === glw.VERTEX_SHADER ? 'VERTEX_SHADER' : 'FRAGMENT_SHADER'}.${glError ? ` WebGlContext Error: ${glError}` : ''}`); | ||
} | ||
@@ -32,3 +33,3 @@ glw.shaderSource(shader, source); | ||
} | ||
console.log(glw.getShaderInfoLog(shader)); | ||
console.error(glw.getShaderInfoLog(shader)); | ||
glw.deleteShader(shader); | ||
@@ -48,3 +49,3 @@ } | ||
} | ||
console.log(glw.getProgramInfoLog(program)); | ||
console.warn(glw.getProgramInfoLog(program)); | ||
glw.deleteProgram(program); | ||
@@ -51,0 +52,0 @@ return undefined; |
@@ -108,6 +108,6 @@ /* | ||
name: 'a_position', | ||
size: 2, | ||
type: glw.FLOAT, | ||
normalized: false, | ||
stride, | ||
size: 2, // 2 components per iteration | ||
type: glw.FLOAT, // the data is 32bit floats | ||
normalized: false, // don't normalize the data | ||
stride, // 0 = move forward size * sizeof(type) each iteration to get the next position | ||
offset: 0, // start at the beginning of the buffer | ||
@@ -114,0 +114,0 @@ }, |
@@ -21,3 +21,2 @@ /* | ||
import { WebGlCoreShader } from './WebGlCoreShader.js'; | ||
const MAX_TEXTURES = 8; // TODO: get from gl | ||
/** | ||
@@ -94,3 +93,3 @@ * Can render multiple quads with multiple textures (up to vertex shader texture limit) | ||
const { x, y, width, height } = this.clippingRect; | ||
const pixelRatio = options.pixelRatio; | ||
const pixelRatio = this.parentHasRenderTexture ? 1 : options.pixelRatio; | ||
const canvasHeight = options.canvas.height; | ||
@@ -100,3 +99,10 @@ const clipX = Math.round(x * pixelRatio); | ||
const clipHeight = Math.round(height * pixelRatio); | ||
const clipY = Math.round(canvasHeight - clipHeight - y * pixelRatio); | ||
let clipY = Math.round(canvasHeight - clipHeight - y * pixelRatio); | ||
// if parent has render texture, we need to adjust the scissor rect | ||
// to be relative to the parent's framebuffer | ||
if (this.parentHasRenderTexture) { | ||
clipY = this.framebufferDimensions | ||
? this.framebufferDimensions.height - this.dimensions.height | ||
: 0; | ||
} | ||
glw.setScissorTest(true); | ||
@@ -103,0 +109,0 @@ glw.scissor(clipX, clipY, clipWidth, clipHeight); |
@@ -82,3 +82,8 @@ /* | ||
if (!vertexShader || !fragmentShader) { | ||
throw new Error(`Unable to create shader type: ${glw.FRAGMENT_SHADER}. Source: ${fragmentSource}`); | ||
throw new Error(`Unable to create the following shader(s): ${[ | ||
!vertexShader && 'VERTEX_SHADER', | ||
!fragmentShader && 'FRAGMENT_SHADER', | ||
] | ||
.filter(Boolean) | ||
.join(' and ')}`); | ||
} | ||
@@ -85,0 +90,0 @@ const program = createProgram(glw, vertexShader, fragmentShader); |
@@ -418,3 +418,3 @@ /* | ||
letterSpacing: props.letterSpacing ?? 0, | ||
lineHeight: props.lineHeight, | ||
lineHeight: props.lineHeight, // `undefined` is a valid value | ||
maxLines: props.maxLines ?? 0, | ||
@@ -421,0 +421,0 @@ textBaseline: props.textBaseline ?? 'alphabetic', |
@@ -57,3 +57,5 @@ /* | ||
// Pre-load it | ||
this.texture.ctxTexture.load(); | ||
stage.txManager.once('initialized', () => { | ||
this.texture.ctxTexture.load(); | ||
}); | ||
// Set this.data to the fetched data from dataUrl | ||
@@ -66,3 +68,2 @@ fetch(atlasDataUrl) | ||
// Add all the glyphs to the glyph map | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
let maxCharHeight = 0; | ||
@@ -76,6 +77,4 @@ this.data.chars.forEach((glyph) => { | ||
}); | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
this.maxCharHeight = maxCharHeight; | ||
// We know `data` is defined here, because we just set it | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
this.shaper = new SdfFontShaper(this.data, this.glyphMap); | ||
@@ -82,0 +81,0 @@ // If the metrics aren't provided explicitly in the font face options, |
@@ -446,6 +446,6 @@ /* | ||
name: 'a_position', | ||
size: 2, | ||
type: glw.FLOAT, | ||
normalized: false, | ||
stride, | ||
size: 2, // 2 components per iteration | ||
type: glw.FLOAT, // the data is 32bit floats | ||
normalized: false, // don't normalize the data | ||
stride, // 0 = move forward size * sizeof(type) each iteration to get the next position | ||
offset: 0, // start at the beginning of the buffer | ||
@@ -452,0 +452,0 @@ }, |
@@ -116,3 +116,3 @@ /* | ||
}, | ||
writable: false, | ||
writable: false, // Prevents property from being changed | ||
configurable: false, // Prevents property from being deleted | ||
@@ -119,0 +119,0 @@ }); |
@@ -99,2 +99,10 @@ import type { CoreTextureManager } from '../CoreTextureManager.js'; | ||
hasAlphaChannel(mimeType: string): boolean; | ||
loadImageFallback(src: string, hasAlpha: boolean): Promise<{ | ||
data: HTMLImageElement; | ||
premultiplyAlpha: boolean; | ||
}>; | ||
createImageBitmap(blob: Blob, premultiplyAlpha: boolean | null, sx: number | null, sy: number | null, sw: number | null, sh: number | null): Promise<{ | ||
data: ImageBitmap | HTMLImageElement; | ||
premultiplyAlpha: boolean; | ||
}>; | ||
loadImage(src: string): Promise<TextureData>; | ||
@@ -101,0 +109,0 @@ getTextureData(): Promise<TextureData>; |
@@ -47,47 +47,60 @@ /* | ||
} | ||
async loadImage(src) { | ||
const { premultiplyAlpha, sx, sy, sw, sh, width, height } = this.props; | ||
if (this.txManager.imageWorkerManager !== null) { | ||
return await this.txManager.imageWorkerManager.getImage(src, premultiplyAlpha, sx, sy, sw, sh); | ||
async loadImageFallback(src, hasAlpha) { | ||
const img = new Image(); | ||
return new Promise((resolve) => { | ||
img.onload = () => { | ||
resolve({ data: img, premultiplyAlpha: hasAlpha }); | ||
}; | ||
img.onerror = () => { | ||
console.warn('Image loading failed, returning fallback object.'); | ||
resolve({ data: img, premultiplyAlpha: hasAlpha }); | ||
}; | ||
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 && | ||
sx !== null && | ||
sy !== null && | ||
sw !== null && | ||
sh !== null) { | ||
// createImageBitmap with crop | ||
const bitmap = await createImageBitmap(blob, sx, sy, sw, sh, { | ||
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}); | ||
return { data: bitmap, premultiplyAlpha: hasAlphaChannel }; | ||
} | ||
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, | ||
}; | ||
} | ||
else if (imageBitmapSupported.options === true) { | ||
// createImageBitmap without crop but with options | ||
const bitmap = await createImageBitmap(blob, { | ||
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}); | ||
return { data: bitmap, premultiplyAlpha: hasAlphaChannel }; | ||
} | ||
else { | ||
// basic createImageBitmap without options or crop | ||
// this is supported for Chrome v50 to v52/54 that doesn't support options | ||
return { | ||
data: await createImageBitmap(blob, { | ||
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}), | ||
data: await createImageBitmap(blob), | ||
premultiplyAlpha: hasAlphaChannel, | ||
}; | ||
} | ||
else { | ||
const img = new Image(); | ||
if (!src.startsWith('data:')) { | ||
img.crossOrigin = 'Anonymous'; | ||
} | ||
async loadImage(src) { | ||
const { premultiplyAlpha, sx, sy, sw, sh } = this.props; | ||
if (this.txManager.hasCreateImageBitmap === true) { | ||
if (this.txManager.hasWorker === true && | ||
this.txManager.imageWorkerManager !== null) { | ||
return this.txManager.imageWorkerManager.getImage(src, premultiplyAlpha, sx, sy, sw, sh); | ||
} | ||
img.src = src; | ||
await new Promise((resolve, reject) => { | ||
img.onload = () => resolve(); | ||
img.onerror = () => reject(new Error(`Failed to load image`)); | ||
}).catch((e) => { | ||
console.error(e); | ||
}); | ||
return { | ||
data: img, | ||
premultiplyAlpha: premultiplyAlpha ?? true, | ||
}; | ||
const blob = await fetch(src).then((response) => response.blob()); | ||
return this.createImageBitmap(blob, premultiplyAlpha, sx, sy, sw, sh); | ||
} | ||
return this.loadImageFallback(src, premultiplyAlpha ?? true); | ||
} | ||
@@ -158,3 +171,3 @@ async getTextureData() { | ||
src: props.src ?? '', | ||
premultiplyAlpha: props.premultiplyAlpha ?? true, | ||
premultiplyAlpha: props.premultiplyAlpha ?? true, // null, | ||
key: props.key ?? null, | ||
@@ -161,0 +174,0 @@ type: props.type ?? null, |
@@ -15,3 +15,3 @@ /** | ||
export declare const isPowerOfTwo: (value: number) => boolean | 0; | ||
export declare const getTimingFunction: (str: string) => (time: number) => number | undefined; | ||
export declare const getTimingFunction: (str: string) => ((time: number) => number | undefined); | ||
/** | ||
@@ -18,0 +18,0 @@ * Convert bytes to string of megabytes with 2 decimal points |
import type { FpsUpdatePayload, FrameTickPayload } from '../common/CommonTypes.js'; | ||
import type { ShaderMap } from '../core/CoreShaderManager.js'; | ||
import type { INode, INodeWritableProps, ITextNode, ITextNodeWritableProps } from './INode.js'; | ||
import type { IShaderController } from './IShaderController.js'; | ||
import type { RendererMain, RendererMainSettings, SpecificShaderRef } from './RendererMain.js'; | ||
import type { RendererMain, RendererMainSettings } from './RendererMain.js'; | ||
/** | ||
@@ -18,3 +16,2 @@ * This interface is to be implemented by Core Drivers | ||
createTextNode(props: ITextNodeWritableProps): ITextNode; | ||
createShaderController<ShType extends keyof ShaderMap>(shaderRef: SpecificShaderRef<ShType>): IShaderController; | ||
destroyNode(node: INode): void; | ||
@@ -21,0 +18,0 @@ getRootNode(): INode; |
@@ -8,3 +8,2 @@ import type { ShaderMap } from '../core/CoreShaderManager.js'; | ||
import { EventEmitter } from '../common/EventEmitter.js'; | ||
import type { IShaderController } from './IShaderController.js'; | ||
/** | ||
@@ -217,2 +216,6 @@ * An immutable reference to a specific Texture type | ||
enableInspector?: boolean; | ||
/** | ||
* Renderer mode | ||
*/ | ||
renderMode?: 'webgl' | 'canvas'; | ||
} | ||
@@ -356,3 +359,3 @@ /** | ||
*/ | ||
createShader<ShType extends keyof ShaderMap>(shaderType: ShType, props?: SpecificShaderRef<ShType>['props']): IShaderController; | ||
createShader<ShType extends keyof ShaderMap>(shaderType: ShType, props?: SpecificShaderRef<ShType>['props']): SpecificShaderRef<ShType>; | ||
/** | ||
@@ -359,0 +362,0 @@ * Get a Node by its ID |
@@ -88,2 +88,3 @@ /* | ||
enableInspector: settings.enableInspector ?? false, | ||
renderMode: settings.renderMode ?? 'webgl', | ||
}; | ||
@@ -204,6 +205,6 @@ this.settings = resolvedSettings; | ||
letterSpacing: props.letterSpacing ?? 0, | ||
lineHeight: props.lineHeight ?? fontSize, | ||
lineHeight: props.lineHeight, // `undefined` is a valid value | ||
maxLines: props.maxLines ?? 0, | ||
textBaseline: props.textBaseline ?? 'alphabetic', | ||
verticalAlign: props.verticalAlign ?? 'top', | ||
verticalAlign: props.verticalAlign ?? 'middle', | ||
overflowSuffix: props.overflowSuffix ?? '...', | ||
@@ -270,2 +271,3 @@ debug: props.debug ?? {}, | ||
rotation: props.rotation ?? 0, | ||
rtt: props.rtt ?? false, | ||
data: data, | ||
@@ -334,7 +336,7 @@ }; | ||
createShader(shaderType, props) { | ||
return this.driver.createShaderController({ | ||
return { | ||
descType: 'shader', | ||
shType: shaderType, | ||
props: props, | ||
}); | ||
}; | ||
} | ||
@@ -341,0 +343,0 @@ /** |
import type { ICoreDriver } from '../../main-api/ICoreDriver.js'; | ||
import type { INode, INodeWritableProps, ITextNodeWritableProps } from '../../main-api/INode.js'; | ||
import type { RendererMain, RendererMainSettings, SpecificShaderRef } from '../../main-api/RendererMain.js'; | ||
import type { RendererMain, RendererMainSettings } from '../../main-api/RendererMain.js'; | ||
import { MainOnlyTextNode } from './MainOnlyTextNode.js'; | ||
import type { FpsUpdatePayload, FrameTickPayload } from '../../common/CommonTypes.js'; | ||
import type { ShaderMap } from '../../core/CoreShaderManager.js'; | ||
import type { IShaderController } from '../../main-api/IShaderController.js'; | ||
export declare class MainCoreDriver implements ICoreDriver { | ||
@@ -15,3 +13,2 @@ private root; | ||
createTextNode(props: ITextNodeWritableProps): MainOnlyTextNode; | ||
createShaderController<ShType extends keyof ShaderMap>(shaderRef: SpecificShaderRef<ShType>): IShaderController; | ||
destroyNode(node: INode): void; | ||
@@ -18,0 +15,0 @@ releaseTexture(id: number): void; |
@@ -24,3 +24,2 @@ /* | ||
import { loadCoreExtension } from '../utils.js'; | ||
import { MainOnlyShaderController } from './MainOnlyShaderController.js'; | ||
export class MainCoreDriver { | ||
@@ -44,2 +43,3 @@ root = null; | ||
numImageWorkers: rendererSettings.numImageWorkers, | ||
renderMode: rendererSettings.renderMode, | ||
debug: { | ||
@@ -86,5 +86,2 @@ monitorTextureCache: false, | ||
} | ||
createShaderController(shaderRef) { | ||
return new MainOnlyShaderController(shaderRef); | ||
} | ||
// TODO: Remove? | ||
@@ -91,0 +88,0 @@ destroyNode(node) { |
@@ -5,6 +5,5 @@ import type { CustomDataMap, INode, INodeAnimatableProps, INodeWritableProps } from '../../main-api/INode.js'; | ||
import { CoreNode } from '../../core/CoreNode.js'; | ||
import type { RendererMain, TextureRef } from '../../main-api/RendererMain.js'; | ||
import type { RendererMain, ShaderRef, TextureRef } from '../../main-api/RendererMain.js'; | ||
import type { AnimationSettings } from '../../core/animations/CoreAnimation.js'; | ||
import { EventEmitter } from '../../common/EventEmitter.js'; | ||
import type { MainOnlyShaderController } from './MainOnlyShaderController.js'; | ||
export declare function getNewId(): number; | ||
@@ -15,3 +14,3 @@ export declare class MainOnlyNode extends EventEmitter implements INode { | ||
readonly id: number; | ||
readonly coreNode: CoreNode; | ||
protected coreNode: CoreNode; | ||
protected _children: MainOnlyNode[]; | ||
@@ -21,3 +20,3 @@ protected _src: string; | ||
protected _texture: TextureRef | null; | ||
protected _shader: MainOnlyShaderController | null; | ||
protected _shader: ShaderRef | null; | ||
protected _data: CustomDataMap | undefined; | ||
@@ -88,2 +87,5 @@ constructor(props: INodeWritableProps, rendererMain: RendererMain, stage: Stage, coreNode?: CoreNode); | ||
set texture(texture: TextureRef | null); | ||
get rtt(): boolean; | ||
set rtt(value: boolean); | ||
get parentHasRenderTexture(): boolean; | ||
private onTextureLoaded; | ||
@@ -96,4 +98,4 @@ private onTextureFailed; | ||
private onInViewport; | ||
get shader(): MainOnlyShaderController | null; | ||
set shader(shader: MainOnlyShaderController | null); | ||
get shader(): ShaderRef | null; | ||
set shader(shader: ShaderRef | null); | ||
get data(): CustomDataMap | undefined; | ||
@@ -100,0 +102,0 @@ set data(d: CustomDataMap); |
@@ -82,2 +82,3 @@ /* | ||
textureOptions: null, | ||
rtt: props.rtt, | ||
}); | ||
@@ -97,2 +98,3 @@ // Forward loaded/failed events | ||
this.src = props.src; | ||
this.rtt = props.rtt; | ||
this._data = props.data; | ||
@@ -116,2 +118,8 @@ } | ||
set width(value) { | ||
if (value !== this.coreNode.width && this.coreNode.rtt) { | ||
this.texture = this.rendererMain.createTexture('RenderTexture', { | ||
width: this.width, | ||
height: this.height, | ||
}, { preload: true, flipY: true }); | ||
} | ||
this.coreNode.width = value; | ||
@@ -123,2 +131,8 @@ } | ||
set height(value) { | ||
if (value !== this.coreNode.height && this.coreNode.rtt) { | ||
this.texture = this.rendererMain.createTexture('RenderTexture', { | ||
width: this.width, | ||
height: this.height, | ||
}, { preload: true, flipY: true }); | ||
} | ||
this.coreNode.height = value; | ||
@@ -335,2 +349,17 @@ } | ||
} | ||
get rtt() { | ||
return this.coreNode.rtt; | ||
} | ||
set rtt(value) { | ||
if (value) { | ||
this.texture = this.rendererMain.createTexture('RenderTexture', { | ||
width: this.width, | ||
height: this.height, | ||
}, { preload: true, flipY: true }); | ||
} | ||
this.coreNode.rtt = value; | ||
} | ||
get parentHasRenderTexture() { | ||
return this.coreNode.parentHasRenderTexture; | ||
} | ||
onTextureLoaded = (target, payload) => { | ||
@@ -367,3 +396,3 @@ this.emit('loaded', payload); | ||
if (shader) { | ||
shader.attachNode(this); | ||
this.coreNode.loadShader(shader.shType, shader.props); | ||
} | ||
@@ -370,0 +399,0 @@ } |
@@ -7,3 +7,3 @@ import type { ITextNode, ITextNodeWritableProps } from '../../main-api/INode.js'; | ||
export declare class MainOnlyTextNode extends MainOnlyNode implements ITextNode { | ||
readonly coreNode: CoreTextNode; | ||
protected coreNode: CoreTextNode; | ||
constructor(props: ITextNodeWritableProps, rendererMain: RendererMain, stage: Stage); | ||
@@ -10,0 +10,0 @@ get text(): string; |
@@ -78,2 +78,3 @@ /* | ||
shaderProps: null, | ||
rtt: false, | ||
})); | ||
@@ -163,5 +164,3 @@ } | ||
set lineHeight(value) { | ||
if (value) { | ||
this.coreNode.lineHeight = value; | ||
} | ||
this.coreNode.lineHeight = value; | ||
} | ||
@@ -168,0 +167,0 @@ get maxLines() { |
@@ -31,2 +31,3 @@ import { BufferStruct } from '@lightningjs/threadx'; | ||
rotation: number; | ||
rtt: boolean; | ||
} | ||
@@ -91,2 +92,4 @@ export declare class NodeStruct extends BufferStruct implements NodeStructWritableProps { | ||
set zIndexLocked(value: number); | ||
get rtt(): boolean; | ||
set rtt(value: boolean); | ||
} |
@@ -196,2 +196,8 @@ /* | ||
} | ||
get rtt() { | ||
return false; | ||
} | ||
set rtt(value) { | ||
// Decorator will handle this | ||
} | ||
} | ||
@@ -282,2 +288,5 @@ __decorate([ | ||
], NodeStruct.prototype, "zIndexLocked", null); | ||
__decorate([ | ||
structProp('boolean') | ||
], NodeStruct.prototype, "rtt", null); | ||
//# sourceMappingURL=NodeStruct.js.map |
@@ -39,2 +39,3 @@ import type { NodeStruct, NodeStructWritableProps } from './NodeStruct.js'; | ||
zIndexLocked: number; | ||
rtt: boolean; | ||
} |
@@ -57,2 +57,3 @@ /* | ||
rotation: sharedNodeStruct.rotation, | ||
rtt: sharedNodeStruct.rtt, | ||
}); | ||
@@ -59,0 +60,0 @@ } |
@@ -187,3 +187,5 @@ /* | ||
__decorate([ | ||
structProp('number') | ||
structProp('number', { | ||
allowUndefined: true, | ||
}) | ||
], TextNodeStruct.prototype, "lineHeight", null); | ||
@@ -190,0 +192,0 @@ __decorate([ |
import type { INode, INodeWritableProps, ITextNode, ITextNodeWritableProps } from '../../main-api/INode.js'; | ||
import type { ICoreDriver } from '../../main-api/ICoreDriver.js'; | ||
import type { RendererMain, RendererMainSettings, SpecificShaderRef } from '../../main-api/RendererMain.js'; | ||
import type { RendererMain, RendererMainSettings } from '../../main-api/RendererMain.js'; | ||
import type { FpsUpdatePayload, FrameTickPayload } from '../../common/CommonTypes.js'; | ||
import type { IShaderController } from '../../main-api/IShaderController.js'; | ||
import type { ShaderMap } from '../../core/CoreShaderManager.js'; | ||
export interface ThreadXRendererSettings { | ||
@@ -21,3 +19,2 @@ coreWorkerUrl: string; | ||
createTextNode(props: ITextNodeWritableProps): ITextNode; | ||
createShaderController<ShType extends keyof ShaderMap>(shaderRef: SpecificShaderRef<ShType>): IShaderController; | ||
destroyNode(node: INode): void; | ||
@@ -24,0 +21,0 @@ releaseTexture(textureDescId: number): void; |
@@ -26,3 +26,2 @@ /* | ||
import { ThreadXMainTextNode } from './ThreadXMainTextNode.js'; | ||
import { ThreadXMainShaderController } from './ThreadXMainShaderController.js'; | ||
export class ThreadXCoreDriver { | ||
@@ -133,2 +132,3 @@ settings; | ||
rotation: props.rotation, | ||
rtt: props.rtt, | ||
}); | ||
@@ -178,2 +178,3 @@ const node = new ThreadXMainNode(rendererMain, bufferStruct); | ||
rotation: props.rotation, | ||
rtt: props.rtt, | ||
// Text specific properties | ||
@@ -209,5 +210,2 @@ text: props.text, | ||
} | ||
createShaderController(shaderRef) { | ||
return new ThreadXMainShaderController(shaderRef); | ||
} | ||
// TODO: Remove? | ||
@@ -214,0 +212,0 @@ destroyNode(node) { |
@@ -0,7 +1,8 @@ | ||
import { EventEmitter } from '../../common/EventEmitter.js'; | ||
import type { AnimationControllerState, IAnimationController } from '../../common/IAnimationController.js'; | ||
import type { ThreadXMainNode } from './ThreadXMainNode.js'; | ||
export declare class ThreadXMainAnimationController implements IAnimationController { | ||
export declare class ThreadXMainAnimationController extends EventEmitter implements IAnimationController { | ||
private node; | ||
private id; | ||
stoppedPromise: Promise<void> | null; | ||
stoppedPromise: Promise<void>; | ||
/** | ||
@@ -11,4 +12,4 @@ * If this is null, then the animation is in a finished / stopped state. | ||
stoppedResolve: (() => void) | null; | ||
state: AnimationControllerState; | ||
constructor(node: ThreadXMainNode, id: number); | ||
state: AnimationControllerState; | ||
start(): IAnimationController; | ||
@@ -19,4 +20,7 @@ stop(): IAnimationController; | ||
waitUntilStopped(): Promise<void>; | ||
private onAnimationFinished; | ||
private sendStart; | ||
private sendStop; | ||
private makeStoppedPromise; | ||
private onFinished; | ||
private onAnimating; | ||
} |
@@ -19,7 +19,9 @@ /* | ||
*/ | ||
/* eslint-disable @typescript-eslint/unbound-method */ | ||
import { EventEmitter } from '../../common/EventEmitter.js'; | ||
import { assertTruthy } from '../../utils.js'; | ||
export class ThreadXMainAnimationController { | ||
export class ThreadXMainAnimationController extends EventEmitter { | ||
node; | ||
id; | ||
stoppedPromise = null; | ||
stoppedPromise; | ||
/** | ||
@@ -29,25 +31,32 @@ * If this is null, then the animation is in a finished / stopped state. | ||
stoppedResolve = null; | ||
state; | ||
constructor(node, id) { | ||
super(); | ||
this.node = node; | ||
this.id = id; | ||
this.onAnimationFinished = this.onAnimationFinished.bind(this); | ||
this.state = 'stopped'; | ||
// Initial stopped promise is resolved (since the animation is stopped) | ||
this.stoppedPromise = Promise.resolve(); | ||
// Bind event handlers | ||
this.onAnimating = this.onAnimating.bind(this); | ||
this.onFinished = this.onFinished.bind(this); | ||
} | ||
state; | ||
start() { | ||
if (this.stoppedResolve === null) { | ||
if (this.state !== 'running') { | ||
this.makeStoppedPromise(); | ||
this.node.on('animationFinished', this.onAnimationFinished); | ||
this.sendStart(); | ||
this.state = 'running'; | ||
} | ||
this.state = 'running'; | ||
this.node.emit('startAnimation', { id: this.id }); | ||
return this; | ||
} | ||
stop() { | ||
this.node.emit('stopAnimation', { id: this.id }); | ||
this.node.off('animationFinished', this.onAnimationFinished); | ||
if (this.stoppedResolve !== null) { | ||
this.stoppedResolve(); | ||
this.stoppedResolve = null; | ||
if (this.state === 'stopped') { | ||
return this; | ||
} | ||
this.sendStop(); | ||
// if (this.stoppedResolve !== null) { | ||
// this.stoppedResolve(); | ||
// this.stoppedResolve = null; | ||
// this.emit('stopped', this); | ||
// } | ||
this.state = 'stopped'; | ||
@@ -65,15 +74,18 @@ return this; | ||
waitUntilStopped() { | ||
this.makeStoppedPromise(); | ||
const promise = this.stoppedPromise; | ||
assertTruthy(promise); | ||
return promise; | ||
return this.stoppedPromise; | ||
} | ||
onAnimationFinished(target, { id, loop }) { | ||
if (id === this.id) { | ||
this.node.off('animationFinished', this.onAnimationFinished); | ||
this.stoppedResolve?.(); | ||
this.stoppedResolve = null; | ||
this.state = 'stopped'; | ||
} | ||
sendStart() { | ||
// Hook up event listeners | ||
this.node.on('animationFinished', this.onFinished); | ||
this.node.on('animationAnimating', this.onAnimating); | ||
// Then register the animation | ||
this.node.emit('startAnimation', { id: this.id }); | ||
} | ||
sendStop() { | ||
// First unregister the animation | ||
this.node.emit('stopAnimation', { id: this.id }); | ||
// Then unhook event listeners | ||
this.node.off('animationFinished', this.onFinished); | ||
this.node.off('animationAnimating', this.onAnimating); | ||
} | ||
makeStoppedPromise() { | ||
@@ -86,3 +98,20 @@ if (this.stoppedResolve === null) { | ||
} | ||
onFinished(target, { id }) { | ||
if (id === this.id) { | ||
assertTruthy(this.stoppedResolve); | ||
this.node.off('animationFinished', this.onFinished); | ||
this.node.off('animationAnimating', this.onAnimating); | ||
// resolve promise | ||
this.stoppedResolve(); | ||
this.stoppedResolve = null; | ||
this.emit('stopped', this); | ||
this.state = 'stopped'; | ||
} | ||
} | ||
onAnimating(target, { id }) { | ||
if (id === this.id) { | ||
this.emit('animating', this); | ||
} | ||
} | ||
} | ||
//# sourceMappingURL=ThreadXMainAnimationController.js.map |
import type { IAnimationController } from '../../common/IAnimationController.js'; | ||
import type { CustomDataMap, INode, INodeAnimatableProps } from '../../main-api/INode.js'; | ||
import type { RendererMain, TextureRef } from '../../main-api/RendererMain.js'; | ||
import type { RendererMain, ShaderRef, TextureRef } from '../../main-api/RendererMain.js'; | ||
import type { NodeStruct } from './NodeStruct.js'; | ||
import { SharedNode } from './SharedNode.js'; | ||
import type { AnimationSettings } from '../../core/animations/CoreAnimation.js'; | ||
import type { ThreadXMainShaderController } from './ThreadXMainShaderController.js'; | ||
export declare class ThreadXMainNode extends SharedNode implements INode { | ||
@@ -14,5 +13,6 @@ private rendererMain; | ||
protected _texture: TextureRef | null; | ||
protected _shader: ThreadXMainShaderController | null; | ||
protected _shader: ShaderRef | null; | ||
protected _data: CustomDataMap | undefined; | ||
private _src; | ||
private _parentHasRenderTexture; | ||
/** | ||
@@ -31,4 +31,4 @@ * FinalizationRegistry for animation controllers. When an animation | ||
set texture(texture: TextureRef | null); | ||
get shader(): ThreadXMainShaderController | null; | ||
set shader(shader: ThreadXMainShaderController | null); | ||
get shader(): ShaderRef | null; | ||
set shader(shader: ShaderRef | null); | ||
get scale(): number | null; | ||
@@ -41,2 +41,4 @@ set scale(scale: number | null); | ||
set parent(newParent: ThreadXMainNode | null); | ||
set parentHasRenderTexture(hasRenderTexture: boolean); | ||
get parentHasRenderTexture(): boolean; | ||
get children(): ThreadXMainNode[]; | ||
@@ -43,0 +45,0 @@ get props(): this["z$__type__Props"]; |
@@ -32,2 +32,3 @@ /* | ||
_src = ''; | ||
_parentHasRenderTexture = false; | ||
/** | ||
@@ -79,4 +80,3 @@ * FinalizationRegistry for animation controllers. When an animation | ||
if (shader) { | ||
shader.attachNode(this); | ||
// this.emit('loadShader', shader as unknown as Record<string, unknown>); | ||
this.emit('loadShader', shader); | ||
} | ||
@@ -138,2 +138,8 @@ } | ||
} | ||
set parentHasRenderTexture(hasRenderTexture) { | ||
this._parentHasRenderTexture = hasRenderTexture; | ||
} | ||
get parentHasRenderTexture() { | ||
return this._parentHasRenderTexture; | ||
} | ||
get children() { | ||
@@ -140,0 +146,0 @@ return this._children; |
@@ -73,2 +73,3 @@ /* | ||
numImageWorkers: message.numImageWorkers, | ||
renderMode: 'webgl', | ||
debug: { | ||
@@ -110,2 +111,3 @@ monitorTextureCache: false, | ||
rotation: coreRootNode.rotation, | ||
rtt: coreRootNode.rtt, | ||
}); | ||
@@ -112,0 +114,0 @@ // Share the root node that was created by the Stage with the main worker. |
@@ -55,2 +55,8 @@ /* | ||
const animation = new CoreAnimation(this.coreNode, props, settings); | ||
animation.on('animating', () => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
this.emit('animationAnimating', { | ||
id: id, | ||
}); | ||
}); | ||
animation.on('finished', () => { | ||
@@ -167,2 +173,3 @@ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
rotation: sharedNodeStruct.rotation, | ||
rtt: sharedNodeStruct.rtt, | ||
// These are passed in via message handlers | ||
@@ -169,0 +176,0 @@ shader: null, |
@@ -55,2 +55,3 @@ /* | ||
rotation: sharedNodeStruct.rotation, | ||
rtt: sharedNodeStruct.rtt, | ||
// These are passed in via message handlers | ||
@@ -57,0 +58,0 @@ shader: null, |
@@ -42,3 +42,8 @@ import { CoreExtension } from '../../exports/core-api.js'; | ||
export function santizeCustomDataMap(d) { | ||
const validTypes = { boolean: true, string: true, number: true }; | ||
const validTypes = { | ||
boolean: true, | ||
string: true, | ||
number: true, | ||
undefined: true, | ||
}; | ||
const keys = Object.keys(d); | ||
@@ -45,0 +50,0 @@ for (let i = 0; i < keys.length; i++) { |
{ | ||
"name": "@lightningjs/renderer", | ||
"version": "2.8.0", | ||
"version": "2.9.0-beta1", | ||
"description": "Lightning 3 Renderer", | ||
@@ -34,7 +34,7 @@ "type": "module", | ||
"@types/node": "^20.0.0", | ||
"@typescript-eslint/eslint-plugin": "^5.55.0", | ||
"@typescript-eslint/parser": "^5.55.0", | ||
"@vitest/coverage-v8": "^1.6.0", | ||
"@typescript-eslint/eslint-plugin": "^8.16.0", | ||
"@typescript-eslint/parser": "^8.16.0", | ||
"@vitest/coverage-v8": "^2.1.5", | ||
"concurrently": "^8.0.1", | ||
"eslint": "^8.35.0", | ||
"eslint": "^9.15.0", | ||
"eslint-config-prettier": "^8.7.0", | ||
@@ -44,6 +44,6 @@ "husky": "^8.0.3", | ||
"prettier": "^2.8.4", | ||
"typedoc": "^0.25.1", | ||
"typescript": "^5.2.2", | ||
"vitest": "^1.6.0", | ||
"vitest-mock-extended": "^1.3.1" | ||
"typedoc": "^0.26.11", | ||
"typescript": "~5.6.3", | ||
"vitest": "^2.0.0", | ||
"vitest-mock-extended": "^2.0.2" | ||
}, | ||
@@ -66,3 +66,2 @@ "lint-staged": { | ||
], | ||
"packageManager": "pnpm@8.9.2", | ||
"engines": { | ||
@@ -78,3 +77,3 @@ "npm": ">= 10.0.0", | ||
"build": "tsc --build", | ||
"build:docker": "docker build -t visual-regression .", | ||
"build:docker": "cd visual-regression && pnpm build:docker", | ||
"watch": "tsc --build --watch", | ||
@@ -81,0 +80,0 @@ "test": "vitest", |
@@ -6,5 +6,3 @@ # Lightning 3 Renderer | ||
The Renderer is not designed for direct application development but instead | ||
to provide a lightweight API for front-end application frameworks like Bolt and | ||
Solid. | ||
The Renderer is part of the [LightningJS](https://lightningjs.io) project. While it is possible to use the renderer directly, it is not recommended. Instead, Lightning 3 works best when combined with [Blits](https://lightningjs.io/v3-docs/blits/getting_started/intro.html). | ||
@@ -49,2 +47,4 @@ ## Setup & Commands | ||
For a more detailed and comprehensive list of browsers and their features please see [browsers](./BROWSERS.md). | ||
## Example Tests | ||
@@ -64,2 +64,6 @@ | ||
A hosted version can be found [here](https://lightning-js.github.io/renderer/). | ||
This supports modern browsers as well as Chrome 38 and above through a legacy build. | ||
See [examples/README.md](./examples/README.md) for more info. | ||
@@ -147,2 +151,1 @@ | ||
| Canvas | N | Y | | ||
| | | | |
@@ -28,2 +28,3 @@ /* | ||
import type { Texture } from './textures/Texture.js'; | ||
import { EventEmitter } from '../common/EventEmitter.js'; | ||
@@ -46,2 +47,8 @@ /** | ||
export interface CreateImageBitmapSupport { | ||
basic: boolean; // Supports createImageBitmap(image) | ||
options: boolean; // Supports createImageBitmap(image, options) | ||
full: boolean; // Supports createImageBitmap(image, sx, sy, sw, sh, options) | ||
} | ||
export type ExtractProps<Type> = Type extends { z$__type__Props: infer Props } | ||
@@ -143,3 +150,3 @@ ? Props | ||
export class CoreTextureManager { | ||
export class CoreTextureManager extends EventEmitter { | ||
/** | ||
@@ -162,2 +169,8 @@ * Map of textures by cache key | ||
hasCreateImageBitmap = !!self.createImageBitmap; | ||
imageBitmapSupported = { | ||
basic: false, | ||
options: false, | ||
full: false, | ||
}; | ||
hasWorker = !!self.Worker; | ||
@@ -185,13 +198,41 @@ /** | ||
constructor(numImageWorkers: number) { | ||
// Register default known texture types | ||
if (this.hasCreateImageBitmap && this.hasWorker && numImageWorkers > 0) { | ||
this.imageWorkerManager = new ImageWorkerManager(numImageWorkers); | ||
} | ||
super(); | ||
this.validateCreateImageBitmap() | ||
.then((result) => { | ||
this.hasCreateImageBitmap = | ||
result.basic || result.options || result.full; | ||
this.imageBitmapSupported = result; | ||
if (!this.hasCreateImageBitmap) { | ||
console.warn( | ||
'[Lightning] createImageBitmap is not supported on this browser. ImageTexture will be slower.', | ||
); | ||
} | ||
if (!this.hasCreateImageBitmap) { | ||
console.warn( | ||
'[Lightning] createImageBitmap is not supported on this browser. ImageTexture will be slower.', | ||
); | ||
} | ||
if ( | ||
this.hasCreateImageBitmap && | ||
this.hasWorker && | ||
numImageWorkers > 0 | ||
) { | ||
this.imageWorkerManager = new ImageWorkerManager( | ||
numImageWorkers, | ||
result, | ||
); | ||
} else { | ||
console.warn( | ||
'[Lightning] Imageworker is 0 or not supported on this browser. Image loading will be slower.', | ||
); | ||
} | ||
this.emit('initialized'); | ||
}) | ||
.catch((e) => { | ||
console.warn( | ||
'[Lightning] createImageBitmap is not supported on this browser. ImageTexture will be slower.', | ||
); | ||
// initialized without image worker manager and createImageBitmap | ||
this.emit('initialized'); | ||
}); | ||
this.registerTextureType('ImageTexture', ImageTexture); | ||
@@ -204,2 +245,139 @@ this.registerTextureType('ColorTexture', ColorTexture); | ||
private async validateCreateImageBitmap(): Promise<CreateImageBitmapSupport> { | ||
// Test if createImageBitmap is supported using a simple 1x1 PNG image | ||
// prettier-ignore (this is a binary PNG image) | ||
const pngBinaryData = new Uint8Array([ | ||
0x89, | ||
0x50, | ||
0x4e, | ||
0x47, | ||
0x0d, | ||
0x0a, | ||
0x1a, | ||
0x0a, // PNG signature | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x0d, // IHDR chunk length | ||
0x49, | ||
0x48, | ||
0x44, | ||
0x52, // "IHDR" chunk type | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x01, // Width: 1 | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x01, // Height: 1 | ||
0x01, // Bit depth: 1 | ||
0x03, // Color type: Indexed | ||
0x00, // Compression method: Deflate | ||
0x00, // Filter method: None | ||
0x00, // Interlace method: None | ||
0x25, | ||
0xdb, | ||
0x56, | ||
0xca, // CRC for IHDR | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x03, // PLTE chunk length | ||
0x50, | ||
0x4c, | ||
0x54, | ||
0x45, // "PLTE" chunk type | ||
0x00, | ||
0x00, | ||
0x00, // Palette entry: Black | ||
0xa7, | ||
0x7a, | ||
0x3d, | ||
0xda, // CRC for PLTE | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x01, // tRNS chunk length | ||
0x74, | ||
0x52, | ||
0x4e, | ||
0x53, // "tRNS" chunk type | ||
0x00, // Transparency for black: Fully transparent | ||
0x40, | ||
0xe6, | ||
0xd8, | ||
0x66, // CRC for tRNS | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x0a, // IDAT chunk length | ||
0x49, | ||
0x44, | ||
0x41, | ||
0x54, // "IDAT" chunk type | ||
0x08, | ||
0xd7, // Deflate header | ||
0x63, | ||
0x60, | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x02, | ||
0x00, | ||
0x01, // Zlib-compressed data | ||
0xe2, | ||
0x21, | ||
0xbc, | ||
0x33, // CRC for IDAT | ||
0x00, | ||
0x00, | ||
0x00, | ||
0x00, // IEND chunk length | ||
0x49, | ||
0x45, | ||
0x4e, | ||
0x44, // "IEND" chunk type | ||
0xae, | ||
0x42, | ||
0x60, | ||
0x82, // CRC for IEND | ||
]); | ||
const support: CreateImageBitmapSupport = { | ||
basic: false, | ||
options: false, | ||
full: false, | ||
}; | ||
// Test basic createImageBitmap support | ||
const blob = new Blob([pngBinaryData], { type: 'image/png' }); | ||
const bitmap = await createImageBitmap(blob); | ||
bitmap.close?.(); | ||
support.basic = true; | ||
// Test createImageBitmap with options support | ||
try { | ||
const options = { premultiplyAlpha: 'none' as const }; | ||
const bitmapWithOptions = await createImageBitmap(blob, options); | ||
bitmapWithOptions.close?.(); | ||
support.options = true; | ||
} catch (e) { | ||
/* ignore */ | ||
} | ||
// Test createImageBitmap with full options support | ||
try { | ||
const bitmapWithFullOptions = await createImageBitmap(blob, 0, 0, 1, 1, { | ||
premultiplyAlpha: 'none', | ||
}); | ||
bitmapWithFullOptions.close?.(); | ||
support.full = true; | ||
} catch (e) { | ||
/* ignore */ | ||
} | ||
return support; | ||
} | ||
registerTextureType<Type extends keyof TextureMap>( | ||
@@ -206,0 +384,0 @@ textureType: Type, |
@@ -20,2 +20,3 @@ /* | ||
import type { CreateImageBitmapSupport } from '../CoreTextureManager.js'; | ||
import { type TextureData } from '../textures/Texture.js'; | ||
@@ -51,2 +52,5 @@ | ||
function createImageWorker() { | ||
var supportsOptionsCreateImageBitmap = false; | ||
var supportsFullCreateImageBitmap = false; | ||
function hasAlphaChannel(mimeType: string) { | ||
@@ -80,3 +84,8 @@ return mimeType.indexOf('image/png') !== -1; | ||
if (width !== null && height !== null) { | ||
// createImageBitmap with crop and options | ||
if ( | ||
supportsFullCreateImageBitmap === true && | ||
width !== null && | ||
height !== null | ||
) { | ||
createImageBitmap(blob, x || 0, y || 0, width, height, { | ||
@@ -96,7 +105,21 @@ premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', | ||
createImageBitmap(blob, { | ||
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}) | ||
// createImageBitmap without crop but with options | ||
if (supportsOptionsCreateImageBitmap === true) { | ||
createImageBitmap(blob, { | ||
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}) | ||
.then(function (data) { | ||
resolve({ data, premultiplyAlpha: premultiplyAlpha }); | ||
}) | ||
.catch(function (error) { | ||
reject(error); | ||
}); | ||
return; | ||
} | ||
// Fallback for browsers that do not support createImageBitmap with options | ||
// this is supported for Chrome v50 to v52/54 that doesn't support options | ||
createImageBitmap(blob) | ||
.then(function (data) { | ||
@@ -147,4 +170,10 @@ resolve({ data, premultiplyAlpha: premultiplyAlpha }); | ||
constructor(numImageWorkers: number) { | ||
this.workers = this.createWorkers(numImageWorkers); | ||
constructor( | ||
numImageWorkers: number, | ||
createImageBitmapSupport: CreateImageBitmapSupport, | ||
) { | ||
this.workers = this.createWorkers( | ||
numImageWorkers, | ||
createImageBitmapSupport, | ||
); | ||
this.workers.forEach((worker) => { | ||
@@ -169,5 +198,20 @@ worker.onmessage = this.handleMessage.bind(this); | ||
private createWorkers(numWorkers = 1): Worker[] { | ||
const workerCode = `(${createImageWorker.toString()})()`; | ||
private createWorkers( | ||
numWorkers = 1, | ||
createImageBitmapSupport: CreateImageBitmapSupport, | ||
): Worker[] { | ||
let workerCode = `(${createImageWorker.toString()})()`; | ||
// Replace placeholders with actual initialization values | ||
const supportsOptions = createImageBitmapSupport.options ? 'true' : 'false'; | ||
const supportsFull = createImageBitmapSupport.full ? 'true' : 'false'; | ||
workerCode = workerCode.replace( | ||
'var supportsOptionsCreateImageBitmap = false;', | ||
`var supportsOptionsCreateImageBitmap = ${supportsOptions};`, | ||
); | ||
workerCode = workerCode.replace( | ||
'var supportsFullCreateImageBitmap = false;', | ||
`var supportsFullCreateImageBitmap = ${supportsFull};`, | ||
); | ||
const blob: Blob = new Blob([workerCode.replace('"use strict";', '')], { | ||
@@ -179,3 +223,11 @@ type: 'application/javascript', | ||
for (let i = 0; i < numWorkers; i++) { | ||
workers.push(new Worker(blobURL)); | ||
const worker = new Worker(blobURL); | ||
// Pass `createImageBitmap` support level during worker initialization | ||
worker.postMessage({ | ||
type: 'init', | ||
support: createImageBitmapSupport, | ||
}); | ||
workers.push(worker); | ||
} | ||
@@ -182,0 +234,0 @@ return workers; |
@@ -87,2 +87,4 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
public readonly COLOR_ATTACHMENT0; | ||
public readonly INVALID_ENUM: number; | ||
public readonly INVALID_OPERATION: number; | ||
//#endregion WebGL Enums | ||
@@ -179,2 +181,4 @@ | ||
this.COLOR_ATTACHMENT0 = gl.COLOR_ATTACHMENT0; | ||
this.INVALID_ENUM = gl.INVALID_ENUM; | ||
this.INVALID_OPERATION = gl.INVALID_OPERATION; | ||
} | ||
@@ -1027,2 +1031,14 @@ /** | ||
* ``` | ||
* gl.getError(type); | ||
* ``` | ||
* | ||
* @returns | ||
*/ | ||
getError() { | ||
const { gl } = this; | ||
return gl.getError(); | ||
} | ||
/** | ||
* ``` | ||
* gl.createVertexArray(); | ||
@@ -1029,0 +1045,0 @@ * ``` |
@@ -33,4 +33,9 @@ /* | ||
import { CanvasCoreTexture } from './CanvasCoreTexture.js'; | ||
import { getBorder, getRadius, strokeLine } from './internal/C2DShaderUtils.js'; | ||
import { | ||
getBorder, | ||
getRadius, | ||
roundRect, | ||
strokeLine, | ||
} from './internal/C2DShaderUtils.js'; | ||
import { | ||
formatRgba, | ||
@@ -170,3 +175,3 @@ parseColorRgba, | ||
const path = new Path2D(); | ||
path.roundRect(tx, ty, width, height, radius); | ||
roundRect.call(path, tx, ty, width, height, radius); | ||
ctx.clip(path); | ||
@@ -229,3 +234,4 @@ } | ||
if (radius) { | ||
ctx.roundRect( | ||
roundRect.call( | ||
ctx, | ||
tx + borderInnerWidth, | ||
@@ -232,0 +238,0 @@ ty + borderInnerWidth, |
@@ -87,2 +87,95 @@ /* | ||
export function roundRect( | ||
this: CanvasRenderingContext2D | Path2D, | ||
x: number, | ||
y: number, | ||
width: number, | ||
height: number, | ||
radius: number | DOMPointInit | (number | DOMPointInit)[], | ||
) { | ||
const context = Object.getPrototypeOf(this) as Path2D; | ||
if (!context.roundRect) { | ||
const fixOverlappingCorners = (radii: { | ||
topLeft: number; | ||
topRight: number; | ||
bottomRight: number; | ||
bottomLeft: number; | ||
}) => { | ||
const maxRadius = Math.min(width / 2, height / 2); | ||
const totalHorizontal = | ||
radii.topLeft + radii.topRight + radii.bottomRight + radii.bottomLeft; | ||
if (totalHorizontal > width || totalHorizontal > height) { | ||
const scale = | ||
maxRadius / | ||
Math.max( | ||
radii.topLeft, | ||
radii.topRight, | ||
radii.bottomRight, | ||
radii.bottomLeft, | ||
); | ||
radii.topLeft *= scale; | ||
radii.topRight *= scale; | ||
radii.bottomRight *= scale; | ||
radii.bottomLeft *= scale; | ||
} | ||
}; | ||
const radii = | ||
typeof radius === 'number' | ||
? { | ||
topLeft: radius, | ||
topRight: radius, | ||
bottomRight: radius, | ||
bottomLeft: radius, | ||
} | ||
: { topLeft: 0, topRight: 0, bottomRight: 0, bottomLeft: 0, ...radius }; | ||
fixOverlappingCorners(radii); | ||
this.moveTo(x + radii.topLeft, y); | ||
this.lineTo(x + width - radii.topRight, y); | ||
this.ellipse( | ||
x + width - radii.topRight, | ||
y + radii.topRight, | ||
radii.topRight, | ||
radii.topRight, | ||
0, | ||
1.5 * Math.PI, | ||
2 * Math.PI, | ||
); | ||
this.lineTo(x + width, y + height - radii.bottomRight); | ||
this.ellipse( | ||
x + width - radii.bottomRight, | ||
y + height - radii.bottomRight, | ||
radii.bottomRight, | ||
radii.bottomRight, | ||
0, | ||
0, | ||
0.5 * Math.PI, | ||
); | ||
this.lineTo(x + radii.bottomLeft, y + height); | ||
this.ellipse( | ||
x + radii.bottomLeft, | ||
y + height - radii.bottomLeft, | ||
radii.bottomLeft, | ||
radii.bottomLeft, | ||
0, | ||
0.5 * Math.PI, | ||
Math.PI, | ||
); | ||
this.lineTo(x, y + radii.topLeft); | ||
this.ellipse( | ||
x + radii.topLeft, | ||
y + radii.topLeft, | ||
radii.topLeft, | ||
radii.topLeft, | ||
0, | ||
Math.PI, | ||
1.5 * Math.PI, | ||
); | ||
} else { | ||
this.roundRect(x, y, width, height, radius); | ||
} | ||
} | ||
export function strokeLine( | ||
@@ -89,0 +182,0 @@ ctx: CanvasRenderingContext2D, |
@@ -103,4 +103,10 @@ /* | ||
if (!shader) { | ||
throw new Error(`Unable to create shader type: ${type}. Source: ${source}`); | ||
const glError = glw.getError(); | ||
throw new Error( | ||
`Unable to create the shader: ${ | ||
type === glw.VERTEX_SHADER ? 'VERTEX_SHADER' : 'FRAGMENT_SHADER' | ||
}.${glError ? ` WebGlContext Error: ${glError}` : ''}`, | ||
); | ||
} | ||
glw.shaderSource(shader, source); | ||
@@ -113,3 +119,3 @@ glw.compileShader(shader); | ||
console.log(glw.getShaderInfoLog(shader)); | ||
console.error(glw.getShaderInfoLog(shader)); | ||
glw.deleteShader(shader); | ||
@@ -136,5 +142,5 @@ } | ||
console.log(glw.getProgramInfoLog(program)); | ||
console.warn(glw.getProgramInfoLog(program)); | ||
glw.deleteProgram(program); | ||
return undefined; | ||
} |
@@ -26,7 +26,5 @@ /* | ||
import type { Dimensions } from '../../../common/CommonTypes.js'; | ||
import type { Rect, RectWithValid } from '../../lib/utils.js'; | ||
import type { RectWithValid } from '../../lib/utils.js'; | ||
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
const MAX_TEXTURES = 8; // TODO: get from gl | ||
/** | ||
@@ -99,3 +97,3 @@ * Can render multiple quads with multiple textures (up to vertex shader texture limit) | ||
const { x, y, width, height } = this.clippingRect; | ||
const pixelRatio = options.pixelRatio; | ||
const pixelRatio = this.parentHasRenderTexture ? 1 : options.pixelRatio; | ||
const canvasHeight = options.canvas.height; | ||
@@ -106,3 +104,12 @@ | ||
const clipHeight = Math.round(height * pixelRatio); | ||
const clipY = Math.round(canvasHeight - clipHeight - y * pixelRatio); | ||
let clipY = Math.round(canvasHeight - clipHeight - y * pixelRatio); | ||
// if parent has render texture, we need to adjust the scissor rect | ||
// to be relative to the parent's framebuffer | ||
if (this.parentHasRenderTexture) { | ||
clipY = this.framebufferDimensions | ||
? this.framebufferDimensions.height - this.dimensions.height | ||
: 0; | ||
} | ||
glw.setScissorTest(true); | ||
@@ -109,0 +116,0 @@ glw.scissor(clipX, clipY, clipWidth, clipHeight); |
@@ -145,5 +145,11 @@ /* | ||
); | ||
if (!vertexShader || !fragmentShader) { | ||
throw new Error( | ||
`Unable to create shader type: ${glw.FRAGMENT_SHADER}. Source: ${fragmentSource}`, | ||
`Unable to create the following shader(s): ${[ | ||
!vertexShader && 'VERTEX_SHADER', | ||
!fragmentShader && 'FRAGMENT_SHADER', | ||
] | ||
.filter(Boolean) | ||
.join(' and ')}`, | ||
); | ||
@@ -150,0 +156,0 @@ } |
@@ -96,3 +96,5 @@ /* | ||
// Pre-load it | ||
this.texture.ctxTexture.load(); | ||
stage.txManager.once('initialized', () => { | ||
this.texture.ctxTexture.load(); | ||
}); | ||
@@ -106,3 +108,3 @@ // Set this.data to the fetched data from dataUrl | ||
// Add all the glyphs to the glyph map | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
let maxCharHeight = 0; | ||
@@ -116,6 +118,6 @@ this.data.chars.forEach((glyph) => { | ||
}); | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
(this.maxCharHeight as number) = maxCharHeight; | ||
// We know `data` is defined here, because we just set it | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
(this.shaper as FontShaper) = new SdfFontShaper( | ||
@@ -122,0 +124,0 @@ this.data, |
@@ -135,57 +135,90 @@ /* | ||
async loadImage(src: string) { | ||
const { premultiplyAlpha, sx, sy, sw, sh, width, height } = this.props; | ||
async loadImageFallback(src: string, hasAlpha: boolean) { | ||
const img = new Image(); | ||
if (this.txManager.imageWorkerManager !== null) { | ||
return await this.txManager.imageWorkerManager.getImage( | ||
src, | ||
premultiplyAlpha, | ||
sx, | ||
sy, | ||
sw, | ||
sh, | ||
); | ||
} else if (this.txManager.hasCreateImageBitmap === true) { | ||
const response = await fetch(src); | ||
const blob = await response.blob(); | ||
const hasAlphaChannel = | ||
premultiplyAlpha ?? this.hasAlphaChannel(blob.type); | ||
return new Promise<{ data: HTMLImageElement; premultiplyAlpha: boolean }>( | ||
(resolve) => { | ||
img.onload = () => { | ||
resolve({ data: img, premultiplyAlpha: hasAlpha }); | ||
}; | ||
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, | ||
img.onerror = () => { | ||
console.warn('Image loading failed, returning fallback object.'); | ||
resolve({ data: img, premultiplyAlpha: hasAlpha }); | ||
}; | ||
} | ||
img.src = src; | ||
}, | ||
); | ||
} | ||
async createImageBitmap( | ||
blob: Blob, | ||
premultiplyAlpha: boolean | null, | ||
sx: number | null, | ||
sy: number | null, | ||
sw: number | null, | ||
sh: number | null, | ||
): Promise<{ | ||
data: ImageBitmap | HTMLImageElement; | ||
premultiplyAlpha: boolean; | ||
}> { | ||
const hasAlphaChannel = premultiplyAlpha ?? blob.type.includes('image/png'); | ||
const imageBitmapSupported = this.txManager.imageBitmapSupported; | ||
if ( | ||
imageBitmapSupported.full === true && | ||
sx !== null && | ||
sy !== null && | ||
sw !== null && | ||
sh !== null | ||
) { | ||
// createImageBitmap with crop | ||
const bitmap = await createImageBitmap(blob, sx, sy, sw, sh, { | ||
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}); | ||
return { data: bitmap, premultiplyAlpha: hasAlphaChannel }; | ||
} else if (imageBitmapSupported.options === true) { | ||
// createImageBitmap without crop but with options | ||
const bitmap = await createImageBitmap(blob, { | ||
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}); | ||
return { data: bitmap, premultiplyAlpha: hasAlphaChannel }; | ||
} else { | ||
// basic createImageBitmap without options or crop | ||
// this is supported for Chrome v50 to v52/54 that doesn't support options | ||
return { | ||
data: await createImageBitmap(blob, { | ||
premultiplyAlpha: hasAlphaChannel ? 'premultiply' : 'none', | ||
colorSpaceConversion: 'none', | ||
imageOrientation: 'none', | ||
}), | ||
data: await createImageBitmap(blob), | ||
premultiplyAlpha: hasAlphaChannel, | ||
}; | ||
} else { | ||
const img = new Image(); | ||
if (!src.startsWith('data:')) { | ||
img.crossOrigin = 'Anonymous'; | ||
} | ||
} | ||
async loadImage(src: string) { | ||
const { premultiplyAlpha, sx, sy, sw, sh } = this.props; | ||
if (this.txManager.hasCreateImageBitmap === true) { | ||
if ( | ||
this.txManager.hasWorker === true && | ||
this.txManager.imageWorkerManager !== null | ||
) { | ||
return this.txManager.imageWorkerManager.getImage( | ||
src, | ||
premultiplyAlpha, | ||
sx, | ||
sy, | ||
sw, | ||
sh, | ||
); | ||
} | ||
img.src = src; | ||
await new Promise<void>((resolve, reject) => { | ||
img.onload = () => resolve(); | ||
img.onerror = () => reject(new Error(`Failed to load image`)); | ||
}).catch((e) => { | ||
console.error(e); | ||
}); | ||
return { | ||
data: img, | ||
premultiplyAlpha: premultiplyAlpha ?? true, | ||
}; | ||
const blob = await fetch(src).then((response) => response.blob()); | ||
return this.createImageBitmap(blob, premultiplyAlpha, sx, sy, sw, sh); | ||
} | ||
return this.loadImageFallback(src, premultiplyAlpha ?? true); | ||
} | ||
@@ -192,0 +225,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
148
2113472
487
46452
1