# Write your own

Build an ASCII background from scratch on AsciiShader: a tone, a shape, then motion, props and colour.

> 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).

This guide builds an ASCII ripple background in four steps. You write one GLSL function, the field, and `AsciiShader` does the rest: picking glyphs, colouring them, resizing, motion and reduced motion. The same steps make any background you can describe with a little math. Your agent follows them too, through the [asciicn skill](/docs/installation).

You need the engine installed (see [Installation](/docs/installation)) and a section with a size to put the background in.

## Have your agent write it [#have-your-agent-write-it]

Your agent can follow this page. Fill in the two blanks and paste this in:

```text
Build an ASCII background for <where it goes>
that looks like <what you want>, drawn in <charset>.

Follow https://asciicn.dev/docs/engine/write-your-own.md
and the asciicn skill's references/fields.md.
Write a GLSL field on AsciiShader from lib/ascii/shader
and colour it from the theme. Check it in a browser in
light and dark mode and with reduced motion.
```

If the engine isn't installed yet, start with the [quick start prompt](/docs/installation#quick-start-with-your-agent) instead. The rest of this page is the same build, step by step.

## 1. Return a tone [#1-return-a-tone]

The field runs once for every sample and returns a tone from 0 to 1: 0 is an empty cell, 1 the densest glyph in the charset. A field that returns the height draws a ramp of glyphs from bottom to top:

```tsx
"use client";

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

const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  return uv.y;
}
`;

export function Ripples() {
  return (
    <AsciiShader
      field={FIELD}
      charset="standard"
      palette={["--ascii-paper", "blue"]}
      className="absolute inset-0"
    />
  );
}
```

```tsx title="examples/engine-field-tone.tsx"
"use client";

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

// Step 1: the tone is just the height. 0 at the bottom (empty), 1 at the top (densest glyph).
const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  return uv.y;
}
`;

export default function EngineFieldTone() {
  return (
    <AsciiShader
      field={FIELD}
      charset="standard"
      animate={false}
      palette={["--ascii-paper", "blue"]}
      className="absolute inset-0"
    />
  );
}
```

`uv` is the sample's position from 0 to 1 on both axes, with y pointing up. `t` is time in seconds. `res.y` is the number of sample rows and `res.x / res.y` is the box's true aspect ratio, so `1.0 / res.y` is the height of one sample row.

A sample is usually one cell, but not always. Braille packs 2×4 samples into each glyph and quadrants 2×2, so the same field drawn in Braille has four times the vertical detail. Write fields in `uv` and never assume one sample per glyph, and they look right in every charset.

## 2. Draw a shape [#2-draw-a-shape]

`uv` stretches with the box, so a circle measured in `uv` comes out as an oval. Multiply x by the aspect ratio first, and distances are true. Then compute anything that has a distance in it, here rings around a point that fade as they spread:

```glsl
float field(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  vec2 p = vec2(uv.x * aspect, uv.y);
  vec2 drop = vec2(0.5 * aspect, 0.45);
  float d = length(p - drop);
  float rings = 0.5 + 0.5 * cos(d * 48.0);
  float fade = exp(-d * 3.0);
  return rings * fade;
}
```

```tsx title="examples/engine-field-rings.tsx"
"use client";

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

// Step 2: square units, then rings around a point that fade with distance.
const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  vec2 p = vec2(uv.x * aspect, uv.y);
  vec2 drop = vec2(0.5 * aspect, 0.45);
  float d = length(p - drop);
  float rings = 0.5 + 0.5 * cos(d * 48.0);
  float fade = exp(-d * 3.0);
  return rings * fade;
}
`;

export default function EngineFieldRings() {
  return (
    <AsciiShader
      field={FIELD}
      charset="dense"
      animate={false}
      palette={["--ascii-paper", "blue"]}
      className="absolute inset-0"
    />
  );
}
```

`cos` turns distance into repeating bands, and `exp` fades them out. Every tone between 0 and 1 picks a different glyph, so the rings shade from `.` to `@` instead of having hard edges. The engine measures how much ink each glyph in the font really uses and spaces the ramp to match, so a tone of 0.5 looks half as dense as 1.

## 3. Add motion and props [#3-add-motion-and-props]

Three changes finish the shape. Noise bends the rings so they look like water, `t` moves them outwards, and uniforms turn the fixed numbers into props:

```tsx
"use client";

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

