Advanced features
This section covers advanced features of textmode.js that enable sophisticated graphics and rendering techniques, including offscreen rendering with framebuffers, custom shaders, and 3D transformations.
Framebuffers and offscreen rendering
Framebuffers allow you to render graphics offscreen and then use them as textures or images. This enables complex multi-pass rendering effects, feedback loops, and better performance optimization through render-to-texture techniques.
Creating framebuffers
Create a framebuffer using the createFramebuffer() method with specified grid dimensions:
const t = textmode.create({ width: 800, height: 600 });
let fb;
t.setup(() => {
// Create a framebuffer with 50x30 grid cells
fb = t.createFramebuffer({
width: 50,
height: 30
});
});The framebuffer uses the same Multiple Render Target (MRT) structure as the main rendering pipeline, with 3 attachments for character and color data.
Rendering to framebuffers
Use begin() and end() to redirect rendering operations to the framebuffer:
t.draw(() => {
// Render to offscreen buffer
fb.begin();
// All drawing operations now render to the framebuffer
t.background(255, 0, 0);
t.char('A');
t.charColor(255);
t.rect(20, 10);
// Return to main canvas
fb.end();
// Render framebuffer to main canvas
t.background(0);
t.rotateZ(t.frameCount * 2);
t.image(fb);
});Transforming framebuffer content
Framebuffers can be drawn with any transformation applied:
t.draw(() => {
// Draw something to framebuffer
fb.begin();
t.clear();
t.charColor(255, 0, 0);
t.char('A');
t.rect(20, 10);
fb.end();
// Clear main canvas
t.background(0);
// Draw framebuffer with rotation
t.push();
t.translate(-10, 0);
t.rotateZ(t.frameCount * 2);
t.image(fb);
t.pop();
// Draw another copy with different transforms
t.push();
t.translate(10, 0);
t.rotateZ(-t.frameCount * 2);
t.image(fb);
t.pop();
});Scaling framebuffer content
You can specify dimensions when drawing a framebuffer to scale its content:
t.draw(() => {
// Render at original size
t.image(fb);
// Render scaled to specific dimensions
t.push();
t.image(fb, 30, 20); // Scale to 30x20 cells
t.pop();
});Custom shaders
textmode.js supports custom GLSL shaders for advanced visual effects. You can create custom filter shaders that process the character grid with GPU-accelerated effects.
Creating filter shaders
Use createFilterShader() to create custom shaders from GLSL source code:
const t = textmode.create({ width: 800, height: 600 });
let waveShader;
t.setup(async () => {
waveShader = await t.createFilterShader(`#version 300 es
precision highp float;
in vec2 v_uv;
uniform float u_time;
layout(location = 0) out vec4 o_character;
layout(location = 1) out vec4 o_primaryColor;
layout(location = 2) out vec4 o_secondaryColor;
void main() {
float wave = sin(v_uv.x * 10.0 + u_time) * 0.5 + 0.5;
vec3 color = vec3(wave, 1.0 - wave, 0.5);
o_character = vec4(wave, 0.0, 0.0, 0.0);
o_primaryColor = vec4(color, 1.0);
o_secondaryColor = vec4(color * 0.5, 1.0);
}
`);
});You can also load shaders from external files:
t.setup(async () => {
waveShader = await t.createFilterShader('./shader.frag');
});Applying shaders
Use the shader() method to set the current shader, and setUniform() or setUniforms() to pass values:
t.draw(() => {
t.shader(waveShader);
t.setUniform('u_time', t.frameCount * 0.003);
t.rect(t.grid.cols, t.grid.rows);
});Setting multiple uniforms
Use setUniforms() to pass multiple values in one call:
const t = textmode.create({ width: 800, height: 600 });
let rippleShader;
t.setup(async () => {
rippleShader = await t.createFilterShader(`#version 300 es
precision highp float;
in vec2 v_uv;
uniform float u_time;
uniform vec2 u_center;
layout(location = 0) out vec4 o_character;
layout(location = 1) out vec4 o_primaryColor;
layout(location = 2) out vec4 o_secondaryColor;
void main() {
float dist = length(v_uv - u_center);
float wave = sin(dist * 20.0 - u_time * 2.0) * 0.5 + 0.5;
vec3 color = mix(vec3(0.2, 0.4, 0.8), vec3(0.9, 0.6, 0.3), wave);
o_character = vec4(wave, 0.0, 0.0, 0.0);
o_primaryColor = vec4(color, 1.0);
o_secondaryColor = vec4(color * 0.4, 1.0);
}
`);
});
t.draw(() => {
if (rippleShader) {
t.shader(rippleShader);
t.setUniforms({
u_time: t.frameCount * 0.0005,
u_center: [0.5, 0.5]
});
t.rect(t.grid.cols, t.grid.rows);
}
});Shader output requirements
Custom filter shaders must output to three MRT attachments:
layout(location = 0) out vec4 o_character- Character/transform datalayout(location = 1) out vec4 o_primaryColor- Primary (foreground) colorlayout(location = 2) out vec4 o_secondaryColor- Secondary (background) color
Advanced shader techniques
Multi-pass rendering with framebuffers
Combine framebuffers with shaders for complex multi-pass effects:
const t = textmode.create({ width: 800, height: 600 });
let fb1, fb2, blurShader;
t.setup(async () => {
fb1 = t.createFramebuffer({ width: 80, height: 60 });
fb2 = t.createFramebuffer({ width: 80, height: 60 });
blurShader = await t.createFilterShader('./blur.frag');
});
t.draw(() => {
// First pass: render scene to fb1
fb1.begin();
t.background(0);
t.char('A');
t.charColor(255, 100, 200);
t.rotateZ(t.frameCount * 2);
t.rect(20, 20);
fb1.end();
// Second pass: apply blur shader to fb2
fb2.begin();
t.shader(blurShader);
t.setUniform('u_intensity', 0.5);
t.image(fb1);
fb2.end();
// Final pass: render to screen
t.background(0);
t.image(fb2);
});Feedback loops
Create visual feedback effects by ping-ponging between two framebuffers. This technique samples the previous frame's textures in the shader to create decay and trail effects:
const t = textmode.create({ width: 800, height: 600 });
let prevFramebuffer, nextFramebuffer, feedbackShader;
t.setup(async () => {
// Create two framebuffers for ping-ponging
prevFramebuffer = t.createFramebuffer();
nextFramebuffer = t.createFramebuffer();
// Clear both framebuffers once so we start from a blank slate
[prevFramebuffer, nextFramebuffer].forEach((fb) => {
fb.begin();
t.clear();
fb.end();
});
feedbackShader = await t.createFilterShader(`#version 300 es
precision highp float;
in vec2 v_uv;
uniform vec2 u_gridSize;
uniform float u_decay;
uniform sampler2D u_previousCharTexture;
uniform sampler2D u_previousPrimaryTexture;
uniform sampler2D u_previousSecondaryTexture;
uniform float u_clearThreshold;
layout(location = 0) out vec4 o_character;
layout(location = 1) out vec4 o_primaryColor;
layout(location = 2) out vec4 o_secondaryColor;
vec2 quantizedGridUV() {
// Reconstruct texel centers exactly like the internal copy shader
vec2 flippedUV = vec2(v_uv.x, 1.0 - v_uv.y);
vec2 uvTex = flippedUV * u_gridSize;
return (floor(uvTex) + 0.5) / u_gridSize;
}
void main() {
vec2 uv = quantizedGridUV();
// Sample previous frame's data at exact cell centers
vec4 prevChar = texture(u_previousCharTexture, uv);
vec4 prevPrimary = texture(u_previousPrimaryTexture, uv);
vec4 prevSecondary = texture(u_previousSecondaryTexture, uv);
o_character = prevChar;
o_primaryColor = vec4(prevPrimary.rgb - 0.01, 1.0);
o_secondaryColor = prevSecondary;
}
`);
});
t.draw(() => {
// Swap framebuffers (ping-pong)
[prevFramebuffer, nextFramebuffer] = [nextFramebuffer, prevFramebuffer];
// Render to next framebuffer
nextFramebuffer.begin();
// Apply decay shader with previous frame's textures
t.shader(feedbackShader);
t.setUniform('u_gridSize', [prevFramebuffer.width, prevFramebuffer.height]);
t.setUniform('u_previousCharTexture', prevFramebuffer.textures[0]);
t.setUniform('u_previousPrimaryTexture', prevFramebuffer.textures[1]);
t.setUniform('u_previousSecondaryTexture', prevFramebuffer.textures[2]);
t.rect(t.grid.cols, t.grid.rows);
// Return to the default textmode pipeline before drawing new glyphs
t.shader(null);
// Draw new content
t.push();
t.char('*');
t.charColor(255);
t.translate(
Math.cos(t.frameCount * 0.05) * 20,
Math.sin(t.frameCount * 0.05) * 15
);
t.point();
t.pop();
nextFramebuffer.end();
// Display next framebuffer on main canvas
t.background(0);
t.image(nextFramebuffer);
});3D transformations
textmode.js supports 3D transformations for creating depth and perspective effects with your ASCII graphics.
Rotation in 3D space
Use rotate(), rotateX(), rotateY(), or rotateZ():
const t = textmode.create({ width: 800, height: 600 });
t.draw(() => {
t.background(0);
// Draw three rectangles rotating in 3D space
for (let i = 0; i < 3; i++) {
t.push();
t.translate(i * 15 - 15, 0, 0);
const angle = t.frameCount * (1.5 + i * 0.5);
// Each shape rotates around different axes
t.rotate(angle * 0.7, angle * 0.5, angle);
t.char(['T', 'X', 'T'][i]);
t.charColor(100 + i * 60, 200 - i * 40, 255);
t.rect(10, 10);
t.pop();
}
});Translation in 3D space
Use translate(), translateX(), translateY(), or translateZ():
t.draw(() => {
t.background(0);
// Animate shapes with different Z positions
for (let i = 0; i < 3; i++) {
t.push();
const z = Math.sin(t.frameCount * 0.05 + i) * 20;
t.translate(i * 12 - 12, 0, z);
t.char('O');
t.charColor(180 + z * 3, 120, 255);
t.rect(8, 8);
t.pop();
}
});Orthographic projection
By default, textmode.js uses perspective projection. Switch to orthographic projection with ortho():
const t = textmode.create({ width: 800, height: 600 });
let useOrtho = false;
// Toggle with spacebar
t.keyPressed((data) => {
if (data.key === ' ') {
useOrtho = !useOrtho;
}
});
t.draw(() => {
t.background(0);
// Enable orthographic projection if toggled on
if (useOrtho) {
t.ortho();
}
// Animate rectangle on z-axis
const zPos = Math.sin(t.frameCount * 0.01) * 50;
t.push();
t.translate(0, 0, zPos);
t.rotateZ(t.frameCount * 2);
t.rotateX(t.frameCount * 1.5);
t.char('A');
t.charColor(255, 100, 200);
t.rect(16, 16);
t.pop();
});Note: The projection mode resets to perspective at the start of each frame, so ortho() must be called in every frame where you want orthographic projection.
State management with push/pop
Use push() and pop() to isolate transformation and style changes:
t.draw(() => {
t.background(0);
// Draw multiple shapes with isolated transformations
for (let i = 0; i < 3; i++) {
t.push(); // Save state
t.translate(i * 12 - 12, 0);
t.rotateZ(t.frameCount * (1 + i * 0.5));
t.charColor(100 + i * 70, 255 - i * 50, 150);
t.char(['*', '@', '#'][i]);
t.rect(8, 8);
t.pop(); // Restore state - next iteration starts fresh
}
});Summary
These advanced features unlock the full potential of textmode.js for creating sophisticated graphics and optimized applications. By combining framebuffers, custom shaders, 3D transformations, and multi-pass rendering techniques, you can create complex visual effects while maintaining excellent performance.
Next steps
-> For working with images and videos, refer to the Images and videos section.
-> For basic drawing concepts, refer to the Fundamentals section.
-> For working with custom fonts, refer to the Fonts section.
-> For exporting your creations, refer to the Exporting section.