bevy_director 0.7.0-dev

Unreal-Sequencer-inspired cinematic camera and sequence system for Bevy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# bevy_director

Camera director, shots, sequences: an Unreal-Sequencer-inspired
cinematic layer for Bevy 0.19. Sequences are RON assets made of shots,
shots are camera rigs (eased keys, constant-speed dolly rails, orbits)
with a film-literate lens (focal length against a filmback, focus pulls,
aperture, exposure), and playback hands off from your gameplay camera and
glides back onto it when the take ends.

## Quickstart

```rust,ignore
use bevy::prelude::*;
use bevy_director::prelude::*;

app.add_plugins(DirectorPlugin);

// Let the director borrow the frame: gate your own camera-driving
// system so it yields during a take and runs during the glide home.
app.add_systems(Update, my_camera_rig.run_if(gameplay_camera_free));

// Anywhere, later:
commands.play_sequence(asset_server.load("sequences/intro.dir.ron"));
```

The director finds your active `Camera3d`, snapshots it, and drives that
same camera in place for the take — your clear color, tonemapping,
bloom, skybox, and the rest of the post stack never leave the frame,
because no camera swap happens. At the end it glides back to the pose it
took over and hands everything back: transform, projection, viewport,
and any depth-of-field or exposure the camera already had.

Spawn your own `CineCamera` entity when you want a dedicated cinematic
camera instead — a separate post stack, or a gameplay camera that is
parented to a rig (directed poses are world space and would fight the
parent). The director then adopts it and swaps `is_active` across the
handoff, blending home toward wherever your live camera has got to,
sampled every frame. Control can return to the player while the camera
is still gliding.

Listen for `SequenceStarted`, `MarkerReached { name, .. }` (cues,
stingers; on-screen text has its own track now), and
`SequenceFinished { reason }` (story flags belong here, never on
markers). `commands.skip_sequence()` jumps to the end and takes
the normal blend home; `stop_sequence()` cuts hard.

## Director's Cut (feature `editor`, opt-in)

Director's Cut is the docked, mouse-driven authoring surface. It uses the
same `ViewfinderSession`, evaluator, and `.dir.ron` assets as the runtime,
so there is no conversion or editor-only project format.

```rust,ignore
use bevy::prelude::*;
use bevy_director::prelude::*;

App::new()
    .add_plugins((DefaultPlugins, DirectorsCutPlugin))
    .run();
```

Run the included stage with:

```text
cargo run --example directors_cut --features editor
```

Press Backquote to possess the camera; Director's Cut opens automatically.
Press Tab to switch between the editor cursor and camera piloting.

| Action | Control |
| --- | --- |
| Move a shot | Drag its body |
| Resize a shot | Drag its bright right edge |
| Retime a transform or lens key | Drag the diamond |
| Scrub | Click or drag the timeline |
| Edit camera values | Select a key; use the inspector for position, rotation, FOV/focal value, and time |
| Capture the current camera | Key button (the key lands in the shot under the playhead) |
| Auto-key | Auto button — while piloting (Tab), moving the camera keys the shot under the playhead automatically |
| Add a camera cut | Shot button |
| Add a text block at the playhead | Text button |
| Move / resize a text block | Drag its body / its bright right edge |
| Edit a text block | Select it; the inspector has start, duration, fades, size, and anchor; click the content to type, Enter confirms |
| Shake a shot | Select it; the inspector's SHAKE section toggles handheld on and sets amplitude, frequency, position, seed, fov wobble, focus hunt, and a build/fade ramp |
| Vignette, distortion, aberration, motion blur, grading | Select the shot; the FX section. Every value previews live while scrubbing |
| Focus, aperture, exposure, depth of field | Select the shot; the LENS section |
| Swap keyframed / held, ease a mount | Select the shot; the Rig row cycles, and a held shot gains Mount ease |
| Type an exact value | Click the number on any value row; Enter confirms, Escape cancels |
| Preview | Play button or Space |
| Freeze or resume the game world | Freeze button (pauses `Time<Virtual>`) |
| Undo / redo | Ctrl+Z / Ctrl+Y |
| Open a sequence | Open button or Ctrl+O lists the folder's `.dir.ron` files; click one to load it; Escape closes |
| Save | Ctrl+S |
| Rename the sequence file | Click the pencil filename; Enter confirms and Escape cancels |
| Zoom timeline | zoom-out / zoom-in buttons in the toolbar |

