> For the complete documentation index, see [llms.txt](https://infinitode-docs.gitbook.io/documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://infinitode-docs.gitbook.io/documentation/package-documentation/qrforge-package-documentation/qrforge-reference/qrforge-shapes-and-shaders.md).

# qrforge Shapes & Shaders

qrforge reference for module shapes, the SDF shape registry, the 20 built-in shaders, custom shader functions, expression shaders and the shader Context.

## Shapes

Every shape is defined once as a *signed distance field* (SDF) in module units — a module cell spans `-0.5 .. 0.5`. The raster renderer evaluates the SDF per pixel (which gives free anti-aliasing), while the SVG renderer emits the shape as a reusable path plus one `<use>` per module.

### Built-in module shapes

| Shape             | Description                                                                  |
| ----------------- | ---------------------------------------------------------------------------- |
| `square` / `rect` | The classic full-cell module.                                                |
| `rounded`         | Rounded square; `module_radius` sets the corner radius (defaults to `0.22`). |
| `circle` / `dot`  | Circle filling the cell / a slightly smaller circle (`0.44` radius).         |
| `diamond`         | Rotated square.                                                              |
| `hexagon`         | Flat-topped hexagon.                                                         |
| `triangle`        | Equilateral triangle.                                                        |
| `star`, `star6`   | Five- and six-pointed stars.                                                 |
| `cross`, `plus`   | Rounded cross / plus signs.                                                  |
| `bar_v`, `bar_h`  | Vertical and horizontal bars (useful for function modules).                  |
| `ring`            | Hollow circle.                                                               |
| `squircle`        | Superellipse (softer than a rounded square).                                 |
| `leaf`            | Vesica piscis (two overlapping circles).                                     |
| `heart`           | Heart shape.                                                                 |

```python
import qrforge

qrforge.shapes.names()
# ['bar_h', 'bar_v', 'circle', 'cross', 'diamond', 'dot', 'heart', 'hexagon',
#  'leaf', 'plus', 'rect', 'ring', 'rounded', 'square', 'squircle', 'star',
#  'star6', 'triangle']

qrforge.shapes.get("hexagon")        # -> Shape('hexagon')
qrforge.shapes.SHAPES["leaf"]        # registry of every built-in shape
```

Aliases are accepted anywhere a shape name is: `dots → dot`, `round → circle`, `roundrect`/`rrect`/`rect_round` → `rounded`, `blob` → `squircle`, `hex` → `hexagon`, `tri` → `triangle`, `ellipse` → `circle`, `none` → `square`.

### Random per-module mixes

Pass a list of shapes with a `seed` for a reproducible mixture; `module_jitter` adds random scale and rotation variation.

```python
qr = qrforge.QR("mix", ec="h").use_style(
    module_shape=["dot", "star", "hexagon", "leaf"],
    module_scale=0.9, module_jitter=0.2, seed=3,
)
qr.save("mix.png", scale=12)
```

### Custom shapes

Register an SDF in module units (return a negative value inside the shape, positive outside). Shapes receive an optional `radius` argument, so honour it if you want `module_radius` to work with your shape.

```python
import math
import qrforge

@qrforge.shape("wave")               # decorator form
def wave(x, y, radius=0.0):
    return qrforge.shapes.sd_circle(x, y + 0.08 * math.sin(x * 9), 0.5)

# or register an existing function / Shape explicitly
def triangle_sdf(x, y, radius=0.0):
    return qrforge.shapes.sd_triangle(x, y, 0.5)

qrforge.shapes.register("tri", triangle_sdf)
qrforge.register_shape("tri2", triangle_sdf)   # top-level alias of the same call

qr = qrforge.QR("custom").use_style(module_shape="wave")
qr.save("wave.svg")
```

A bare callable is also accepted directly (`module_shape=my_sdf`), and a `Shape` instance can be passed instead of a name. Register with `svg=` to control the vector outline, and `gradient_scale=` to tweak how gradients are sampled over the shape.

### SDF helpers

`qrforge.shapes` exposes the primitives used by the built-ins, so custom shapes can reuse them:

| Helper                                           | Description                         |
| ------------------------------------------------ | ----------------------------------- |
| `sd_box(x, y, hx, hy)`                           | Axis-aligned box with half-extents. |
| `sd_round_box(x, y, hx, hy, r)`                  | Rounded box.                        |
| `sd_circle(x, y, r)`                             | Circle.                             |
| `sd_diamond(x, y, r)`                            | Diamond.                            |
| `sd_hexagon(x, y, r)`                            | Hexagon.                            |
| `sd_triangle(x, y, r)`                           | Triangle.                           |
| `sd_star(x, y, outer=0.5, inner=0.27, points=5)` | Star.                               |
| `sd_heart(x, y, r=0.5)`                          | Heart.                              |
| `sd_leaf(x, y, r=0.5)`                           | Vesica piscis.                      |
| `sd_cross(x, y, arm=0.17, r=0.5)`                | Cross.                              |
| `sd_ring(x, y, radius=0.36, thickness=0.14)`     | Ring.                               |
| `sd_squircle(x, y, r=0.5, n=4.0)`                | Superellipse.                       |

### Shape registry

| Member                                                     | Description                                                       |
| ---------------------------------------------------------- | ----------------------------------------------------------------- |
| `Shape(name, sdf, svg=None, gradient_scale=1.0)`           | Build a shape from an SDF callable.                               |
| `shape.distance(x, y, radius=0.0)`                         | Evaluate the SDF at a point.                                      |
| `shape.outline(n=96, radius=0.0, max_radius=0.72)`         | Sample the boundary by bisecting the SDF along 360°.              |
| `shape.svg_body(radius=0.0)`                               | SVG markup for the shape, centred on the origin, one module wide. |
| `shapes.get(spec)`                                         | Look up a shape by name/alias, `Shape`, or callable.              |
| `shapes.names()`                                           | Sorted list of built-in shape names.                              |
| `shapes.register(name, sdf, svg=None, gradient_scale=1.0)` | Add a shape to the registry.                                      |
| `qrforge.shape(name)`                                      | Decorator that registers a function as a shape.                   |

***

## Shaders

Shaders change the rendered symbol. They come in four kinds:

| Kind      | What it does                                                       | Built-ins                                                                                                 |
| --------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `pixel`   | Recolours every pixel using the incoming colour and context.       | `grain`, `noise`, `duotone`, `posterize`, `sepia`, `greyscale`, `invert`, `vignette`, `outline`, `stroke` |
| `warp`    | Displaces the sampling coordinates.                                | `wave`, `twist`, `swirl`, `bulge`, `glitch`                                                               |
| `post`    | Needs the whole image (blur, offsets, screen passes).              | `glow`, `bloom`, `chromatic`, `halftone`                                                                  |
| `overlay` | Drawn on top in SVG; applied as pixels in raster when implemented. | `scanlines`                                                                                               |

### Built-in shaders

| Shader               | Kind    | Parameters (defaults)                                           | SVG           |
| -------------------- | ------- | --------------------------------------------------------------- | ------------- |
| `grain` / `noise`    | pixel   | `amount=0.12`, `mono=True`, `scale=1.0`                         | yes           |
| `vignette`           | pixel   | `amount=0.55`, `color="#000000"`, `softness=0.45`               | yes (overlay) |
| `duotone`            | pixel   | `shadow="#12002e"`, `highlight="#ffe6a7"`, `amount=1.0`         | yes           |
| `posterize`          | pixel   | `levels=4`, `amount=1.0`                                        | yes           |
| `sepia`              | pixel   | `amount=1.0`                                                    | yes           |
| `greyscale`          | pixel   | `amount=1.0`                                                    | yes           |
| `invert`             | pixel   | —                                                               | yes           |
| `wave`               | warp    | `amplitude=6.0`, `wavelength=48.0`, `angle=0.0`                 | yes           |
| `twist` / `swirl`    | warp    | `strength=1.2`, `radius=1.0`                                    | skipped       |
| `bulge`              | warp    | `amount=0.35`                                                   | skipped       |
| `glitch`             | warp    | `offset=0.3`, `band=1`, `seed=1`, `probability=0.45`            | skipped       |
| `chromatic`          | post    | `offset=2.5`, `angle=0.0`                                       | yes           |
| `glow` / `bloom`     | post    | `radius=3.0`, `amount=0.9`, `color=None`                        | yes           |
| `outline` / `stroke` | pixel   | `color="#ffffff"`, `width=1.5`                                  | yes           |
| `scanlines`          | overlay | `gap=4`, `amount=0.28`, `color="#000000"`                       | yes           |
| `halftone`           | post    | `cell=0` (snap to module grid), `color=None`, `background=None` | skipped       |

```python
import qrforge

qr = qrforge.QR("shaders", ec="h").use_style(
    module_shape="rounded", module_radius=0.28,
    fg=["#00f0ff", "#7b2ff7", "#ff2ec4"], bg="#0a0118",
    shaders=["glow:radius=2,amount=0.75", "grain:amount=0.06"],
)
qr.save("shaded.png", scale=12)
```

{% hint style="warning" %}
Renderer coverage in 1.0.1: `twist`, `swirl`, `bulge`, `glitch` and `halftone` only apply to raster output (PNG, Pillow, HTML preview) and are skipped in SVG; `scanlines`, `outline` and `stroke` apply to SVG but not to raster output. PDF and terminal output do not apply shaders at all.
{% endhint %}

### Shader specs

`Style.shaders` accepts any mixture of names, `"name:key=value"` strings, `Shader` instances and callables:

```python
import qrforge

@qrforge.shader(name="tint")
def tint(x, y, color, ctx):
    return color.mix("#ff4400", ctx.uv(x, y)[1])

qr = qrforge.QR("specs", ec="h").use_style(shaders=[
    "grain",                              # a name
    "glow:radius=2,amount=0.75",          # a "name:key=value,key=value" string
    qrforge.shaders.Glow(radius=2),       # a Shader instance
    tint,                                 # a callable (or "tint" once registered)
])
qr.save("specs.png", scale=12)
```

Names and parameters are validated by `qrforge.shaders.build()`, which raises `StyleError` for anything unknown.

### Custom shaders

The `@qrforge.shader` decorator turns a function into a reusable shader. The signature depends on the kind:

```python
import qrforge

@qrforge.shader                                  # kind="pixel" (default)
def heat(x, y, color, ctx):
    return color.mix("#ff4400", ctx.uv(x, y)[1])

@qrforge.shader(kind="warp", name="ripple")      # warp: return (source_x, source_y)
def ripple(x, y, ctx):
    offset = ctx.noise(x / 12, y / 12) * 3
    return x + offset, y

@qrforge.shader(kind="post", name="fade")        # post: return a Pixmap
def fade(image, ctx):
    return image
```

Then use the callable (or its registered name) anywhere shaders are accepted:

```python
qr = qrforge.QR("custom", ec="h").use_style(shaders=[heat, "vignette:amount=0.4"])
qr.save("custom.png", scale=12)
```

You can also subclass `Shader` for reusable shaders with parameters:

```python
from qrforge import shaders
from qrforge.color import Color

class ScanlineTint(shaders.Shader):
    kind = "pixel"

    def __init__(self, color="#000000", amount=0.2, gap=4):
        super().__init__(name="scanline_tint", color=color, amount=amount, gap=gap)
        self.color, self.amount, self.gap = Color.parse(color), amount, max(2, int(gap))

    def pixel(self, x, y, color, ctx):
        if y % self.gap < self.gap // 2:
            return color.mix(self.color, self.amount)
        return color
```

Shader classes may implement `pixel(x, y, color, ctx)`, `warp(x, y, ctx)`, `post(image, ctx)`, and optionally `svg_filter(uid, ctx)` / `svg_overlay(uid, ctx)` to participate in SVG output.

### Expression shaders

`qrforge.expression()` compiles a one-line formula into a pixel shader — handy for experimentation. The formula is evaluated per pixel and **must return a `Color`** (use the `Color(...)` helper):

```python
import qrforge

shader = qrforge.expression("mix(c, Color((255 * u, 128 * v, 200 * r)), 0.6)")
qr = qrforge.QR("expression", ec="h").use_style(shaders=[shader])
qr.save("expression.png", scale=12)
```

Available in a formula: `x`, `y` (pixels), `u`, `v` (0–1 over the canvas), `r` (radius from the centre), `theta` (angle), `c` (the incoming `Color`), `ctx`, and the helpers `sin`, `cos`, `tan`, `sqrt`, `abs`, `min`, `max`, `pow`, `exp`, `log`, `floor`, `ceil`, `atan2`, `hypot`, `clamp`, `mix`, `smoothstep`, `noise`, `rand`, `pi` and `Color`. The string is compiled once with a restricted namespace (no builtins), but you should still only run formulas you trust.

### Shader Context

Custom shaders receive a `Context` describing the image being shaded:

| Member                       | Description                                              |
| ---------------------------- | -------------------------------------------------------- |
| `ctx.width`, `ctx.height`    | Canvas size in pixels.                                   |
| `ctx.symbol`                 | `(x, y, w, h)` of the symbol area inside the canvas.     |
| `ctx.module_size`            | Pixels per module.                                       |
| `ctx.seed`                   | Seed for randomised effects.                             |
| `ctx.texture`, `ctx.matrix`  | The texture pixmap and the `QRMatrix`, when available.   |
| `ctx.uv(x, y)`               | Normalised `(u, v)` coordinates over the whole canvas.   |
| `ctx.radius(x, y)`           | Distance from the centre, normalised to 0–1 at the edge. |
| `ctx.noise(x, y, octaves=1)` | Value noise.                                             |
| `ctx.rand(x, y)`             | Deterministic hash in `[0, 1)`.                          |
| `ctx.sample_texture(u, v)`   | RGBA sample from the texture.                            |

### Shader registry

| Member                                                | Description                                               |
| ----------------------------------------------------- | --------------------------------------------------------- |
| `shaders.names()`                                     | Sorted list of shader names (`grain`, `glow`, `wave`, …). |
| `shaders.build(spec)`                                 | Coerce a spec into a `Shader`.                            |
| `shaders.FACTORIES`                                   | The name → factory registry used by `build()`.            |
| `qrforge.shader(fn=None, *, kind="pixel", name=None)` | Decorator that registers a function as a shader.          |
| `qrforge.expression(formula, name="expression")`      | Build a shader from a formula string.                     |

{% hint style="info" %}
In 1.0.1 the `("name", {"param": value})` tuple form still fails with `TypeError: '…' object is not callable` — pass `"name:param=value"` strings, `Shader` instances or callables instead. Prefer `Style.stroke` for a module outline; the `outline`/`stroke` *shaders* are SVG-only in this release.
{% endhint %}
