# 2D graphics

The [gfx](../pkg/gfx.md) package is one drawing context for a window.
All of a 2D game's drawing goes through it. This guide covers the parts
in the order a game needs them, from the frame to the batch statistics
at the end.

## The frame

`Draw` is called once per frame with the screen already cleared to
`ctx.Clear`. Every `Draw*` call queues work and the engine submits it
when `Draw` returns, so order matters only within a layer.

Coordinates are float32 view units with the origin at the top-left and
+Y down. Angles are radians, clockwise on screen. Rectangles are
`lin.Rect` (top-left corner and size), positions `lin.Vec2`. Colours are
`gfx.Color`, linear and non-premultiplied, from `gfx.RGB`, `gfx.RGBA`,
`gfx.Hex` or `gfx.FromHSV`; a zero colour where a tint is expected means
white.

By default the view is the window's size in points, so `ctx.Width` and
`ctx.Height` change on a resize. For a game designed at one resolution,
set `Config.ViewWidth` and `ViewHeight` instead. The engine then scales
that fixed view into the window and centres it, and the two values stay
constant. `Config.Scaling` chooses the scaling. `ScaleFit` keeps the
aspect ratio and adds bars. `ScaleInteger` scales by whole numbers only,
which keeps pixel art crisp; leave those textures on the default nearest
sampling. `ScaleStretch` fills the window. The
[window guide](window.md) has the rest.

```go
engine.Run(engine.Config{
	Title: "Shooter", Width: 1280, Height: 720, Resizable: true,
	ViewWidth: 320, ViewHeight: 180, Scaling: engine.ScaleInteger,
}, &game{})
```

## Textures and atlases

`Graphics.NewTexture` uploads an `image.Image`. `TextureOptions` chooses
`Linear` filtering for smooth scaling, `NoMipmaps`, `Repeat` to tile,
and `Data` for pixels that are not sRGB colour, such as masks and normal
maps. The renderer owns its textures and releases them when the game
closes, including when setup or drawing fails. Call `Destroy` to release
one earlier, for example when unloading a level. To load a
texture by name, use the [asset](../pkg/asset.md) package. It resolves
a name against loose directories, pack files and embedded filesystems in
that order, so a loose file takes priority over a shipped one.

```go
//go:embed assets
var embedded embed.FS

g.fs, err = asset.OpenFS(asset.Dir("assets"), asset.FSSource(embedded))
g.tex, err = asset.Texture(ctx.Gfx, g.fs, "sprites/hero.png", gfx.TextureOptions{})
```

A `Region` is a rectangle of a texture, made by `gfx.NewRegion` or
returned by a sheet or an atlas. A `Sheet` cuts a texture into a grid of
equal frames numbered row-major from the top-left; a `Sheet` literal
takes `Margin` and `Spacing` when the grid has padding. `ParseAtlas`
reads packed atlases with named frames, in TexturePacker JSON (hash or
array) or Aseprite's JSON export; `AtlasData.Bind` ties the description
to the uploaded texture, `Atlas.Region` looks a frame up by name and
`Tag` returns an animation tag's frames in play order, with `Durations`
for their per-frame times. `Atlas.Animation` wraps a tag as a
`RegionAnimation` that plays with the timings the file gave each frame:
`At(t)` returns the region to draw at a time from the start. With the
asset package, `asset.Atlas` reads the JSON, loads the image it names
from beside it and binds them in one call.

```go
sheet := gfx.NewSheet(tex, 16, 16)
icon := gfx.NewRegion(tex, lin.R(96, 0, 16, 16))

atlas, err := asset.Atlas(ctx.Gfx, fs, "sprites/hero.json", gfx.TextureOptions{})
run := atlas.Animation("run")          // frames and durations as authored
frame, _ := run.At(ctx.Time)
gr.DrawRegion(frame, gfx.Sprite{Pos: g.hero})
idle, ok := atlas.Region("hero_idle_0")
```

## Aseprite files

`ParseAseprite` reads Aseprite's own `.aseprite` and `.ase` files, so a
game ships the file the artist saved rather than an export. It
composites each frame from the layers that are visible in the editor,
packs the frames into one image and describes them as an `AtlasData`,
with the file's tags as animations that play at the timings it recorded.
RGBA, greyscale and indexed files all read, layer opacity and group
visibility are honoured, and a layer in a blend mode other than normal
is drawn as normal. `asset.Aseprite` reads, packs, uploads and binds in
one call.

Composited frames are named by number, `"0"` upwards. Set
`AsepriteOptions.Layers` and each layer's own frames are packed beside
them as `"<layer>/<number>"`, a layer inside a group carrying the
group's name first, for a hat or a damage overlay drawn on its own.
Slices come through as named rectangles, and the result also carries the
layers, tags and palette.

```go
hero, err := asset.Aseprite(ctx.Gfx, fs, "sprites/hero.aseprite",
	gfx.AsepriteOptions{Layers: true}, gfx.TextureOptions{})
g.run = hero.Atlas.Animation("run")
hitbox, _ := hero.Slice("hitbox")
hat, _ := hero.Atlas.Region("gear/hat/2")

frame, _ := g.run.At(ctx.Time)
gr.DrawRegion(frame, gfx.Sprite{Pos: g.hero})
```

`DrawNineSlice` stretches a `NineSlice` over any rectangle while its
corners keep their size, so a 24 by 24 png draws a panel or a speech
bubble at any size. Set `Tile` to repeat the edges and centre instead of
stretching them. `NewBlankTexture` creates writable storage and
`Texture.Write` replaces a rectangle of its pixels. Writes during
`Draw` stream without waiting for the GPU; writes outside a frame wait.
A painting tool or a video can change one every frame;
`Texture.Read` copies pixels back. `Texture.Replace` swaps the whole
image, at a new size if need be, and keeps the `*Texture` the game
holds, so every material and sprite that names it draws the new picture;
it is what `asset.Reloader` calls when a texture's file changes on
disk.

`Read` returns an ordinary Go RGBA image, with colour premultiplied in
sRGB space, so it can be uploaded again or encoded as PNG. Data textures
retain their raw channel values. Readback waits for completed GPU work
and returns an error during an active frame, including `Update` and
`Draw`; queued uploads and render-target draws have not been submitted
yet. `Texture.WritePNG(writer)` leaves its writer open, and
`Texture.SavePNG(path)` creates or truncates and closes its own file.
Both use the same readback timing. Render textures are written through
`DrawTo`, rather than through their borrowed texture view's `Write`.

`destination.CopyFrom(source, sourceRect, destinationPoint)` copies
texture pixels on the GPU and rebuilds destination mipmaps. A zero
source rectangle selects the full source. Regions must fit exactly and
formats and `Graphics` owners must match. Copies preserve stored bytes;
they do not scale, blend or convert colour. Non-overlapping self-copies
work. Compressed textures, overlapping self-copies and render-texture
destinations return errors. During a frame, copies run before all
queued draws, like texture writes. A source already queued through
`DrawTo` is rejected because its new pixels are not available yet.

For editable CPU pixels, `gfx.NewImage(source)` makes an independent,
zero-based NRGBA copy. Its straight-alpha pixels work with the standard
`image` and `image/draw` packages through the embedded `*image.NRGBA`.
`FlipHorizontal` and `FlipVertical` mutate that copy; `Mask(colour)`
makes exact straight-RGB matches transparent, ignoring the supplied
alpha and preserving RGB. `CopyFrom(source, point)` copies full source
bounds with clipping and `draw.Src` semantics, including overlapping
subimages. Subimages share storage; use `NewImage` for another independent
copy. `WritePNG` borrows a writer and `SavePNG` owns its file. The zero
`gfx.Image` is empty and cannot be exported or used as a copy destination.