Every numeric row takes a typed value as well as the -/+ nudges: click
the number, type it, press Enter. Typed values go through the same
clamps the buttons use, so typing cannot reach anywhere stepping could
not, and angles read in degrees the way the row shows them. Rows that
cycle a word rather than a number (Rig, Focus, DoF, Ramp, Anchor) step
only.

The inspector scrolls once a shot has more to say than the column is
tall. Shot FX are constant over the shot — keyed curves for those
channels are still authored in RON — and stepping a value down to
nothing clears it: an effect dialed to zero and an absent one render the
same, so the field leaves the file entirely.

New/Open/Save use `assets/sequences/<sequence name>.dir.ron` by default.
The Open panel lists that folder; loading a file also renames the
working take after it, so a following Ctrl+S writes back to the same
file. The filename field accepts either a plain name or the full
`.dir.ron` filename; the extension is added exactly once.
Set `DirectorsCutConfig::sequence_path` when a fixed path is preferable;
the Open panel then lists that file's folder instead.
The editor feature is not enabled by default, keeping UI and picking out
of shipping builds unless requested.

### Integrating with a live game

The editor runs inside your game, over the live scene. That is the
point: you frame shots against real lighting, real actors, real physics.
It also means the game still sees the same keyboard and mouse while you
direct. Anything you do not gate will fight the editor: the character
walks away while you fly the camera on WASD, or a click-to-grab-cursor
handler locks the pointer mid-drag and the timeline scrub sticks.

The crate tells you when to stand down. `DirectorState.phase` says who
owns the frame, and three run conditions wrap the common cases:

| Condition | True when | Gate with it |
| --- | --- | --- |
| `gameplay_camera_free` | `Idle` or `Handback` | your camera-driving system |
| `director_idle` | `Idle` only | gameplay input, cursor grabbing, click-to-act handlers |
| `director_active` | anything but `Idle` | editor-only helpers |

The prerequisites, in order of pain avoided:

1. **Yield the camera.** Run your camera rig under
   `gameplay_camera_free`, as in the quickstart. That condition stays
   true through `Handback` so your rig is somewhere real to glide to —
   which is what a dedicated `CineCamera` wants. When the director is
   borrowing your camera there is nothing to chase (it glides back to
   the pose it took over), so gate the rig on `director_idle` instead
   and it will not fight the blend.
2. **Yield the keyboard and mouse.** While the director is not `Idle`,
   stop reading gameplay input. Per-system `run_if(director_idle)`
   works; if your game has a state machine, one bridge system is
   cleaner because every state-gated system and input context follows
   automatically:

   ```rust,ignore
   fn yield_to_director(
       director: Res<DirectorState>,
       state: Res<State<GameState>>,
       mut next: ResMut<NextState<GameState>>,
   ) {
       match (director.phase, state.get()) {
           (DirectorPhase::Viewfinder, GameState::Playing) => {
               next.set(GameState::Directing)
           }
           (DirectorPhase::Idle, GameState::Directing) => {
               next.set(GameState::Playing)
           }
           _ => {}
       }
   }
   ```

3. **Leave the cursor alone.** The viewfinder manages the cursor
   itself: locked while piloting, free while the editor cursor is up,
   and your previous grab state is restored on eject. A gameplay system
   that re-locks the cursor on click breaks timeline dragging; gate it
   like the rest of the input.
