# Cameras & expressions

> Choose what each player sees, and make cameras, visuals, disc paint and HUD styles follow the match every frame.

Each player's screen shows part of the world. Your game decides which part
with a **camera**. Without one, every player sees the default view: 340 × 160
world units around the center.

Cameras, visuals, disc paint and HUD styles can also take **expressions**:
small formulas that every player's client works out on each frame it draws,
such as "halfway between me and the ball" or "fade in with speed". You
declare them once; they follow the match smoothly without any code in your
`tick` handler.

## Frame a field

`world.camera` takes a center and a width and/or height in world units. With
both, the area is fitted to the screen; with one, the other follows the
screen's shape. Cameras belong to the match, like the world.

```ts title="camera.ts"
// Frame a 600 × 300 field, for everyone.
world.camera({ center: { x: 0, y: 0 }, width: 640, height: 340 });
```

## Expressions

Expressions come from the standard module
[`disko:expr`](/reference/expr). Its functions have the same names and
meanings as [`disko:math`](/reference/math)'s, but return expressions
instead of numbers. Import it as a namespace so the names never clash with
`disko:math`:

```ts title="camera.ts"
// Follow each player's own disc, leaning toward the ball and zooming out
// as they get farther apart. Spectators have no `me`, so `or` gives them
// the ball instead.
const b = expr.disc(ball);

world.camera({
  center: expr.or(expr.lerp(expr.me.position, b.position, 0.3), b.position),
  width: expr.or(
    expr.clamp(
      expr.times(expr.distance(expr.me.position, b.position), 2.5),
      300,
      900,
    ),
    640,
  ),
});
```

Expressions read:

- `expr.disc(disc)`: a disc as the player sees it: `.position`, `.velocity`,
  `.x`, `.y`, `.radius`, `.speed`;
- `expr.me`: the player's own disc (spectators have none);
- `expr.time`: match time in seconds (it stops while paused);
- `expr.screen`: the player's screen, `.width`, `.height`, `.aspect`.

When something an expression reads doesn't exist on a frame (a destroyed
disc, `me` for a spectator), the expression has no value. `expr.or` gives a
fallback; otherwise a camera is skipped (the player sees the previous camera,
or the default view), a visual is hidden for that frame, and a paint or HUD
number uses its default.

Each player uses the newest camera whose `audience` includes them and that
has a value, so one camera can follow every player's own disc.

## Visuals and disc paint

Every number in a visual and in disc paint can be an expression, and both
take an `opacity` from 0 to 1:

```ts title="camera.ts"
// A ring under the player's disc that only they see: it grows with their
// speed, and fades from half-transparent when still to opaque at speed.
const disc = expr.disc(body);

world.visual({
  anchor: { body },
  appearance: {
    type: "ring",
    radius: {
      bodyRadius: true,
      gap: expr.plus(3, expr.times(disc.speed, 2)),
    },
    width: 1.5,
    color: "#ffffff",
  },
  opacity: expr.clamp(expr.plus(0.5, expr.times(disc.speed, 0.25)), 0.5, 1),
  order: "behind-body",
  audience: { include: [player] },
});
```

## HUD styles

Numeric HUD styles (sizes, spacing, radius, font size, opacity) take
expressions too. The HUD isn't rebuilt each frame; only the value changes:

```ts title="camera.ts"
// A HUD bar that fills over the first 10 seconds of the match. Its width is
// worked out on every player's screen, every frame.
game.on("start", () => {
  ui.hud(
    ui.box({
      style: ui.style({
        width: expr.min(expr.times(expr.time, 20), 200),
        height: 6,
        background: "primary",
      }),
    }),
  );
});
```
