AsciiShader draws every glyph on the GPU, so it can finish the frame the way a monitor would: scanlines, glow, curved glass. It can also react to the pointer and to sound. All of it is off by default, and none of it changes your field.
CRT effects
Pass fx with any of six effects, each from 0 (off) to 1 (strong):
<AsciiShader
field={FIELD}
phosphor="p1"
fx={{ scanlines: 0.5, bloom: 0.6, curvature: 0.35, vignette: 0.45 }}
/>| Effect | What it does |
|---|---|
scanlines | Darkens every third CSS pixel row, like a CRT raster. |
bloom | Bright glyphs glow into their neighbours. Scaled by the theme's --ascii-glow. |
curvature | Bends the frame like curved glass. The corners go black (or transparent, with background="transparent"). |
vignette | Darkens toward the corners. |
flicker | Varies the brightness slightly from frame to frame. |
glitch | On random frames, a band of rows jumps sideways, a few glyphs flip and the colour channels split. |
CRT_FX from @/lib/ascii is a ready-made full CRT look; spread it and override what you like: fx={{ ...CRT_FX, glitch: 0.2 }}. NO_FX turns everything off.
The effects run in the same pass that draws the glyphs, so they cost almost nothing. Bloom adds two small blur passes at one pixel per cell.
The effects are visual only. capture() on the shader's handle, and every text, ANSI and SVG export built from it, returns the glyph grid without them: no glitch row shifts, curvature, scanlines, bloom, flicker, vignette or colour split. The few glyphs a glitch frame flips are part of the grid, so a capture on that frame keeps them. PNG and video exports read the canvas, so they do include the effects.
Pointer interaction
interaction makes the pointer push the field around. Move over the preview, and click it for ripple:
<AsciiShader field={FIELD} interaction="repel" fps={60} />repelpushes glyphs away from the pointer and clears a small hole under it.ripplesends a ring out from each click or tap, displacing and brightening the glyphs it passes.trailleaves a fading wake of ink behind the pointer.
The canvas still ignores clicks (pointer-events: none), so buttons and links on top of it keep working; the engine watches the pointer on the window instead. Pointer motion is smoothed, so glyphs glide after it. Interaction is motion: under reduced motion the shader holds its still frame and ignores the pointer. Raise fps to 60 for interactive backgrounds, so the response feels immediate.
Reading the pointer in a field
Fields can read the pointer directly for parallax and steering. u_pointer.xy is the pointer in uv and u_pointer.z fades from 0 to 1 while it's over the box, so multiplying by it eases the effect in and out. This one slides three layers of rings by different amounts:
float parallaxRings(vec2 p, float scale) {
return smoothstep(0.12, 0.0, abs(fract(length(p) * scale) - 0.5));
}
float field(vec2 uv, vec2 res, float t) {
vec2 p = (uv - 0.5) * vec2(res.x / res.y, 1.0);
vec2 tilt = (u_pointer.xy - 0.5) * u_pointer.z;
float far = parallaxRings(p - tilt * 0.03, 6.0) * 0.3;
float mid = parallaxRings(p - tilt * 0.08 + vec2(0.2, 0.0), 4.0) * 0.6;
float near = parallaxRings(p - tilt * 0.16 - vec2(0.25, 0.1), 2.5);
return max(far, max(mid, near));
}The engine only tracks the pointer when interaction is set or the field mentions u_pointer, u_pointerVelocity or u_click.
Audio input
useAudioBands from @/lib/ascii/audio listens to a source and keeps smoothed bass, mid, treble and level values from 0 to 1. Pass the result to audio, and fields read it as u_audio:
"use client";
import { useAudioBands } from "@/lib/ascii/audio";
import { AsciiShader } from "@/lib/ascii/shader";
export function Visualizer({ track }: { track: HTMLAudioElement | null }) {
const bands = useAudioBands(track);
return <AsciiShader field={FIELD} audio={bands} fps={60} />;
}The source can be:
- an
<audio>or<video>element, which keeps playing through the speakers; - a
MediaStream, such as a WebRTC call; "mic", the microphone, after the visitor allows it;"synth", a silent built-in tone for demos that needs no permission;null, which stops listening.
The hook returns the same object on every render. Its band values change every frame without re-rendering your component, and bands.status (idle, running, denied or unavailable) re-renders it when it changes. Browsers only offer the microphone on secure pages (https or localhost); elsewhere the status is unavailable rather than an error. Audio starts on the first click or tap on the page, since browsers block sound before one.
Props
| Prop | Type | Default |
|---|---|---|
| fx CRT effects, each 0 to 1: scanlines, bloom, curvature, glitch, flicker, vignette. | Type: PostFx | Default all 0 |
| interaction What the pointer does to the field. | Type: "none" | "repel" | "ripple" | "trail" | Default "none" |
| audio Bands from useAudioBands, read by fields as u_audio. | Type: AudioInput | null | Default null |
| seed A number fields read as u_seed. | Type: number | Default 0 |
Notes
- Canvas2D components (
AsciiGrid) can't run these effects in a shader. They take the samefxprop and approximate it with a CSS overlay,<CrtOverlay fx />. capture()on the shader'shandlereturns the glyphs the effects are drawn over, so exported text is never curved or blurred. A capture taken on a glitch frame includes the flipped glyphs.