4. **Drive gameplay off the default clock.** The Freeze button pauses
   `Time<Virtual>`, so actors hold still while you frame a shot.
   Systems that read `Time<Real>` ignore the freeze. The editor itself
   runs on real time, so flying, scrubbing, and previewing keep working
   in a frozen world; ejecting resumes the clock if the editor paused
   it. A physics engine with its own clock (avian's `Time<Physics>`,
   for example) needs a small mirror system that pauses it whenever
   `Time<Virtual>` is paused.

Key bindings that overlap are harmless once input is gated: the
editor's pilot/editor toggle defaults to Tab, and if your game also
binds Tab, the two never see it at the same time. Rebind via
`DirectorsCutConfig::toggle` if you want a different key.

`DirectorsCutState` is a public resource; `open` tells you whether the
dock is up if you want to gate anything the run conditions do not
cover.

### Development vs shipping

Enabling a Cargo feature only makes its code available. A tool becomes
active—and starts reading its shortcuts—only when its plugin is added to
the app.

| Setup | What runs |
| --- | --- |
| `DirectorPlugin` | Sequence playback only. No authoring shortcuts or UI. |
| `(DirectorPlugin, ViewfinderPlugin)` | Playback plus the keyboard/mouse capture mode. Backquote opens the viewfinder. |
| `DirectorsCutPlugin` | The complete authoring setup: runtime, viewfinder, timeline UI, Bevy 0.19's infinite grid, and editor shortcuts. It installs the other required plugins automatically. |

For a game that authors cinematics during development but ships only the
RON player, expose the editor through a game-specific feature:

```toml
[features]
director_editor = ["bevy_director/editor"]

[dependencies]
# default-features = false drops the viewfinder key and the gizmos too;
# keep "titles" so the shipped build still draws the sequence's text.
bevy_director = { version = "0.5", default-features = false, features = [
    "titles",
] }
```

Select the plugin at compile time:

```rust,ignore
#[cfg(feature = "director_editor")]
app.add_plugins(DirectorsCutPlugin);

#[cfg(not(feature = "director_editor"))]
app.add_plugins(DirectorPlugin);
```

Use the authoring build to create and save the asset:

```text
cargo run --features director_editor
```

Then run or release the normal game without that feature:

```text
cargo run
cargo build --release
```

The shipping build loads and plays the saved asset normally:

```rust,ignore
commands.play_sequence(asset_server.load("sequences/intro.dir.ron"));
```

It contains no Director's Cut UI or editor shortcuts because
`DirectorsCutPlugin` is not installed. This also makes Director's Cut
usable as a temporary authoring tool whose only shipped output is the
`.dir.ron` sequence.

The editor's infinite grid appears only while the viewfinder is active.
Disable it when the game already supplies an authoring grid:

```rust,ignore
app.insert_resource(DirectorsCutConfig {
    show_grid: false,
    ..default()
});
```

The spawned entity is marked with `DirectorsCutGrid`; you can despawn it
and add your own `InfiniteGrid` and `InfiniteGridSettings` when you need
custom colors, spacing, or fade distance.

## Viewfinder (feature `viewfinder`, on by default)

The director's chair, no widgets: press Backquote in your running game.

| Key | Action |
| --- | --- |
| Backquote | Toggle the viewfinder (possesses the camera at the current view). Rebind with `ViewfinderConfig::toggle`, or set it to `None` so no key opens it |
| WASD + mouse | Fly and look |
| Q / E | Sink / rise |
| Wheel | Fly speed (Ctrl+Wheel: fov) |
| K | Capture the framing as a key at the playhead (into the shot under it) |
| (auto-key) | On by default: flying or Ctrl+wheel writes the key at the playhead without pressing K; set `ViewfinderConfig::auto_key = false` to opt out |
| [ / ] | Scrub (Shift: single frame) |
| P | Preview the working take |
| Delete | Remove the nearest key |
| Ctrl+S | Write `assets/sequences/<name>.dir.ron` (native only) |

Gizmos draw every shot's path, keys, aim rays, and the playhead ghost
while the viewfinder is up.

## `.dir.ron` format

