# Post effects

The CRT finish, pointer interaction and audio input every AsciiShader can take.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

`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 [#crt-effects]

Pass `fx` with any of six effects, each from 0 (off) to 1 (strong):

```tsx
<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 [#pointer-interaction]

`interaction` makes the pointer push the field around. Move over the preview, and click it for `ripple`:

```tsx
<AsciiShader field={FIELD} interaction="repel" fps={60} />
```



* `repel` pushes glyphs away from the pointer and clears a small hole under it.
* `ripple` sends a ring out from each click or tap, displacing and brightening the glyphs it passes.
* `trail` leaves 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 [#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:

```glsl
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));
}
```

```tsx title="examples/engine-gpu-parallax.tsx"
"use client";

import { AsciiShader } from "@/lib/ascii/shader";

// Fields can read the pointer themselves: three layers of rings slide by
// different amounts, so the scene tilts as the pointer moves.
const FIELD = `
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));
}
`;

export default function EngineGpuParallax() {
  return (
    <AsciiShader
      field={FIELD}
      charset="braille"
      palette={["--ascii-paper", "--ascii-ink"]}
      fps={60}
      className="absolute inset-0"
    />
  );
}
```

The engine only tracks the pointer when `interaction` is set or the field mentions `u_pointer`, `u_pointerVelocity` or `u_click`.

## Audio input [#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`:

```tsx
"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} />;
}
```

```tsx title="examples/engine-gpu-audio.tsx"
"use client";

import { useState } from "react";

import { useAudioBands } from "@/lib/ascii/audio";
import type { AudioSource } from "@/lib/ascii/audio";
import { AsciiShader } from "@/lib/ascii/shader";

// Bass swells the ring, mids bend it, treble sparks the glyphs around it.
const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  vec2 p = (uv - 0.5) * vec2(res.x / res.y, 1.0);
  float a = atan(p.y, p.x);
  float bend = sin(a * 5.0 + t) * 0.04 * (0.3 + u_audio.y * 2.0);
  float radius = 0.18 + u_audio.x * 0.16 + bend;
  float ring = smoothstep(0.07, 0.0, abs(length(p) - radius));
  // Cells are about half as wide as a res unit, so x counts double.
  vec2 cell = floor(uv * res * vec2(2.0, 1.0));
  float sparks = step(0.985 - u_audio.z * 0.08, hash21(cell + floor(t * 12.0)));
  return max(ring, sparks * 0.7);
}
`;

const SOURCES = ["off", "synth", "mic"] as const;
type Choice = (typeof SOURCES)[number];

const toSource = (choice: Choice): AudioSource =>
  choice === "off" ? null : choice;

export default function EngineGpuAudio() {
  const [choice, setChoice] = useState<Choice>("synth");
  const bands = useAudioBands(toSource(choice));
  return (
    <div className="relative size-full">
      <AsciiShader
        field={FIELD}
        audio={bands}
        charset="dense"
        palette={["--ascii-paper", "--ascii-ink"]}
        fps={60}
        stillTime={2}
        className="absolute inset-0"
      />
      <div className="absolute bottom-3 left-3 flex items-center gap-1 rounded-md border bg-background/80 p-1 font-mono text-xs backdrop-blur">
        {SOURCES.map((option) => (
          <button
            key={option}
            type="button"
            aria-pressed={choice === option}
            onClick={() => setChoice(option)}
            className="rounded px-2 py-1 text-muted-foreground aria-pressed:bg-foreground aria-pressed:text-background focus-visible:outline-2 focus-visible:outline-ring"
          >
            {option}
          </button>
        ))}
        <span className="px-2 text-muted-foreground">{bands.status}</span>
      </div>
    </div>
  );
}
```

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 [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `fx` | `PostFx` | `all 0` | CRT effects, each 0 to 1: scanlines, bloom, curvature, glitch, flicker, vignette. |
| `interaction` | `"none" \| "repel" \| "ripple" \| "trail"` | `"none"` | What the pointer does to the field. |
| `audio` | `AudioInput \| null` | `null` | Bands from useAudioBands, read by fields as u_audio. |
| `seed` | `number` | `0` | A number fields read as u_seed. |

## Notes [#notes]

* Canvas2D components (`AsciiGrid`) can't run these effects in a shader. They take the same `fx` prop and approximate it with a CSS overlay, `<CrtOverlay fx />`.
* `capture()` on the shader's `handle` returns 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.