```go
pixels, err := gfx.NewImage(source)
if err != nil {
	return err
}
pixels.FlipHorizontal()
pixels.Mask(color.NRGBA{R: 255, B: 255, A: 255})
return pixels.SavePNG("edited.png")
```

```go
ns := gfx.NineSlice{Tex: g.panel, Left: 8, Top: 8, Right: 8, Bottom: 8}
gr.DrawNineSlice(ns, lin.R(12, 12, 300, 92), gfx.White)
```

### Compressed textures

A texture uploaded from a PNG costs four bytes a texel on the GPU, and
the mip chain requested by linear filtering costs about a third again.
Nearest-filtered textures do not generate mipmaps by default. `bunyip-tex`
compresses an image to one of the BC block formats ahead of time and
writes it with its whole mip chain as a KTX2 file, which
`gfx.NewCompressedTexture` uploads block for block:

```
bunyip-tex -format bc7 sprites/hero.png       # writes sprites/hero.ktx2
bunyip-tex -format bc5 -linear normals/*.png  # tangent-space normal maps
```

```go
g.hero, err = asset.Texture(ctx.Gfx, fs, "sprites/hero.ktx2", gfx.TextureOptions{Linear: true})
```

`asset.Texture` picks the path from the name, so a game changes the
extension and nothing else. Which format suits what:

| Format | Bits a texel | For |
|---|---|---|
| `bc1` | 4 | opaque colour where a quarter of the size matters more than the last of the quality |
| `bc3` | 8 | colour with alpha: sprites with soft edges |
| `bc4` | 4 | one channel: masks, height fields, roughness |
| `bc5` | 8 | two channels: tangent-space normal maps |
| `bc7` | 8 | colour, with or without alpha, that has to hold up close |

`-linear` says the file holds data rather than sRGB colour, which is
what a normal map, a mask or a roughness map is; `bc4` and `bc5` are
always linear. `-no-mips` writes level 0 alone, `-fast` skips BC7's
search through its two-subset partitions, and `-v` reports each file's
size and its peak signal-to-noise ratio so a choice of format can be
judged rather than guessed at. Nothing is compressed or downsampled
while the game runs: the blocks and the levels go straight to the GPU.
KTX2 reading and writing reject mip chains longer than the dimensions
permit: `floor(log2(max(width, height))) + 1` levels at most. Partial
chains remain valid, and a header requesting generated levels is read
as its supplied base level.

A device that cannot sample the format, which some MoltenVK
configurations cannot for the BC formats, decodes level 0 on the
processor into a plain texture instead, so the same file works
on those devices at the cost of the memory it was meant to save. This
fallback supports the CPU decoder's formats, with BC7 limited to modes
1 and 6, and generates any requested mipmaps from the decoded base level.
A KTX2 file
holding ASTC uploads on a device that samples it; nothing here encodes
or decodes ASTC, so there is no fallback for one.

## Sprites

A `Sprite` is one textured quad: a position, a size in view units, the
UV window into the texture, a tint, a rotation, an origin as a fraction
of the size, `FlipX` and `FlipY`, and a `Filter` that overrides the
texture's own sampling for that draw. `Graphics.Draw` queues one, and a
nil texture draws a 1x1 white pixel, so a plain coloured quad is a
tinted sprite. `DrawTexture` places a whole texture at its own size;
`DrawRegion` and `DrawFrame` fill the UVs in from a region or a sheet
frame, where a zero `Size` means its own size. `DrawIndexed` and
`DrawTriangles` take raw `Vertex2D` geometry. Sheet animation uses two
values: an `Animation` lists frames and a rate, and an `AnimState` plays
it, with `Advance`, `Frame` and `Done`.

```go
gr.Draw(g.tex, gfx.Sprite{
	Pos:    e.pos,
	Size:   lin.V2(48, 48),
	Origin: lin.V2(0.5, 0.5), // rotate about the middle
	Rotation: e.angle, FlipX: e.facingLeft, Color: gfx.RGB(255, 220, 180),
})

g.walk = gfx.Animation{Frames: []int{4, 5, 6, 7}, FPS: 8, Loop: true}
g.anim.Play(&g.walk)
g.anim.Advance(ctx.Delta)                                      // Update
gr.DrawFrame(sheet, g.anim.Frame(), gfx.Sprite{Pos: g.player}) // Draw
```

Consecutive sprites that share a texture, blend mode, shader, clip and
colour matrix merge into one draw call. Changing any of those breaks the
batch, so a hundred sprites from one atlas cost one draw and alternating
between two textures costs a hundred. Pack sprites into one atlas to
keep them grouped by texture, and use `SetLayer` to control what draws
in front without breaking that grouping.

### Bounds and placement

`Sprite.Pos` is the pivot position. With `Origin: lin.V2(0.5, 0.5)`,
that position is the sprite's centre. `Transform2.Apply` places a sprite
template at the transform's centre, with its rotation and scale.

`Sprite.Corners` returns the four placed corners; `Sprite.Bounds` encloses
them in an axis-aligned rectangle. Both include the sprite's size, origin
and rotation, but exclude the graphics transform stack and camera.
`lin.Affine.TransformRect` bounds a rectangle after a transform:

```go
func spriteWorldBounds(sprite gfx.Sprite, transform lin.Affine) lin.Rect {
	return transform.TransformRect(sprite.Bounds())
}
```

This encloses the sprite after the transform. For the tightest bounds
after combining rotations, transform each point from `sprite.Corners()`
and enclose those points instead of transforming its already axis-aligned
bounds. Bounds are useful for selection and broad collision checks;
they do not test the texture's transparent pixels.

## Reusable 2D geometry

`DrawTriangles` and `DrawIndexed` copy supplied vertices into the current
frame. For geometry reused over many frames, `NewGeometry2D` uploads it
once. Drawing it uses the same layers, sort keys, camera, transforms,
clipping, blending and shaders as sprites, without copying or uploading
the vertices again. Create it during `Init`:

```go
func newBadge(gr *gfx.Graphics) (*gfx.Geometry2D, error) {
	vertices := []gfx.Vertex2D{
		{Pos: lin.V2(24, 0), Color: gfx.RGB(255, 180, 60)},
		{Pos: lin.V2(48, 24), Color: gfx.RGB(255, 80, 40)},
		{Pos: lin.V2(24, 48), Color: gfx.RGB(180, 40, 80)},
		{Pos: lin.V2(0, 24), Color: gfx.RGB(255, 180, 60)},
	}
	return gr.NewGeometry2D(vertices, []uint32{0, 1, 2, 0, 2, 3})
}

func drawBadge(gr *gfx.Graphics, badge *gfx.Geometry2D) {
	gr.Transformed(lin.Translate2(100, 80), func() {
		gr.DrawGeometry(nil, badge)
	})
}
```

A nil texture draws the vertex colours. Supply a texture and vertex `UV`
coordinates for textured geometry. Nil indices mean consecutive triangles,
three vertices each. Invalid indices and incomplete triangles return an
error. Empty geometry is valid and draws nothing. `Geometry2D.Bounds`
reports the local bounds of all uploaded vertices, including unused ones.
You can reuse the input slices as soon as creation or `Update` returns.

