# ViewsHandle

> Talos 0.7 · Since 0.7

`ctx.views`: split screen for local multiplayer.

Views are numbered from 1, like players: view 1 is `ctx.camera` itself, and views 2 to 4 each have a camera rig of their own with exactly `ctx.camera`'s `set` and `follow`. A view number outside 1..4 throws a `ViewsError` (`views-out-of-range`). The UI is drawn once over every view.

## Signature

```ts
export interface ViewsHandle {
  /** Splits the window. Throws `ViewsError` (`views-layout-unknown`) for a name that is not a {@link ViewLayout}. */
  setLayout(layout: ViewLayout): void
  /** The layout in force: `"single"` until one is set. */
  readonly layout: ViewLayout
  /** How many views the layout has. */
  readonly count: number
  /** Where view `view` is in the window; `null` when the layout has no such view. */
  rect(view: number): ViewRect | null
  /** `ctx.camera.set` for view `view`. */
  set(view: number, kind: CameraKind, params?: CameraParams): void
  /** `ctx.camera.follow` for view `view`. */
  follow(view: number, target: EntityHandle | string | null): void
  /**
   * A camera that keeps every one of `targets` in view: at their centre, from `yaw`/`pitch`, pulled
   * back (and with `maxFov`, widened) to fit them all, eased on game time. For several players on one
   * shared screen: `ctx.views.frame(1, players)`. An empty list, `set` or `follow` ends it.
   */
  frame(view: number, targets: (EntityHandle | string)[], params?: FrameParams): void
  /**
   * One camera for players sharing a screen that splits when they spread out. View 1 frames every
   * target (as {@link ViewsHandle.frame}); once their spread is beyond `split.beyond` for `split.hold`
   * seconds the window splits into one view per live target, in the order given (two to four), each
   * framing its own target from the same direction, and it merges back once the spread is under
   * `split.back` for as long. Each switch raises `views-split` ({@link ViewsSplitEvent})
   * or `views-merged` ({@link ViewsMergedEvent}). The same call again changes nothing; a different one
   * starts shared. `setLayout`, `frame(1, …)` or `ctx.camera.set`/`follow` end it where it stands;
   * `shared([])` ends it and goes back to one view. Throws `ViewsError` (`views-layout-unknown`) for a
   * `split.layout` that is not `"auto"`, `"left-right"` or `"top-bottom"`.
   */
  shared(targets: (EntityHandle | string)[], params?: SharedParams): void
  /** View `view`'s rig yaw now, radians (`ctx.camera.yaw` per view). */
  yaw(view: number): number
  /** View `view`'s rig pitch now, radians. */
  pitch(view: number): number
  /**
   * The heading view `view` is drawn from, radians about +Y with 0 facing -Z, as of the top of the tick:
   * the frame that view's player reads a camera-relative stick in (`ctx.camera.viewYaw` per view). 0 for
   * a view the layout does not have.
   */
  viewYaw(view: number): number
  /** The vertical field of view view `view` is drawn at, degrees (`ctx.camera.fov` per view). */
  fov(view: number): number
}
```

Import: `import type { ViewsHandle } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `setLayout` | `setLayout(layout: ViewLayout): void` | Splits the window. Throws `ViewsError` (`views-layout-unknown`) for a name that is not a {@link ViewLayout}. |
| `layout` | `readonly layout: ViewLayout` | The layout in force: `"single"` until one is set. |
| `count` | `readonly count: number` | How many views the layout has. |
| `rect` | `rect(view: number): ViewRect \| null` | Where view `view` is in the window; `null` when the layout has no such view. |
| `set` | `set(view: number, kind: CameraKind, params?: CameraParams): void` | `ctx.camera.set` for view `view`. |
| `follow` | `follow(view: number, target: EntityHandle \| string \| null): void` | `ctx.camera.follow` for view `view`. |
| `frame` | `frame(view: number, targets: (EntityHandle \| string)[], params?: FrameParams): void` | A camera that keeps every one of `targets` in view: at their centre, from `yaw`/`pitch`, pulled back (and with `maxFov`, widened) to fit them all, eased on game time. For several players on one shared screen: `ctx.views.frame(1, players)`. An empty list, `set` or `follow` ends it. |
| `shared` | `shared(targets: (EntityHandle \| string)[], params?: SharedParams): void` | One camera for players sharing a screen that splits when they spread out. View 1 frames every target (as {@link ViewsHandle.frame}); once their spread is beyond `split.beyond` for `split.hold` seconds the window splits into one view per live target, in the order given (two to four), each framing its own target from the same direction, and it merges back once the spread is under `split.back` for as long. Each switch raises `views-split` ({@link ViewsSplitEvent}) or `views-merged` ({@link ViewsMergedEvent}). The same call again changes nothing; a different one starts shared. `setLayout`, `frame(1, …)` or `ctx.camera.set`/`follow` end it where it stands; `shared([])` ends it and goes back to one view. Throws `ViewsError` (`views-layout-unknown`) for a `split.layout` that is not `"auto"`, `"left-right"` or `"top-bottom"`. |
| `yaw` | `yaw(view: number): number` | View `view`'s rig yaw now, radians (`ctx.camera.yaw` per view). |
| `pitch` | `pitch(view: number): number` | View `view`'s rig pitch now, radians. |
| `viewYaw` | `viewYaw(view: number): number` | The heading view `view` is drawn from, radians about +Y with 0 facing -Z, as of the top of the tick: the frame that view's player reads a camera-relative stick in (`ctx.camera.viewYaw` per view). 0 for a view the layout does not have. |
| `fov` | `fov(view: number): number` | The vertical field of view view `view` is drawn at, degrees (`ctx.camera.fov` per view). |

## Example

The tested fixture `fixtures/app/agent.ts`.

```ts
import type { Behavior } from "@argus/api"

// Sends a nav agent to `props.target` once, then reports arrival through ctx.state so the
// test can read it without polling the crowd directly.
export default {
  init(ctx) {
    ctx.state.arrived = false
    ctx.nav?.goTo(ctx.props.target as [number, number, number])
  },
  update(ctx) {
    if (ctx.nav?.arrived) ctx.state.arrived = true
  },
} satisfies Behavior
```
