From a559cfda90d053a9632d07b474a25798758ca25d Mon Sep 17 00:00:00 2001 From: jorge graca Date: Fri, 3 Jul 2026 18:49:16 +0100 Subject: [PATCH 1/7] Support OffscreenCanvas for canvas text rendering Use bitmap/canvas instead of ImageData --- src/core/CoreTextNode.ts | 2 +- src/core/platforms/Platform.ts | 8 +-- src/core/platforms/web/WebPlatform.ts | 5 +- src/core/renderers/canvas/CanvasTexture.ts | 4 +- .../renderers/webgl/WebGlCtxSubTexture.ts | 1 + src/core/renderers/webgl/WebGlCtxTexture.ts | 6 +- src/core/text-rendering/CanvasTextRenderer.ts | 68 ++++++++++++++----- src/core/text-rendering/TextRenderer.ts | 2 +- src/core/textures/ImageTexture.ts | 23 ++++--- src/core/textures/Texture.ts | 1 + 10 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index b8c392503..b32370d77 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 da76af6d6..d0766ae3a 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -58,14 +58,14 @@ export abstract class Platform { }; // If a canvas was provided in the settings, use it. Otherwise, create a new one. - this.canvas = settings.canvas || this.createCanvas(); + this.canvas = settings.canvas || document.createElement('canvas'); } /** - * Creates a new canvas element. - * @returns The created HTMLCanvasElement. + * Creates a new canvas. + * @returns The created HTMLCanvasElement or OffscreenCanvas. */ - abstract createCanvas(): HTMLCanvasElement; + abstract createCanvas(): HTMLCanvasElement | OffscreenCanvas; /** * 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 1024e1bec..f8f376a75 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -78,7 +78,10 @@ export class WebPlatform extends Platform { // Platform-specific methods //////////////////////// - override createCanvas(): HTMLCanvasElement { + override createCanvas() { + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(0, 0); + } return document.createElement('canvas'); } diff --git a/src/core/renderers/canvas/CanvasTexture.ts b/src/core/renderers/canvas/CanvasTexture.ts index cc8461e0b..ddbafea47 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 68ca1a40a..8b7ca621d 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 29a643e0d..ef1f3d206 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 3a8da6e28..7aaea3616 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,25 @@ 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 - | CanvasRenderingContext2D - | OffscreenCanvasRenderingContext2D; + canvas = stage.platform.createCanvas(); + isOffscreen = + typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas; + context = canvas.getContext('2d'); + 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 +168,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 +373,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 798e23e37..88831b3b1 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 8eeda8f4f..45e64f11c 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 e1d18b0eb..79eb5ac01 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 From 7b765f8a3029756d0e5fe751c1854976ae8d234d Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 15 Jul 2026 14:27:52 +0200 Subject: [PATCH 2/7] added createOffscreenCanvas fn --- src/core/platforms/Platform.ts | 6 ++++-- src/core/platforms/web/WebPlatform.ts | 4 ++++ src/core/text-rendering/CanvasTextRenderer.ts | 6 ++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/platforms/Platform.ts b/src/core/platforms/Platform.ts index d0766ae3a..d51f4490e 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -63,9 +63,11 @@ export abstract class Platform { /** * Creates a new canvas. - * @returns The created HTMLCanvasElement or OffscreenCanvas. + * @returns The created HTMLCanvasElement. */ - abstract createCanvas(): HTMLCanvasElement | OffscreenCanvas; + abstract createCanvas(): HTMLCanvasElement; + + abstract createOffscreenCanvas(): OffscreenCanvas | HTMLCanvasElement; /** * 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 f8f376a75..506d9e114 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -79,6 +79,10 @@ export class WebPlatform extends Platform { //////////////////////// override createCanvas() { + return document.createElement('canvas'); + } + + override createOffscreenCanvas() { if (typeof OffscreenCanvas !== 'undefined') { return new OffscreenCanvas(0, 0); } diff --git a/src/core/text-rendering/CanvasTextRenderer.ts b/src/core/text-rendering/CanvasTextRenderer.ts index 7aaea3616..036214da2 100644 --- a/src/core/text-rendering/CanvasTextRenderer.ts +++ b/src/core/text-rendering/CanvasTextRenderer.ts @@ -65,10 +65,12 @@ const init = (_stage: Stage): void => { const dpr = stage.options.devicePhysicalPixelRatio; // Drawing canvas and context - canvas = stage.platform.createCanvas(); + canvas = stage.platform.createOffscreenCanvas(); isOffscreen = typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas; - context = canvas.getContext('2d'); + context = canvas.getContext('2d') as + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D; assertTruthy(context, '.getContext(2d) failed'); context.setTransform(dpr, 0, 0, dpr, 0, 0); From 6def141c4046b057ade400b18ddef86a6e45f8b7 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 15 Jul 2026 16:08:45 +0200 Subject: [PATCH 3/7] updated createOffscreenCanvas --- src/core/platforms/Platform.ts | 2 +- src/core/platforms/web/WebPlatform.ts | 5 +---- src/core/text-rendering/CanvasTextRenderer.ts | 3 +++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/platforms/Platform.ts b/src/core/platforms/Platform.ts index d51f4490e..53774dd36 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -67,7 +67,7 @@ export abstract class Platform { */ abstract createCanvas(): HTMLCanvasElement; - abstract createOffscreenCanvas(): OffscreenCanvas | 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 506d9e114..11ac90095 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -83,10 +83,7 @@ export class WebPlatform extends Platform { } override createOffscreenCanvas() { - if (typeof OffscreenCanvas !== 'undefined') { - return new OffscreenCanvas(0, 0); - } - return document.createElement('canvas'); + return new OffscreenCanvas(1, 1); } override createContext(): GlContextWrapper { diff --git a/src/core/text-rendering/CanvasTextRenderer.ts b/src/core/text-rendering/CanvasTextRenderer.ts index 036214da2..8d3c66eaf 100644 --- a/src/core/text-rendering/CanvasTextRenderer.ts +++ b/src/core/text-rendering/CanvasTextRenderer.ts @@ -68,6 +68,9 @@ const init = (_stage: Stage): void => { canvas = stage.platform.createOffscreenCanvas(); isOffscreen = typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas; + if (canvas === null) { + canvas = stage.platform.createCanvas(); + } context = canvas.getContext('2d') as | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; From 81f138a1e0e53d1dea91b647588890e026cea63e Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 15 Jul 2026 16:15:09 +0200 Subject: [PATCH 4/7] fix change --- src/core/platforms/web/WebPlatform.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/platforms/web/WebPlatform.ts b/src/core/platforms/web/WebPlatform.ts index 11ac90095..4b92bc379 100644 --- a/src/core/platforms/web/WebPlatform.ts +++ b/src/core/platforms/web/WebPlatform.ts @@ -83,7 +83,10 @@ export class WebPlatform extends Platform { } override createOffscreenCanvas() { - return new OffscreenCanvas(1, 1); + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(0, 0); + } + return null; } override createContext(): GlContextWrapper { From df859fae743e29ba9206df87c50b3e3547f7b8ca Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 16 Jul 2026 10:19:25 +0200 Subject: [PATCH 5/7] use this.createCanvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jorge Graça <3399019+eXecutable@users.noreply.github.com> --- src/core/platforms/Platform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/platforms/Platform.ts b/src/core/platforms/Platform.ts index 53774dd36..59bbb6aab 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -58,7 +58,7 @@ export abstract class Platform { }; // If a canvas was provided in the settings, use it. Otherwise, create a new one. - this.canvas = settings.canvas || document.createElement('canvas'); + this.canvas = settings.canvas || this.createCanvas(); } /** From 925a634bcddc923b4ee10778141e15f57ab166a2 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 16 Jul 2026 10:19:55 +0200 Subject: [PATCH 6/7] adjust type comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jorge Graça <3399019+eXecutable@users.noreply.github.com> --- src/core/platforms/Platform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/platforms/Platform.ts b/src/core/platforms/Platform.ts index 59bbb6aab..f04b82676 100644 --- a/src/core/platforms/Platform.ts +++ b/src/core/platforms/Platform.ts @@ -62,7 +62,7 @@ export abstract class Platform { } /** - * Creates a new canvas. + * Creates a new canvas element. * @returns The created HTMLCanvasElement. */ abstract createCanvas(): HTMLCanvasElement; From 39e61234054686e2d4eaf9640f000c9de1ba584b Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 16 Jul 2026 10:21:57 +0200 Subject: [PATCH 7/7] improve if canvas is offscreen check --- src/core/text-rendering/CanvasTextRenderer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/text-rendering/CanvasTextRenderer.ts b/src/core/text-rendering/CanvasTextRenderer.ts index 8d3c66eaf..798fcad11 100644 --- a/src/core/text-rendering/CanvasTextRenderer.ts +++ b/src/core/text-rendering/CanvasTextRenderer.ts @@ -66,11 +66,13 @@ const init = (_stage: Stage): void => { // Drawing canvas and context canvas = stage.platform.createOffscreenCanvas(); - isOffscreen = - typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas; - if (canvas === null) { + if (canvas !== null) { + isOffscreen = true; + } else { + isOffscreen = false; canvas = stage.platform.createCanvas(); } + context = canvas.getContext('2d') as | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;