textmode.js
Version:
textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.
381 lines (380 loc) • 13.6 kB
TypeScript
import type { GLFramebuffer } from '../../../rendering';
import type { TextmodeFont } from '../../loadables/font';
import type { TextmodeLayerBlendMode } from '../types';
import type { FilterName, BuiltInFilterName, BuiltInFilterParams } from '../../filters';
/**
* A single layer within a multi-layered textmode rendering context.
*
* Layers are composited together using various blend modes
* to create complex visual effects. Each layer can be independently
* manipulated in terms of visibility, opacity, blend mode, and position.
*
* You can draw on each layer by providing a draw callback function,
* like you would with the base layer's {@link Textmodifier.draw} method.
*
* You can also apply a sequence of post-processing filters to each layer's
* rendered output using the {@link ITextmodeLayer.filter} method.
*
* The base layer, which is always present at the bottom of the layer stack,
* can be accessed via {@link Textmodifier.baseLayer}.
*/
export interface ITextmodeLayer {
/**
* Returns the WebGL texture of the final ASCII framebuffer.
* If the layer is not yet initialized, returns undefined.
*/
readonly texture: WebGLTexture | undefined;
/**
* Returns the width of the final ASCII framebuffer in pixels.
* If the layer is not yet initialized, returns 0.
*/
readonly width: number;
/**
* Returns the height of the final ASCII framebuffer in pixels.
* If the layer is not yet initialized, returns 0.
*/
readonly height: number;
/**
* Returns the draw framebuffer for this layer.
* If the layer is not yet initialized, returns undefined.
*/
readonly drawFramebuffer: GLFramebuffer | undefined;
/** The font used by this layer. */
readonly font: TextmodeFont;
/**
* Define this layer's draw callback. The callback is executed each frame
* and should contain all drawing commands for this layer.
*
* Inside the callback, use `t` (your `Textmodifier` instance) to access drawing
* methods like `char()`, `charColor()`, `translate()`, and `rect()`.
*
* @param callback The function to call when drawing this layer.
*
* @example
* ```javascript
* const t = textmode.create();
*
* // Create layers with different blend modes
* const glowLayer = t.layers.add({ blendMode: 'additive', opacity: 0.7 });
* const particleLayer = t.layers.add({ blendMode: 'screen', opacity: 0.5 });
*
* // Base layer: animated background with subtle wave pattern
* t.draw(() => {
* const time = t.frameCount * 0.02;
* t.background(8, 12, 24);
*
* // Draw undulating grid pattern
* for (let y = -t.grid.rows / 2; y < t.grid.rows / 2; y++) {
* for (let x = -t.grid.cols / 2; x < t.grid.cols / 2; x++) {
* const wave = Math.sin(x * 0.3 + time) * Math.cos(y * 0.3 + time * 0.7);
* const brightness = 20 + wave * 15;
*
* t.push();
* t.charColor(brightness, brightness + 5, brightness + 15);
* t.char(wave > 0.3 ? '+' : wave > -0.3 ? '·' : '.');
* t.translate(x, y);
* t.point();
* t.pop();
* }
* }
* });
*
* // Glow layer: pulsing orbital ring
* glowLayer.draw(() => {
* t.clear();
* const time = t.frameCount * 0.03;
* const ringCount = 24;
*
* for (let i = 0; i < ringCount; i++) {
* const angle = (i / ringCount) * Math.PI * 2 + time;
* const pulse = Math.sin(time * 2 + i * 0.5) * 0.5 + 0.5;
* const radius = 8 + Math.sin(time * 1.5) * 2;
*
* t.push();
* t.charColor(255, 180 + pulse * 75, 80 + pulse * 100);
* t.char('#*+=-'[i % 5]);
* t.translate(Math.round(Math.cos(angle) * radius), Math.round(Math.sin(angle) * radius * 0.6));
* t.point();
* t.pop();
* }
* });
*
* // Particle layer: floating sparkles
* particleLayer.draw(() => {
* t.clear();
* const time = t.frameCount * 0.015;
*
* for (let i = 0; i < 12; i++) {
* const seed = i * 137.5; // Golden angle for distribution
* const x = Math.sin(seed + time) * (6 + i * 0.8);
* const y = Math.cos(seed * 1.3 + time * 0.8) * (4 + i * 0.5);
* const flicker = Math.sin(time * 4 + i) * 0.5 + 0.5;
*
* t.push();
* t.charColor(200 + flicker * 55, 220, 255);
* t.char('*');
* t.translate(Math.round(x), Math.round(y));
* t.point();
* t.pop();
* }
* });
* ```
*/
draw(callback: () => void): void;
/** Get or set the font size for this layer. */
fontSize(size?: number): number | void;
/**
* Load a font from the given source into this layer.
*
* @param fontSource The URL or path to the font file.
* @returns The loaded TextmodeFont instance.
*
* @example
* ```js
* const layer = t.layers.add();
*
* t.setup(async () => {
* await layer.loadFont('./fonts/custom.ttf');
* });
* ```
*/
loadFont(fontSource: string | TextmodeFont): Promise<TextmodeFont>;
/**
* Show this layer for rendering.
*/
show(): void;
/**
* Hide this layer from rendering.
*/
hide(): void;
/**
* Define or retrieve the layer's opacity.
* @param opacity The opacity value to set (between 0 and 1).
* @returns The current opacity if no parameter is provided.
*/
opacity(opacity?: number): number | void;
/**
* Set or get the layer's blend mode for compositing with layers below.
*
* @param mode The blend mode to set.
* @returns The current blend mode if no parameter is provided.
*
* **Available blend modes:**
* - `'normal'` - Standard alpha compositing
* - `'additive'` - Adds colors together (great for glow/energy effects)
* - `'multiply'` - Darkens by multiplying colors
* - `'screen'` - Lightens; inverse of multiply
* - `'subtract'` - Subtracts layer from base
* - `'darken'` - Takes minimum of each channel
* - `'lighten'` - Takes maximum of each channel
* - `'overlay'` - Combines multiply/screen for contrast
* - `'softLight'` - Subtle contrast enhancement
* - `'hardLight'` - Intense overlay effect
* - `'colorDodge'` - Brightens base by blend color
* - `'colorBurn'` - Darkens base by blend color
* - `'difference'` - Absolute difference; creates inverted effects
* - `'exclusion'` - Softer difference effect
*
* @example
* ```javascript
* const t = textmode.create();
*
* // Create 5 layers with different blend modes
* const blendModes = ['additive', 'screen', 'overlay', 'difference', 'multiply'];
* const colors = [[255, 80, 150], [80, 180, 255], [255, 200, 80], [150, 255, 120], [200, 120, 255]];
* const layers = blendModes.map(mode => t.layers.add({ blendMode: mode, opacity: 0.85 }));
*
* t.draw(() => {
* const time = t.frameCount * 0.2;
* t.background(12, 8, 20, 255);
*
* layers.forEach((layer, i) => {
* layer.draw(() => {
* t.charColor(...colors[i], 255);
*
* // Draw spiral of characters
* for (let j = 0; j < 30; j++) {
* const angle = j * 0.2 + time * (i % 2 ? 1 : -1);
* const radius = 3 + j * 0.4 + Math.sin(time + j) * 2;
* const x = Math.cos(angle) * radius;
* const y = Math.sin(angle) * radius * 0.6;
*
* t.char('#*+=-.'[j % 6]);
* t.translate(Math.round(x), Math.round(y));
* t.rect(1, 1);
* }
* });
*
* // Offset each layer
* layer.offset(Math.sin(time * 0.6 + i) * 6, Math.cos(time * 0.3 + i) * 4);
* });
* });
* ```
*/
blendMode(mode: TextmodeLayerBlendMode): TextmodeLayerBlendMode | void;
/**
* Set or get the layer's offset in pixels.
* @param x The x offset in pixels.
* @param y The y offset in pixels.
* @returns The current offset if no parameters are provided.
*
* @example
* ```javascript
* const t = textmode.create();
*
* const LAYER_COUNT = 32;
* const LABEL = 'textmode.js';
*
* // Create trailing layers
* const layers = Array.from({ length: LAYER_COUNT }, () =>
* t.layers.add({ blendMode: 'normal', opacity: 1.0 })
* );
*
* // Snake segments for smooth trailing effect
* const segments = Array.from({ length: LAYER_COUNT + 1 }, () => ({ x: 0, y: 0 }));
*
* // Helper to draw text label centered
* const drawLabel = (color) => {
* t.charColor(...color);
* t.cellColor(0, 0, 0, 0);
* [...LABEL].forEach((char, i) => {
* t.push();
* t.char(char);
* t.translate(i - Math.floor(LABEL.length / 2), 0);
* t.rect(1, 1);
* t.pop();
* });
* };
*
* // Set up layer draw callbacks
* layers.forEach((layer, index) => {
* layer.draw(() => {
* t.background(0, 0, 0, 0);
* const brightness = 255 - (index / LAYER_COUNT) * 180;
* drawLabel([brightness, brightness * 0.8, 255]);
* });
* });
*
* t.draw(() => {
* t.background(20, 20, 40);
* t.clear();
*
* // Compute head position (circular motion)
* const time = t.frameCount * 0.06;
* const head = {
* x: Math.cos(time) * 24,
* y: Math.sin(time * 0.7) * 12
* };
*
* // Update snake segments with elastic follow
* segments[0] = head;
* for (let i = 1; i < segments.length; i++) {
* const prev = segments[i - 1];
* segments[i].x += (prev.x - segments[i].x) * 0.3;
* segments[i].y += (prev.y - segments[i].y) * 0.3;
* }
*
* // Draw head on base layer
* t.layers.base.offset(Math.round(head.x), Math.round(head.y));
* drawLabel([255, 200, 100]);
*
* // Offset each trailing layer to its segment position
* layers.forEach((layer, index) => {
* const seg = segments[index + 1];
* layer.offset(Math.round(seg.x), Math.round(seg.y));
* });
* });
* ```
*/
offset(x?: number, y?: number): {
x: number;
y: number;
} | void;
/**
* Set or get the layer's rotation in degrees around its center.
*
* The rotation is applied during compositing around the center of the layer's
* rectangular bounds. The rotation origin remains at the center even when
* an offset is applied.
*
* @param z The rotation angle in degrees. Positive values rotate clockwise.
* @returns The current rotation in degrees if no parameter is provided.
*
* @example
* ```javascript
* const t = textmode.create();
*
* const rotatingLayer = t.layers.add({ blendMode: 'difference', opacity: 1.0 });
*
* rotatingLayer.draw(() => {
* t.clear();
* t.charColor(255, 200, 100);
* t.char('#');
* t.rect(10, 5);
* });
*
* t.draw(() => {
* t.background(20, 20, 40);
*
* // Rotate the layer over time
* rotatingLayer.rotateZ(t.frameCount * 2);
*
* t.charColor(100, 200, 255);
* t.char('-');
* t.rect(t.grid.cols, t.grid.rows);
* });
* ```
*/
rotateZ(z?: number): number | void;
/**
* Apply a post-processing filter to this layer's rendered output.
*
* Filters are applied after ASCII conversion in the order they are called.
* Call this method within your layer's draw callback to apply effects.
*
* **Built-in filters:**
* - `'invert'` - Inverts all colors
* - `'grayscale'` - Converts to grayscale (param: amount 0-1, default 1)
* - `'sepia'` - Applies sepia tone (param: amount 0-1, default 1)
* - `'threshold'` - Black/white threshold (param: threshold 0-1, default 0.5)
*
* @param name The name of the filter to apply (built-in or custom registered filter)
* @param params Optional parameters for the filter
*
* @example
* ```javascript
* const t = textmode.create();
*
* // Create a layer with filters applied
* const effectLayer = t.layers.add({ blendMode: 'normal', opacity: 1.0 });
*
* t.draw(() => {
* // Base layer: draw a simple pattern
* t.background(20, 20, 40);
* t.charColor(255, 200, 100);
* t.char('#');
* t.rect(t.grid.cols, t.grid.rows);
* });
*
* effectLayer.draw(() => {
* t.clear();
* t.charColor(100, 150, 255);
* t.char('*');
* t.rect(10, 10);
*
* // Apply filters in sequence
* if (t.frameCount % 120 < 60) {
* effectLayer.filter('invert');
* }
* effectLayer.filter('grayscale', Math.sin(t.frameCount * 0.05) * 0.5 + 0.5);
* });
* ```
*/
filter<T extends BuiltInFilterName>(name: FilterName, params?: BuiltInFilterParams[T]): void;
/**
* Apply a custom filter registered via `t.layers.filters.register()`.
* @param name The name of the custom filter
* @param params Optional parameters for the custom filter
*/
filter(name: FilterName, params?: unknown): void;
}