`Update` replaces the GPU geometry. Each queued draw keeps the version
that existed when it was queued, so this draws the old shape on the left
and the replacement on the right:

```go
func replaceAndDraw(gr *gfx.Graphics, shape *gfx.Geometry2D,
	vertices []gfx.Vertex2D, indices []uint32) error {
	gr.DrawGeometry(nil, shape)
	if err := shape.Update(vertices, indices); err != nil {
		return err
	}
	gr.Transformed(lin.Translate2(80, 0), func() {
		gr.DrawGeometry(nil, shape)
	})
	return nil
}
```

Graphics owns the geometry and releases it at shutdown, including after
setup or drawing fails. `Destroy` releases it earlier; queued and in-flight
draws still finish using their captured buffers. Later draws of destroyed
geometry do nothing, and updating it returns an error. A failed `Update`
leaves the existing geometry intact. Textures supplied to `DrawGeometry`
have their own lifetime and are not destroyed with the geometry.

## Tint, blending and transforms

The sprite's `Color` multiplies the texture. Use it for team colours,
fading and a damage flash. For more, a `ColorMatrix` recolours
everything drawn inside it: `Saturation`, `HueRotate`, `Brightness`,
`Contrast`, `Invert`, `Grayscale`, `Sepia` and `Tint`, composed with
`Mul`. To set a blend mode for a stretch of drawing, call `Blended`.
`BlendAdd` suits glows and explosions and `BlendMultiply` suits shadows
and tinted glass; `BlendScreen`, `BlendLighten`, `BlendDarken`,
`BlendReplace` and `BlendErase` are the rest, and `BlendErase` cuts a
hole.

`Transformed` maps everything drawn inside it through a `lin.Affine`
(`Translate2`, `Rotate2`, `Scale2`, `Shear2`, composed with `Mul`), and
`Clip` limits drawing to a rectangle, which keeps a scrolling list
inside its panel. `SetColorMatrix`, `SetBlend`, `PushTransform`/
`PopTransform` and `PushClip`/`PopClip` are the non-closure forms.

```go
gr.ColorMatrixed(gfx.Brightness(0.6).Mul(gfx.Tint(gfx.RGB(255, 90, 90))), func() {
	gr.DrawRegion(hurt, gfx.Sprite{Pos: g.player})
})
gr.Blended(gfx.BlendAdd, func() { gr.Draw(g.glow, muzzleFlash) })
```

## Custom blend equations and masks

`CustomBlended` controls colour and alpha equations separately. Start with
a preset's `Options()` and edit the factors you need; explicit zero factors
stay zero. Inputs are premultiplied, as with built-in blend modes:

```go
func drawColourOnly(gr *gfx.Graphics, tex *gfx.Texture, sprite gfx.Sprite) {
	blend := gfx.BlendAlpha.Options()
	blend.SrcAlpha = gfx.FactorZero
	blend.DstAlpha = gfx.FactorOne // keep the destination alpha
	gr.CustomBlended(blend, func() { gr.Draw(tex, sprite) })
}
```

Use `Masked` for a shape that clips other drawing. The first closure draws
coverage without changing colour; the second draws through that coverage.
For example, a circular portrait needs no intermediate texture:

```go
func drawPortrait(gr *gfx.Graphics, portrait *gfx.Texture) {
	gr.Masked(func() {
		gr.FillCircle(48, 48, 40, gfx.White)
	}, func() {
		gr.Draw(portrait, gfx.Sprite{Pos: lin.V2(8, 8), Size: lin.V2(80, 80)})
	})
}
```

Masks nest up to eight levels. Their setup, coverage, contents and cleanup
form ordered phases: layers and sort keys sort within a phase, never across
one. This also holds for reusable geometry and flat particles, so callbacks
can use their normal layer choices. Scopes restore stencil, layer and sort
key even on panic; drawing already queued remains. Each nesting level uses
and clears one low stencil bit, preserving other bits. An enclosing advanced
stencil configuration is suspended during `Masked` and restored afterward.
A drawing helper that uses `Masked` can itself build another mask: its
clipped drawing contributes coverage without writing visible colour.

Coverage follows rasterized fragments, including transparent ones unless
the shader discards them. The edge is stencil coverage rather than a soft
alpha mask. Use a shader or a render texture for feathered alpha masking.

`Stenciled(StencilOptions{...}, draw)` exposes comparison, reference, read
and write masks, and pass/fail operations for custom effects. Its zero value
draws normally without changing stencil; zero masks select all eight bits,
and `DisableWrite` prevents updates. `NoColor` disables colour writes for
mask-building draws. Advanced scopes retain ordinary layer/key ordering.
`ClearStencil(value)` clears only the current view and clip and adds an
ordering boundary, leaving colour and depth unchanged. Stencil contents last
for that target's frame. These operations require a stencil attachment;
render textures made with `NoDepth` reject them before running callbacks.

## The camera, layers and the HUD

`SetCamera2D` makes later drawing world-space. A `Camera2D` has a
position (the world point at the centre of the view), a `Zoom` where 2
shows half as much, and a `Rotation`. `Follow` moves it towards a target
at a rate per second, the same at any frame rate; `Clamp` keeps the view
inside the level's rectangle; `Shake` throws the view about for a moment
and `Advance`, called once per update, runs the shake and lets it settle.
`ViewToWorld` maps the pointer back into the world, `WorldToView` goes
the other way for a marker pinned to an entity, and `VisibleRect`
returns the world rectangle on screen. Sprites wholly outside that
rectangle are dropped before they reach the vertex stream, and
`FrameStats.Culled2D` counts them. Without a camera the same happens
against the view itself. The test is the sprite's own four corners
against the view, so a long thin rotated sprite is culled as soon as its
quad clears the view, and it holds under `Transformed` as well: a sprite
the transform stack pushes off screen costs nothing. `DrawTilemap` takes
the view back through the transform stack into the map's own units and
only visits the tiles inside it, with or without a camera, and a text
layout wholly off screen is dropped in one test. `ScreenSpace` returns to
view coordinates for the HUD.

Use `WithCamera2D` and `Layered` when the state belongs to a block of
drawing. Each restores the previous state when its closure returns,
including on panic:

```go
gr.WithCamera2D(g.cam, func() {
	gr.Layered(2, func() { g.player.Draw(gr) })
})
gr.Layered(100, func() { gr.DebugText(8, 8, "Score: 10") })
```

The other drawing scopes (`Blended`, `Transformed`, `Shaded`,
`ColorMatrixed`, `Clip` and `DrawTo`) also restore their original queue's
state on panic. They do not undo drawing already queued.

```go
g.cam.Follow(g.player.Pos, 8, ctx.Delta) // in Update
g.cam.Clamp(g.level.Bounds(), ctx.Width, ctx.Height)
g.cam.Advance(ctx.Delta)
if landed {
	g.cam.Shake(6, 0.3)
}
```

`SetLayer` orders drawing across calls. Sprites draw in ascending layer
order and, within a layer, by sort key and then in submission order, so
a game can write its draw code in any order and still get the right
stacking. For parallax, put each background on its own layer and
translate it by its share of the camera's offset. To sort sprites by
their feet in a top-down game, set `SetSortKey` to each sprite's foot
position before drawing it: a character standing lower on the screen
then draws over one behind it, whatever order the draw calls came in.

