Skip to content

View state and drawing

A beat is a single step of the story: everything the reader sees at one place in the screenplay.

type Beat = {
characters: Array<CharacterLexia>;
content: Record<number, null | Partial<ContentLexia>>;
expressionValues: Record<string, any>;
lexiaRef: null | LexiaRef;
slugline: null | SluglineLexia;
transition: null | TransitionLexia;
};
FieldDescription
charactersThe characters in the scene, with their current labels.
contentThe dialogue and narration lines shown, keyed by line number, including any changes made by label handlers. null hides a line.
expressionValuesThe value of each inline expression, keyed by expression ID.
lexiaRefThe beat’s position in the screenplay.
sluglineThe scene heading for the beat.
transitionThe transition into the beat.

View state holds the information your interface needs. Every plugin reads and writes the same object, and interface code watches it with onChange to update as it changes.

View state isn’t saved in bookmarks.

To add your own fields, extend the ViewState interface:

declare module "@inkweaverdev/inkweaver-sdk" {
interface ViewState {
scoreboard: { points: number };
}
}

Returns the current view state.

getViewState(): ViewState
Example
const { scoreboard } = getViewState();

Merges fields into the view state, and returns the result. The merge is shallow.

updateViewState(partialViewState: Partial<ViewState>): ViewState
Example
updateViewState({ scoreboard: { points: 10 } });

The canvas is a stack of slots, drawn back to front. Each slot shows one picture at a time, and a frame key names a slot.

To add a slot of your own, extend the FrameKeys interface. Every key you add becomes part of the FrameKey type.

declare module "@inkweaverdev/inkweaver-sdk" {
interface FrameKeys {
"title-card": true;
}
}

A draw returns a DrawUpdate, which says what each slot on the canvas should show. The renderer keeps showing what it was given until something replaces it, so an update only needs to describe what changed.

type DrawUpdate = {
id: number;
order?: Array<FrameKey>;
slots: SlotUpdate;
timers?: TimerDeclarations;
};
type SlotUpdate = Partial<Record<FrameKey, null | SlotContent>>;
FieldDescription
idIncreases with every draw. The renderer ignores an update with a lower id than one it has already applied, so a slow draw can’t undo a newer one.
orderThe slots to draw, back to front. If left out, the order doesn’t change.
slotsWhat each slot should show. A slot that’s left out keeps what it has. A slot set to null is cleared immediately, with no transition.
timersNamed timers to declare or remove. See Timers.
type SlotContent = {
enter?: Array<ShaderInstruction>;
frame: null | Frame;
occurrence: string;
replace?: Array<ShaderInstruction>;
};
FieldDescription
enterShaders for the arriving frame.
frameThe picture the slot should show, or null to leave the slot empty once the current frame has left.
occurrenceIdentifies what the slot is showing.
replaceShaders for the frame being replaced. It keeps drawing until they finish.

occurrence decides whether a draw starts a transition:

  • The same occurrence swaps the picture in place. Nothing transitions, and running animations carry on. Drawing the same thing again is always safe, so you can draw at any time.
  • A new occurrence means the slot is showing something new. The old frame stays underneath, drawn with replace, while the new frame plays its enter.

Both sets of shaders come from the arriving content. An empty replace leaves the old frame underneath, unchanged. To cut, use a replace that draws nothing.

A slot holds one frame, plus at most one frame that’s leaving:

  • The leaving frame is removed when enter finishes, even if its replace hasn’t. An enter with no shaders finishes immediately, which gives you a plain cut.
  • If a third frame arrives mid-transition, the leaving frame is removed immediately, and the frame above it jumps to its finished state.
  • To keep a picture visible underneath another, give it its own slot.

enter always finishes, and ends on the frame as drawn. For a look that should last after the frame arrives, use the frame’s own shaders.

Animations run on a timer. By default, that’s the frame’s own timer, which starts when the frame’s occurrence first appears and ends when the frame is removed. Drawing the same occurrence again keeps the timer running.

A shader or camera can use a named timer instead:

type ShaderInstruction = {
// ...
timer?: string;
};
type CameraInstruction = {
// ...
timer?: string;
};

Shaders and cameras share timer names, so a pan and a fade with the same timer move together.

A draw update declares which named timers exist:

type TimerDeclarations = Record<string, null | TimerDeclaration>;
type TimerDeclaration = {
parent?: string;
state: "paused" | "running";
};
  • Declaring a timer again keeps its current position.
  • Setting a timer to null removes it. If it’s declared again later, it starts from zero.
  • A paused timer holds its position until it’s declared running again.
  • A shader or camera that uses an undeclared timer falls back to the draw’s timer, which starts when the draw is shown.
  • Using a timer that’s already running picks it up where it is. To restart an animation, use a different timer name.

A timer with a parent runs under that timer, and pauses whenever its parent does, however deep the chain goes.

Every frame runs under its slot’s timer, so pausing a slot’s timer pauses everything in it: frames, cameras, shaders, and any transition in progress. Other slots keep running.

To pause a single shader, give it its own timer with the slot’s timer as its parent, and declare it paused. It still pauses with the slot, and you can also pause it on its own.