---
title: Writing your own dither
description: Implement DitherField for any shape you can describe with maths, then make it fast with a shader.
---

Most new looks are a new field. A field is one function, so a new look costs a few lines.

```kotlin
val spiral = DitherField { x, y, t, aspect ->
    val dx = (x - 0.5f) * aspect
    val dy = y - 0.5f
    0.5f + 0.5f * sin(hypot(dx, dy) * 14f - atan2(dy, dx) * 2f - t * 2f)
}

Box(Modifier.size(240.dp).dither(spiral, DitherPatterns.Bayer8, animated = true, stillTime = 1f))
```

<iframe class="live-example" src="https://stipple-demo.vercel.app/?embed=1&animated=1#custom-fields" title="Live example: custom-fields" loading="lazy" allow="autoplay"></iframe>

## Rules for a good field

- **Return smooth tones.** Let the pattern make the dots. A field that returns only 0 and 1 will look like a plain shape.
- **Scale `x` by `aspect`** before you measure a distance, or circles turn into ellipses on wide elements.
- **Stay pure.** No state, no allocation per call, no platform code. A field is called once per cell per frame.
- **Reach 0 and 1.** If the range is narrower the dither looks washed out. Values outside 0..1 are clamped.
- **Pick a still frame.** For an animated field choose `stillTime` so the frozen picture looks intentional.

## Give it a shader

For a large surface the cell renderer is slow. Implement `ShaderField` and the shader renderer can draw your field in one pass:

```kotlin top
object Plasma : ShaderField {
    override val shaderSource = """
        float tone(float2 uv, float t, float aspect) {
            float2 p = float2(uv.x * aspect, uv.y) * 6.0;
            return (sin(p.x + t) + sin(p.y * 1.3 - t * 0.7) + 2.0) / 4.0;
        }
    """.trimIndent()

    override fun tone(x: Float, y: Float, t: Float, aspect: Float): Float {
        val u = x * aspect * 6f
        val v = y * 6f
        return (sin(u + t) + sin(v * 1.3f - t * 0.7f) + 2f) / 4f
    }
}
```

The shader source is a function named `tone` in the language AGSL and SkSL share. Keep it equal to the Kotlin version, because the renderer is chosen per device: a fallback to cells must not change the picture. See [choosing a renderer](/guides/renderers).
