textmode.js 0.7.0-beta.1 → 0.7.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ import type { GLFramebuffer } from '../../../rendering';
2
+ import type { TextmodeLayerBlendMode } from '../types';
3
+ import type { FilterName, BuiltInFilterName, BuiltInFilterParams } from '../../filters';
4
+ /**
5
+ * A single layer within a multi-layered textmode rendering context.
6
+ *
7
+ * Layers are composited together using various blend modes
8
+ * to create complex visual effects. Each layer can be independently
9
+ * manipulated in terms of visibility, opacity, blend mode, and position.
10
+ *
11
+ * You can draw on each layer by providing a draw callback function,
12
+ * like you would with the base layer's {@link Textmodifier.draw} method.
13
+ *
14
+ * You can also apply a sequence of post-processing filters to each layer's
15
+ * rendered output using the {@link ITextmodeLayer.filter} method.
16
+ *
17
+ * The base layer, which is always present at the bottom of the layer stack,
18
+ * can be accessed via {@link Textmodifier.baseLayer}.
19
+ */
20
+ export interface ITextmodeLayer {
21
+ /**
22
+ * Returns the WebGL texture of the final ASCII framebuffer.
23
+ * If the layer is not yet initialized, returns undefined.
24
+ */
25
+ readonly texture: WebGLTexture | undefined;
26
+ /**
27
+ * Returns the width of the final ASCII framebuffer in pixels.
28
+ * If the layer is not yet initialized, returns 0.
29
+ */
30
+ readonly width: number;
31
+ /**
32
+ * Returns the height of the final ASCII framebuffer in pixels.
33
+ * If the layer is not yet initialized, returns 0.
34
+ */
35
+ readonly height: number;
36
+ /**
37
+ * Returns the draw framebuffer for this layer.
38
+ * If the layer is not yet initialized, returns undefined.
39
+ */
40
+ readonly drawFramebuffer: GLFramebuffer | undefined;
41
+ /**
42
+ * Define this layer's draw callback. The callback is executed each frame
43
+ * and should contain all drawing commands for this layer.
44
+ *
45
+ * Inside the callback, use `t` (your textmode instance) to access drawing
46
+ * methods like `char()`, `charColor()`, `translate()`, and `rect()`.
47
+ *
48
+ * @param callback The function to call when drawing this layer.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const t = textmode.create();
53
+ *
54
+ * // Create layers with different blend modes
55
+ * const glowLayer = t.layers.add({ blendMode: 'additive', opacity: 0.7 });
56
+ * const particleLayer = t.layers.add({ blendMode: 'screen', opacity: 0.5 });
57
+ *
58
+ * // Base layer: animated background with subtle wave pattern
59
+ * t.draw(() => {
60
+ * const time = t.frameCount * 0.02;
61
+ * t.background(8, 12, 24);
62
+ *
63
+ * // Draw undulating grid pattern
64
+ * for (let y = -t.grid.rows / 2; y < t.grid.rows / 2; y++) {
65
+ * for (let x = -t.grid.cols / 2; x < t.grid.cols / 2; x++) {
66
+ * const wave = Math.sin(x * 0.3 + time) * Math.cos(y * 0.3 + time * 0.7);
67
+ * const brightness = 20 + wave * 15;
68
+ *
69
+ * t.push();
70
+ * t.charColor(brightness, brightness + 5, brightness + 15);
71
+ * t.char(wave > 0.3 ? '+' : wave > -0.3 ? '·' : '.');
72
+ * t.translate(x, y);
73
+ * t.point();
74
+ * t.pop();
75
+ * }
76
+ * }
77
+ * });
78
+ *
79
+ * // Glow layer: pulsing orbital ring
80
+ * glowLayer.draw(() => {
81
+ * t.clear();
82
+ * const time = t.frameCount * 0.03;
83
+ * const ringCount = 24;
84
+ *
85
+ * for (let i = 0; i < ringCount; i++) {
86
+ * const angle = (i / ringCount) * Math.PI * 2 + time;
87
+ * const pulse = Math.sin(time * 2 + i * 0.5) * 0.5 + 0.5;
88
+ * const radius = 8 + Math.sin(time * 1.5) * 2;
89
+ *
90
+ * t.push();
91
+ * t.charColor(255, 180 + pulse * 75, 80 + pulse * 100);
92
+ * t.char('#*+=-'[i % 5]);
93
+ * t.translate(Math.round(Math.cos(angle) * radius), Math.round(Math.sin(angle) * radius * 0.6));
94
+ * t.point();
95
+ * t.pop();
96
+ * }
97
+ * });
98
+ *
99
+ * // Particle layer: floating sparkles
100
+ * particleLayer.draw(() => {
101
+ * t.clear();
102
+ * const time = t.frameCount * 0.015;
103
+ *
104
+ * for (let i = 0; i < 12; i++) {
105
+ * const seed = i * 137.5; // Golden angle for distribution
106
+ * const x = Math.sin(seed + time) * (6 + i * 0.8);
107
+ * const y = Math.cos(seed * 1.3 + time * 0.8) * (4 + i * 0.5);
108
+ * const flicker = Math.sin(time * 4 + i) * 0.5 + 0.5;
109
+ *
110
+ * t.push();
111
+ * t.charColor(200 + flicker * 55, 220, 255);
112
+ * t.char('*');
113
+ * t.translate(Math.round(x), Math.round(y));
114
+ * t.point();
115
+ * t.pop();
116
+ * }
117
+ * });
118
+ * ```
119
+ */
120
+ draw(callback: () => void): void;
121
+ /**
122
+ * Show this layer for rendering.
123
+ */
124
+ show(): void;
125
+ /**
126
+ * Hide this layer from rendering.
127
+ */
128
+ hide(): void;
129
+ /**
130
+ * Define or retrieve the layer's opacity.
131
+ * @param opacity The opacity value to set (between 0 and 1).
132
+ * @returns The current opacity if no parameter is provided.
133
+ */
134
+ opacity(opacity?: number): number | void;
135
+ /**
136
+ * Set or get the layer's blend mode for compositing with layers below.
137
+ *
138
+ * @param mode The blend mode to set.
139
+ * @returns The current blend mode if no parameter is provided.
140
+ *
141
+ * **Available Blend Modes:**
142
+ * - `'normal'` - Standard alpha compositing
143
+ * - `'additive'` - Adds colors together (great for glow/energy effects)
144
+ * - `'multiply'` - Darkens by multiplying colors
145
+ * - `'screen'` - Lightens; inverse of multiply
146
+ * - `'subtract'` - Subtracts layer from base
147
+ * - `'darken'` - Takes minimum of each channel
148
+ * - `'lighten'` - Takes maximum of each channel
149
+ * - `'overlay'` - Combines multiply/screen for contrast
150
+ * - `'softLight'` - Subtle contrast enhancement
151
+ * - `'hardLight'` - Intense overlay effect
152
+ * - `'colorDodge'` - Brightens base by blend color
153
+ * - `'colorBurn'` - Darkens base by blend color
154
+ * - `'difference'` - Absolute difference; creates inverted effects
155
+ * - `'exclusion'` - Softer difference effect
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const t = textmode.create();
160
+ *
161
+ * // Create 5 layers with different blend modes
162
+ * const blendModes = ['additive', 'screen', 'overlay', 'difference', 'multiply'];
163
+ * const colors = [[255, 80, 150], [80, 180, 255], [255, 200, 80], [150, 255, 120], [200, 120, 255]];
164
+ * const layers = blendModes.map(mode => t.layers.add({ blendMode: mode, opacity: 0.85 }));
165
+ *
166
+ * t.draw(() => {
167
+ * const time = t.frameCount * 0.2;
168
+ * t.background(12, 8, 20, 255);
169
+ *
170
+ * layers.forEach((layer, i) => {
171
+ * layer.draw(() => {
172
+ * t.charColor(...colors[i], 255);
173
+ *
174
+ * // Draw spiral of characters
175
+ * for (let j = 0; j < 30; j++) {
176
+ * const angle = j * 0.2 + time * (i % 2 ? 1 : -1);
177
+ * const radius = 3 + j * 0.4 + Math.sin(time + j) * 2;
178
+ * const x = Math.cos(angle) * radius;
179
+ * const y = Math.sin(angle) * radius * 0.6;
180
+ *
181
+ * t.char('#*+=-.'[j % 6]);
182
+ * t.translate(Math.round(x), Math.round(y));
183
+ * t.rect(1, 1);
184
+ * }
185
+ * });
186
+ *
187
+ * // Offset each layer
188
+ * layer.offset(Math.sin(time * 0.6 + i) * 6, Math.cos(time * 0.3 + i) * 4);
189
+ * });
190
+ * });
191
+ * ```
192
+ */
193
+ blendMode(mode: TextmodeLayerBlendMode): TextmodeLayerBlendMode | void;
194
+ /**
195
+ * Set or get the layer's offset in pixels.
196
+ * @param x The x offset in pixels.
197
+ * @param y The y offset in pixels.
198
+ * @returns The current offset if no parameters are provided.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * const t = textmode.create();
203
+ *
204
+ * const LAYER_COUNT = 32;
205
+ * const LABEL = 'textmode.js';
206
+ *
207
+ * // Create trailing layers
208
+ * const layers = Array.from({ length: LAYER_COUNT }, () =>
209
+ * t.layers.add({ blendMode: 'normal', opacity: 1.0 })
210
+ * );
211
+ *
212
+ * // Snake segments for smooth trailing effect
213
+ * const segments = Array.from({ length: LAYER_COUNT + 1 }, () => ({ x: 0, y: 0 }));
214
+ *
215
+ * // Helper to draw text label centered
216
+ * const drawLabel = (color) => {
217
+ * t.charColor(...color);
218
+ * t.cellColor(0, 0, 0, 0);
219
+ * [...LABEL].forEach((char, i) => {
220
+ * t.push();
221
+ * t.char(char);
222
+ * t.translate(i - Math.floor(LABEL.length / 2), 0);
223
+ * t.rect(1, 1);
224
+ * t.pop();
225
+ * });
226
+ * };
227
+ *
228
+ * // Set up layer draw callbacks
229
+ * layers.forEach((layer, index) => {
230
+ * layer.draw(() => {
231
+ * t.background(0, 0, 0, 0);
232
+ * const brightness = 255 - (index / LAYER_COUNT) * 180;
233
+ * drawLabel([brightness, brightness * 0.8, 255]);
234
+ * });
235
+ * });
236
+ *
237
+ * t.draw(() => {
238
+ * t.background(20, 20, 40);
239
+ * t.clear();
240
+ *
241
+ * // Compute head position (circular motion)
242
+ * const time = t.frameCount * 0.06;
243
+ * const head = {
244
+ * x: Math.cos(time) * 24,
245
+ * y: Math.sin(time * 0.7) * 12
246
+ * };
247
+ *
248
+ * // Update snake segments with elastic follow
249
+ * segments[0] = head;
250
+ * for (let i = 1; i < segments.length; i++) {
251
+ * const prev = segments[i - 1];
252
+ * segments[i].x += (prev.x - segments[i].x) * 0.3;
253
+ * segments[i].y += (prev.y - segments[i].y) * 0.3;
254
+ * }
255
+ *
256
+ * // Draw head on base layer
257
+ * t.layers.base.offset(Math.round(head.x), Math.round(head.y));
258
+ * drawLabel([255, 200, 100]);
259
+ *
260
+ * // Offset each trailing layer to its segment position
261
+ * layers.forEach((layer, index) => {
262
+ * const seg = segments[index + 1];
263
+ * layer.offset(Math.round(seg.x), Math.round(seg.y));
264
+ * });
265
+ * });
266
+ * ```
267
+ */
268
+ offset(x?: number, y?: number): {
269
+ x: number;
270
+ y: number;
271
+ } | void;
272
+ /**
273
+ * Apply a post-processing filter to this layer's rendered output.
274
+ *
275
+ * Filters are applied after ASCII conversion in the order they are called.
276
+ * Call this method within your layer's draw callback to apply effects.
277
+ *
278
+ * **Built-in Filters:**
279
+ * - `'invert'` - Inverts all colors
280
+ * - `'grayscale'` - Converts to grayscale (param: amount 0-1, default 1)
281
+ * - `'sepia'` - Applies sepia tone (param: amount 0-1, default 1)
282
+ * - `'threshold'` - Black/white threshold (param: threshold 0-1, default 0.5)
283
+ *
284
+ * @param name The name of the filter to apply (built-in or custom registered filter)
285
+ * @param params Optional parameters for the filter
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * const t = textmode.create();
290
+ *
291
+ * // Create a layer with filters applied
292
+ * const effectLayer = t.layers.add({ blendMode: 'normal', opacity: 1.0 });
293
+ *
294
+ * t.draw(() => {
295
+ * // Base layer: draw a simple pattern
296
+ * t.background(20, 20, 40);
297
+ * t.charColor(255, 200, 100);
298
+ * t.char('#');
299
+ * t.rect(t.grid.cols, t.grid.rows);
300
+ * });
301
+ *
302
+ * effectLayer.draw(() => {
303
+ * t.clear();
304
+ * t.charColor(100, 150, 255);
305
+ * t.char('*');
306
+ * t.rect(10, 10);
307
+ *
308
+ * // Apply filters in sequence
309
+ * if (t.frameCount % 120 < 60) {
310
+ * effectLayer.filter('invert');
311
+ * }
312
+ * effectLayer.filter('grayscale', Math.sin(t.frameCount * 0.05) * 0.5 + 0.5);
313
+ * });
314
+ * ```
315
+ */
316
+ filter<T extends BuiltInFilterName>(name: FilterName, params?: BuiltInFilterParams[T]): void;
317
+ /**
318
+ * Apply a custom filter registered via `t.layers.filters.register()`.
319
+ * @param name The name of the custom filter
320
+ * @param params Optional parameters for the custom filter
321
+ */
322
+ filter(name: FilterName, params?: unknown): void;
323
+ }
@@ -2,7 +2,7 @@ import type { GLFramebuffer } from '../../rendering';
2
2
  import type { TextmodeGrid } from '../Grid';