```go
for _, a := range g.actors {
	gr.SetSortKey(a.pos.Y + a.height)
	gr.DrawRegion(a.region, gfx.Sprite{Pos: a.pos})
}
gr.SetSortKey(0)
```

```go
// Update: ease towards the player, then shake.
g.cam.Position = g.cam.Position.Lerp(g.player, 1-float32(math.Pow(0.02, ctx.Delta)))
if g.shake -= float32(ctx.Delta) * 3; g.shake > 0 {
	kick := lin.V2(g.rng.Between(-1, 1), g.rng.Between(-1, 1)).Mul(g.shake * 8)
	g.cam.Position = g.cam.Position.Add(kick)
}

// Draw
gr.SetCamera2D(g.cam)
for i, bg := range g.parallax { // 0 is furthest
	gr.SetLayer(i)
	share := 0.8 - 0.3*float32(i)
	gr.Transformed(lin.Translate2(g.cam.Position.X*share, 0), func() { gr.DrawTexture(bg, 0, 0) })
}
gr.SetLayer(10)
gr.DrawTilemap(g.tilemap, 0, 0, gfx.White)
gr.SetLayer(20)
g.drawActors(gr)

gr.ScreenSpace() // the HUD, in view coordinates
gr.SetLayer(100)
gr.DrawText(g.font, fmt.Sprintf("HP %d", g.hp), 12, 12, gfx.White)
gr.SetLayer(0)
```

### Views inside a view

`WithView` fits a local coordinate space into a rectangle of the enclosing
view. A minimap can occupy 200 by 150 view units while drawing a world area
1000 by 750 units across, without a render texture or a manual scale:

```go
func drawMinimap(gr *gfx.Graphics, mapGeometry *gfx.Geometry2D, centre lin.Vec2) {
	view := gfx.View2D{
		Viewport: lin.R(20, 20, 200, 150),
		Size:     lin.V2(1000, 750),
	}
	gr.WithView(view, func() {
		gr.WithCamera2D(gfx.Camera2D{Position: centre}, func() {
			gr.DrawGeometry(nil, mapGeometry)
		})
	})
}
```

The viewport uses enclosing **view coordinates**. A camera or drawing
transform does not move the rectangle itself. Drawing inside inherits the
camera, recalculated for the local virtual size; `WithCamera2D` can replace
it, or `ScreenSpace` can draw a local HUD. Sprites, paths, text, reusable
geometry and 2D particles share the view and clip to its rectangle and any
enclosing clips. Views nest, and `Graphics.View()` reports the current local
size. A zero component of `Size` uses the corresponding viewport dimension.

Use the same value to map pointer input into the minimap's world:

```go
func minimapPoint(view gfx.View2D, camera gfx.Camera2D, pointer lin.Vec2) (lin.Vec2, bool) {
	if !view.Viewport.Contains(pointer) {
		return lin.Vec2{}, false
	}
	return view.ParentToWorld(pointer, camera), true
}
```

`WorldToParent` maps world markers back to the enclosing view.
`ParentToLocal` and `LocalToParent` perform the layout mapping without a
camera. None clamp points: test `Viewport.Contains` when input outside the
rectangle should be ignored. For nested views, map through each enclosing
view in turn. Engine input and context dimensions remain in the main view.

Viewport dimensions must be positive, virtual dimensions nonnegative, and
all values finite; invalid views panic before changing drawing state.
`WithView` restores the previous view, camera and clips even on panic, while
retaining queued drawing. Shader frame size and pixel density follow each
draw's local view. These scopes affect 2D drawing; use render textures for
separate 3D camera passes.
Configure the main output with `SetView` and `SetViewport` outside these
scopes; calling either inside `WithView` panics. `DrawTo` can still select
another render texture inside a view and restores the view on return.

## Tilemaps

A `Tilemap` is a grid of frame indices into a `Sheet`; -1 is an empty
cell. `TileW` and `TileH` set the drawn size of a tile, so 16-pixel art
can be drawn at 32 units. `DrawTilemap` skips the cells outside the
active camera's view, so the cost depends on what is on screen rather
than on the size of the map.

A cell can carry flip bits above the frame index, so one sheet frame
serves eight orientations: `TileFlipped` packs them, `TileFrame` unpacks
a cell, and the bits (`TileFlipX`, `TileFlipY`, `TileFlipDiag`) match
the Tiled editor's convention. `Animate` makes one frame index cycle
through others, for water and torches, and `Advance` steps every
animation on the map. For a very large or infinite world, keep one
tilemap per chunk and draw the chunks that intersect `VisibleRect`;
building a tilemap is cheap.

```go
g.tilemap = gfx.NewTilemap(sheet, 256, 256)
g.tilemap.TileW, g.tilemap.TileH = 32, 32
g.tilemap.Set(x, y, frameWall)
g.tilemap.Set(x+1, y, gfx.TileFlipped(frameArrow, true, false, false))
solid := g.tilemap.Get(x, y) == frameWall
g.tilemap.Animate(frameWater, gfx.TileAnimation{
	Frames: []int{frameWater, frameWaterB}, Durations: []float32{0.4}})
g.tilemap.Advance(ctx.Delta) // in Update
```

## Autotiling

To keep a map of plain terrain ids and let the tiles pick themselves,
use the [autotile](../pkg/grid/autotile.md) package. A
`Mapper` turns terrain ids into frame indices: `Apply` fills a whole
tilemap, `Cell` patches the neighbourhood of one edited cell, and
`Region` patches everything an edited rectangle can affect. Five
rule kinds cover the usual tilesets: `Edge16` matches the four edge
neighbours with 16 tiles (walls, pipes, fences), `Edge64` is its
hexagonal counterpart matching the six sides of a hexagon with 64
tiles, `Blob47` matches all eight neighbours with the 47 distinct blob
tiles, `Corner16` is the dual grid where each tile sits on a corner
between four cells, and `Wang` matches terrain colours on tile edges or
corners for any number of terrains meeting with transitions.
`ExpandBlob` composes the 47 blob tiles from a six-tile template, so an
artist draws six tiles instead of 47. Variants weight alternative
frames per neighbourhood, chosen by a stable hash of the cell position.

```go
img, frames := autotile.ExpandBlob(template, 16)
tex, _ := ctx.Gfx.NewTexture(img, gfx.TextureOptions{})
grassMap := gfx.NewTilemap(gfx.NewSheet(tex, 16, 16), w, h)
grass := &autotile.Mapper{Rules: autotile.Blob47(1, frames)}
grass.Apply(w, h, terrainAt, grassMap.Set)   // the whole map once
grass.Cell(x, y, w, h, terrainAt, grassMap.Set) // after one edit
```

To re-tile after a brush stroke, a fill or a pasted block, call
`Region` once with the rectangle of edited cells rather than `Cell` for
each of them. It takes the rectangle's top-left cell, its width and
height in cells, and the map size, and sets the same frames that calling
`Cell` for every edited cell would set. A cell next to several edited
cells is computed once, and each affected cell goes to the setter once,
in row-major order. The rectangle may extend past the map's edges.

```go
// A 5 by 3 block pasted with its top-left cell at (px, py).
grass.Region(px, py, 5, 3, w, h, terrainAt, grassMap.Set)
```

