We want the canvas to run in different environments like browsers, Node.js, WebWorker, etc. Since different environments provide different functionalities, we referenced Pixi.js's implementation and provide an Adapter interface along with default BrowserAdapter and WebWorkerAdapter for developers to choose from.
export interface Adapter {
createCanvas: (
width?: number,
height?: number,
) => HTMLCanvasElement | OffscreenCanvas;
createTexImageSource: (
canvas: HTMLCanvasElement | OffscreenCanvas,
) => TexImageSource;
createImage: (src: string) => Promise<ImageType>;
getWindow: () => typeof globalThis;
getDocument: () => Document;
getXMLSerializer: () => XMLSerializer | null;
getDOMParser: () => DOMParser | null;
splitGraphemes: (s: string) => string[];
requestAnimationFrame: (callback: FrameRequestCallback) => number;
cancelAnimationFrame: (handle: number) => void;
}The system uses BrowserAdapter by default. If you need to use other adapters, you can set them using the DOMAdapter.set() method.
import { DOMAdapter, WebWorkerAdapter } from '@infinite-canvas-tutorial/ecs';
DOMAdapter.set(WebWorkerAdapter);Although we don't provide a built-in Node.js adapter, you can refer to the NodeJSAdapter used in SSR unit tests for implementation:
import { DOMAdapter } from '@infinite-canvas-tutorial/ecs';
const NodeJSAdapter: Adapter = {
createCanvas: (width?: number, height?: number) =>
getCanvas(width ?? 0, height ?? 0), // Use headless-gl and node-canvas
getDocument: () => new JSDOM().window._document, // Use JSDOM
//...
};
DOMAdapter.set(NodeJSAdapter);Let's look at each method provided by the adapter in detail.
createCanvas
As the name suggests, this method is used to create a <canvas>-like element. It's called when measuring text, determining if a point is within a path, and other operations.
createCanvas: (width?: number, height?: number) =>
HTMLCanvasElement | OffscreenCanvas;- In browser environment: uses
document.createElement('canvas') - In WebWorker environment: uses
OffscreenCanvas - In Node.js environment: uses headless-gl and node-canvas
createTexImageSource
When implementing Gradient and Pattern, you need to convert the <canvas> into a TexImageSource before calling the WebGL API. In a browser environment, you can return it directly. For details, see: Lesson 17 - Gradients and patterns
In a Node.js environment, you can use pngparse-sync to convert Image objects from node-canvas into the image formats required by headless-gl:
import parsePNG from 'pngparse-sync';
export const NodeJSAdapter: Adapter = {
createTexImageSource: (canvas) => {
// convert Image in node-canvas to ImageData in headless-gl
const buffer = canvas.toBuffer();
const png = parsePNG(buffer);
return png.data;
},createImage
Create an image ImageBitmap based on the image src, using loaders.gl in the browser environment:
import { load } from '@loaders.gl/core';
export const BrowserAdapter: Adapter = {
createImage: (src: string) => load(src, ImageLoader),
};In a NodeJS environment, you can use get-pixels to load images via URL and pass them into headless-gl:
import getPixels from 'get-pixels';
export const NodeJSAdapter: Adapter = {
createImage: (src: string) => loadImage(__dirname + '/canvas.png') as any,
};
export function loadImage(path: string) {
// Load local image instead of fetching remote URL.
// @see https://github.com/stackgl/headless-gl/pull/53/files#diff-55563b6c0b90b80aed19c83df1c51e80fd45d2fbdad6cc047ee86e98f65da3e9R83
return new Promise((resolve, reject) => {
getPixels(path, function (err, image) {
if (err) {
reject('Bad image path');
} else {
image.width = image.shape[0];
image.height = image.shape[1];
resolve(image);
}
});
});
}getWindow
Returns window in the browser environment and self in WebWorkers:
getWindow: () => window,
getWindow: () => self,getDocument
Gets the current Document object. Mainly used when exporting SVG with ImageExporter.
getDocument: () => Document;- In browser environment: uses
document - In WebWorker environment: not available
- In Node.js environment: uses
JSDOM
getXMLSerializer
Gets the current XMLSerializer object. Used when exporting SVG with ImageExporter.
getXMLSerializer: () => XMLSerializer | null;In WebWorker and Node.js environments, you can use @xmldom/xmldom:
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';getDOMParser
Gets the current DOMParser object. Used when parsing bitmap fonts in XML format. Bitmap Font Example
getDOMParser: () => DOMParser | null;splitGraphemes
Splits a string into individual characters, supporting compound characters.
splitGraphemes: (s: string) => string[];- In browser and WebWorker environments: uses Intl.Segmenter
- In Node.js environment: can use grapheme-splitter
import GraphemeSplitter from 'grapheme-splitter';
splitGraphemes: (s: string) => {
const splitter = new GraphemeSplitter();
return splitter.splitGraphemes(s);
},requestAnimationFrame
Used for camera animations.
- In browser and WebWorker environments: uses requestAnimationFrame
- In Node.js environment: uses
setTimeout
cancelAnimationFrame
Used to cancel camera animations.
- In browser and WebWorker environments: uses cancelAnimationFrame
- In Node.js environment: uses
clearTimeout