```ron
SequenceAsset(
    name: "intro",
    shots: [
        Shot(
            start: 0.0, duration: 8.0,
            rig: Rail(
                points: [(-2.0, 2.0, -6.0), (0.0, 2.2, 0.0), (2.0, 2.5, 6.0)],
                constant_speed: true,
                progress: ScalarTrack(keys: [
                    ScalarKey(time: 0.0, value: 0.0, ease: SmoothStep),
                    ScalarKey(time: 8.0, value: 1.0),
                ]),
            ),
            look: At(target: Entity("gate"), damping: Some(8.0)),
            lens: Lens(
                fov: FocalLengthMm(track: ScalarTrack(keys: [ScalarKey(time: 0.0, value: 35.0)])),
                focus: Some(Target(target: Entity("gate"))),
                aperture_f_stops: Some(ScalarTrack(keys: [ScalarKey(time: 0.0, value: 2.0)])),
                // Filmic extras, all optional and all put back after.
                vignette: Some(ScalarTrack(keys: [ScalarKey(time: 0.0, value: 0.4)])),
                aberration: Some(ScalarTrack(keys: [ScalarKey(time: 0.0, value: 0.02)])),
                motion_blur: Some(MotionBlurSpec(shutter_angle: 0.5, samples: 2)),
                grading: Some(GradeTrack(
                    temperature: Some(ScalarTrack(keys: [ScalarKey(time: 0.0, value: 0.2)])),
                )),
            ),
        ),
        Shot(
            // Cut to a CineCamera the level already placed.
            start: 8.0, duration: 4.0,
            camera: Some("crane"),
            rig: Keys(keys: [Key(time: 0.0, pos: (6.0, 3.0, 2.0))]),
            look: At(target: Entity("gate")),
        ),
    ],
    markers: [Marker(time: 1.0, name: "stinger_one")],
    texts: [
        TextBlock(
            start: 1.0, duration: 3.0,
            text: "THE GATE",
            anchor: Center,
            fade_in: 0.5, fade_out: 0.5,
        ),
    ],
    actors: [
        ActorTrack(
            target: Entity("hero"),
            cues: [
                ActorCue(start: 0.0, duration: 6.0, anim: "sleeping", fade_out: 0.4),
                ActorCue(start: 6.0, duration: 3.0, anim: "wake_up", fade_in: 0.4),
            ],
        ),
    ],
    blend_out: Some(Blend(secs: 1.5, ease: SmoothStep)),
)
```

