Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/CoreTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/core/platforms/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down
9 changes: 8 additions & 1 deletion src/core/platforms/web/WebPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,17 @@ export class WebPlatform extends Platform {
// Platform-specific methods
////////////////////////

override createCanvas(): HTMLCanvasElement {
override createCanvas() {
return document.createElement('canvas');
}

override createOffscreenCanvas() {
Comment thread
jfboeve marked this conversation as resolved.
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.');
Expand Down
4 changes: 2 additions & 2 deletions src/core/renderers/canvas/CanvasTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ export class CanvasTexture extends CoreContextTexture {
private async onLoadRequest(
data: NonNullable<NonNullable<Texture['textureData']>['data']>,
): Promise<Dimensions> {
// 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;
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/core/renderers/webgl/WebGlCtxSubTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class WebGlCtxSubTexture extends WebGlCtxTexture {
| SubTextureProps
| CompressedData
| HTMLImageElement
| HTMLCanvasElement
| null,
): Dimensions {
if (data === null) {
Expand Down
6 changes: 5 additions & 1 deletion src/core/renderers/webgl/WebGlCtxTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
71 changes: 55 additions & 16 deletions src/core/text-rendering/CanvasTextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -56,26 +60,32 @@ const renderInfoCache = new Map<string, CanvasRenderInfo>();
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();
}
Comment on lines +68 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
canvas = stage.platform.createOffscreenCanvas();
isOffscreen =
typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas;
if (canvas === null) {
canvas = stage.platform.createCanvas();
}
canvas = stage.platform.createOffscreenCanvas();
if (canvas) {
isOffscreen = true;
} else {
isOffscreen = false;
canvas = stage.platform.createCanvas();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied this, it looks a lot neater! Thanks!


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';

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/core/text-rendering/TextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export type SdfRenderInfo = RenderInfo & {

export type CanvasRenderInfo = RenderInfo & {
type: 'canvas';
imageData: ImageData;
imageData: ImageBitmap | HTMLCanvasElement;
};

export type TextRenderInfo = SdfRenderInfo | CanvasRenderInfo;
Expand Down
23 changes: 12 additions & 11 deletions src/core/textures/ImageTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
};
}
Expand Down
1 change: 1 addition & 0 deletions src/core/textures/Texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface TextureData {
data:
| ImageBitmap
| ImageData
| HTMLCanvasElement
| SubTextureProps
| CompressedData
| HTMLImageElement
Expand Down
Loading