textmode.js
Version:
textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.
450 lines (449 loc) • 16.6 kB
TypeScript
import type { TextmodeGrid } from '../Grid';
import type { TextmodeFont } from '../loadables/font';
import type { TextmodeImage } from '../loadables/TextmodeImage';
import type { TextmodeCanvas } from '../Canvas';
import type { AnimationController } from '../AnimationController';
import type { GLShader } from '../../rendering';
import type { GLRenderer } from '../../rendering/webgl/core/Renderer';
import type { MouseManager } from '../managers/MouseManager';
import type { KeyboardManager } from '../managers/KeyboardManager';
import type { TouchManager } from '../managers/TouchManager';
import type { IRenderingMixin } from '../mixins/interfaces/IRenderingMixin';
import type { IKeyboardMixin } from '../mixins/interfaces/IKeyboardMixin';
import type { ITouchMixin } from '../mixins/interfaces/ITouchMixin';
import type { IMouseMixin } from '../mixins/interfaces/IMouseMixin';
import type { IAnimationMixin } from '../mixins/interfaces/IAnimationMixin';
import type { LoadingScreenManager } from '../loading/LoadingScreenManager';
import type { TextmodeLayerManager } from '../layers';
import type { BuiltInFilterName, BuiltInFilterParams, TextmodeFilterManager, FilterName } from '../filters';
import type { TextmodeConversionManager } from '../conversion';
/**
* Manages textmode rendering on a [`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) and provides methods for drawing,
* exporting, font management, event handling, and animation control.
*
* If the `Textmodifier` instance is created without a canvas parameter,
* it creates a new `HTMLCanvasElement` to draw on using the `textmode.js` drawing API.
* If a canvas is provided, it will use that canvas instead.
*/
export interface ITextmodifier extends IRenderingMixin, IAnimationMixin, IMouseMixin, ITouchMixin, IKeyboardMixin {
/** Core WebGL renderer @ignore */
readonly _renderer: GLRenderer;
/** Canvas management @ignore */
readonly _canvas: TextmodeCanvas;
/** Animation controller for managing rendering loop @ignore */
readonly _animationController: AnimationController;
/** Mouse interaction manager @ignore */
readonly _mouseManager: MouseManager;
/** Touch interaction manager @ignore */
readonly _touchManager: TouchManager;
/** Keyboard interaction manager @ignore */
readonly _keyboardManager: KeyboardManager;
/** Shader used for converting pixels to textmode grid format @ignore */
readonly _textmodeConversionShader: GLShader;
/** Shader used for final presentation to the screen @ignore */
readonly _presentShader: GLShader;
/** Loading screen manager for boot-time UX @ignore */
readonly _loading: LoadingScreenManager;
/** Conversion manager for image-to-ASCII conversion @ignore */
readonly _conversionManager: TextmodeConversionManager;
/** Filter manager for applying post-processing effects @ignore */
readonly _filterManager: TextmodeFilterManager;
/** Layer manager for handling multiple layers @ignore */
readonly _layerManager: TextmodeLayerManager;
/** Active font based on layer, or loading screen font override @ignore */
_activeFont?: TextmodeFont;
/** Active grid based on layer currently being rendered @ignore */
_activeGrid?: TextmodeGrid;
/** Main render method @ignore */
$render(): void;
/**
* Load a font for the base layer and return it.
*
* The returned font can be reused on other layers via {@link TextmodeLayer.loadFont}.
*
* @param fontSource The URL of the font to load.
* @returns The loaded TextmodeFont instance (base layer font).
*
* @example
* ```javascript
* const t = textmode.create();
*
* t.setup(async () => {
* // Load font for the base layer
* const font = await t.loadFont('./fonts/myfont.ttf');
* // const font = await t.layers.base.loadFont('./fonts/myfont.ttf'); // Equivalent
*
* // Use the same font on another layer
* const layer = t.layers.add();
* await layer.loadFont(font);
*
* // Or load a different font for a layer
* await layer.loadFont('./fonts/otherfont.ttf');
* });
* ```
*/
loadFont(fontSource: string): Promise<TextmodeFont>;
/**
* Set the font size used for rendering.
* @param size The font size to set.
*
* @example
* ```javascript
* // Create a Textmodifier instance
* const t = textmode.create();
*
* t.setup(() => {
* // Set the font size to 32
* t.fontSize(32);
* });
*
* t.draw(() => {
* t.background(0);
* t.char('A');
* t.rect(5, 5);
* });
* ```
*/
fontSize(size: number): void;
/**
* Get or set the grid used for mouse and touch input coordinate mapping.
*
* By default, input coordinates are mapped to the topmost visible layer's grid,
* which changes dynamically as layers are shown/hidden. Use this method to lock
* input mapping to a specific grid or layer, or to return to responsive mode.
*
* When called without arguments, returns the current input grid mode:<br/>
* - `'topmost'` if using responsive mode (default)<br/>
* - The specific `TextmodeGrid` if locked
*
* @example
* ```javascript
* const t = textmode.create();
*
* // Add a UI layer on top
* const uiLayer = t.layers.add({ fontSize: 16 });
*
* t.setup(() => {
* // Lock input to the base layer's grid for game controls
* // even though the UI layer is rendered on top
* t.inputGrid(t.layers.base.grid);
* });
*
* t.draw(() => {
* // Mouse positions now always use base layer's grid
* t.text(`Mouse: ${t.mouseX}, ${t.mouseY}`, 0, 0);
* });
*
* // Switch back to responsive mode
* // t.inputGrid('topmost');
*
* // Or check current mode
* // const current = t.inputGrid(); // 'topmost' or the locked grid
* ```
*/
inputGrid(target?: 'topmost' | TextmodeGrid): 'topmost' | TextmodeGrid | void;
/**
* Set a setup callback function that will be executed once when initialization is complete.
*
* This callback is called after font loading and grid initialization, allowing access to
* properties like `textmodifier.grid.cols` for calculating layout or setup variables.
*
* @param callback The function to call when setup is complete
*
* @example
* ```javascript
* const textmodifier = textmode.create({
* width: 800,
* height: 600,
* fontSize: 16
* });
*
* // Setup callback - called once when ready
* textmodifier.setup(() => {
* // Now you can access grid properties
* const cols = textmodifier.grid.cols;
* const rows = textmodifier.grid.rows;
*
* // Initialize any variables that depend on grid size
* rectWidth = Math.floor(cols / 3);
* rectHeight = Math.floor(rows / 2);
* });
*
* // Draw callback - called every frame
* textmodifier.draw(() => {
* textmodifier.background(128);
* textmodifier.char('A');
* textmodifier.rotateZ(textmodifier.frameCount * 2);
* textmodifier.rect(rectWidth, rectHeight);
* });
* ```
*/
setup(callback: () => void): void;
/**
* Set a draw callback function for the base layer.
*
* This callback function is where all drawing commands should be placed for textmode rendering on the main layer.
*
* If multiple layers are added via {@link Textmodifier.layers}, each layer has its own draw callback set via {@link TextmodeLayer.draw}.
* This allows for complex multi-layered compositions with independent rendering logic per layer.
*
* Calling this method is equivalent to setting the draw callback on the base layer,
* while the direct layer callback has precedence if both are set.
* ```js
* textmodifier.layers.base.draw(callback);
* ```
*
* @param callback The function to call before each render
*
* @example
* ```javascript
* // Create a textmodifier instance
* const t = textmode.create({
* width: 800,
* height: 600,
* });
*
* // Set up draw callback
* t.draw(() => {
* // Set background color
* t.background(128);
*
* // Draw a textmode rectangle
* t.char('A');
* t.rotateZ(t.frameCount * 2);
* t.rect(16, 16);
* });
* ```
*/
draw(callback: () => void): void;
/**
* Set a callback function that will be called when the window is resized.
* @param callback The function to call when the window is resized.
*
* @example
* ```javascript
* // Create a standalone textmodifier instance
* const t = textmode.create({
* width: window.innerWidth,
* height: window.innerHeight,
* });
*
* // Draw callback to update content
* t.draw(() => {
* // Set background color
* t.background(128);
* t.char('A');
* t.rotateZ(t.frameCount * 2);
* t.rect(16, 16);
* });
*
* // Set up window resize callback
* t.windowResized(() => {
* // Resize the canvas to match window size
* t.resizeCanvas(window.innerWidth, window.innerHeight);
* });
* ```
*/
windowResized(callback: () => void): void;
/**
* Resize the canvas and adjust all related components accordingly.
*
* @param width The new width of the canvas.
* @param height The new height of the canvas.
*
* @example
* ```javascript
* // Create a standalone textmodifier instance
* const t = textmode.create({
* width: window.innerWidth,
* height: window.innerHeight,
* });
*
* // Draw callback to update content
* t.draw(() => {
* // Set background color
* t.background(128);
* t.char('A');
* t.rotateZ(t.frameCount * 2);
* t.rect(16, 16);
* });
*
* // Set up window resize callback
* t.windowResized(() => {
* // Resize the canvas to match window size
* t.resizeCanvas(window.innerWidth, window.innerHeight);
* });
* ```
*/
resizeCanvas(width: number, height: number): void;
/**
* Completely destroy this Textmodifier instance and free all associated resources.
*
* After calling this method, the instance should not be used and will be eligible for garbage collection.
*
* @example
* ```js
* // Create a textmodifier instance
* const textmodifier = textmode.create();
*
* // ...
*
* // When done, completely clean up
* textmodifier.destroy();
*
* // Instance is now safely disposed and ready for garbage collection
* ```
*/
destroy(): void;
/**
* Apply a filter to the final composited output.
*
* Filters are applied after all layers are composited but before
* the result is presented to the canvas. Multiple filters can be
* queued per frame and will be applied in order.
*
* @param name The name of the filter to apply (built-in or custom)
* @param params Optional parameters for the filter
*
* @example
* ```ts
* t.draw(() => {
* t.background(0);
* t.charColor(255);
* t.char('A');
* t.rect(10, 10);
*
* // Apply built-in filters
* t.filter('grayscale', 0.5);
* t.filter('invert');
*
* // Chain multiple filters
* t.filter('sepia', { amount: 0.3 });
* t.filter('threshold', 0.5);
* });
* ```
*/
filter<T extends BuiltInFilterName>(name: T, params?: BuiltInFilterParams[T]): void;
filter(name: FilterName, params?: unknown): void;
filter(name: FilterName, params?: unknown): void;
/**
* Get the grid whose layer is currently being drawn to.
* If called outside of a layers draw callback, returns the base layer's grid.
*
* If no grid is set (e.g., before user setup()), returns `undefined`.
*/
readonly grid: TextmodeGrid | undefined;
/** Get the current font object used for rendering the base layer. */
readonly font: TextmodeFont;
/** Get the width of the canvas in pixels. */
readonly width: number;
/** Get the height of the canvas in pixels. */
readonly height: number;
/** Get the textmodifier canvas containing the rendered output. */
readonly canvas: HTMLCanvasElement;
/** Check if the instance has been disposed/destroyed. */
readonly isDisposed: boolean;
/**
* Access the filter manager for this Textmodifier instance.
*
* Use this to register custom filters that can be applied both globally
* (via {@link filter}) and on individual layers (via {@link TextmodeLayer.filter}).
*
* @example
* ```ts
* // Register a custom filter once
* await t.filters.register('vignette', vignetteShader, {
* u_intensity: ['intensity', 0.5]
* });
*
* t.draw(() => {
* t.background(0);
* t.char('A');
* t.rect(10, 10);
*
* // Apply filter globally to final output
* t.filter('vignette', { intensity: 0.8 });
*
* // Or apply to a specific layer
* t.layers.base.filter('vignette', 0.5);
* });
* ```
*/
readonly filters: TextmodeFilterManager;
/**
* Access the layer manager for this Textmodifier instance.
*
* Use this to create and manage multiple layers within the textmode rendering context.
* Each layer has its own grid, font, draw callback, and filters.
*/
readonly layers: TextmodeLayerManager;
/**
* Access the conversion manager for this Textmodifier instance.
*
* Use this to register custom conversion strategies that can be used
* when converting images/videos/canvases into textmode representations.
*/
readonly conversions: TextmodeConversionManager;
/**
* If in overlay mode, returns the {@link TextmodeImage} instance capturing the target canvas/video content,
* allowing further configuration of the conversion parameters.
*
* @example
* ```js
* // Create the textmode instance using the p5 canvas as input overlay
* const t = textmode.create({ fontSize: 16, canvas: p.canvas, overlay: true });
*
* // Configure overlay conversion once fonts and grid are ready
* t.setup(() => {
* t.overlay
* .characters(' .:-=+*#%@') // Character set for brightness mapping
* .cellColorMode('fixed') // Use fixed background cell color
* .cellColor(0, 0, 0) // Black background for each cell
* .charColorMode('sampled') // Sample the character color from the image
* .background(0, 0, 0, 255); // Black fallback for transparent pixels
* });
*
* // In the draw loop, pass the overlay into the text grid
* t.draw(() => {
* t.clear();
* t.image(t.overlay, t.grid.cols, t.grid.rows);
* });
*```
*/
readonly overlay: TextmodeImage | undefined;
/**
* Provides access to the loading screen manager to control boot-time loading UX.
*
* @example
* ```javascript
* const t = textmode.create({ width: 800, height: 600, loadingScreen: { message: 'loading...' } });
*
* t.setup(async () => {
* // Initialize two loading phases
* const phase1 = t.loading.addPhase('Loading assets');
* const phase2 = t.loading.addPhase('Initializing game');
*
* // Start the first phase and simulate asset loading
* await phase1.track(async () => {
* for (let i = 0; i <= 5; i++) {
* phase1.report(i / 5);
* // Small delay - increases visibility of the loading animation
* await new Promise((r) => setTimeout(r, 200));
* }
* });
*
* // Start the second phase and simulate initialization
* await phase2.track(async () => {
* for (let i = 0; i <= 5; i++) {
* phase2.report(i / 5);
* await new Promise((r) => setTimeout(r, 150));
* }
* });
*
* // Optionally set a final message before the screen transitions away
* t.loading.message('Ready - enjoy!');
* });
* ```
*/
readonly loading: LoadingScreenManager;
}