Rules worth knowing: shot starts are absolute and sorted; a shot with no
`blend_in` cuts hard; during a blend the previous shot is sampled
clamped (extend it into the overlap for a moving crossfade); markers
fire only while playing across them (skips and seeks never retro-fire);
text blocks are the opposite — level-triggered, so whatever the playhead
sits inside is visible at the fade-resolved alpha, and they may overlap
each other and span camera cuts (their window is half-open, and anything
past the last shot's end never shows because playback stops there);
actor cues follow the same level-triggered rule as text blocks;
`constant_speed` rails cover equal distance in equal time via an
arc-length table, the piece Bevy itself does not ship.

## Text overlays

Text blocks carry a duration, fades, an anchor (Center for title cards,
LowerThird for subtitles, TopCenter for location stamps), and a style
(size, color, shadow, optional background box). The core crate always
parses and evaluates them: every frame, the `ActiveTexts` resource holds
the blocks visible right now with their fade alphas, which is the whole
contract for rendering captions with your own UI (localization included:
treat `text` as a key and look it up when you draw).

The `titles` feature — on by default — adds the built-in renderer,
`TitlesPlugin`: bevy_ui text on the directed camera with the fades
applied for you. `DirectorPlugin` installs it, so text a sequence
carries renders in the game with no further wiring and no editor;
`DirectorsCutPlugin` inherits the same renderer, so the TEXT lane
previews exactly what ships. Because the overlay targets the directed
camera, layout happens inside its viewport, so anchors respect the
letterbox crop with no extra work. It enables Bevy's `default_font` so
text renders out of the box; set `TitlesConfig::font` to ship your own
face (if you assemble Bevy features by hand and text is invisible, the
missing default font is why).

To render captions yourself instead, take the crate with
`default-features = false` and read `ActiveTexts` — the evaluation still
runs. Markers remain the right tool for cues and stingers; captions
belong to text blocks.

## Actor tracks

A sequence can drive more than the camera. An `actors:` track aims named
animation cues at world entities — the classic opening where the camera
pushes in on a sleeping character, she plays a wake-up animation, and
control returns to the player once the take hands back.

The crate stays out of your animation setup entirely. It never touches an
`AnimationPlayer`; instead, every frame it publishes the `ActiveActorCues`
resource — the cues the playhead is inside right now, each with its
target name, blend weight, cue-local time, and a stable
`(track, cue_index)` identity. Your game reads it and does the playing:

```rust
fn drive_cutscene_actors(
    cues: Res<bevy_director::ActiveActorCues>,
    names: Query<(Entity, &Name)>,
    mut commands: Commands,
    // ... your animation graph / clip lookup ...
) {
    for cue in &cues.cues {
        let bevy_director::TargetRef::Entity(target) = &cue.target else { continue };
        // Find the entity named `target`, look up `cue.cue.anim` in your
        // own clip set, and play it. Derive "just started" / "just ended"
        // by tracking which (cue.track, cue.cue_index) pairs you saw last
        // frame — the resource is level-triggered, so there are no edges
        // to miss and seeks/skips/loops need no special handling.
    }
}
```

Because it is level-triggered, a skip or a scrub simply changes which
cues are reported; when a take ends (or is skipped or stopped) the
resource empties, which is your signal to return the actor to gameplay
control. `TargetRef::Point` is reported as authored but never names a
performer — match on `Entity`. Fade in/out shape `cue.weight` for games
that blend by weight; `cue.cue.speed` is a playback-rate hint. Author
cues by hand in the `.dir.ron` file or on the Director's Cut ACTORS lane
(drag to move/resize, inspector rows for timing and speed, inline edits
for the anim name and target); the viewfinder previews them live as you
scrub, so a game binding the cues sees the character perform in editor.

## Multi-camera cuts

Cut-based coverage, the way a cutscene in a Resident Evil-shaped game is
built: place the cameras in the level, then cut between them.

```rust,ignore
commands.spawn((
    Name::new("crane"),
    CineCamera,
    Camera3d::default(),
    Camera { is_active: false, ..default() },
    // ...this camera's own bloom, grade, clear color: kept across the cut
));
```

A shot naming that camera (`camera: Some("crane")`, or `.camera("crane")`
in code) takes the frame for its span. The take's playback stays on its
anchor camera — the one it borrowed or adopted at the start — so
`DirectorState::camera` is whichever camera is on air right now and
`anchor` is where the playhead lives. Titles and the letterbox follow the
cut, and every camera a take visited is put back the way it was found
when the take ends.

Cuts are hard. A `blend_in` authored across a camera change would be
mixing poses meant for two different cameras, so it is ignored and said
so at bake time; likewise a take that ends while cut away hands back
without a glide. Cross-camera dissolves need render-target compositing
and are not here yet.

Listen for `SequenceCut { from, to, shot }` to land a stinger or a rumble
on the cut. Naming a camera no entity carries holds the current one
rather than cutting to black. `cargo run --example multicam` runs the
whole thing.

### Held cameras

A cut target does not have to be driven at all. `Rig::Held` keeps the
camera's own pose and fov — the director cuts to it, shakes it, and
pulls its focus, nothing more. Because directed poses land on the local
`Transform`, a held camera parented to a moving entity rides its mount:

```rust,ignore
// Spawned as a child of the bike:
bike.spawn((Name::new("mirror"), CineCamera, Camera3d::default(),
    Camera { is_active: false, ..default() },
    Transform::from_xyz(0.8, 1.2, 0.25)));

// In the sequence — the mirror shot, shaking with the ride:
let mut shot = Shot::held(4.0, 3.5).mount_damping(7.0).camera("mirror");
shot.shake = Some(Shake { amplitude_deg: 1.0, frequency_hz: 9.0, ..  });
```

A rigid mount inherits every jolt, which on anything that leans is too
much. `mount_damping` softens it: the camera keeps its mount point but
its aim lags the mount's rotation, the way a gimbal does. Higher is
tighter; leave it off to weld the camera down. Position is never damped —
a filter on it would trail the mount at any steady speed.

Shake itself can do more than constant handheld: `ramp` keys a 0..1
multiplier over the shot, `fov_amplitude_deg` makes the zoom breathe,
and `focus_amplitude` makes a focus track hunt — the erratic snap-zoom
look. Low frequencies read as an operator hunting the frame; high ones
read as buzz. All of it is seeded and deterministic, so scrubs and
replays are identical.

**Shaking a zoom.** Amplitude is in degrees, and a long lens sees a much
smaller slice of the world, so the same shake covers far more of the
frame as you push in — on Super35, 0.8 deg is about 2% of frame height
at 24mm and 15% at 200mm. A shake that looks like handheld at the wide
end is a seizure at the long end. Ramp it *down* as the lens gets long:
`bike_intro`'s vista shot blooms to full while the zoom is still wide,
then decays to 0.07 by 200mm, which holds the apparent movement between
roughly 5% and 1% of the frame across the whole move.
`cargo run --example bike_intro` plays the whole storyboard: tracking
pan, mirror cam, and a 24mm→200mm vista zoom with building shake.

## Glossary for Unreal users

| Unreal Sequencer | bevy_director |
| --- | --- |
| Level Sequence asset | `SequenceAsset` (`.dir.ron`) |
| Sequencer (the editor) | Director's Cut (`editor` feature) |
| Camera Cuts track | the shot list, with `shot.camera` naming the cut |
| CineCameraActor | a `CineCamera` entity, found by its `Name` |
| Ease In/Out on a section | `Blend` (`blend_in` / `blend_out`) |
| Camera Rail rig | `Rig::Rail` |
| Crane / orbit rig | `Rig::Orbit` |
| Transform keyframes | `Rig::Keys` |
| Look-at Tracking | `Look::At { target, damping }` |
| Filmback | `Filmback` presets (Super35 default) |
| Focal Length | `FovSpec::FocalLengthMm` |
| Focus Settings | `FocusTrack` -> bevy `DepthOfField` |
| Event track | `Marker` -> `MarkerReached` |
| Text / subtitle track | `TextBlock` (`texts:`), rendered by the `titles` feature |
| Skeletal / animation track | `ActorTrack` (`actors:`) -> `ActiveActorCues`, played by your game |
| Play Rate | `SequencePlayer::rate` (negative plays backward) |
| Possess / Eject | the handoff and `DirectorPhase::Handback` |
| Pilot camera | viewfinder fly mode |
| Camera Shake | `Shake` (seeded, deterministic; keyed ramp, fov and focus wobble) |

## Features

- `viewfinder` (default): the in-game capture mode. Implies `gizmos`.
- `gizmos` (default): path and key overlay drawing.
- `titles` (default): the built-in text overlay renderer (`TitlesPlugin`),
  installed by `DirectorPlugin` itself so a sequence's text shows up in a
  plain game build. Pulls bevy_ui/bevy_text and the default font; turn it
  off with `default-features = false` if you draw captions from
  `ActiveTexts` yourself. The text data model and `ActiveTexts`
  evaluation are in the core crate regardless.
- `editor` (opt-in): Director's Cut dock, camera inspector, timeline,
  undo/redo, and the Open panel. Implies `viewfinder` and `titles`.

With everything off, the crate is the data model, the evaluator, the
RON loader, and the playback state machine: enough to ship cutscenes.

## Compatibility

| bevy | bevy_director |
| --- | --- |
| 0.19 | 0.1–0.4 |

## License

Dual-licensed under MIT or Apache-2.0, at your option.