`Mapper.Layout` is the shape of the grid, and the zero value is a
square one. `HexRowsOdd`, `HexRowsEven`, `HexColsOdd` and
`HexColsEven` are hexagons in staggered rows or columns, named the way
the Tiled editor names its stagger axis and index; `HexAxial` is the
same six directions in axial coordinates. A hexagonal layout has six
neighbours and no diagonals, so it takes `Edge64` or `Wang` rules.
`IsoDiamond` keeps eight neighbours but turns them a quarter turn, so
the direction names are the tile's directions on screen and the cell
north of `(x, y)` is `(x-1, y-1)`. `Layout.Neighbour` and `Layout.Dirs`
are the same walk for a game's own code, such as moving a unit across a
hex map.

```go
hex := &autotile.Mapper{Rules: autotile.Edge64(1, frames), Layout: autotile.HexRowsOdd}
hex.Apply(w, h, terrainAt, setFrame)
x, y = autotile.HexRowsOdd.Neighbour(x, y, autotile.DirNE)
```

Terrain sets painted in the Tiled editor's terrain tool come in through
the tiled package: `Map.WangSet` finds a set by name and its `Rules`
method converts it, with tile ids as frames. On a hexagonal map, call
`Map.Layout` for the layout, pass it to `WangSet.RulesFor` and give the
same layout to the `Mapper`; the conversion moves each colour into the
direction slot the layout uses, because Tiled stores a hexagon's six
sides one slot back in the eight-slot wangid. An isometric map gets
`Square`, since Tiled's own terrain tool matches an isometric map on
the plain grid neighbours. The
[autotile example](https://github.com/matjam/bunyip/tree/main/examples/autotile)
paints grass, walls and water with the mouse over one shared terrain
grid, one rule kind each, and draws a hexagonal edge set in the strip
below them.

## Maps from the Tiled editor

The [tiled](../pkg/tiled.md) package reads maps saved by the Tiled
editor in its JSON form (`.tmj`, `.tsj`) or its XML form (`.tmx`,
`.tsx`). `Parse` tells the forms apart by the first byte; `Load` reads
from disk and `LoadFS` through an asset filesystem. `Build` loads the
tileset images, makes one `gfx.Tilemap` per tileset a layer uses, wires
up the per-tile animations and returns a `Level` whose `Draw` draws the
layers in order with the group state above them applied. `Build` draws
a rectangular grid regardless of the parsed orientation; isometric and
hexagonal maps need a custom drawing path. Image layers and tilesets
made from separate per-tile images are parsed but not drawn. `Advance` steps
the tile animations, `Size` gives the map's pixel size, `Layer` finds a
layer by name and `Destroy` releases the textures.

Infinite tile layers retain their flattened cell origin in `StartX` and
`StartY`, but `Build` does not apply it to the drawing offset. Use a custom
drawing path for infinite chunks whose origin is not (0, 0).

Object layers are left to the game. They hold rectangles, ellipses,
points, polygons and polylines with names, classes and typed custom
properties; read them for spawn points, triggers and collision shapes.
Horizontal, vertical and diagonal flips on rectangular maps survive
the drawing bridge. For hexagonal maps, interpret the raw global ID's
rotation flags yourself; `SplitGID` discards the 120-degree bit. Tile layer data
decodes in every form Tiled writes: CSV, and base64 plain or compressed
with zlib, gzip or zstd.

```go
m, err := tiled.LoadFS(g.fs, "maps/level1.tmj")
g.level, err = tiled.Build(ctx.Gfx, m, tiled.ImagesFrom(g.fs, "maps"))
for _, l := range g.level.Layers {
	for _, o := range l.Objects { // empty on tile layers
		switch o.Class {
		case "spawn":
			g.player = lin.V2(o.X, o.Y).Add(l.Offset)
		case "solid":
			g.solids = append(g.solids, o.Rect())
		case "door":
			g.doors = append(g.doors, door{o.Rect(), o.Properties.String("target")})
		}
	}
}
```

## Text

`NewFont` parses an OpenType font and rasterises its glyphs at one size
into an atlas. Text is shaped with HarfBuzz, so kerning, ligatures, mark
placement and Arabic joining are right, right-to-left runs are
reordered, and lines break by the Unicode rules. Glyphs render at the
framebuffer's pixel density and draw in view units, so text is crisp on
a high-DPI display. `FontOptions` adds fallback fonts for scripts the
main one lacks, OpenType features, variable-font axes and ranges to
render up front.

`DrawText` draws from the top-left. `DrawTextBlock` wraps,
aligns and rotates a paragraph through `TextOptions`: a `Width` to wrap
in, an `Align` (`AlignLeft`, `AlignCenter`, `AlignRight`,
`AlignJustify`), `LineSpacing`, a `Size`, an `Angle`, `LetterSpacing`,
`Baseline`, a `Hyphenate` hyphenator, an `AutoHyphenate` that picks one
for the `Language`, a `Direction` and a `Language`. `Font.Measure` sizes text without
drawing it. `Font.Layout` returns an immutable `*TextLayout` for repeated
drawing and queries. A font caches layouts by text and options, and
drawing, `Measure` and `Layout` share that cache, so a label measured and
drawn is shaped once and repeating it every frame costs a lookup with no
allocation. Text asked for once, such as a counter that changes every
frame, is laid out and drawn but only kept once it is asked for again, so
it does not push out what is drawn every frame. The cache grows to hold
everything a frame draws, however much that is, and falls back once
frames draw less. A glyph drawn for the first time is rasterised and
uploaded during that frame, so new text shows up in the frame that asks
for it. To skip even the lookup, keep the `*TextLayout` and draw it with
`DrawTextLayout`.

`Layout` can allocate or upload atlas pages and returns an error. Immediate
draw helpers report those failures through frame submission and `engine.Run`.
Invalid sizes and exhausted atlas capacity are errors, rather than missing
letters. `Font.Shape` also returns an error for its low-level glyph path.

```go
g.font, err = asset.Font(ctx.Gfx, g.fs, "fonts/body.ttf", 18, gfx.FontOptions{
	Fallbacks: [][]byte{cjkTTF, emojiTTC}, // consulted in order
	Features:  []string{"-liga"},
})

opts := gfx.TextOptions{Width: 420, Align: gfx.AlignJustify, Hyphenate: gfx.EnglishHyphenator()}
w, h := g.font.Measure(story, opts)
gr.FillRect(38, y-4, w+4, h+8, gfx.RGBA(0, 0, 0, 160))
gr.DrawTextBlock(g.font, story, 40, y, opts, gfx.White)
```

Build a layout when the label changes, then retain it as ordinary Go data:

```go
func label(font *gfx.Font, text string) (*gfx.TextLayout, error) {
	return font.Layout(text, gfx.TextOptions{
		Width: 420, Underline: true,
		OutlineWidth: 1.5, OutlineColor: gfx.RGB(20, 30, 50),
	})
}

func drawLabel(gr *gfx.Graphics, layout *gfx.TextLayout, mouse lin.Vec2) gfx.TextCaret {
	gr.DrawTextLayout(layout, 40, 80, gfx.White)
	caret := layout.HitTest(mouse.Sub(lin.V2(40, 80)))
	r := layout.Caret(caret)
	gr.FillRect(40+r.X, 80+r.Y, r.W, r.H, gfx.RGB(255, 180, 80))
	return caret
}
```

`Bounds` describes advances and line boxes; `InkBounds` includes glyph
overhangs, outlines and decorations, excluding atlas padding and the sampling
filter's antialias fringe. Both include alignment and `Angle`. `Lines` returns
source ranges and baselines. Wrapped trailing whitespace keeps its source
boundaries with zero advance; unwrapped spaces retain their width. Blank lines
still have a logical line height.

Caret indices address bytes in the original UTF-8 string, including newlines
and wrapped paragraphs. Generated hyphens add no source bytes. Combining
sequences and ligatures are atomic: interior indices snap to the nearest
boundary, choosing the lower index on a tie. At a wrap or bidi transition,
`CaretLeading` follows the next cluster and `CaretTrailing` the previous one.
Hit testing takes layout-local coordinates, including its rotation; subtract
the draw origin and undo any surrounding drawing transform first. Vertical
text runs down in columns that progress left. Multiple trimmed whitespace
boundaries can share one caret position.

Layouts borrow their fonts and need no `Destroy`. The owning `Graphics`
releases fonts at shutdown; `Font.Destroy` releases them earlier. Queries on a
layout remain valid afterward, but new draws require live fonts from the same
`Graphics`. A draw already queued retains its atlas through completion.

`Underline`, `Strikethrough` and `OutlineWidth` are available in `TextOptions`
and rich runs. Outline width is in view units; a zero `OutlineColor` follows
the effective text colour. A layout's outlines are drawn beneath all of its
glyphs, so a neighbour's outline never covers a letter, and outlines that
share a colour and width are one draw call. Bitmap and SDF fonts lazily build coverage pages
with reusable distance ranges, so modest width animation reuses pages.
`FontOptions.OutlinePages` sets the per-font page budget (zero means 16).
Oversized outlines or glyphs return descriptive errors; they are never clipped
to fit an atlas. Choose another text size or reduce the outline width if it
exceeds the supported distance range.

Hyphenation uses the TeX patterns, by Liang's method. The engine ships
patterns for American and British English, German, French, Spanish,
Italian, Dutch, Portuguese, Swedish, Danish, Norwegian, Finnish, Polish
and Russian; `gfx/hyph/README.md` lists the files and their licences.
`EnglishHyphenator` returns the American English one and
`HyphenatorFor("de-AT")` any other, falling back from a regional tag to
its primary language and loading the patterns on first use. Text that
sets `AutoHyphenate` and a `Language` picks its own hyphenator, so a
translated interface hyphenates in the language it is showing, and a
language with no shipped patterns is left unhyphenated.
`ParseTeXPatterns` loads any other pattern file a game ships.

```go
opts := gfx.TextOptions{Width: 420, Align: gfx.AlignJustify,
	Language: g.tr.Lang(), AutoHyphenate: true} // g.tr is a locale.Translator
```

`NewSDFFont` builds a signed-distance atlas that stays sharp at any size
and angle, so one font object serves damage numbers and a zooming
strategy map; it draws a colour glyph as its outline.

Colour glyphs draw in their own colours, whichever way the font
describes them: a bitmap strike (`sbix` or `CBDT`, which is Apple's and
Google's emoji), COLR layers, or an SVG document per glyph. COLR version
1 paints are drawn too, with their gradients, transforms and
compositing, so a font like Noto Color Emoji comes out right. A colour
glyph preserves its RGB and uses the effective text alpha. Give the emoji font as a
`Fallbacks` entry and emoji appear in ordinary strings. A collection
(`.ttc`) contributes its first face, and only that face is read. Fonts
made from the same bytes share one parse of them while any of those fonts
is alive, so an emoji collection behind several sizes of a font is read
once, and the parsed tables are copies, so the file's bytes need not be
kept after `NewFont` returns. A font's glyph atlas is held as one byte a
texel until its first colour glyph. What is not
drawn is listed in `docs/design/gaps.md`: the variable paint tables'
deltas in COLR, and strokes, clipping and filters in SVG.

`ParseRich` reads a small
markup (`[b]`, `[i]`, `[u]`, `[s]`, `[#ff8800]`, `[link=name]`) into a
`RichText` that `DrawRichText` lays out across regular, bold and italic
faces, returning each link's rectangle for clicks. Every stretch of rich
text in one face is shaped as a whole, so kerning and ligatures work
across a colour or link change inside it; the glyphs are cut apart by
cluster afterwards. A cluster crossing a style change takes its colour,
decorations and link from its first source byte. Font changes start a new
shaped run at a grapheme boundary. Plain and rich text share Unicode wrapping,
hyphenation and the same `TextLayout` queries. `RichFonts.Layout` constructs a
reusable rich layout; its indices address `RichText.Plain`, not markup tags.
`DrawTextLayout` multiplies explicit run colours by its tint, with zero tint
meaning white.

```go
rich := gfx.ParseRich("You found the [#ffcc44]Brass Key[/#]. [link=map]Open it[/link].")
links := gr.DrawRichText(gfx.RichFonts{Regular: g.body, Bold: g.bold}, rich, 40, y,
	gfx.TextOptions{Width: 700}, gfx.White)
for _, l := range links {
	if l.Rect.Contains(mouse) && clicked {
		g.open(l.Name)
	}
}
```

`DrawTextOnPath` draws a line of text along a path with each glyph
rotated to follow it, for labels such as a river name on a strategy map.
`Font.Shape` returns `([]Glyph, error)` for custom single-line drawing, each
glyph carrying its source byte and pen advance. `DrawGlyphs` draws that slice.
This low-level path, including text on paths and billboards, leaves block
wrapping and decorations to the caller; use `TextLayout` for reusable block
drawing and caret queries.

## Shapes and paths

`FillRect`, `FillCircle`, `FillPolygon`, `StrokeRect`, `StrokeCircle`
and `StrokeLine` draw simple shapes. They go through the sprite stream,
so they sort by layer and clip like everything else.

A `Path` collects lines, quadratic and cubic curves and arcs, with
`Rect`, `RoundRect`, `Circle`, `Ellipse` and `Polygon` helpers. It holds
no GPU state, so one path value can be reset and rebuilt every frame.
`FillPath` fills it under the non-zero or even-odd rule and `StrokePath`
outlines it, both anti-aliased. `StrokeOptions` sets the width, `Cap`,
`Join`, miter limit and a `Dash` pattern; `FillOptions` maps a `Texture`
over the path or colours it with a `Gradient`. A `Gradient` is baked
from stops and given a direction with `Linear` or `Radial`; it holds a
small texture, which the renderer releases at shutdown; `Destroy` releases
it earlier.

```go
var p gfx.Path
p.MoveTo(40, 340).QuadTo(140, 220, 240, 340).CubicTo(300, 420, 360, 220, 420, 340)
gr.StrokePath(&p, gfx.RGB(120, 220, 160), gfx.StrokeOptions{Width: 6, Cap: gfx.CapRound})

p.Reset()
p.Circle(470, 110, 80).Circle(470, 110, 45) // a ring: the inner circle is a hole
gr.FillPath(&p, gfx.RGB(90, 170, 220), gfx.FillOptions{Rule: gfx.FillEvenOdd})

g.sky, err = ctx.Gfx.NewGradient(
	gfx.GradientStop{T: 0, Color: gfx.RGB(20, 30, 70)},
	gfx.GradientStop{T: 1, Color: gfx.RGB(220, 120, 60)})
g.sky.Linear(lin.V2(0, 0), lin.V2(0, 180))
gr.FillGradient(lin.R(0, 0, ctx.Width, 180), g.sky)
```

`Path.Bounds` computes the bounds of the stored lines and Bézier curves,
including their extrema and isolated `MoveTo` points. It excludes stroke
width, antialias fringes and drawing transforms; an empty path has zero
bounds. Circles and arcs are bounded as the cubic curves stored by `Path`.

### Compile paths that stay the same

`CompilePath` tessellates a path once into GPU geometry. `DrawPath` reuses
those triangles with the current drawing state. This avoids flattening
curves and rebuilding fills and strokes every frame:

```go
func newPanel(gr *gfx.Graphics) (*gfx.CompiledPath, error) {
	path := new(gfx.Path).RoundRect(0, 0, 180, 64, 12)
	return gr.CompilePath(path, gfx.PathOptions{
		Fill:        &gfx.FillOptions{},
		FillColor:   gfx.RGB(30, 70, 120),
		Stroke:      &gfx.StrokeOptions{Width: 2, Join: gfx.JoinRound},
		StrokeColor: gfx.White,
	})
}

func drawPanel(gr *gfx.Graphics, panel *gfx.CompiledPath) {
	gr.Transformed(lin.Translate2(20, 20), func() {
		gr.DrawPath(panel)
	})
}
```

`PathOptions{}` means a white fill. If either `Fill` or `Stroke` is
non-nil, only those explicitly selected paints are compiled; a lone
`Stroke` therefore makes an outline. Fill draws before stroke. Zero
colours mean white; omit an unwanted paint, or use a nonzero colour with
zero alpha for a transparent paint.

`PixelsPerUnit` chooses tessellation precision and antialias fringe width.
Zero selects one framebuffer pixel per local path unit. For a path drawn
at twice that density, set `PixelsPerUnit: 2`; include framebuffer density,
camera zoom and drawing scale when choosing it. Recompile when the expected
density changes substantially. Drawing a compiled path never retessellates it.

The result captures the path, paint coordinates and colours. Changing the
source path or options afterwards does not change it. Paint textures are
borrowed: keep them alive while drawing the compiled path. Graphics owns
the compiled triangles; `CompiledPath.Destroy` releases those triangles
early without destroying the textures, and queued draws still finish.
`CompiledPath.Bounds` encloses the resulting triangles, including stroke
and antialias fringes, before the drawing transform and camera.

## Particles

The [particle](../pkg/particle.md) package simulates emitters on the
CPU and draws them through the sprite stream, so they batch and sort
with everything else. An `Emitter` is a plain struct of documented
zero-default fields: a `Rate` or a `Burst`, a `Shape` to emit from, the
speed, direction and spread, the lifetime, acceleration and damping,
size and colour curves over each particle's life, a texture, region or
sheet, a `Blend` and a `Layer`. `Fire`, `Smoke`, `Sparks`, `Rain` and
`Confetti` are presets to start from.

A `System` owns the live particles: `Update` advances it, `Draw` queues
them, `SetPosition` moves it, `Burst` fires a one-shot and `Finished`
reports one that has run out, so the game can drop it. `WorldSpace`
decides whether particles stay where they were emitted when the system
moves, which suits smoke, or travel with it, which suits a thruster
flame. Thousands are cheap; tens of thousands still draw as one batch
but cost CPU in `Update`.

```go
e := particle.Fire()
e.Position = hearth
e.Texture = g.soft // particle.SoftCircle(64), uploaded as a texture
e.Prewarm = 1.5    // already burning on its first frame
e.Layer = 2
g.fire = particle.New(e)

g.fire.Update(ctx.Delta) // Update
g.fire.Draw(gr)          // Draw
```

### Hundreds of thousands at once

`GPUSystem` runs the same `Emitter` over far more particles. It keeps
each particle as a few numbers in parallel arrays, moves them with plain
loops over those arrays, and draws the whole system as one instanced
draw through `gfx.DrawParticles`, so no sprite is built and no vertices
are written. `NewGPU` replaces `New`; everything else (`Update`, `Draw`,
`SetPosition`, `Burst`, `Finished`) is similar, and the stateful random
stream matches the CPU path particle for particle. GPU appearance curves
use 64-entry lookup tables, so their interpolation is quantized. Raise
`Emitter.Max`, which defaults to the
1000 the CPU path assumes.

`SetEmitter` retunes the appearance and future births. Existing stateful
particles retain the palette tint selected when they were born, even
when the palette is replaced, shortened or removed; new particles use
the new palette. Colour curves still apply to all live particles.

On a desktop machine, two hundred thousand particles cost about 0.3 ms
to simulate, 1.1 ms to pack into instance records and 1.8 ms to upload
and draw, against about 15 ms to draw the same count through the sprite
stream.

```go
e := particle.Rain()
e.Max = 200_000
e.Rate = 60_000
g.storm = particle.NewGPU(e)

g.storm.Update(ctx.Delta)
g.storm.Draw(gr) // one instanced draw call
```

A batch takes the layer its emitter names and interleaves with the
sprite stream by layer, so sprites on lower layers still draw under it;
within one layer the particles draw over the sprites. Particles are
drawn in the order the system holds them, without depth sorting, which
is what additive effects want.

Set `Emitter.Stateless` and the system keeps no per-particle state at
all: every particle is a closed-form function of the seed, its index in
the stream and the clock. It needs no simulation history, but the system
still allocates arrays up to `Max` and buffers for the drawn instances.
The effect is identical for the same settings and clock and already runs at time
zero with no `Prewarm`, and `SetClock` scrubs it to any time without
simulating the gap. It suits the effects whose particles never interact:
rain, snow, sparks, dust. The cost is per-particle work each frame, so a
stateless emitter is slower per particle than a simulated one;
`Burst`, `Prewarm`, `WorldSpace` and the radial and tangential
accelerations do not apply to it.

`Stop` fixes the stateless stream's final birth time and lets existing
particles age out. `Finished` checks their lifetimes at the current clock,
even before the next draw; `Alive` reports the last draw's count.
`SetClock` retains the stop time when scrubbing, and `Start` resets the
clock to zero and resumes births.

`Clear` removes stateful particles without stopping emission. For a
stateless emitter it only resets the cached `Alive` count; the next draw
reconstructs the stream. Use `Stop` and continue advancing time to drain it.

### Particles in the 3D scene

`Draw3D` draws the same system as camera-facing quads in the 3D scene
through `gfx.DrawParticles3D`: smoke over a battlefield, embers above a
fire, snow through a forest. `SetPlane` says where the simulated plane
sits in the world, so a particle at (x, y) lands at
`origin + xAxis*x + yAxis*y`; the default puts it in the world's xy
plane with the emitter's up (`-Y`, as on screen) pointing at the world's
up. Sizes are then world units.

The particles are drawn over the finished scene and are hidden by the
geometry in front of them. The `soft` argument fades a particle out over
that many world units as it approaches the surface behind it, which
hides the hard line a quad otherwise cuts where it meets the ground.

```go
g.smoke.SetPlane(lin.V3(0, 0, -4), lin.V3(1, 0, 0), lin.V3(0, -1, 0))
g.smoke.Draw3D(gr, 1.5) // fade over the last 1.5 units
```

## Lights on sprites

`SetLights2D` places an ambient colour and up to eight `Light2D` point
lights above the sprite plane for the frame, and `DrawLit` draws a
sprite lit by them through a tangent-space normal map uploaded with
`TextureOptions{Data: true}`. Eight is the most the lit shader holds:
lights past the eighth are dropped and `FrameStats.Lights2DDropped`
counts them, so pass the lights nearest the action first.

Set `Shadows` on a light and it is blocked by the occluders the frame
adds with `AddOccluder2D`, which takes a closed polygon in the same
units as sprite positions; two points make a single wall. `Softness` is
the width of the shadow's soft edge in view units, 8 by default. Both
the lights and the occluders are set every frame, and every lit sprite
in the frame sees the same set, so it does not matter whether the
occluders are added before or after the draws.

Each shadowed light gets a polar shadow map built on the CPU: 512
directions around the light, each holding the distance to the nearest
occluder edge, uploaded as one small texture the lit shader reads. The
cost is the occluder edges times the shadowed lights, so a few hundred
edges are free; only edges within a light's radius are visited. Add the
walls near the player, not the whole level. A frame whose shadowed
lights and occluders are the same as the frame before keeps the maps it
already has, so a still scene pays for its shadows once.

```go
gr.SetLights2D(gfx.RGB(30, 30, 45),
	gfx.Light2D{Pos: g.player, Radius: 220, Color: gfx.RGB(255, 200, 120), Shadows: true},
	gfx.Light2D{Pos: brazier, Radius: 140, Height: 20, Color: gfx.RGB(255, 140, 60)},
)
for _, w := range g.walls { // a rectangle's four corners
	gr.AddOccluder2D(w.TopLeft(), w.TopRight(), w.BottomRight(), w.BottomLeft())
}
gr.DrawLit(g.wallTex, g.wallNormal, gfx.Sprite{Pos: lin.V2(x, y), Size: lin.V2(64, 64)})
```

The [sprites example](https://github.com/matjam/bunyip/tree/main/examples/sprites)
draws a lit floor in the corner of the window with a lamp circling three
crates that block it.

## Render textures

A `RenderTexture` is an offscreen surface that draws like the screen and
is then used like a texture, for minimaps, portraits, mirrors or a whole
low-resolution scene. Set `RenderTextureOptions.Nearest` to keep its
pixels sharp when it is scaled up. `DrawTo` runs a closure with the
render texture as the output; it is rendered before the main frame, so
the result can be drawn in the same frame.

```go
g.mini, err = ctx.Gfx.NewRenderTextureOptions(320, 180, gfx.RenderTextureOptions{Nearest: true})
g.mini.SetView(320, 180)
// in Draw:
gr.DrawTo(g.mini, gfx.RGB(5, 5, 12), func() {
	gr.SetCamera2D(gfx.Camera2D{Position: g.level.Size().Mul(0.5), Zoom: 0.1})
	gr.DrawTilemap(g.tilemap, 0, 0, gfx.White)
})
gr.ScreenSpace()
gr.Draw(g.mini.Texture(), gfx.Sprite{Pos: lin.V2(ctx.Width-232, 12), Size: lin.V2(220, 124)})
```

A render texture also gives a game its own post effect: render the whole
game into one and draw it back as a single sprite with a sprite shader on
that draw, for a CRT curve, a palette swap or anything else the engine's
own post pass does not offer. `RenderTexture.Read` copies the pixels
back, and `ctx.Screenshot(path)` writes the whole frame to a PNG, which
is what every example does with `-shot`.

## Post-processing on a 2D frame

A frame with no 3D draws in it goes straight to the screen and skips the
post pass, which is what a 2D game usually wants and costs nothing. Set
`PostSettings.Post2D` to send it through the composite instead:

```go
p := gfx.PostSettings{
	Post2D: true, Exposure: 1, Saturation: 1.1, Contrast: 1,
	Bloom: 0.3, BloomThreshold: 0.9, Vignette: 0.3,
	Aberration: 0.6, Grain: 0.03,
}
gr.SetPost(p)
```

Bloom, the vignette, saturation and contrast, the LUT grade, chromatic
aberration, lens distortion, lens ghosts, film grain and FXAA all apply.
The effects that need a depth buffer or a velocity buffer do not:
ambient occlusion, depth of field, motion blur, temporal anti-aliasing
and god rays stay off however they are set.

Two things to know about the mode. Exposure and tone mapping are skipped,
so a frame with `Post2D` on and nothing else turned up comes back with
the colours the game drew; that also means bloom needs
`BloomThreshold` below 1 to catch anything, since 2D colours are already
inside the displayable range. And `Saturation` and `Contrast` are
absolute values whose zero drains the frame, so start from
`gfx.DefaultPost()` or write both as 1, the way the example above does.
FXAA follows `NoAntiAlias` as it does in 3D; a game of hard-edged pixel
art wants `NoAntiAlias: true`.

Post applies to everything in the frame, including text and the interface,
because there is one image and one grade. The
[sprites example](https://github.com/matjam/bunyip/tree/main/examples/sprites)
puts this on the P key.

## Shaders

A game's own fragment shader, compiled to SPIR-V offline with
`bunyip-shader`, colours 2D drawing. `SetShader` or `Shaded` sets it for
a stretch of sprites, `Shader.SetUniforms` passes a struct of parameters
and `Shader.SetImage` passes up to four extra textures. Everything drawn
while one shader is set is one batch as long as nothing else changes.
The [shaders guide](shaders.md) covers writing and building them.

## Performance

`ctx.Stats` and `Graphics.Stats` report the last frame's cost. `Draws2D`
is the 2D draw calls after batching and `Vertices2D` the vertices they
covered; `Draws3D`, `Instances` and `Culled` are the 3D counterparts. A
rising `Draws2D` for the same scene means state changes are breaking
batches: alternating textures, a blend mode toggled per sprite, a clip
pushed around each item, a colour matrix set and unset. Pack sprites
into one atlas, group draws by texture with layers, and set blend modes
and shaders around groups rather than around single draws.

F3 toggles an overlay with the frame time, the update and draw times,
the draw counts and any `ctx.Profile` scopes, refreshed four times a
second; `Config.Debug` shows it from the start. `Config.DrawBudget` sets the number of draw calls a
frame should stay under, and the overlay warns when a frame goes over,
so a batching regression shows up as soon as it appears. Sprites outside
the 2D camera's view are dropped before they cost anything, as are
tilemap cells and 3D meshes, but the game still walks its entity list to
issue the draws; in a large world, test against the camera's visible
rectangle first so that walk is short too.

```go
view := g.cam.VisibleRect(ctx.Width, ctx.Height).Inset(-64) // margin for big sprites
for _, e := range g.entities {
	if view.Contains(e.pos) {
		gr.DrawRegion(e.region, gfx.Sprite{Pos: e.pos})
	}
}
```

## Debug drawing

`DebugText` and `Debugf` put a line of text on screen in the engine's
own font with a dark shadow, so you can print a value before any font is
loaded; `DebugFont` returns that font for measuring. For shapes, call
`StrokeRect` and `StrokeCircle` on a high layer to draw collision boxes
and trigger volumes over the scene.

```go
gr.SetLayer(1000)
gr.Debugf(8, 8, "player %.0f,%.0f  draws %d", g.player.X, g.player.Y, ctx.Stats.Draws2D)
for _, s := range g.solids {
	gr.StrokeRect(s.X, s.Y, s.W, s.H, 1, gfx.RGBA(255, 80, 80, 140))
}
gr.SetLayer(0)
```

The `sprites`, `tiles`, `tiled`, `text`, `vector`, `particles`,
`roguelike` and `tetris` examples are complete programs for each of
these areas, and every one runs with `-seconds 3 -shot out.png`.
