# EF-Map Director — sequence specification

**Schema version: 1**
Last updated: 2026-08-04
Canonical URL: https://ef-map.com/director-spec.md
Open the tool: https://ef-map.com/?panel=director

This document is written for both people and language models. If you are a
language model being asked to write a sequence, read the whole thing and then
return **one JSON object and nothing else** — no prose, no explanation, no
markdown code fence.

---

## 1. What EF-Map Director is

[EF-Map](https://ef-map.com) is an interactive 3D map of the EVE Frontier star
cluster (24,000+ solar systems). **Director** is EF-Map's supported interface for
driving the map's camera and visual presentation from a JSON document, so that a
creator can record cinematic footage without knowing anything about how EF-Map is
built.

The workflow is:

1. Give your AI assistant the Director instructions — the panel's **Copy
   instructions** and **Download instructions** buttons both produce them, and
   they can be pasted or attached as a file. (This document is the long-form
   version of the same thing.)
2. Describe the shot you want as a **separate message**. The instructions contain
   nothing you need to edit; sent on their own they will ask what you want made.
3. Paste the JSON into the Director panel in EF-Map (Tools Library → Map Visuals
   → EF-Map Director, or open <https://ef-map.com/?panel=director>).
4. Press **Validate**. It checks the structure *and* that every system you named
   exists.
5. Press **Preview** to watch it.
6. Start your screen recorder (OBS, ShareX, or your operating system's recorder).
7. Press **Run for recording**. A five second countdown runs, then EF-Map
   performs the sequence.

**Preview and recording differ.** A preview keeps Director's own controls on
screen because you are watching it. **Run for recording** takes Director out of
frame for the whole run — the Director panel, the Stop button and the mouse
cursor are all hidden — even when the sequence deliberately leaves the normal
EF-Map interface visible. While recording, **Escape** stops the sequence.

**EF-Map does not record anything.** It performs the camera work; capturing the
browser window is your job, and Director cannot change your window size or your
recorder's capture region. Sequences run entirely in your browser and are never
uploaded.

---

## 2. Document structure

```json
{
  "schemaVersion": 1,
  "name": "Route to Virtue",
  "description": "Optional one-line summary.",
  "tailHoldMs": 2500,
  "actions": [
    { "do": "reset" },
    { "do": "wait", "durationMs": 1500 },
    { "do": "focusSystem", "system": "Coherence", "holdMs": 800 },
    { "do": "orbit", "degrees": 90, "durationMs": 8000 }
  ]
}
```

| Field | Required | Type | Notes |
|---|---|---|---|
| `schemaVersion` | yes | number | Must be exactly `1`. |
| `name` | yes | string | Max 120 characters. Shown in the panel and the run log. |
| `description` | no | string | Max 600 characters. |
| `tailHoldMs` | no | number | 0–30000. Silent hold on the final frame, so you can cut on it and stop the recorder cleanly. |
| `actions` | yes | array | 1–200 entries. See below. |

Any other top-level field is a validation error. In particular there is **no
`leadInMs`**: it used to run before the first action — that is, before `reset` —
so it held whatever the map happened to look like *before* the sequence rather
than your opening frame. Operator pre-roll is the recording countdown's job; to
hold an opening shot, put a `wait` action in after you have set it up.

Every action is an object whose `do` field names the verb. Any action may also
carry a `note` (string, max 200 characters); it is never executed and appears in
the run log. **Any argument not listed under its verb is a validation error** —
this is deliberate, so a typo such as `duration` instead of `durationMs` is
reported rather than silently ignored.

### Limits

| Limit | Value |
|---|---|
| Longest single action | 120000 ms |
| Longest whole sequence (estimated) | 20 minutes |
| Actions written in the document | 200 |
| Actions after `repeat` expansion | 400 |
| `repeat` nesting | not allowed inside another `repeat` |
| `repeat` count | 50 |

---

## 3. The camera model

Director uses an **absolute** camera pose, which is what makes a sequence
repeatable — the same numbers always frame the same view.

- **`distance`** — camera-to-target distance in map units.
  About `5000` shows the whole cluster (this is the map's default),
  `1500` covers a region, `500`–`900` frames a single system.
  Valid range 10–10000.
- **`bearingDeg`** — rotation around the target. `0` is the map's default facing.
- **`elevationDeg`** — height above the galactic plane. `0` is edge-on, positive
  looks down from above. Valid range −89 to 89.
- **`fov`** — vertical field of view in degrees. The map default is `75`.
  Valid range 20–110.

The cluster is a flat disc, so a shallow elevation leaves a tall frame mostly
empty. For vertical video use `elevationDeg` around 50–60.

`orbit` and `dolly` are **relative** — they apply a change to whatever the camera
is doing right now. `setCamera` is **absolute**.

---

## 4. Actions

### Free-form travel versus route travel

There are two different ways to move the camera between systems, and picking the
wrong one is the most common mistake in a first sequence.

**Free-form travel — no route is drawn.** Use several `focusSystem` actions, with
`orbit`, `dolly`, `setCamera` and `wait` between them, to visit systems in
whatever order the shot wants. Nothing is planned, nothing is drawn, and the
systems need no connection to each other. This is what a tour, a travelogue or a
"showcase these places" brief usually wants:

```json
{
  "schemaVersion": 1,
  "name": "Free-form pattern",
  "actions": [
    { "do": "reset" },
    { "do": "focusSystem", "system": "Ulam", "holdMs": 600 },
    { "do": "setCamera", "distance": 650, "bearingDeg": 135, "elevationDeg": 22, "target": "selection", "durationMs": 2600 },
    { "do": "orbit", "degrees": 80, "durationMs": 7000 },
    { "do": "dolly", "scale": 4, "durationMs": 3500, "easing": "easeIn" },
    { "do": "focusSystem", "system": "Mandr", "holdMs": 600 },
    { "do": "setCamera", "distance": 1350, "bearingDeg": 135, "elevationDeg": 30, "target": "selection", "durationMs": 2600 },
    { "do": "orbit", "degrees": -70, "durationMs": 7000 }
  ]
}
```

`Ulam` and `Mandr` have no stargate path between them — neither has a stargate at
all — which is the point: free-form travel does not care. The `dolly` before each
`focusSystem` is what makes the journey read, pulling back to show where you are
leaving from so the next arrival lands somewhere new rather than cutting between
two anonymous close-ups. Setting `bearingDeg` matters as much as `distance`; see
[Verified system names](#8-verified-system-names).

**Route travel — a calculated route is drawn and followed.** Use `planRoute` and
then `followRoute`. This requires an actual stargate (or jump) path between the
two systems, draws the line on the map, and visits every system the solver
returns, in order. Use it only when the *route itself* is the subject.

Do not plan a route merely to move the camera between two systems. It draws a
line the brief did not ask for, and most pairs of systems in the cluster have no
path at all at the default range.

### `reset`

Returns the map to a known starting state: clears any route, selection,
waypoints and avoid-list, and restores the default camera pose.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `camera` | boolean | `true` | Also restore the default pose (distance 5000, bearing 0°, elevation 0°, fov 75). |
| `durationMs` | number 0–120000 | `0` | Animate back over this long. `0` cuts instantly. |

**Begin every sequence with `reset`.** It is what makes a preview run and a
recording run start from the same frame.

### `wait`

Holds the current shot without changing anything.

| Argument | Type | Required | Notes |
|---|---|---|---|
| `durationMs` | number 0–120000 | yes | How long to hold. |

### `focusSystem`

Selects a system and flies the camera to it along the map's own smooth fly-to
path, then waits until the camera has actually arrived.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `system` | string | — | **Required.** Exact system name, case-insensitive. |
| `holdMs` | number 0–120000 | `0` | Extra hold after the camera arrives. |

The fly-to duration is chosen by the map from the distance travelled (500–2800
ms) and cannot be set by the sequence. Use `holdMs` or a following `wait` to
control the beat.

An unknown system name **stops the run** with an `unknown-system` error listing
close matches. Director does not guess: matching is case-insensitive exact, plus
a separator-insensitive form (so `u6r506` finds `U6R-506`), and nothing looser.

### `setCamera`

Moves the camera to an absolute pose. Omitted fields keep their current value.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `distance` | number 10–10000 | current | See the camera model above. |
| `bearingDeg` | number −360–360 | current | Takes the short way round. |
| `elevationDeg` | number −89–89 | current | |
| `fov` | number 20–110 | current | |
| `target` | `"origin"` \| `"selection"` | current | Omit to keep looking at the same point; `"origin"` centres the cluster, `"selection"` centres the selected system. |
| `durationMs` | number 0–120000 | `0` | `0` cuts instantly. |
| `easing` | `"linear"` \| `"easeIn"` \| `"easeOut"` \| `"easeInOut"` | `"easeInOut"` | |

`target: "selection"` requires a selected system — run `focusSystem` first, or
the run stops with a `not-ready` error.

### `orbit`

Swings the camera around its current target, optionally climbing and dollying at
the same time.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `degrees` | number −1440–1440 | — | **Required.** How far to swing. |
| `elevationDegrees` | number −180–180 | `0` | Climb (positive) or descend. |
| `distanceScale` | number 0.05–20 | `1` | Multiply distance across the move. |
| `fovDelta` | number −60–60 | `0` | A small negative value reads as a push-in. |
| `durationMs` | number 0–120000 | `6000` | |
| `easing` | see above | `"easeInOut"` | |

### `dolly`

Moves the camera towards or away from its target without changing bearing.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `scale` | number 0.05–20 | — | **Required.** Below 1 moves in, above 1 pulls back. |
| `fovDelta` | number −60–60 | `0` | |
| `durationMs` | number 0–120000 | `4000` | |
| `easing` | see above | `"easeInOut"` | |

### `planRoute`

Plans a route between two systems, draws it on the map, waits for the solver to
finish and confirms a path was actually found.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `from` | string | — | **Required.** Origin system. |
| `to` | string | — | **Required.** Destination system. |
| `rangeLy` | number 0–2000 | `0` | Ship jump range in light-years. `0` routes on stargates only. |
| `optimize` | `"fuel"` \| `"jumps"` \| `"explore"` | `"fuel"` | |
| `holdMs` | number 0–120000 | `0` | Extra hold after the route is drawn. |

Most system pairs in the cluster are further apart than any stargate chain
reaches. If no path exists the run stops with a `route-not-found` error naming
the range the solver would have needed.

This action opens the Routing panel, because that is what a real user sees. If
you do not want it in frame, follow it with `{ "do": "setUi", "visible": false }`.

### `clearRoute`

Removes any drawn route, leaving the camera where it is. No arguments.

### `setUi`

Shows or hides the normal EF-Map interface (rail, top bar, panels, banners).

| Argument | Type | Required |
|---|---|---|
| `visible` | boolean | yes |

Director hides the interface with a presentation stylesheet rather than the
in-app Hide UI button, so the running sequence is never torn down mid-flight.
In a **preview**, a **Stop sequence** button stays on screen throughout. In a
**recording** that button is deliberately out of frame — use **Escape**. The interface
is always restored when the sequence ends, is stopped, or fails.

### `setLayer`

Turns one of the map's supported overlays on or off.

| Argument | Type | Required | Notes |
|---|---|---|---|
| `layer` | `"planetCounts"` \| `"stations"` \| `"regionHighlight"` \| `"smartGates"` | yes | |
| `enabled` | boolean | yes | |

`smartGates` also opens the Smart Gates panel, as it does for a real user.

### `setCinematicMode`

Enters or leaves EF-Map's dedicated **Cinematic Mode**: the full-screen
presentation experience.

| Argument | Type | Default |
|---|---|---|
| `enabled` | boolean | **required** |
| `holdMs` | number 0–120000 | `1200` |

**Cinematic Mode HIDES the stargate and smart-gate network.** That is the point
of it — it is for shots about the star field. If your shot is about a route, a
gate or the network, this action would delete the subject; use
`setCinematicEffects` instead.

### `setCinematicEffects`

Applies EF-Map's cinematic visual treatment to the **normal map** — the Display
Settings option of the same name.

| Argument | Type | Default |
|---|---|---|
| `enabled` | boolean | **required** |
| `holdMs` | number 0–120000 | `1200` |

**The stargate and smart-gate network stays visible.** This is usually what you
want for footage of routes, gates or the jump network: the same richer visuals as
Cinematic Mode, without losing the lines the shot is about.

### `followRoute`

Travels the route currently drawn on the map, flying to each of its systems in
order. **Requires a route** — run `planRoute` first.

| Argument | Type | Default | Notes |
|---|---|---|---|
| `holdMs` | number 0–120000 | `600` | Hold at each system after the camera arrives. |
| `maxSystems` | integer 2–40 | `12` | Refuse to travel a route containing more systems than this. |
| `skipFirst` | boolean | `true` | Skip the origin, which a preceding `planRoute` has usually already framed. |

`maxSystems` counts **systems, not jumps**: a route A → B → C is three systems and
two jumps. (It was called `maxHops` in an earlier draft. It always counted
systems, so the value carries over unchanged — only the name was wrong.)

This is the only action that follows a *calculated* route. A `focusSystem` from
origin to destination is one continuous fly-to that passes over the drawn line
without stopping anywhere; `followRoute` visits each system on the way. If you
want a tour of several systems that is **not** a calculated route, use repeated
`focusSystem` actions instead — see [Free-form travel versus route
travel](#free-form-travel-versus-route-travel).

If the route has more systems than `maxSystems`, the run stops with a clear error
rather than silently visiting only some of them — footage of a partial journey
presented as a complete one is exactly the kind of quiet lie this format avoids.
Raise `maxSystems`, or choose closer systems.

Because route length is not known until the solver has run, a sequence containing
`followRoute` shows an approximate duration in the panel.

### `setStarColor`

Changes how stars are coloured.

| Argument | Type | Required | Values |
|---|---|---|---|
| `mode` | string | yes | `"realistic"`, `"purple"`, `"white"`, `"blue"`, `"green"`, `"red"`, `"yellow"` |

### `repeat`

Runs a group of actions several times.

| Argument | Type | Required | Notes |
|---|---|---|---|
| `times` | integer 1–50 | yes | |
| `actions` | array (max 50) | yes | `repeat` may not appear inside a `repeat`. |

---

## 5. Execution semantics

**Actions run strictly in order.** The next action does not start until the
previous one has visibly finished, not merely been requested. `focusSystem` waits
for the camera to arrive; `planRoute` waits for the route solver to return a
result; `setCinematicMode` and `setCinematicEffects` wait for the treatment to
actually change; `followRoute` waits for the camera to arrive at each system in
turn. This matters
because EF-Map's internal command dispatcher returns immediately — Director
observes the resulting state instead of trusting the request.

**Timing.** Every action runs in order, then `tailHoldMs`. There is no lead-in:
operator pre-roll is the recording countdown's job, and holding an opening shot
is a `wait` action's job.
The duration shown in the panel is an estimate: actions that wait on a real
completion signal (`focusSystem`, `planRoute`) may take longer than estimated.

**Stopping.** **Escape** stops a run at any time — during a preview, during a
recording, and while the sequence has the interface hidden. In a preview you can
also click the **Stop sequence** button; during a recording that button is
deliberately out of frame, so Escape is the route. Stopping aborts immediately,
part-way through a hold or a camera move.

**What happens when a run ends.** Whether it finished, was stopped or failed:

- the normal interface comes back, and any auto-orbit that was paused resumes;
- visual settings the sequence changed — Cinematic Mode, Cinematic Effects, star
  colouring, overlays — are put back the way you had them;
- the **camera and any route are deliberately left as they ended**, so you can
  look at the result;
- the restore happens *after* `tailHoldMs`, so it can never alter the final frame
  the sequence was still holding.

**Failure.** The run stops at the failing action. Actions after it do not run.
The interface is restored regardless.

### Failure kinds

| Kind | Meaning |
|---|---|
| `invalid-json` | The text is not valid JSON. |
| `schema` | The JSON parsed but does not match this specification. |
| `unknown-system` | A named system does not exist. |
| `route-not-found` | The solver found no path at the given range. |
| `route-too-long` | A route was found, but it contains more systems than `followRoute`'s `maxSystems`. |
| `unsupported-action` | The `do` verb is not one of the verbs above. |
| `timeout` | An operation did not complete in time. |
| `not-ready` | The map is still loading, or another mode owns the camera. |
| `cancelled` | You pressed Stop, or Escape. |
| `internal` | Something unexpected went wrong. Please report it. |

---

## 6. What is and is not repeatable

**Repeatable:** the camera path. Starting with `reset` puts the camera at a known
pose, and all subsequent moves are computed from it, so two runs of the same
sequence follow the same path and hold the same framings.

**Not repeatable:** the exact pixels. EF-Map continuously animates its star
field, and both Cinematic Mode and Cinematic Effects add further continuous
motion. Live events and the
news ticker, if enabled, change over time. Two runs will look alike and cut
together fine, **but they are not frame-identical**, and Director does not claim
frame-deterministic rendering.

For a seamless loop, shoot one continuous take rather than trying to match two.

---

## 7. Safety

The Director format is deliberately smaller than EF-Map's internal automation
vocabulary. It **cannot**:

- execute JavaScript or any other code;
- reference CSS selectors, DOM elements or React internals;
- write arbitrary browser storage, preferences or settings;
- reach wallet, identity, subscription or tribe-scoped features;
- open arbitrary panels.

One honest caveat: `planRoute` opens the Routing panel and `setLayer` with
`smartGates` opens the Smart Gates panel, exactly as they would for a person
clicking. EF-Map remembers which panels are open, so running such a sequence
does leave that ordinary panel state behind.

`setCinematicEffects` and `setLayer` also write the preference behind the setting
they change, exactly as the equivalent checkbox would. Director puts those back
when the run ends, so the net effect is normally nil — but if you close the tab
mid-run the restore never happens and the preference stays written. Nothing else
is written.

Only the verbs in section 4 exist. Every argument is range-checked, every enum is
membership-checked, and unknown verbs and unknown arguments are both rejected.

---

## 8. Verified system names

System names change between game cycles, and only about 100 of the cluster's
24,000 systems have word-names — the rest are codes like `U6R-506`. The following
were verified against the shipped map database on 2026-08-04:

`Coherence`, `Virtue`, `Order`, `Koloh`, `Yzgor`, `Mraka`, `Sprag`, `Lisi`,
`Mandr`, `Ulam`, `Avarice`

How crowded a system's neighbourhood is decides how close the camera can sit,
and which way it should face. `distance` is in light years and so is the 400 ly
radius these counts are measured over, so they scale together:

| System | Region | Neighbours <=400 ly | Frame at | Best `bearingDeg` |
|---|---|---|---|---|
| `Ulam` | I682:C | 516 | 650 | 90 |
| `Lisi` | Vyzha Precinct | 140 | 950 | 90 |
| `Mandr` | N1LN:C | 84 | 1350 | 135 |
| `Sprag` | Komor Province | 61 | 900 | 90 |
| `Avarice` | ES.N8.SL | 58 | 1300 | 90 |

A sparse system framed tightly reads as an empty screen, which is why the thin
ones are framed wider. **Bearing matters as much as distance:** all five lie east
of the cluster's mass, so the default bearing of `0` points the camera away from
the stars, and for all of them a bearing between **45 and 135** looks best. That
column is measured, not estimated — all eight 45-degree bearings were swept at
each system's own framing in a real browser. `Mandr` at bearing `135` puts about
eight times as many stars in frame as bearing `0` does; `Sprag`, which sits above
the galactic slab, is *not* an exception to the rule, and `0` is its worst angle
of the eight. Keep orbits small enough to stay inside that band.

`Sprag` -> `Lisi` is 1,042 ly and `Lisi` -> `Mandr` is 764 ly, turning 76 degrees
in between; `Ulam` -> `Avarice` is 1,873 ly in another direction entirely.

**None of those pairs has a stargate path**, at any hop count. They are for
free-form `focusSystem` travel, where no connection is needed. For route travel
you need the verified pairs below.

These pairs are verified to route on **stargates alone** (`rangeLy: 0`). The
systems listed are the exact path the solver returns, which is what `followRoute`
will travel:

| From | To | Path |
|---|---|---|
| `Coherence` | `Virtue` | Coherence → Strife → Virtue |
| `Coherence` | `Order` | Coherence → Strife → Virtue → Order |
| `Koloh` | `Yzgor` | Koloh → Korost → Q:194N → Yzgor |

Note that the intermediate systems (`Strife`, `Korost`, `Q:194N`) are real
systems the route passes through; you do not need to name them yourself.

If you need a different system, search for it in EF-Map first. A name that does
not exist stops the run.

---

## 9. Complete examples

Every sequence below is one of the examples built into the Director panel, and is
executable exactly as written. A test validates each of them against the
validator on every build, so they cannot drift out of date.

### Slow galaxy overview

A wide, drifting establishing shot of the whole cluster. Interface left visible.

```json
{
  "schemaVersion": 1,
  "name": "Slow galaxy overview",
  "description": "Wide establishing drift across the cluster. Good opening shot for anything.",
  "tailHoldMs": 2000,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setCamera",
      "distance": 6200,
      "elevationDeg": 12,
      "fov": 70,
      "note": "wide and slightly above the plane"
    },
    {
      "do": "wait",
      "durationMs": 1800,
      "note": "hold the opening frame"
    },
    {
      "do": "orbit",
      "degrees": 40,
      "elevationDegrees": -6,
      "distanceScale": 0.82,
      "durationMs": 14000,
      "easing": "easeInOut"
    },
    {
      "do": "orbit",
      "degrees": 25,
      "elevationDegrees": 10,
      "distanceScale": 1.15,
      "durationMs": 10000,
      "easing": "easeInOut"
    }
  ]
}
```

### Focus and orbit a system

Fly to Coherence, settle, then orbit it slowly with a gentle push in. Interface hidden.

```json
{
  "schemaVersion": 1,
  "name": "Coherence hero shot",
  "description": "Approach one system and hold on it. The classic single-subject beauty shot.",
  "tailHoldMs": 2500,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setUi",
      "visible": false
    },
    {
      "do": "wait",
      "durationMs": 1200,
      "note": "hold the wide opening frame"
    },
    {
      "do": "focusSystem",
      "system": "Coherence",
      "holdMs": 900
    },
    {
      "do": "setCamera",
      "distance": 900,
      "elevationDeg": 18,
      "target": "selection",
      "durationMs": 3500,
      "easing": "easeInOut"
    },
    {
      "do": "orbit",
      "degrees": 120,
      "durationMs": 12000,
      "easing": "easeInOut",
      "note": "slow reveal around the star"
    },
    {
      "do": "dolly",
      "scale": 0.6,
      "fovDelta": -6,
      "durationMs": 5000,
      "note": "creep in to finish"
    }
  ]
}
```

### Tour three systems (no route)

Visits Sprag, Lisi and Mandr with an orbit at each and a pull-back between them. Nothing is planned and no route line is drawn.

```json
{
  "schemaVersion": 1,
  "name": "Three-system tour — Sprag, Lisi, Mandr",
  "description": "A free-form journey: repeated focusSystem actions with orbits and pull-backs between them. No planRoute, no followRoute, no line on the map. This is the pattern for a travelogue or a \"show me these places\" video, and the only one that works for systems with no stargate path between them — as these three have none.",
  "tailHoldMs": 3000,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setUi",
      "visible": false
    },
    {
      "do": "setCamera",
      "distance": 5200,
      "elevationDeg": 14,
      "fov": 72,
      "note": "establishing wide"
    },
    {
      "do": "wait",
      "durationMs": 1600,
      "note": "hold the opening frame"
    },
    {
      "do": "focusSystem",
      "system": "Sprag",
      "holdMs": 500
    },
    {
      "do": "setCamera",
      "distance": 900,
      "bearingDeg": 60,
      "elevationDeg": 14,
      "target": "selection",
      "durationMs": 3200,
      "easing": "easeInOut",
      "note": "Sprag sits above the slab; stay low and keep the cluster behind it"
    },
    {
      "do": "orbit",
      "degrees": 60,
      "durationMs": 9000,
      "easing": "easeInOut",
      "note": "stays inside the 45-135 bearing band, where the cluster is behind the subject"
    },
    {
      "do": "dolly",
      "scale": 3.6,
      "fovDelta": 4,
      "durationMs": 4000,
      "easing": "easeIn",
      "note": "pull away — this is what makes it read as leaving"
    },
    {
      "do": "focusSystem",
      "system": "Lisi",
      "holdMs": 500
    },
    {
      "do": "setCamera",
      "distance": 950,
      "bearingDeg": 120,
      "elevationDeg": 26,
      "target": "selection",
      "durationMs": 3200,
      "easing": "easeInOut",
      "note": "facing back towards the cluster mass"
    },
    {
      "do": "orbit",
      "degrees": -60,
      "elevationDegrees": 12,
      "durationMs": 8500,
      "easing": "easeInOut",
      "note": "reverse the swing so the two stops do not repeat"
    },
    {
      "do": "dolly",
      "scale": 3.4,
      "durationMs": 3600,
      "easing": "easeIn"
    },
    {
      "do": "focusSystem",
      "system": "Mandr",
      "holdMs": 500
    },
    {
      "do": "setCamera",
      "distance": 1350,
      "bearingDeg": 135,
      "elevationDeg": 30,
      "target": "selection",
      "durationMs": 3400,
      "easing": "easeInOut",
      "note": "thinner field here, so frame it wider"
    },
    {
      "do": "orbit",
      "degrees": -35,
      "distanceScale": 2.2,
      "elevationDegrees": -8,
      "durationMs": 10000,
      "easing": "easeInOut",
      "note": "closing composition: pull back and let the whole cluster rise behind Mandr"
    }
  ]
}
```

### Cinematic journey between systems

Cinematic Effects on, then one long crossing from crowded Ulam out to isolated Avarice. Slower rhythm than the tour, and still no route.

```json
{
  "schemaVersion": 1,
  "name": "Cinematic journey — Ulam to Avarice",
  "description": "Two systems 1,873 light years apart, with the cinematic treatment applied to the normal map so the gate network stays visible. Fewer and longer moves than the three-system tour, and a deliberate contrast: Ulam sits in the densest star field of any named system, Avarice in near-empty space. No route is planned — there is no stargate path between them, which is exactly when free-form travel is the only option.",
  "tailHoldMs": 3500,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setUi",
      "visible": false
    },
    {
      "do": "setCinematicEffects",
      "enabled": true,
      "holdMs": 1800,
      "note": "Effects, not Mode — the gate lines stay"
    },
    {
      "do": "setCamera",
      "distance": 6000,
      "elevationDeg": 8,
      "fov": 78,
      "durationMs": 3000,
      "easing": "easeInOut"
    },
    {
      "do": "focusSystem",
      "system": "Ulam",
      "holdMs": 800
    },
    {
      "do": "setCamera",
      "distance": 650,
      "bearingDeg": 90,
      "elevationDeg": 22,
      "target": "selection",
      "durationMs": 4200,
      "easing": "easeInOut",
      "note": "sit close: 65 neighbours inside 150 ly"
    },
    {
      "do": "orbit",
      "degrees": 45,
      "elevationDegrees": 14,
      "durationMs": 11000,
      "easing": "easeInOut",
      "note": "slow rise out of a very crowded field"
    },
    {
      "do": "dolly",
      "scale": 6,
      "fovDelta": 6,
      "durationMs": 6000,
      "easing": "easeInOut",
      "note": "long pull back before the crossing"
    },
    {
      "do": "wait",
      "durationMs": 800
    },
    {
      "do": "focusSystem",
      "system": "Avarice",
      "holdMs": 900,
      "note": "the crossing, into empty space"
    },
    {
      "do": "setCamera",
      "distance": 1300,
      "bearingDeg": 90,
      "elevationDeg": 15,
      "target": "selection",
      "durationMs": 4500,
      "easing": "easeInOut",
      "note": "isolated: pull back and let the near field carry it"
    },
    {
      "do": "orbit",
      "degrees": 40,
      "distanceScale": 1.9,
      "fovDelta": -4,
      "durationMs": 14000,
      "easing": "easeInOut",
      "note": "closing orbit swings TOWARDS the cluster, so Avarice ends on the edge of the void"
    }
  ]
}
```

### Plot and showcase a route

Plan Coherence to Virtue over stargates, then pull back to reveal the whole path.

```json
{
  "schemaVersion": 1,
  "name": "Route showcase — Coherence to Virtue",
  "description": "Shows the route planner doing real work, then frames the finished path.",
  "tailHoldMs": 3000,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "focusSystem",
      "system": "Coherence",
      "holdMs": 800
    },
    {
      "do": "planRoute",
      "from": "Coherence",
      "to": "Virtue",
      "rangeLy": 0,
      "optimize": "fuel",
      "holdMs": 1200
    },
    {
      "do": "setUi",
      "visible": false,
      "note": "hide the routing panel now the route is drawn"
    },
    {
      "do": "dolly",
      "scale": 3.4,
      "durationMs": 6000,
      "easing": "easeOut",
      "note": "pull back to reveal the path"
    },
    {
      "do": "orbit",
      "degrees": 55,
      "elevationDegrees": 14,
      "durationMs": 11000
    }
  ]
}
```

### Travel a route system by system

Plots Koloh to Yzgor, then actually flies the route: Koloh, Korost, Q:194N, Yzgor. Cinematic Effects keeps the gate lines visible.

```json
{
  "schemaVersion": 1,
  "name": "Riding the line — Koloh to Yzgor",
  "description": "Plots a stargate route and then travels it, stopping at each system on the way. Uses Cinematic Effects rather than Cinematic Mode, because Cinematic Mode hides the very gate network this shot is about.",
  "tailHoldMs": 3500,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setCinematicEffects",
      "enabled": true,
      "holdMs": 1500
    },
    {
      "do": "setUi",
      "visible": false
    },
    {
      "do": "focusSystem",
      "system": "Koloh",
      "holdMs": 600
    },
    {
      "do": "planRoute",
      "from": "Koloh",
      "to": "Yzgor",
      "rangeLy": 0,
      "holdMs": 1500
    },
    {
      "do": "setCamera",
      "distance": 1400,
      "elevationDeg": 22,
      "target": "selection",
      "durationMs": 3000
    },
    {
      "do": "orbit",
      "degrees": 45,
      "durationMs": 6000,
      "note": "look along the line before travelling it"
    },
    {
      "do": "followRoute",
      "holdMs": 1100,
      "maxSystems": 12,
      "note": "the actual journey, stop by stop"
    },
    {
      "do": "dolly",
      "scale": 2.6,
      "durationMs": 6000,
      "easing": "easeOut",
      "note": "pull back on arrival"
    },
    {
      "do": "orbit",
      "degrees": -40,
      "elevationDegrees": 12,
      "durationMs": 9000
    }
  ]
}
```

### Cinematic Mode beauty pass

The dedicated presentation mode: richest visuals, and NO gate lines. For shots about the stars rather than the network.

```json
{
  "schemaVersion": 1,
  "name": "Cinematic Mode beauty pass",
  "description": "Cinematic Mode hides the stargate and smart-gate network, which is exactly what you want when the subject is the star field itself. Use setCinematicEffects instead if your shot needs the gate lines.",
  "tailHoldMs": 3000,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setUi",
      "visible": false
    },
    {
      "do": "setCinematicMode",
      "enabled": true,
      "holdMs": 2200,
      "note": "gate lines disappear here"
    },
    {
      "do": "setCamera",
      "distance": 4200,
      "elevationDeg": 16,
      "fov": 70,
      "durationMs": 4000
    },
    {
      "do": "orbit",
      "degrees": 70,
      "elevationDegrees": -8,
      "distanceScale": 0.75,
      "durationMs": 16000
    },
    {
      "do": "dolly",
      "scale": 1.6,
      "fovDelta": 6,
      "durationMs": 7000,
      "easing": "easeOut"
    }
  ]
}
```

### Tall-frame composition (UI hidden)

Steep angle and tight subject, composed for a portrait-shaped window. Director does not change your window or recording size — you do that.

```json
{
  "schemaVersion": 1,
  "name": "Tall-frame composition — Koloh",
  "description": "Composed for a portrait-shaped frame. Director cannot resize your browser or your recorder: size the window (or your capture region) yourself first. The cluster is a flat disc, so a steep elevation is what fills a tall frame — a shallow angle leaves the bottom third empty.",
  "tailHoldMs": 1500,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setUi",
      "visible": false
    },
    {
      "do": "setCamera",
      "distance": 5200,
      "elevationDeg": 55,
      "fov": 80,
      "note": "steep, to fill a tall frame"
    },
    {
      "do": "wait",
      "durationMs": 1400,
      "note": "hold the opening frame"
    },
    {
      "do": "focusSystem",
      "system": "Koloh",
      "holdMs": 700
    },
    {
      "do": "setCamera",
      "distance": 700,
      "elevationDeg": 40,
      "target": "selection",
      "durationMs": 3200
    },
    {
      "do": "repeat",
      "times": 2,
      "actions": [
        {
          "do": "orbit",
          "degrees": 90,
          "durationMs": 7000,
          "easing": "linear",
          "note": "linear so the two halves match"
        }
      ]
    }
  ]
}
```

### Tutorial (interface visible)

Interface stays on screen while a route is planned. In a recording run the Director panel and cursor are still kept out of frame.

```json
{
  "schemaVersion": 1,
  "name": "Tutorial — planning a route",
  "description": "Leaves the normal interface visible so viewers can see the panels the narration is describing. Nothing here hides the UI; \"Run for recording\" still keeps Director's own panel, its Stop button and the mouse cursor out of the footage.",
  "tailHoldMs": 4000,
  "actions": [
    {
      "do": "reset"
    },
    {
      "do": "setCamera",
      "distance": 5000,
      "elevationDeg": 20,
      "durationMs": 2500
    },
    {
      "do": "wait",
      "durationMs": 1800,
      "note": "room for \"here is the cluster\""
    },
    {
      "do": "focusSystem",
      "system": "Coherence",
      "holdMs": 2500,
      "note": "room for \"pick your origin\""
    },
    {
      "do": "planRoute",
      "from": "Coherence",
      "to": "Order",
      "rangeLy": 0,
      "optimize": "fuel",
      "holdMs": 3000
    },
    {
      "do": "dolly",
      "scale": 2.2,
      "durationMs": 5000,
      "easing": "easeOut",
      "note": "reveal the whole route"
    },
    {
      "do": "wait",
      "durationMs": 2500
    }
  ]
}
```

---

## 10. Not currently supported

- **Fly-through mode.** EF-Map's fly-through rig drives the camera continuously
  and never ends on its own, so it cannot share the camera with a scripted move.
  Director refuses camera actions while a fly-through is running rather than
  fighting it and producing a stutter. To travel a route, use `planRoute`
  followed by `followRoute` — that visits each system on the way, which is a
  real journey rather than a fly-through.
- **Setting the window or video size.** Director cannot resize your browser or
  your recorder. Size the window, or set your recorder's capture region, before
  running — including for portrait/vertical video, where you should also compose
  with a steep `elevationDeg` (around 50–60) because the cluster is a flat disc.
- **Text, captions, music and transitions.** Add those in your editor.
- **Solar-system view.** Director drives the universe-view camera only.

---

## 11. For automation

A validated JSON-only surface is available in the page as `window.__efDirector`:

```js
window.__efDirector.schemaVersion            // 1
window.__efDirector.actions                  // supported verb names
window.__efDirector.validate(jsonOrObject)   // { ok, issues, estimatedMs }
window.__efDirector.run(jsonOrObject, { mode: 'preview' })
window.__efDirector.stop()
window.__efDirector.reset()
window.__efDirector.state()                  // current run state
window.__efDirector.loadExample('route-showcase')
```

`run` takes the same validation path as the panel; there is no way to reach an
action through it that the panel could not also reach, and no way to skip
validation.