const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  vec2 p = vec2(uv.x * aspect, uv.y);
  vec2 drop = vec2(rippleDrop.x * aspect, 1.0 - rippleDrop.y);
  float wobble = fbm(p * 2.2 + vec2(t * 0.03, 0.0)) - 0.5;
  float d = length(p - drop) + wobble * 0.08;
  float rings = 0.5 + 0.5 * cos(d * rippleRings - t * 0.9);
  float fade = exp(-d * 2.6);
  return rippleStrength * rings * fade;
}
`;

export type RipplesProps = Omit<AsciiShaderProps, "field" | "uniforms"> & {
  /** Where the drop falls, from the top left, 0 to 1. */
  drop?: readonly [number, number];
  /** How tightly the rings are packed. */
  rings?: number;
  /** Peak tone, 0 to 1. */
  strength?: number;
};

export const Ripples = ({
  drop = [0.5, 0.42],
  rings = 46,
  strength = 0.85,
  ...props
}: RipplesProps) => (
  <AsciiShader
    {...props}
    field={FIELD}
    stillTime={4}
    uniforms={{
      rippleDrop: drop,
      rippleRings: rings,
      rippleStrength: strength,
    }}
  />
);
```

```tsx title="examples/engine-field-ripples.tsx"
"use client";

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

// Step 3: noise bends the rings, time moves them outwards, and uniforms
// turn the numbers into props.
const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  vec2 p = vec2(uv.x * aspect, uv.y);
  vec2 drop = vec2(rippleDrop.x * aspect, 1.0 - rippleDrop.y);
  float wobble = fbm(p * 2.2 + vec2(t * 0.03, 0.0)) - 0.5;
  float d = length(p - drop) + wobble * 0.08;
  float rings = 0.5 + 0.5 * cos(d * rippleRings - t * 0.9);
  float fade = exp(-d * 2.6);
  return rippleStrength * rings * fade;
}
`;

type RipplesProps = Omit<AsciiShaderProps, "field" | "uniforms"> & {
  /** Where the drop falls, from the top left, 0 to 1. */
  drop?: readonly [number, number];
  /** How tightly the rings are packed. */
  rings?: number;
  /** Peak tone, 0 to 1. */
  strength?: number;
};

const Ripples = ({
  drop = [0.5, 0.42],
  rings = 46,
  strength = 0.85,
  ...props
}: RipplesProps) => (
  <AsciiShader
    {...props}
    field={FIELD}
    stillTime={4}
    uniforms={{
      rippleDrop: drop,
      rippleRings: rings,
      rippleStrength: strength,
    }}
  />
);

export default function EngineFieldRipples() {
  return (
    <div className="relative isolate flex size-full items-end overflow-hidden">
      <Ripples
        charset="braille"
        palette={["--ascii-paper", "blue"]}
        className="absolute inset-0 -z-10"
      />
      <p className="text-foreground p-6 text-2xl font-medium tracking-tight text-balance">
        Ripples, in eight lines of GLSL.
      </p>
    </div>
  );
}
```

* `fbm` is one of the helpers every field gets, with `snoise` and `hash21`. It returns soft, cloudy noise from 0 to 1.
* Each key in `uniforms` is declared for you: a number becomes a `float`, a pair a `vec2`. Changing a prop updates the uniform without recompiling. Prefix the names (`ripple…`) so they never clash with another field.
* `stillTime={4}` is the moment shown when the visitor prefers reduced motion. Pick one that looks finished.
* The rest of `AsciiShaderProps` passes through, so `charset`, `colorMode`, `palette`, `cell`, `fx` and `speed` work on `Ripples` as they do on every background. The preview above passes `charset="braille"`.

## 4. Colour it [#4-colour-it]

By default glyphs take the palette's ink. To colour each glyph on its own, add a `colorField`: the same arguments as `field`, returning an sRGB colour. Setting it switches the colour mode to `truecolor`; `colorMode="ansi16"` snaps the same colours to the 16 ANSI colours instead.

```glsl
vec3 colorField(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  float d = length(vec2(uv.x * aspect, uv.y) - vec2(0.5 * aspect, 0.55));
  return toSrgb(oklch(0.78, 0.14, d * 9.0 - t * 0.4));
}
```

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

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

// The ripples from step 3, coloured by a second function: hue turns with
// the distance from the drop, in OKLCH so every hue reads equally bright.
const FIELD = `
float field(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  vec2 p = vec2(uv.x * aspect, uv.y);
  float wobble = fbm(p * 2.2 + vec2(t * 0.03, 0.0)) - 0.5;
  float d = length(p - vec2(0.5 * aspect, 0.55)) + wobble * 0.08;
  return 0.85 * (0.5 + 0.5 * cos(d * 46.0 - t * 0.9)) * exp(-d * 2.6);
}
`;

const COLOR = `
vec3 colorField(vec2 uv, vec2 res, float t) {
  float aspect = res.x / res.y;
  float d = length(vec2(uv.x * aspect, uv.y) - vec2(0.5 * aspect, 0.55));
  return toSrgb(oklch(0.78, 0.14, d * 9.0 - t * 0.4));
}
`;

export default function EngineGpuColor() {
  return (
    <AsciiShader
      field={FIELD}
      colorField={COLOR}
      charset="dense"
      stillTime={4}
      className="absolute inset-0"
    />
  );
}
```