3
3
  import type { GLRenderer } from '../../rendering/webgl/core/Renderer';
4
4
  import type { TextmodeFont } from '../loadables/font';
5
- import type { LayerFilterManager } from './filters';
5
+ import type { TextmodeFilterManager } from '../filters';
6
6
  /**
7
7
  * Blend modes available for {@link TextmodeLayer} compositing in 2D mode.
8
8
  *
@@ -59,7 +59,12 @@ export interface LayerDependencies {
59
59
  /**
60
60
  * The shared filter manager for applying post-ASCII filters.
61
61
  */
62
- filterManager: LayerFilterManager;
62
+ filterManager: TextmodeFilterManager;
63
+ /**
64
+ * Ping-pong buffers for layer filter chain processing (grid-sized).
65
+ * Shared across all layers within the LayerManager.
66
+ */
67
+ layerPingPongBuffers: [GLFramebuffer, GLFramebuffer];
63
68
  /**
64
69
  * Optional external draw framebuffer. When provided, the layer will use this
65
70
  * instead of creating its own. Used for the base layer which shares the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "textmode.js",
3
- "version": "0.7.0-beta.1",
3
+ "version": "0.7.0-beta.3",
4
4
  "description": "textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",
@@ -1,87 +0,0 @@
1
- import type { TextmodeFilterStrategy, FilterName } from './types';
2
- /**
3
- * Register a custom filter strategy.
4
- *
5
- * Once registered, the filter can be used with `layer.filter('filterId', params)`.
6
- *
7
- * @param strategy The filter strategy to register
8
- * @throws Error if a filter with the same ID is already registered
9
- *
10
- * @example
11
- * ```typescript
12
- * import { registerFilterStrategy } from 'textmode.js';
13
- *
14
- * registerFilterStrategy({
15
- * id: 'customGlow',
16
- * createShader(context) {
17
- * return context.renderer.$createShader(vertexSrc, fragmentSrc);
18
- * },
19
- * createUniforms(params, context) {
20
- * return {
21
- * u_intensity: params?.intensity ?? 1.0,
22
- * u_color: params?.color ?? [1, 1, 1],
23
- * u_resolution: [context.width, context.height]
24
- * };
25
- * }
26
- * });
27
- *
28
- * // Then in your sketch:
29
- * layer.draw(() => {
30
- * // ... drawing code
31
- * this.filter('customGlow', { intensity: 0.8, color: [1, 0.5, 0] });
32
- * });
33
- * ```
34
- */
35
- export declare function registerFilterStrategy(strategy: TextmodeFilterStrategy): void;
36
- /**
37
- * Unregister a previously registered filter strategy.
38
- *
39
- * @param id The ID of the filter to unregister
40
- * @returns true if the filter was unregistered, false if it wasn't registered
41
- *
42
- * @example
43
- * ```typescript
44
- * import { unregisterFilterStrategy } from 'textmode.js';
45
- *
46
- * unregisterFilterStrategy('customGlow');
47
- * ```
48
- */
49
- export declare function unregisterFilterStrategy(id: FilterName): boolean;
50
- /**
51
- * Get a registered filter strategy by its ID.
52
- *
53
- * @param id The ID of the filter
54
- * @returns The filter strategy, or undefined if not found
55
- * @internal
56
- */
57
- export declare function getFilterStrategy(id: FilterName): TextmodeFilterStrategy | undefined;
58
- /**
59
- * Check if a filter is registered.
60
- *
61
- * @param id The ID of the filter to check
62
- * @returns true if the filter is registered
63
- *
64
- * @example
65
- * ```typescript
66
- * import { isFilterRegistered } from 'textmode.js';
67
- *
68
- * if (isFilterRegistered('myFilter')) {
69
- * layer.filter('myFilter');
70
- * }
71
- * ```
72
- */
73
- export declare function isFilterRegistered(id: FilterName): boolean;
74
- /**
75
- * Get a list of all registered filter IDs.
76
- *
77
- * @returns An array of all registered filter IDs
78
- *
79
- * @example
80
- * ```typescript
81
- * import { getRegisteredFilters } from 'textmode.js';
82
- *
83
- * console.log('Available filters:', getRegisteredFilters());
84
- * // ['invert', 'grayscale', 'sepia', 'hueRotate', ...]
85
- * ```
86
- */
87
- export declare function getRegisteredFilters(): FilterName[];
@@ -1,71 +0,0 @@
1
- import type { GLRenderer, GLFramebuffer } from '../../../rendering';
2
- import type { QueuedFilter } from './types';
3
- /**
4
- * Manages filter shader compilation and application for layers.
5
- *
6
- * This manager:
7
- * - Lazily compiles filter shaders on first use
8
- * - Uses ping-pong rendering to chain multiple filters
9
- * - Caches compiled shaders for performance
10
- *
11
- * @internal
12
- */
13
- export declare class LayerFilterManager {
14
- private readonly _renderer;
15
- private readonly _shaderCache;
16
- private readonly _copyShader;
17
- private _pingPongBuffers;
18
- private _currentBufferIndex;
19
- private _isInitialized;
20
- /**
21
- * Create a new LayerFilterManager.
22
- * @param renderer The WebGL renderer instance
23
- */
24
- constructor(renderer: GLRenderer);
25
- /**
26
- * Initialize ping-pong buffers for filter chain processing.
27
- * @param width Buffer width in pixels
28
- * @param height Buffer height in pixels
29
- */
30
- $initialize(width: number, height: number): void;
31
- /**
32
- * Apply a chain of filters to the source texture, outputting to target.
33
- *
34
- * @param sourceTexture The input texture (raw ASCII framebuffer)
35
- * @param targetFramebuffer The output framebuffer (layer's ASCII framebuffer)
36
- * @param filters The queue of filters to apply in order
37
- * @param width Framebuffer width
38
- * @param height Framebuffer height
39
- */
40
- $applyFilters(sourceTexture: WebGLTexture, targetFramebuffer: GLFramebuffer, filters: QueuedFilter[], width: number, height: number): void;
41
- /**
42
- * Apply a single filter pass.
43
- */
44
- private _applyFilter;
45
- /**
46
- * Get or create a cached shader for the given filter.
47
- */
48
- private _getOrCreateShader;
49
- /**
50
- * Copy a texture to a framebuffer using the copy shader.
51
- */
52
- private _copyTexture;
53
- /**
54
- * Get the next ping-pong buffer (not currently in use).
55
- */
56
- private _getNextBuffer;
57
- /**
58
- * Swap to the next ping-pong buffer.
59
- */
60
- private _swapBuffers;
61
- /**
62
- * Resize the ping-pong buffers.
63
- * @param width New width in pixels
64
- * @param height New height in pixels
65
- */
66
- $resize(width: number, height: number): void;
67
- /**
68
- * Dispose of all resources.
69
- */
70
- $dispose(): void;
71
- }