diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index b8c39250..b32370d7 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -295,7 +295,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { this.texture = this.stage.txManager.createTexture('ImageTexture', { premultiplyAlpha: true, - src: result.imageData as ImageData, + src: result.imageData, }); this.props.w = width; diff --git a/src/core/platforms/Platform.ts b/src/core/platforms/Platform.ts index da76af6d..f04b8267 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -67,6 +67,8 @@ export abstract class Platform { */ abstract createCanvas(): HTMLCanvasElement; + abstract createOffscreenCanvas(): OffscreenCanvas | null; + /** * Create new rendering context (only for WebGL, Canvas does not require a context) */ diff --git a/src/core/platforms/web/WebPlatform.ts b/src/core/platforms/web/WebPlatform.ts index 1024e1be..4b92bc37 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -78,10 +78,17 @@ export class WebPlatform extends Platform { // Platform-specific methods //////////////////////// - override createCanvas(): HTMLCanvasElement { + override createCanvas() { return document.createElement('canvas'); } + override createOffscreenCanvas() { + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(0, 0); + } + return null; + } + override createContext(): GlContextWrapper { if (this.canvas === null) { throw new Error('Canvas has not been created yet.'); diff --git a/src/core/renderers/canvas/CanvasTexture.ts b/src/core/renderers/canvas/CanvasTexture.ts index cc8461e0..ddbafea4 100644 --- a/src/core/renderers/canvas/CanvasTexture.ts +++ b/src/core/renderers/canvas/CanvasTexture.ts @@ -152,8 +152,6 @@ export class CanvasTexture extends CoreContextTexture { private async onLoadRequest( data: NonNullable['data']>, ): Promise { - // TODO: canvas from text renderer should be able to provide the canvas directly - // instead of having to re-draw it into a new canvas... if (data instanceof ImageData) { const canvas = document.createElement('canvas'); canvas.width = data.width; @@ -164,6 +162,8 @@ export class CanvasTexture extends CoreContextTexture { return { w: data.width, h: data.height }; } else if ( (typeof ImageBitmap !== 'undefined' && data instanceof ImageBitmap) || + (typeof HTMLCanvasElement !== 'undefined' && + data instanceof HTMLCanvasElement) || data instanceof HTMLImageElement ) { this.image = data; diff --git a/src/core/renderers/webgl/WebGlCtxSubTexture.ts b/src/core/renderers/webgl/WebGlCtxSubTexture.ts index 68ca1a40..8b7ca621 100644 --- a/src/core/renderers/webgl/WebGlCtxSubTexture.ts +++ b/src/core/renderers/webgl/WebGlCtxSubTexture.ts @@ -58,6 +58,7 @@ export class WebGlCtxSubTexture extends WebGlCtxTexture { | SubTextureProps | CompressedData | HTMLImageElement + | HTMLCanvasElement | null, ): Dimensions { if (data === null) { diff --git a/src/core/renderers/webgl/WebGlCtxTexture.ts b/src/core/renderers/webgl/WebGlCtxTexture.ts index 29a643e0..ef1f3d20 100644 --- a/src/core/renderers/webgl/WebGlCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCtxTexture.ts @@ -194,13 +194,17 @@ export class WebGlCtxTexture extends CoreContextTexture { const memoryPadding = 1.1; // Add padding to account for GPU Padding const isImageBitmap = typeof ImageBitmap !== 'undefined' && tdata instanceof ImageBitmap; + const isHTMLCanvas = + typeof HTMLCanvasElement !== 'undefined' && + tdata instanceof HTMLCanvasElement; // If textureData is null, the texture is empty (0, 0) and we don't need to // upload any data to the GPU. if ( isImageBitmap || + isHTMLCanvas || tdata instanceof ImageData || - // not using typeof HTMLI mageElement due to web worker + // not using typeof HTMLImageElement due to web worker isHTMLImageElement(tdata) === true ) { w = tdata.width; diff --git a/src/core/text-rendering/CanvasTextRenderer.ts b/src/core/text-rendering/CanvasTextRenderer.ts index 3a8da6e2..798fcad1 100644 --- a/src/core/text-rendering/CanvasTextRenderer.ts +++ b/src/core/text-rendering/CanvasTextRenderer.ts @@ -35,12 +35,16 @@ import { normalizeCanvasColor } from '../lib/colorCache.js'; const type = 'canvas' as const; const font: FontHandler = CanvasFontHandler; +let stage: Stage | null = null; let canvas: HTMLCanvasElement | OffscreenCanvas | null = null; let context: | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null = null; +// Whether the drawing canvas is an OffscreenCanvas (supports transferToImageBitmap) +let isOffscreen = false; + // Separate canvas and context for text measurements let measureCanvas: HTMLCanvasElement | OffscreenCanvas | null = null; let measureContext: @@ -56,26 +60,32 @@ const renderInfoCache = new Map(); const _richTextResult = new ParseResult(); // Initialize the Text Renderer -const init = (stage: Stage): void => { +const init = (_stage: Stage): void => { + stage = _stage; const dpr = stage.options.devicePhysicalPixelRatio; // Drawing canvas and context - canvas = stage.platform.createCanvas() as HTMLCanvasElement | OffscreenCanvas; - context = canvas.getContext('2d', { willReadFrequently: true }) as + canvas = stage.platform.createOffscreenCanvas(); + if (canvas !== null) { + isOffscreen = true; + } else { + isOffscreen = false; + canvas = stage.platform.createCanvas(); + } + + context = canvas.getContext('2d') as | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; + assertTruthy(context, '.getContext(2d) failed'); context.setTransform(dpr, 0, 0, dpr, 0, 0); context.textRendering = 'optimizeSpeed'; // Separate measuring canvas and context - measureCanvas = stage.platform.createCanvas() as - | HTMLCanvasElement - | OffscreenCanvas; - measureContext = measureCanvas.getContext('2d') as - | CanvasRenderingContext2D - | OffscreenCanvasRenderingContext2D; - + measureCanvas = stage.platform.createCanvas(); + assertTruthy(measureCanvas, 'createCanvas returned null'); + measureContext = measureCanvas.getContext('2d'); + assertTruthy(measureContext, '.getContext(2d) failed'); measureContext.setTransform(dpr, 0, 0, dpr, 0, 0); measureContext.textRendering = 'optimizeSpeed'; @@ -165,8 +175,21 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { const canvasW = Math.ceil(effectiveWidth); const canvasH = Math.ceil(effectiveHeight); - canvas.width = canvasW; - canvas.height = canvasH; + // For OffscreenCanvas we reuse the shared canvas and snapshot via + // transferToImageBitmap at the end. For HTMLCanvasElement we allocate a + // fresh canvas per text node so it can be passed directly as the texture + // source — no pixel readback or intermediate copy needed. + let drawCanvas: HTMLCanvasElement | OffscreenCanvas; + if (isOffscreen) { + drawCanvas = canvas; + } else { + assertTruthy(stage, 'Stage is not available'); + drawCanvas = stage.platform.createCanvas(); + context = drawCanvas.getContext('2d') as CanvasRenderingContext2D; + } + + drawCanvas.width = canvasW; + drawCanvas.height = canvasH; context.fillStyle = 'white'; context.font = baseFont; context.textBaseline = 'hanging'; @@ -357,10 +380,26 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { } } - // Extract image data - let imageData: ImageData | null = null; - if (canvas.width > 0 && canvas.height > 0) { - imageData = context.getImageData(0, 0, canvasW, canvasH); + // Capture the rendered text as a texture source. + // OffscreenCanvas: transferToImageBitmap detaches the backing bitmap + // synchronously (zero-copy). The shared canvas gets a fresh empty bitmap. + // HTMLCanvasElement: drawCanvas was allocated specifically for this text + // node, so it can be passed directly — no copy needed. + let imageData: ImageBitmap | HTMLCanvasElement | null = null; + if (drawCanvas.width > 0 && drawCanvas.height > 0) { + if (isOffscreen) { + assertTruthy( + drawCanvas instanceof OffscreenCanvas, + 'drawCanvas is not an OffscreenCanvas', + ); + imageData = drawCanvas.transferToImageBitmap(); + } else { + assertTruthy( + drawCanvas instanceof HTMLCanvasElement, + 'drawCanvas is not an HTMLCanvasElement', + ); + imageData = drawCanvas; + } } const renderInfo = { type, diff --git a/src/core/text-rendering/TextRenderer.ts b/src/core/text-rendering/TextRenderer.ts index 798e23e3..88831b3b 100644 --- a/src/core/text-rendering/TextRenderer.ts +++ b/src/core/text-rendering/TextRenderer.ts @@ -447,7 +447,7 @@ export type SdfRenderInfo = RenderInfo & { export type CanvasRenderInfo = RenderInfo & { type: 'canvas'; - imageData: ImageData; + imageData: ImageBitmap | HTMLCanvasElement; }; export type TextRenderInfo = SdfRenderInfo | CanvasRenderInfo; diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 8eeda8f4..45e64f11 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -27,15 +27,21 @@ import type { CompressedImageData } from '../platforms/web/lib/textureCompressio */ export interface ImageTextureProps { /** - * Source URL or ImageData for the image to be used as a texture. + * Source URL or image source for the image to be used as a texture. * * @remarks - * The ImageData type is currently only supported internally. End users should - * only set this property to a URL string. + * The ImageData, ImageBitmap, and HTMLCanvasElement types are currently only + * supported internally. End users should only set this property to a URL string. * * @default '' */ - src?: string | Blob | ImageData | (() => ImageData | null); + src?: + | string + | Blob + | ImageData + | ImageBitmap + | HTMLCanvasElement + | (() => ImageData | ImageBitmap | HTMLCanvasElement | null); /** * Whether to premultiply the alpha channel into the color channels of the * image. @@ -218,14 +224,9 @@ export class ImageTexture extends Texture { return platform.createImage(src, premultiply, sx, sy, sw, sh); } - if (src instanceof ImageData) { - return { - data: src, - premultiplyAlpha, - }; - } + // ImageData, ImageBitmap, or HTMLCanvasElement return { - data: src(), + data: typeof src === 'function' ? src() : src, premultiplyAlpha, }; } diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index e1d18b0e..79eb5ac0 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -102,6 +102,7 @@ export interface TextureData { data: | ImageBitmap | ImageData + | HTMLCanvasElement | SubTextureProps | CompressedData | HTMLImageElement