`oklch(L, C, h)` returns linear RGB for a lightness, chroma and hue in radians. Hues at the same lightness look equally bright, which a plain HSV rainbow doesn't. Pass the result through `toSrgb` before returning it. `field` and `colorField` compile into one shader, so a helper defined in `field` can be called from both.

## GLSL reference [#glsl-reference]

Fields are GLSL ES 1.00, the dialect of WebGL1, so most shader snippets you find work as written.

| Name                | What it is                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------ |
| `u_time`            | Seconds of motion, scaled by `speed`. The same value as `t`.                               |
| `u_resolution`      | The same value as `res`.                                                                   |
| `u_pointer`         | `vec3`: the pointer in `uv` (y up), and `z` from 0 to 1 while it's over the box. Smoothed. |
| `u_pointerVelocity` | `vec2`: how fast the pointer moves, in `uv` per second.                                    |
| `u_click`           | `vec3`: where the last press landed in `uv`, and `z` the seconds since.                    |
| `u_audio`           | `vec4`: bass, mid, treble and level from the `audio` prop, each 0 to 1.                    |
| `u_seed`            | The `seed` prop, for seeded variations.                                                    |
| `hash21(p)`         | A stable random value in \[0, 1) for a `vec2`.                                             |
| `snoise(p)`         | 2D simplex noise in \[-1, 1].                                                              |
| `fbm(p)`            | Four octaves of noise in \[0, 1].                                                          |
| `spectrum(h)`       | A saturated rainbow; 0 is red and it wraps at 1.                                           |
| `paletteColor(x)`   | The palette's inks as a ramp from 0 to 1.                                                  |
| `oklch(L, C, h)`    | An OKLCH colour as linear RGB, `h` in radians.                                             |
| `toSrgb(c)`         | Linear RGB to sRGB.                                                                        |
| `aces(c)`           | The ACES filmic tone map, for colours brighter than 1.                                     |

Names starting with `u_` and `ascii_` are reserved. When a field doesn't compile, the console names the line of your `field` or `colorField` that failed, and the shader shows its `fallback`.

## Put it on the page [#put-it-on-the-page]

```tsx
<section className="relative isolate overflow-hidden rounded-3xl">
  <Ripples
    className="absolute inset-0 -z-10"
    charset="dense"
    palette={["--ascii-paper", "blue"]}
  />
  <div className="px-8 py-32">…</div>
</section>
```

The shader fills its positioned parent and ignores pointer clicks, so your content sits in a sibling on top. Keep the tone low where text sits; here `strength` caps it at 0.85, and the rings fade before they reach the edges.

## Where to go next [#where-to-go-next]

* [Charsets](/docs/engine/charsets) and [Colour modes](/docs/engine/color-modes) cover every glyph set and colour mapping a field can use.
* [Post effects](/docs/engine/post-effects) adds the CRT finish, pointer interaction and audio input.
* [Motion](/docs/engine/motion) and [Performance](/docs/engine/performance) cover pausing, reduced motion and what a shader costs.
* The [city skylines](/docs/backgrounds/tokyo) and [shader fields](/docs/fields/nebula) are built this way. Open one's source for a field to start from.